diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d72d33ac4..83de43779 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,6 +23,7 @@ jobs: run: | cargo install cargo-ndk rustup target add x86_64-unknown-linux-gnu + sudo apt update 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 diff --git a/assets/images/glasses-hidden.png b/assets/images/glasses-hidden.png new file mode 100644 index 000000000..9176cc69b Binary files /dev/null and b/assets/images/glasses-hidden.png differ diff --git a/assets/images/glasses.png b/assets/images/glasses.png new file mode 100644 index 000000000..8c9e7dc27 Binary files /dev/null and b/assets/images/glasses.png differ diff --git a/assets/images/litecoin.png b/assets/images/litecoin.png new file mode 100644 index 000000000..17994bd47 Binary files /dev/null and b/assets/images/litecoin.png differ diff --git a/assets/svg/Button.svg b/assets/svg/Button.svg new file mode 100644 index 000000000..37e0d359b --- /dev/null +++ b/assets/svg/Button.svg @@ -0,0 +1,6 @@ +<svg width="91" height="38" viewBox="0 0 91 38" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<rect y="0.5" width="91" height="37" rx="8" fill="#E0E3E3"/> +<path d="M20.7663 24.5H17.4702V14.3182H20.8707C21.8684 14.3182 22.7251 14.522 23.4411 14.9297C24.157 15.334 24.7055 15.9157 25.0866 16.6747C25.4711 17.4304 25.6634 18.3369 25.6634 19.3942C25.6634 20.4548 25.4695 21.3662 25.0817 22.1286C24.6972 22.8909 24.1404 23.4775 23.4112 23.8885C22.6821 24.2962 21.8004 24.5 20.7663 24.5ZM19.0064 23.1577H20.6818C21.4574 23.1577 22.102 23.0118 22.6158 22.7202C23.1295 22.4252 23.514 21.9993 23.7692 21.4425C24.0244 20.8823 24.152 20.1996 24.152 19.3942C24.152 18.5954 24.0244 17.9176 23.7692 17.3608C23.5173 16.804 23.1411 16.3814 22.6406 16.093C22.1402 15.8047 21.5187 15.6605 20.7763 15.6605H19.0064V23.1577ZM27.4544 24.5V16.8636H28.9409V24.5H27.4544ZM28.2051 15.6854C27.9466 15.6854 27.7245 15.5992 27.5389 15.4268C27.3566 15.2512 27.2654 15.0424 27.2654 14.8004C27.2654 14.5552 27.3566 14.3464 27.5389 14.174C27.7245 13.9983 27.9466 13.9105 28.2051 13.9105C28.4636 13.9105 28.684 13.9983 28.8663 14.174C29.0519 14.3464 29.1447 14.5552 29.1447 14.8004C29.1447 15.0424 29.0519 15.2512 28.8663 15.4268C28.684 15.5992 28.4636 15.6854 28.2051 15.6854ZM36.663 18.728L35.3157 18.9666C35.2594 18.7943 35.1699 18.6302 35.0472 18.4744C34.9279 18.3187 34.7655 18.1911 34.56 18.0916C34.3545 17.9922 34.0977 17.9425 33.7894 17.9425C33.3685 17.9425 33.0172 18.0369 32.7354 18.2259C32.4537 18.4115 32.3129 18.6518 32.3129 18.9467C32.3129 19.2019 32.4073 19.4074 32.5962 19.5632C32.7852 19.719 33.0901 19.8466 33.511 19.946L34.7241 20.2244C35.4267 20.3868 35.9504 20.6371 36.2951 20.9751C36.6398 21.3132 36.8121 21.7524 36.8121 22.2926C36.8121 22.75 36.6796 23.1577 36.4144 23.5156C36.1526 23.8703 35.7863 24.1487 35.3157 24.3509C34.8484 24.553 34.3065 24.6541 33.69 24.6541C32.8349 24.6541 32.1372 24.4718 31.5969 24.1072C31.0567 23.7393 30.7253 23.2173 30.6026 22.5412L32.0394 22.3224C32.1289 22.697 32.3129 22.9804 32.5913 23.1726C32.8697 23.3615 33.2326 23.456 33.68 23.456C34.1673 23.456 34.5567 23.3549 34.8484 23.1527C35.14 22.9472 35.2859 22.697 35.2859 22.402C35.2859 22.1634 35.1964 21.9628 35.0174 21.8004C34.8417 21.638 34.5716 21.5154 34.207 21.4325L32.9144 21.1491C32.2018 20.9867 31.6748 20.7282 31.3335 20.3736C30.9954 20.0189 30.8263 19.5698 30.8263 19.0263C30.8263 18.5755 30.9523 18.1811 31.2042 17.843C31.4561 17.505 31.8041 17.2415 32.2482 17.0526C32.6924 16.8603 33.2011 16.7642 33.7745 16.7642C34.5998 16.7642 35.2494 16.9432 35.7234 17.3011C36.1973 17.6558 36.5105 18.1314 36.663 18.728ZM40.6689 24.669C40.185 24.669 39.7475 24.5795 39.3564 24.4006C38.9653 24.2183 38.6554 23.9548 38.4267 23.6101C38.2013 23.2654 38.0886 22.8428 38.0886 22.3423C38.0886 21.9115 38.1715 21.5568 38.3372 21.2784C38.5029 21 38.7266 20.7796 39.0083 20.6172C39.2901 20.4548 39.6049 20.3321 39.9529 20.2493C40.301 20.1664 40.6556 20.1035 41.0169 20.0604C41.4743 20.0073 41.8455 19.9643 42.1305 19.9311C42.4155 19.8946 42.6227 19.8366 42.752 19.7571C42.8812 19.6776 42.9458 19.5483 42.9458 19.3693V19.3345C42.9458 18.9003 42.8232 18.5639 42.5779 18.3253C42.336 18.0866 41.9747 17.9673 41.4941 17.9673C40.9937 17.9673 40.5993 18.0784 40.3109 18.3004C40.0259 18.5192 39.8287 18.7628 39.7193 19.0312L38.3223 18.7131C38.488 18.2491 38.7299 17.8745 39.0481 17.5895C39.3696 17.3011 39.7392 17.0923 40.1568 16.9631C40.5744 16.8305 41.0136 16.7642 41.4743 16.7642C41.7792 16.7642 42.1023 16.8007 42.4437 16.8736C42.7884 16.9432 43.1099 17.0724 43.4082 17.2614C43.7098 17.4503 43.9567 17.7204 44.149 18.0717C44.3412 18.4197 44.4373 18.8722 44.4373 19.429V24.5H42.9856V23.456H42.926C42.8298 23.6482 42.6857 23.8371 42.4934 24.0227C42.3012 24.2083 42.0543 24.3625 41.7527 24.4851C41.4511 24.6077 41.0898 24.669 40.6689 24.669ZM40.992 23.4759C41.403 23.4759 41.7543 23.3946 42.046 23.2322C42.341 23.0698 42.5647 22.8577 42.7172 22.5959C42.8729 22.3307 42.9508 22.0473 42.9508 21.7457V20.7614C42.8978 20.8144 42.795 20.8641 42.6426 20.9105C42.4934 20.9536 42.3227 20.9917 42.1305 21.0249C41.9383 21.0547 41.751 21.0829 41.5687 21.1094C41.3864 21.1326 41.234 21.1525 41.1113 21.169C40.823 21.2055 40.5595 21.2668 40.3208 21.353C40.0855 21.4392 39.8966 21.5634 39.7541 21.7259C39.6149 21.8849 39.5453 22.0971 39.5453 22.3622C39.5453 22.7301 39.6812 23.0085 39.9529 23.1974C40.2247 23.383 40.5711 23.4759 40.992 23.4759ZM46.5366 24.5V14.3182H48.0231V18.1016H48.1126C48.1987 17.9425 48.323 17.7585 48.4854 17.5497C48.6478 17.3409 48.8732 17.1586 49.1616 17.0028C49.4499 16.8438 49.8311 16.7642 50.305 16.7642C50.9215 16.7642 51.4717 16.92 51.9556 17.2315C52.4395 17.5431 52.819 17.9922 53.0941 18.5788C53.3725 19.1655 53.5117 19.8714 53.5117 20.6967C53.5117 21.522 53.3742 22.2296 53.0991 22.8196C52.824 23.4062 52.4461 23.8587 51.9656 24.1768C51.485 24.4917 50.9364 24.6491 50.32 24.6491C49.8559 24.6491 49.4764 24.5713 49.1815 24.4155C48.8898 24.2597 48.6611 24.0774 48.4954 23.8686C48.3297 23.6598 48.2021 23.4742 48.1126 23.3118H47.9883V24.5H46.5366ZM47.9933 20.6818C47.9933 21.2187 48.0711 21.6894 48.2269 22.0938C48.3827 22.4981 48.6081 22.8146 48.9031 23.0433C49.198 23.2687 49.5593 23.3814 49.9869 23.3814C50.431 23.3814 50.8022 23.2637 51.1005 23.0284C51.3988 22.7898 51.6242 22.4666 51.7766 22.0589C51.9324 21.6513 52.0103 21.1922 52.0103 20.6818C52.0103 20.178 51.9341 19.7256 51.7816 19.3246C51.6325 18.9235 51.4071 18.607 51.1055 18.375C50.8072 18.143 50.4343 18.027 49.9869 18.027C49.556 18.027 49.1914 18.138 48.8931 18.3601C48.5981 18.5821 48.3744 18.892 48.2219 19.2898C48.0695 19.6875 47.9933 20.1515 47.9933 20.6818ZM56.6674 14.3182V24.5H55.1809V14.3182H56.6674ZM61.9585 24.6541C61.2061 24.6541 60.5581 24.4934 60.0146 24.1719C59.4743 23.8471 59.0567 23.3913 58.7617 22.8047C58.4701 22.2147 58.3242 21.5237 58.3242 20.7315C58.3242 19.9493 58.4701 19.2599 58.7617 18.6634C59.0567 18.0668 59.4677 17.6011 59.9947 17.2663C60.525 16.9316 61.1448 16.7642 61.854 16.7642C62.2849 16.7642 62.7025 16.8355 63.1069 16.978C63.5112 17.1205 63.8742 17.3442 64.1957 17.6491C64.5172 17.9541 64.7707 18.3501 64.9563 18.8374C65.1419 19.3213 65.2347 19.9096 65.2347 20.6023V21.1293H59.1644V20.0156H63.7781C63.7781 19.6245 63.6985 19.2782 63.5394 18.9766C63.3803 18.6716 63.1566 18.4313 62.8683 18.2557C62.5832 18.08 62.2485 17.9922 61.864 17.9922C61.4464 17.9922 61.0818 18.0949 60.7702 18.3004C60.462 18.5026 60.2234 18.7678 60.0543 19.0959C59.8886 19.4207 59.8058 19.7737 59.8058 20.1548V21.0249C59.8058 21.5353 59.8952 21.9695 60.0742 22.3274C60.2565 22.6854 60.5101 22.9588 60.8349 23.1477C61.1597 23.3333 61.5392 23.4261 61.9734 23.4261C62.2551 23.4261 62.512 23.3864 62.744 23.3068C62.976 23.224 63.1765 23.1013 63.3455 22.9389C63.5146 22.7765 63.6438 22.576 63.7333 22.3374L65.1403 22.5909C65.0276 23.0052 64.8254 23.3681 64.5337 23.6797C64.2454 23.9879 63.8825 24.2282 63.445 24.4006C63.0108 24.5696 62.5153 24.6541 61.9585 24.6541ZM69.7427 24.6491C69.1262 24.6491 68.5761 24.4917 68.0922 24.1768C67.6116 23.8587 67.2337 23.4062 66.9586 22.8196C66.6868 22.2296 66.551 21.522 66.551 20.6967C66.551 19.8714 66.6885 19.1655 66.9636 18.5788C67.242 17.9922 67.6232 17.5431 68.1071 17.2315C68.591 16.92 69.1395 16.7642 69.7527 16.7642C70.2266 16.7642 70.6078 16.8438 70.8961 17.0028C71.1878 17.1586 71.4132 17.3409 71.5723 17.5497C71.7347 17.7585 71.8606 17.9425 71.9501 18.1016H72.0396V14.3182H73.5261V24.5H72.0744V23.3118H71.9501C71.8606 23.4742 71.7314 23.6598 71.5623 23.8686C71.3966 24.0774 71.1679 24.2597 70.8762 24.4155C70.5846 24.5713 70.2067 24.6491 69.7427 24.6491ZM70.0708 23.3814C70.4984 23.3814 70.8597 23.2687 71.1547 23.0433C71.4529 22.8146 71.6783 22.4981 71.8308 22.0938C71.9866 21.6894 72.0645 21.2187 72.0645 20.6818C72.0645 20.1515 71.9882 19.6875 71.8358 19.2898C71.6833 18.892 71.4596 18.5821 71.1646 18.3601C70.8696 18.138 70.505 18.027 70.0708 18.027C69.6234 18.027 69.2505 18.143 68.9522 18.375C68.6539 18.607 68.4286 18.9235 68.2761 19.3246C68.127 19.7256 68.0524 20.178 68.0524 20.6818C68.0524 21.1922 68.1286 21.6513 68.2811 22.0589C68.4335 22.4666 68.6589 22.7898 68.9572 23.0284C69.2588 23.2637 69.63 23.3814 70.0708 23.3814Z" fill="#8E9192"/> +</g> +</svg> diff --git a/assets/svg/about-desktop.svg b/assets/svg/about-desktop.svg new file mode 100644 index 000000000..a80067d9c --- /dev/null +++ b/assets/svg/about-desktop.svg @@ -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> diff --git a/assets/svg/address-book-desktop.svg b/assets/svg/address-book-desktop.svg new file mode 100644 index 000000000..fb85e3e11 --- /dev/null +++ b/assets/svg/address-book-desktop.svg @@ -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> diff --git a/assets/svg/arrow-down.svg b/assets/svg/arrow-down.svg new file mode 100644 index 000000000..c96e43ce3 --- /dev/null +++ b/assets/svg/arrow-down.svg @@ -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> diff --git a/assets/svg/chevron-up.svg b/assets/svg/chevron-up.svg new file mode 100644 index 000000000..630f8df69 --- /dev/null +++ b/assets/svg/chevron-up.svg @@ -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> diff --git a/assets/svg/coin_icons/Litecoin.svg b/assets/svg/coin_icons/Litecoin.svg new file mode 100644 index 000000000..2b89ca50b --- /dev/null +++ b/assets/svg/coin_icons/Litecoin.svg @@ -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> diff --git a/assets/svg/configuration.svg b/assets/svg/configuration.svg new file mode 100644 index 000000000..516bbf320 --- /dev/null +++ b/assets/svg/configuration.svg @@ -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_29086)"> +<path d="M13.5 30.5625C13.5 29.8365 14.0878 29.25 14.8125 29.25H17.0544C17.5605 28.0893 18.7172 27.2812 20.0625 27.2812C21.4078 27.2812 22.5275 28.0893 23.0689 29.25H33.1875C33.9135 29.25 34.5 29.8365 34.5 30.5625C34.5 31.2885 33.9135 31.875 33.1875 31.875H23.0689C22.5275 33.0357 21.4078 33.8438 20.0625 33.8438C18.7172 33.8438 17.5605 33.0357 17.0544 31.875H14.8125C14.0878 31.875 13.5 31.2885 13.5 30.5625ZM21.375 30.5625C21.375 29.8365 20.7885 29.25 20.0625 29.25C19.3365 29.25 18.75 29.8365 18.75 30.5625C18.75 31.2885 19.3365 31.875 20.0625 31.875C20.7885 31.875 21.375 31.2885 21.375 30.5625ZM27.9375 20.7188C29.2828 20.7188 30.4025 21.5268 30.9439 22.6875H33.1875C33.9135 22.6875 34.5 23.274 34.5 24C34.5 24.726 33.9135 25.3125 33.1875 25.3125H30.9439C30.4025 26.4732 29.2828 27.2812 27.9375 27.2812C26.5922 27.2812 25.4355 26.4732 24.9311 25.3125H14.8125C14.0878 25.3125 13.5 24.726 13.5 24C13.5 23.274 14.0878 22.6875 14.8125 22.6875H24.9311C25.4355 21.5268 26.5922 20.7188 27.9375 20.7188ZM29.25 24C29.25 23.274 28.6635 22.6875 27.9375 22.6875C27.2115 22.6875 26.625 23.274 26.625 24C26.625 24.726 27.2115 25.3125 27.9375 25.3125C28.6635 25.3125 29.25 24.726 29.25 24ZM33.1875 16.125C33.9135 16.125 34.5 16.7128 34.5 17.4375C34.5 18.1635 33.9135 18.75 33.1875 18.75H24.3814C23.84 19.9107 22.7203 20.7188 21.375 20.7188C20.0297 20.7188 18.873 19.9107 18.3686 18.75H14.8125C14.0878 18.75 13.5 18.1635 13.5 17.4375C13.5 16.7128 14.0878 16.125 14.8125 16.125H18.3686C18.873 14.9663 20.0297 14.1562 21.375 14.1562C22.7203 14.1562 23.84 14.9663 24.3814 16.125H33.1875ZM20.0625 17.4375C20.0625 18.1635 20.649 18.75 21.375 18.75C22.101 18.75 22.6875 18.1635 22.6875 17.4375C22.6875 16.7128 22.101 16.125 21.375 16.125C20.649 16.125 20.0625 16.7128 20.0625 17.4375Z" fill="black"/> +</g> +<defs> +<clipPath id="clip0_5813_29086"> +<rect width="21" height="21" fill="white" transform="translate(13.5 13.5)"/> +</clipPath> +</defs> +</svg> diff --git a/assets/svg/dark-theme.svg b/assets/svg/dark-theme.svg new file mode 100644 index 000000000..47b5e2d5e --- /dev/null +++ b/assets/svg/dark-theme.svg @@ -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> diff --git a/assets/svg/dollar-sign-circle.svg b/assets/svg/dollar-sign-circle.svg new file mode 100644 index 000000000..03aacffea --- /dev/null +++ b/assets/svg/dollar-sign-circle.svg @@ -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> diff --git a/assets/svg/enabled-button.svg b/assets/svg/enabled-button.svg new file mode 100644 index 000000000..a26359e81 --- /dev/null +++ b/assets/svg/enabled-button.svg @@ -0,0 +1,4 @@ +<svg width="87" height="38" viewBox="0 0 87 38" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect y="0.5" width="87" height="37" rx="8" fill="#B9E9D4"/> +<path d="M17.6167 24.5V14.3182H24.0002V15.6406H19.1529V18.7429H23.6671V20.0604H19.1529V23.1776H24.0598V24.5H17.6167ZM27.4194 19.9659V24.5H25.9329V16.8636H27.3597V18.1065H27.4542C27.6299 17.7022 27.9049 17.3774 28.2795 17.1321C28.6573 16.8868 29.1329 16.7642 29.7063 16.7642C30.2267 16.7642 30.6824 16.8736 31.0735 17.0923C31.4646 17.3078 31.7679 17.6293 31.9833 18.0568C32.1987 18.4844 32.3065 19.013 32.3065 19.6428V24.5H30.82V19.8217C30.82 19.2682 30.6758 18.8357 30.3874 18.5241C30.0991 18.2093 29.703 18.0518 29.1992 18.0518C28.8545 18.0518 28.5479 18.1264 28.2795 18.2756C28.0143 18.4247 27.8039 18.6435 27.6481 18.9318C27.4956 19.2169 27.4194 19.5616 27.4194 19.9659ZM36.5224 24.669C36.0385 24.669 35.601 24.5795 35.2099 24.4006C34.8188 24.2183 34.5089 23.9548 34.2802 23.6101C34.0548 23.2654 33.9421 22.8428 33.9421 22.3423C33.9421 21.9115 34.025 21.5568 34.1907 21.2784C34.3564 21 34.5801 20.7796 34.8619 20.6172C35.1436 20.4548 35.4585 20.3321 35.8065 20.2493C36.1545 20.1664 36.5091 20.1035 36.8704 20.0604C37.3278 20.0073 37.699 19.9643 37.984 19.9311C38.2691 19.8946 38.4762 19.8366 38.6055 19.7571C38.7347 19.6776 38.7994 19.5483 38.7994 19.3693V19.3345C38.7994 18.9003 38.6767 18.5639 38.4315 18.3253C38.1895 18.0866 37.8282 17.9673 37.3477 17.9673C36.8472 17.9673 36.4528 18.0784 36.1644 18.3004C35.8794 18.5192 35.6822 18.7628 35.5728 19.0312L34.1758 18.7131C34.3415 18.2491 34.5835 17.8745 34.9016 17.5895C35.2231 17.3011 35.5927 17.0923 36.0103 16.9631C36.4279 16.8305 36.8671 16.7642 37.3278 16.7642C37.6327 16.7642 37.9558 16.8007 38.2972 16.8736C38.6419 16.9432 38.9634 17.0724 39.2617 17.2614C39.5633 17.4503 39.8103 17.7204 40.0025 18.0717C40.1947 18.4197 40.2908 18.8722 40.2908 19.429V24.5H38.8391V23.456H38.7795C38.6834 23.6482 38.5392 23.8371 38.3469 24.0227C38.1547 24.2083 37.9078 24.3625 37.6062 24.4851C37.3046 24.6077 36.9433 24.669 36.5224 24.669ZM36.8455 23.4759C37.2565 23.4759 37.6078 23.3946 37.8995 23.2322C38.1945 23.0698 38.4182 22.8577 38.5707 22.5959C38.7264 22.3307 38.8043 22.0473 38.8043 21.7457V20.7614C38.7513 20.8144 38.6486 20.8641 38.4961 20.9105C38.3469 20.9536 38.1763 20.9917 37.984 21.0249C37.7918 21.0547 37.6045 21.0829 37.4222 21.1094C37.2399 21.1326 37.0875 21.1525 36.9648 21.169C36.6765 21.2055 36.413 21.2668 36.1744 21.353C35.939 21.4392 35.7501 21.5634 35.6076 21.7259C35.4684 21.8849 35.3988 22.0971 35.3988 22.3622C35.3988 22.7301 35.5347 23.0085 35.8065 23.1974C36.0782 23.383 36.4246 23.4759 36.8455 23.4759ZM42.3901 24.5V14.3182H43.8766V18.1016H43.9661C44.0523 17.9425 44.1766 17.7585 44.339 17.5497C44.5014 17.3409 44.7267 17.1586 45.0151 17.0028C45.3034 16.8438 45.6846 16.7642 46.1586 16.7642C46.775 16.7642 47.3252 16.92 47.8091 17.2315C48.293 17.5431 48.6725 17.9922 48.9476 18.5788C49.226 19.1655 49.3652 19.8714 49.3652 20.6967C49.3652 21.522 49.2277 22.2296 48.9526 22.8196C48.6775 23.4062 48.2997 23.8587 47.8191 24.1768C47.3385 24.4917 46.79 24.6491 46.1735 24.6491C45.7095 24.6491 45.33 24.5713 45.035 24.4155C44.7433 24.2597 44.5146 24.0774 44.3489 23.8686C44.1832 23.6598 44.0556 23.4742 43.9661 23.3118H43.8418V24.5H42.3901ZM43.8468 20.6818C43.8468 21.2187 43.9247 21.6894 44.0804 22.0938C44.2362 22.4981 44.4616 22.8146 44.7566 23.0433C45.0516 23.2687 45.4128 23.3814 45.8404 23.3814C46.2845 23.3814 46.6557 23.2637 46.954 23.0284C47.2523 22.7898 47.4777 22.4666 47.6301 22.0589C47.7859 21.6513 47.8638 21.1922 47.8638 20.6818C47.8638 20.178 47.7876 19.7256 47.6351 19.3246C47.486 18.9235 47.2606 18.607 46.959 18.375C46.6607 18.143 46.2878 18.027 45.8404 18.027C45.4095 18.027 45.0449 18.138 44.7466 18.3601C44.4516 18.5821 44.2279 18.892 44.0755 19.2898C43.923 19.6875 43.8468 20.1515 43.8468 20.6818ZM52.521 14.3182V24.5H51.0344V14.3182H52.521ZM57.812 24.6541C57.0596 24.6541 56.4116 24.4934 55.8681 24.1719C55.3278 23.8471 54.9102 23.3913 54.6152 22.8047C54.3236 22.2147 54.1777 21.5237 54.1777 20.7315C54.1777 19.9493 54.3236 19.2599 54.6152 18.6634C54.9102 18.0668 55.3212 17.6011 55.8482 17.2663C56.3785 16.9316 56.9983 16.7642 57.7076 16.7642C58.1384 16.7642 58.556 16.8355 58.9604 16.978C59.3648 17.1205 59.7277 17.3442 60.0492 17.6491C60.3707 17.9541 60.6242 18.3501 60.8098 18.8374C60.9954 19.3213 61.0882 19.9096 61.0882 20.6023V21.1293H55.0179V20.0156H59.6316C59.6316 19.6245 59.552 19.2782 59.3929 18.9766C59.2338 18.6716 59.0101 18.4313 58.7218 18.2557C58.4367 18.08 58.102 17.9922 57.7175 17.9922C57.2999 17.9922 56.9353 18.0949 56.6238 18.3004C56.3155 18.5026 56.0769 18.7678 55.9078 19.0959C55.7421 19.4207 55.6593 19.7737 55.6593 20.1548V21.0249C55.6593 21.5353 55.7488 21.9695 55.9277 22.3274C56.11 22.6854 56.3636 22.9588 56.6884 23.1477C57.0132 23.3333 57.3927 23.4261 57.8269 23.4261C58.1086 23.4261 58.3655 23.3864 58.5975 23.3068C58.8295 23.224 59.03 23.1013 59.199 22.9389C59.3681 22.7765 59.4973 22.576 59.5868 22.3374L60.9938 22.5909C60.8811 23.0052 60.6789 23.3681 60.3873 23.6797C60.0989 23.9879 59.736 24.2282 59.2985 24.4006C58.8643 24.5696 58.3688 24.6541 57.812 24.6541ZM65.5962 24.6491C64.9798 24.6491 64.4296 24.4917 63.9457 24.1768C63.4651 23.8587 63.0872 23.4062 62.8121 22.8196C62.5404 22.2296 62.4045 21.522 62.4045 20.6967C62.4045 19.8714 62.542 19.1655 62.8171 18.5788C63.0955 17.9922 63.4767 17.5431 63.9606 17.2315C64.4445 16.92 64.993 16.7642 65.6062 16.7642C66.0801 16.7642 66.4613 16.8438 66.7496 17.0028C67.0413 17.1586 67.2667 17.3409 67.4258 17.5497C67.5882 17.7585 67.7141 17.9425 67.8036 18.1016H67.8931V14.3182H69.3796V24.5H67.9279V23.3118H67.8036C67.7141 23.4742 67.5849 23.6598 67.4158 23.8686C67.2501 24.0774 67.0214 24.2597 66.7298 24.4155C66.4381 24.5713 66.0603 24.6491 65.5962 24.6491ZM65.9244 23.3814C66.3519 23.3814 66.7132 23.2687 67.0082 23.0433C67.3065 22.8146 67.5318 22.4981 67.6843 22.0938C67.8401 21.6894 67.918 21.2187 67.918 20.6818C67.918 20.1515 67.8417 19.6875 67.6893 19.2898C67.5368 18.892 67.3131 18.5821 67.0181 18.3601C66.7231 18.138 66.3585 18.027 65.9244 18.027C65.4769 18.027 65.104 18.143 64.8058 18.375C64.5075 18.607 64.2821 18.9235 64.1296 19.3246C63.9805 19.7256 63.9059 20.178 63.9059 20.6818C63.9059 21.1922 63.9821 21.6513 64.1346 22.0589C64.2871 22.4666 64.5124 22.7898 64.8107 23.0284C65.1123 23.2637 65.4835 23.3814 65.9244 23.3814Z" fill="#232323"/> +</svg> diff --git a/assets/svg/exchange-desktop.svg b/assets/svg/exchange-desktop.svg new file mode 100644 index 000000000..8eacfa84e --- /dev/null +++ b/assets/svg/exchange-desktop.svg @@ -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> diff --git a/assets/svg/exit-desktop.svg b/assets/svg/exit-desktop.svg new file mode 100644 index 000000000..abba264cd --- /dev/null +++ b/assets/svg/exit-desktop.svg @@ -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> diff --git a/assets/svg/framed-address-book.svg b/assets/svg/framed-address-book.svg new file mode 100644 index 000000000..157117097 --- /dev/null +++ b/assets/svg/framed-address-book.svg @@ -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> diff --git a/assets/svg/framed-gear.svg b/assets/svg/framed-gear.svg new file mode 100644 index 000000000..749d9803d --- /dev/null +++ b/assets/svg/framed-gear.svg @@ -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> diff --git a/assets/svg/keys.svg b/assets/svg/keys.svg new file mode 100644 index 000000000..cf86daea5 --- /dev/null +++ b/assets/svg/keys.svg @@ -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> diff --git a/assets/svg/language-circle.svg b/assets/svg/language-circle.svg new file mode 100644 index 000000000..700ffede4 --- /dev/null +++ b/assets/svg/language-circle.svg @@ -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> diff --git a/assets/svg/light-mode.svg b/assets/svg/light-mode.svg new file mode 100644 index 000000000..4ff9e2696 --- /dev/null +++ b/assets/svg/light-mode.svg @@ -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> diff --git a/assets/svg/lock-circle.svg b/assets/svg/lock-circle.svg new file mode 100644 index 000000000..f8fd71831 --- /dev/null +++ b/assets/svg/lock-circle.svg @@ -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> diff --git a/assets/svg/lock-open.svg b/assets/svg/lock-open.svg new file mode 100644 index 000000000..f2b00f341 --- /dev/null +++ b/assets/svg/lock-open.svg @@ -0,0 +1,3 @@ +<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7 1.75C5.79141 1.75 4.8125 2.72945 4.8125 3.9375V5.25H11.375C12.3402 5.25 13.125 6.03477 13.125 7V12.25C13.125 13.2152 12.3402 14 11.375 14H2.625C1.6584 14 0.875 13.2152 0.875 12.25V7C0.875 6.03477 1.6584 5.25 2.625 5.25H3.0625V3.9375C3.0625 1.76285 4.82617 0 7 0C8.57227 0 9.92578 0.921211 10.5574 2.24957C10.7652 2.68598 10.5793 3.20742 10.1199 3.41523C9.68242 3.62305 9.18476 3.43711 8.97695 2.99961C8.62422 2.25941 7.87227 1.75 7 1.75ZM7.875 10.5C8.35898 10.5 8.75 10.109 8.75 9.625C8.75 9.14102 8.35898 8.75 7.875 8.75H6.125C5.64102 8.75 5.25 9.14102 5.25 9.625C5.25 10.109 5.64102 10.5 6.125 10.5H7.875Z" fill="#0056D2"/> +</svg> diff --git a/assets/svg/network-wired-2.svg b/assets/svg/network-wired-2.svg new file mode 100644 index 000000000..bbbfa056f --- /dev/null +++ b/assets/svg/network-wired-2.svg @@ -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> diff --git a/assets/svg/node-circle.svg b/assets/svg/node-circle.svg new file mode 100644 index 000000000..bd9353a2b --- /dev/null +++ b/assets/svg/node-circle.svg @@ -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> diff --git a/assets/svg/ocean-breeze-theme.svg b/assets/svg/ocean-breeze-theme.svg new file mode 100644 index 000000000..0deb96ec8 --- /dev/null +++ b/assets/svg/ocean-breeze-theme.svg @@ -0,0 +1,28 @@ +<svg width="200" height="162" viewBox="0 0 200 162" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_518_22068)"> +<rect width="200" height="162" rx="8" fill="url(#paint0_linear_518_22068)"/> +<rect x="10" y="10" width="180" height="20" rx="2" fill="#C2DAE2"/> +<rect x="16" y="16" width="106" height="8" rx="1" fill="#227386"/> +<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="#BDD5DB"/> +<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="#BDD5DB"/> +<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="#BDD5DB"/> +<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="#BDD5DB"/> +<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="#BDD5DB"/> +<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="#BDD5DB"/> +</g> +<defs> +<linearGradient id="paint0_linear_518_22068" x1="100" y1="0" x2="100" y2="162" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F3F7FA"/> +<stop offset="1" stop-color="#E8F2F9"/> +</linearGradient> +<clipPath id="clip0_518_22068"> +<rect width="200" height="162" rx="8" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/assets/svg/oceanBreeze/bell-new.svg b/assets/svg/oceanBreeze/bell-new.svg new file mode 100644 index 000000000..8cef32715 --- /dev/null +++ b/assets/svg/oceanBreeze/bell-new.svg @@ -0,0 +1,5 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.5 17.5C12.5 17.9193 12.2383 18.3672 11.7695 18.6797C11.3008 18.9922 10.6289 19.1667 10 19.1667C9.33594 19.1667 8.69922 18.9922 8.23047 18.6797C7.76172 18.3672 7.5 17.9193 7.5 17.5H12.5Z" fill="#227386"/> +<path d="M11.1903 1.98716V2.67947C13.9059 3.2142 15.9519 5.54245 15.9519 8.33331V9.0112C15.9519 10.7095 16.5955 12.3429 17.7561 13.6122L18.0314 13.9114C18.3439 14.254 18.422 14.7372 18.2286 15.1518C18.0351 15.5665 17.611 15.8333 17.1423 15.8333H2.85739C2.38867 15.8333 1.96351 15.5665 1.77148 15.1518C1.57945 14.7372 1.65626 14.254 1.96771 13.9114L2.24359 13.6122C3.40573 12.3429 4.0478 10.7095 4.0478 9.0112V8.33331C4.0478 5.54245 6.06034 3.2142 8.80945 2.67947V1.98716C8.80945 1.35002 9.34141 0.833313 9.99986 0.833313C10.6583 0.833313 11.1903 1.35002 11.1903 1.98716Z" fill="#227386"/> +<ellipse cx="17.0833" cy="2.91665" rx="2.08333" ry="2.08333" fill="#D34E50"/> +</svg> diff --git a/assets/svg/oceanBreeze/bg.svg b/assets/svg/oceanBreeze/bg.svg new file mode 100644 index 000000000..35fbda281 --- /dev/null +++ b/assets/svg/oceanBreeze/bg.svg @@ -0,0 +1,11 @@ +<svg width="360" height="480" viewBox="0 0 360 480" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M48 174.503C45.4098 171.437 39.6642 169.267 34.578 176.625C29.4917 183.983 22.3192 180.197 19.7431 176.625C16.682 172.38 8.86422 166.438 2.08257 176.625C-0.507645 179.926 -7.2422 184.549 -13.4587 176.625C-19.6752 168.702 -26.4098 171.909 -29 174.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M48 188.503C45.4098 185.437 39.6642 183.267 34.578 190.625C29.4917 197.983 22.3192 194.197 19.7431 190.625C16.682 186.38 8.86422 180.438 2.08257 190.625C-0.507645 193.926 -7.2422 198.549 -13.4587 190.625C-19.6752 182.702 -26.4098 185.909 -29 188.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M48 202.503C45.4098 199.437 39.6642 197.267 34.578 204.625C29.4917 211.983 22.3192 208.197 19.7431 204.625C16.682 200.38 8.86422 194.438 2.08257 204.625C-0.507645 207.926 -7.2422 212.549 -13.4587 204.625C-19.6752 196.702 -26.4098 199.909 -29 202.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 444.503C386.41 441.437 380.664 439.267 375.578 446.625C370.492 453.983 363.319 450.197 360.743 446.625C357.682 442.38 349.864 436.438 343.083 446.625C340.492 449.926 333.758 454.549 327.541 446.625C321.325 438.702 314.59 441.909 312 444.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 458.503C386.41 455.437 380.664 453.267 375.578 460.625C370.492 467.983 363.319 464.197 360.743 460.625C357.682 456.38 349.864 450.438 343.083 460.625C340.492 463.926 333.758 468.549 327.541 460.625C321.325 452.702 314.59 455.909 312 458.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 472.503C386.41 469.437 380.664 467.267 375.578 474.625C370.492 481.983 363.319 478.197 360.743 474.625C357.682 470.38 349.864 464.438 343.083 474.625C340.492 477.926 333.758 482.549 327.541 474.625C321.325 466.702 314.59 469.909 312 472.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 4.5028C386.41 1.4371 380.664 -0.732664 375.578 6.62502C370.492 13.9827 363.319 10.1971 360.743 6.62502C357.682 2.38025 349.864 -3.56244 343.083 6.62502C340.492 9.92648 333.758 14.5485 327.541 6.62502C321.325 -1.29849 314.59 1.90875 312 4.5028" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 18.5028C386.41 15.4371 380.664 13.2673 375.578 20.625C370.492 27.9827 363.319 24.1971 360.743 20.625C357.682 16.3802 349.864 10.4376 343.083 20.625C340.492 23.9265 333.758 28.5485 327.541 20.625C321.325 12.7015 314.59 15.9087 312 18.5028" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 32.5028C386.41 29.4371 380.664 27.2673 375.578 34.625C370.492 41.9827 363.319 38.1971 360.743 34.625C357.682 30.3802 349.864 24.4376 343.083 34.625C340.492 37.9265 333.758 42.5485 327.541 34.625C321.325 26.7015 314.59 29.9087 312 32.5028" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/buy-coins-icon.svg b/assets/svg/oceanBreeze/buy-coins-icon.svg new file mode 100644 index 000000000..d9613bccb --- /dev/null +++ b/assets/svg/oceanBreeze/buy-coins-icon.svg @@ -0,0 +1,18 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_519_18707)"> +<g opacity="0.4"> +<path d="M22.2 6C23.3297 5.37187 24 4.59422 24 3.75C24 1.67906 19.9688 0 15 0C9.98906 0 6 1.67906 6 3.75C6 4.59422 6.67031 5.37187 7.8 6C7.80937 6.00469 7.81758 6.00937 7.82578 6.01406C7.83398 6.01875 7.84219 6.02344 7.85156 6.02813C8.23125 6.00938 8.61094 6 9 6C11.6344 6 14.0906 6.44062 15.9422 7.21406C16.1203 7.28906 16.2984 7.36875 16.4672 7.44844C18.8062 7.28906 20.8359 6.75469 22.2 6Z" fill="#227386"/> +<path d="M19.9435 12.9151C19.7958 12.9551 19.6477 12.9951 19.5 13.0359V13.5C20.7602 13.5 21.9296 13.8885 22.8951 14.5522C23.5995 14.0172 24 13.4028 24 12.75V11.0906C23.4141 11.5734 22.7063 11.9672 21.9422 12.2859C21.3382 12.5376 20.6447 12.7253 19.9435 12.9151Z" fill="#227386"/> +<path d="M18.3703 8.74688C19.0031 9.37969 19.5 10.2234 19.5 11.25V11.4984C20.4328 11.2734 21.2625 10.9781 21.9469 10.6359C21.9739 10.6209 22.0009 10.6021 22.0279 10.5833C22.0852 10.5432 22.1426 10.5032 22.2 10.5C23.3297 9.87187 24 9.09375 24 8.25V6.59063C23.4141 7.07344 22.7063 7.46719 21.9422 7.78594C20.9109 8.2125 19.6969 8.54063 18.3703 8.74688Z" fill="#227386"/> +</g> +<path d="M16.2 13.5C17.3297 12.8719 18 12.0938 18 11.25C18 9.17813 13.9688 7.5 9 7.5C4.02938 7.5 0 9.17813 0 11.25C0 12.0938 0.669375 12.8719 1.79953 13.5C1.85443 13.5031 1.91057 13.5415 1.96782 13.5807C1.9966 13.6004 2.02567 13.6203 2.055 13.6359C3.70594 14.4703 6.20625 15 9 15C11.9438 15 14.5594 14.4094 16.2 13.5Z" fill="#227386"/> +<path d="M14.8788 15.6729C13.1948 16.2046 11.1571 16.5 9 16.5C6.36562 16.5 3.91125 16.0594 2.05922 15.2859C1.29469 14.9672 0.583594 14.5734 0 14.0906V15.75C0 16.5938 0.669375 17.3719 1.79953 18C3.44109 18.9094 6.05625 19.5 9 19.5C10.6471 19.5 12.1916 19.3159 13.5211 18.9937C13.6261 17.7367 14.1186 16.5898 14.8788 15.6729Z" fill="#227386"/> +<path d="M13.5862 20.5191C13.7529 21.4936 14.1547 22.3879 14.731 23.1415C13.1742 23.6778 11.1771 24 9 24C4.02938 24 0 22.3219 0 20.25V18.5906C0.583594 19.0734 1.29469 19.4672 2.05922 19.7859C3.91125 20.5594 6.36562 21 9 21C10.6307 21 12.1932 20.8312 13.5862 20.5191Z" fill="#227386"/> +<path d="M24 19.5C24 21.9844 21.9844 24 19.5 24C17.0156 24 15 21.9844 15 19.5C15 17.0156 17.0156 15 19.5 15C21.9844 15 24 17.0156 24 19.5ZM19 17.4719V18.9719H17.5C17.225 18.9719 17 19.225 17 19.4719C17 19.775 17.225 19.9719 17.5 19.9719H19V21.4719C19 21.775 19.225 21.9719 19.5 21.9719C19.775 21.9719 20 21.775 20 21.4719V19.9719H21.5C21.775 19.9719 22 19.775 22 19.4719C22 19.225 21.775 18.9719 21.5 18.9719H20V17.4719C20 17.225 19.775 16.9719 19.5 16.9719C19.225 16.9719 19 17.225 19 17.4719Z" fill="#227386"/> +</g> +<defs> +<clipPath id="clip0_519_18707"> +<rect width="24" height="24" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/assets/svg/oceanBreeze/exchange-2.svg b/assets/svg/oceanBreeze/exchange-2.svg new file mode 100644 index 000000000..7baeaf87f --- /dev/null +++ b/assets/svg/oceanBreeze/exchange-2.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M19.5 6.5L20.4343 7.33045C20.8552 6.85685 20.8552 6.14315 20.4343 5.66955L19.5 6.5ZM16.4343 1.16955C15.9756 0.653567 15.1855 0.607091 14.6695 1.06574C14.1536 1.52439 14.1071 2.31448 14.5657 2.83045L16.4343 1.16955ZM14.5657 10.1695C14.1071 10.6855 14.1536 11.4756 14.6695 11.9343C15.1855 12.3929 15.9756 12.3464 16.4343 11.8305L14.5657 10.1695ZM0.75 10.5C0.75 11.1904 1.30964 11.75 2 11.75C2.69036 11.75 3.25 11.1904 3.25 10.5H0.75ZM6 7.75H19.5V5.25H6V7.75ZM14.5657 2.83045L18.5657 7.33045L20.4343 5.66955L16.4343 1.16955L14.5657 2.83045ZM16.4343 11.8305L20.4343 7.33045L18.5657 5.66955L14.5657 10.1695L16.4343 11.8305ZM3.25 10.5C3.25 8.98122 4.48122 7.75 6 7.75V5.25C3.10051 5.25 0.75 7.60051 0.75 10.5H3.25Z" fill="#227386"/> +<path opacity="0.4" d="M4.5 18L3.56574 17.1695C3.14475 17.6432 3.14475 18.3568 3.56574 18.8305L4.5 18ZM7.56574 23.3305C8.02439 23.8464 8.81448 23.8929 9.33045 23.4343C9.84643 22.9756 9.89291 22.1855 9.43426 21.6695L7.56574 23.3305ZM9.43426 14.3305C9.89291 13.8145 9.84643 13.0244 9.33046 12.5657C8.81448 12.1071 8.02439 12.1536 7.56574 12.6695L9.43426 14.3305ZM23.25 14C23.25 13.3096 22.6904 12.75 22 12.75C21.3096 12.75 20.75 13.3096 20.75 14L23.25 14ZM18 16.75L4.5 16.75L4.5 19.25L18 19.25L18 16.75ZM9.43426 21.6695L5.43426 17.1695L3.56574 18.8305L7.56574 23.3305L9.43426 21.6695ZM7.56574 12.6695L3.56574 17.1695L5.43426 18.8305L9.43426 14.3305L7.56574 12.6695ZM20.75 14C20.75 15.5188 19.5188 16.75 18 16.75L18 19.25C20.8995 19.25 23.25 16.8995 23.25 14L20.75 14Z" fill="#227386"/> +</svg> diff --git a/assets/svg/oceanBreeze/stack-icon1.svg b/assets/svg/oceanBreeze/stack-icon1.svg new file mode 100644 index 000000000..f316012d7 --- /dev/null +++ b/assets/svg/oceanBreeze/stack-icon1.svg @@ -0,0 +1,5 @@ +<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M41.3715 9.57675C37.2965 7.22564 32.2041 10.1695 32.2041 14.8717C32.2041 19.5739 34.4762 23.6489 38.2004 26.163L53.9717 35.3057L54.0112 35.2908L69.9948 26.0543L41.3715 9.57675Z" fill="#B3B3B3"/> +<path d="M38.2014 26.163C34.4771 23.6489 32.205 19.4159 32.205 14.8717C32.205 12.6342 33.3757 10.7671 35.0402 9.7101C34.9612 9.75455 35.1192 9.66564 35.0402 9.7101L10.0917 23.7279L6.08593 26.1481L3.35449 27.7188C5.07337 26.8446 7.22692 26.7754 9.14831 27.8917L16.0189 31.8037L22.0399 35.2859L38.0236 44.5076L53.9677 35.2958L38.1964 26.1531L38.2014 26.163Z" fill="#666666"/> +<path d="M70 44.5187L38.0278 62.9917L31.992 59.5095L31.9673 59.4848L6.06054 44.5187C4.28733 43.3629 2.84505 41.7872 1.82755 40.014C0.642111 37.9691 0 35.618 0 33.1829C0 30.9899 1.10147 29.1771 2.70181 28.1004C2.91914 27.967 3.13153 27.8435 3.35874 27.725C5.07762 26.8507 7.23116 26.7816 9.15256 27.8979L15.9836 31.8394L22.0047 35.3068L22.0442 35.292L38.0278 44.5137L53.9719 35.3019L70 44.5137V44.5187Z" fill="#232323"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg b/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg new file mode 100644 index 000000000..a54836bba --- /dev/null +++ b/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg @@ -0,0 +1,7 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#0056D2"/> +<path d="M5.30071 12.4C4.91018 12.7905 4.91018 13.4236 5.30071 13.8142C5.69123 14.2047 6.32439 14.2047 6.71492 13.8142L5.30071 12.4ZM13.0789 6.03599L14.0787 6.05567C14.0839 5.78863 13.9821 5.53058 13.796 5.33904C13.6098 5.1475 13.3548 5.03839 13.0877 5.03603L13.0789 6.03599ZM9.00968 5.00004C8.45741 4.99516 8.00576 5.43891 8.00089 5.99117C7.99601 6.54344 8.43976 6.99509 8.99202 6.99996L9.00968 5.00004ZM12.001 9.98032C11.9902 10.5325 12.429 10.9889 12.9812 10.9998C13.5333 11.0107 13.9898 10.5719 14.0007 10.0197L12.001 9.98032ZM18.6429 11.6C19.0334 11.2095 19.0334 10.5764 18.6429 10.1858C18.2524 9.79531 17.6192 9.79531 17.2287 10.1858L18.6429 11.6ZM10.8647 17.964L9.8653 17.9297C9.85604 18.1992 9.95602 18.461 10.1426 18.6557C10.3291 18.8505 10.5864 18.9616 10.856 18.964L10.8647 17.964ZM14.9922 19C15.5444 19.0048 15.996 18.561 16.0008 18.0087C16.0056 17.4564 15.5618 17.0048 15.0096 17L14.9922 19ZM12.0003 14.0343C12.0192 13.4824 11.5871 13.0195 11.0352 13.0006C10.4832 12.9816 10.0204 13.4137 10.0014 13.9657L12.0003 14.0343ZM6.71492 13.8142L13.786 6.7431L12.3718 5.32889L5.30071 12.4L6.71492 13.8142ZM8.99202 6.99996L13.0701 7.03595L13.0877 5.03603L9.00968 5.00004L8.99202 6.99996ZM12.0791 6.01631L12.001 9.98032L14.0007 10.0197L14.0787 6.05567L12.0791 6.01631ZM17.2287 10.1858L10.1576 17.2569L11.5718 18.6711L18.6429 11.6L17.2287 10.1858ZM15.0096 17L10.8734 16.964L10.856 18.964L14.9922 19L15.0096 17ZM11.8641 17.9983L12.0003 14.0343L10.0014 13.9657L9.8653 17.9297L11.8641 17.9983Z" fill="#0056D2"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#C00205"/> +<path d="M19.4395 19.4395L20.5001 20.5001L21.5608 21.5608" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.5 21.5605L20.5607 20.4999L21.6213 19.4392" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg b/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg new file mode 100644 index 000000000..5f9aa4256 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#0056D2"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#F4C517"/> +<path d="M20.5 19V20.5H21.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.30071 12.4C4.91018 12.7905 4.91018 13.4236 5.30071 13.8142C5.69123 14.2047 6.32439 14.2047 6.71492 13.8142L5.30071 12.4ZM13.0789 6.03599L14.0787 6.05567C14.0839 5.78863 13.9821 5.53058 13.796 5.33904C13.6098 5.1475 13.3548 5.03839 13.0877 5.03603L13.0789 6.03599ZM9.00968 5.00004C8.45741 4.99516 8.00576 5.43891 8.00089 5.99117C7.99601 6.54344 8.43976 6.99509 8.99202 6.99996L9.00968 5.00004ZM12.001 9.98032C11.9902 10.5325 12.429 10.9889 12.9812 10.9998C13.5333 11.0107 13.9898 10.5719 14.0007 10.0197L12.001 9.98032ZM18.6429 11.6C19.0334 11.2095 19.0334 10.5764 18.6429 10.1858C18.2524 9.79531 17.6192 9.79531 17.2287 10.1858L18.6429 11.6ZM10.8647 17.964L9.8653 17.9297C9.85605 18.1992 9.95602 18.461 10.1426 18.6557C10.3291 18.8505 10.5864 18.9616 10.856 18.964L10.8647 17.964ZM14.9922 19C15.5444 19.0048 15.996 18.561 16.0008 18.0087C16.0056 17.4564 15.5618 17.0048 15.0096 17L14.9922 19ZM12.0003 14.0343C12.0192 13.4824 11.5871 13.0195 11.0352 13.0006C10.4832 12.9816 10.0204 13.4137 10.0014 13.9657L12.0003 14.0343ZM6.71492 13.8142L13.786 6.7431L12.3718 5.32889L5.30071 12.4L6.71492 13.8142ZM8.99202 6.99996L13.0701 7.03595L13.0877 5.03603L9.00968 5.00004L8.99202 6.99996ZM12.0791 6.01631L12.001 9.98032L14.0007 10.0197L14.0787 6.05567L12.0791 6.01631ZM17.2287 10.1858L10.1576 17.2569L11.5718 18.6711L18.6429 11.6L17.2287 10.1858ZM15.0096 17L10.8734 16.964L10.856 18.964L14.9922 19L15.0096 17ZM11.8641 17.9983L12.0003 14.0343L10.0014 13.9657L9.8653 17.9297L11.8641 17.9983Z" fill="#0056D2"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-exchange-icon.svg b/assets/svg/oceanBreeze/tx-exchange-icon.svg new file mode 100644 index 000000000..fcd3ef9dc --- /dev/null +++ b/assets/svg/oceanBreeze/tx-exchange-icon.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="12" cy="12" r="12" fill="#0056D2"/> +<path d="M5.30071 12.4C4.91018 12.7905 4.91018 13.4236 5.30071 13.8142C5.69123 14.2047 6.32439 14.2047 6.71492 13.8142L5.30071 12.4ZM13.0789 6.03599L14.0787 6.05567C14.0839 5.78863 13.9821 5.53058 13.796 5.33904C13.6098 5.1475 13.3548 5.03839 13.0877 5.03603L13.0789 6.03599ZM9.00968 5.00004C8.45741 4.99516 8.00576 5.43891 8.00089 5.99117C7.99601 6.54344 8.43976 6.99509 8.99202 6.99996L9.00968 5.00004ZM12.001 9.98032C11.9902 10.5325 12.429 10.9889 12.9812 10.9998C13.5333 11.0107 13.9898 10.5719 14.0007 10.0197L12.001 9.98032ZM18.6429 11.6C19.0334 11.2095 19.0334 10.5764 18.6429 10.1858C18.2524 9.79531 17.6192 9.79531 17.2287 10.1858L18.6429 11.6ZM10.8647 17.964L9.8653 17.9297C9.85604 18.1992 9.95602 18.461 10.1426 18.6557C10.3291 18.8505 10.5864 18.9616 10.856 18.964L10.8647 17.964ZM14.9922 19C15.5444 19.0048 15.996 18.561 16.0008 18.0087C16.0056 17.4564 15.5618 17.0048 15.0096 17L14.9922 19ZM12.0003 14.0343C12.0192 13.4824 11.5871 13.0195 11.0352 13.0006C10.4832 12.9816 10.0204 13.4137 10.0014 13.9657L12.0003 14.0343ZM6.71492 13.8142L13.786 6.7431L12.3718 5.32889L5.30071 12.4L6.71492 13.8142ZM8.99202 6.99996L13.0701 7.03595L13.0877 5.03603L9.00968 5.00004L8.99202 6.99996ZM12.0791 6.01631L12.001 9.98032L14.0007 10.0197L14.0787 6.05567L12.0791 6.01631ZM17.2287 10.1858L10.1576 17.2569L11.5718 18.6711L18.6429 11.6L17.2287 10.1858ZM15.0096 17L10.8734 16.964L10.856 18.964L14.9922 19L15.0096 17ZM11.8641 17.9983L12.0003 14.0343L10.0014 13.9657L9.8653 17.9297L11.8641 17.9983Z" fill="#0056D2"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-receive-failed.svg b/assets/svg/oceanBreeze/tx-icon-receive-failed.svg new file mode 100644 index 000000000..189bd15c9 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-receive-failed.svg @@ -0,0 +1,7 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#00A578"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#C00205"/> +<path d="M16 8L8 16M8 16H14M8 16V10" stroke="#00A578" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.4395 19.4395L20.5001 20.5001L21.5608 21.5608" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.5 21.5605L20.5607 20.4999L21.6213 19.4392" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-receive-pending.svg b/assets/svg/oceanBreeze/tx-icon-receive-pending.svg new file mode 100644 index 000000000..64ea8da3d --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-receive-pending.svg @@ -0,0 +1,5 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#00A578"/> +<path d="M16 8L8 16M8 16H14M8 16V10" stroke="#00A578" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5 24C22.433 24 24 22.433 24 20.5C24 18.567 22.433 17 20.5 17C18.567 17 17 18.567 17 20.5C17 22.433 18.567 24 20.5 24ZM21 19C21 18.7239 20.7761 18.5 20.5 18.5C20.2239 18.5 20 18.7239 20 19V20.5C20 20.7761 20.2239 21 20.5 21H21.5C21.7761 21 22 20.7761 22 20.5C22 20.2239 21.7761 20 21.5 20H21V19Z" fill="#F4C517"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-receive.svg b/assets/svg/oceanBreeze/tx-icon-receive.svg new file mode 100644 index 000000000..1076d8d57 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-receive.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="12" cy="12" r="12" fill="#00A578"/> +<path d="M16 8L8 16M8 16H14M8 16V10" stroke="#00A578" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-send-failed.svg b/assets/svg/oceanBreeze/tx-icon-send-failed.svg new file mode 100644 index 000000000..9751b61e8 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-send-failed.svg @@ -0,0 +1,7 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#FE805C"/> +<path d="M8 16L16 8M16 8L10 8M16 8L16 14" stroke="#FE805C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#C00205"/> +<path d="M19.4395 19.4395L20.5001 20.5001L21.5608 21.5608" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.5 21.5605L20.5607 20.4999L21.6213 19.4392" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-send-pending.svg b/assets/svg/oceanBreeze/tx-icon-send-pending.svg new file mode 100644 index 000000000..e4ec777e3 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-send-pending.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#FE805C"/> +<path d="M8 16L16 8M16 8L10 8M16 8L16 14" stroke="#FE805C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#F4C517"/> +<path d="M20.5 19V20.5H21.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-send.svg b/assets/svg/oceanBreeze/tx-icon-send.svg new file mode 100644 index 000000000..ee32aa6b4 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-send.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="12" cy="12" r="12" fill="#FE805C"/> +<path d="M8 16L16 8M16 8L10 8M16 8L16 14" stroke="#FE805C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/plus-circle.svg b/assets/svg/plus-circle.svg new file mode 100644 index 000000000..e673b9b0e --- /dev/null +++ b/assets/svg/plus-circle.svg @@ -0,0 +1,12 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_5370_82626)"> +<path d="M9.99935 18.3337C14.6017 18.3337 18.3327 14.6027 18.3327 10.0003C18.3327 5.39795 14.6017 1.66699 9.99935 1.66699C5.39698 1.66699 1.66602 5.39795 1.66602 10.0003C1.66602 14.6027 5.39698 18.3337 9.99935 18.3337Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M10 6.66699V13.3337" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.66602 10H13.3327" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</g> +<defs> +<clipPath id="clip0_5370_82626"> +<rect width="20" height="20" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/assets/svg/rotate-circle.svg b/assets/svg/rotate-circle.svg new file mode 100644 index 000000000..1940da5f5 --- /dev/null +++ b/assets/svg/rotate-circle.svg @@ -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> diff --git a/assets/svg/sun-circle.svg b/assets/svg/sun-circle.svg new file mode 100644 index 000000000..eba7d031d --- /dev/null +++ b/assets/svg/sun-circle.svg @@ -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> diff --git a/assets/svg/wallet-desktop.svg b/assets/svg/wallet-desktop.svg new file mode 100644 index 000000000..0b0acdae3 --- /dev/null +++ b/assets/svg/wallet-desktop.svg @@ -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> diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 51f74f05d..de29931da 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 51f74f05d465a92e0118cf7c2bcfb049df21af42 +Subproject commit de29931dacc9aefaf42a9ca139a8754a42adc40d diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4b7b1058d..2e5d4bac0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = 79; + 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.9; + 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 = 79; + 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.9; + 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 = 79; + 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.9; + MARKETING_VERSION = 1.5.11; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/lib/hive/db.dart b/lib/hive/db.dart index e1232696b..1a52d64df 100644 --- a/lib/hive/db.dart +++ b/lib/hive/db.dart @@ -9,7 +9,6 @@ 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'; @@ -33,6 +32,7 @@ class DB { static const String boxNamePriceCache = "priceAPIPrice24hCache"; static const String boxNameDBInfo = "dbInfo"; static const String boxNameTheme = "theme"; + static const String boxNameDesktopData = "desktopData"; String boxNameTxCache({required Coin coin}) => "${coin.name}_txCache"; String boxNameSetCache({required Coin coin}) => @@ -42,22 +42,23 @@ class DB { static bool _initialized = false; - late final Box<dynamic> _boxAddressBook; - late final Box<String> _boxDebugInfo; - late final Box<NodeModel> _boxNodeModels; - late final Box<NodeModel> _boxPrimaryNodes; - late final Box<dynamic> _boxAllWalletsData; - late final Box<NotificationModel> _boxNotifications; - late final Box<NotificationModel> _boxWatchedTransactions; - late final Box<NotificationModel> _boxWatchedTrades; - late final Box<ExchangeTransaction> _boxTrades; - late final Box<Trade> _boxTradesV2; - late final Box<String> _boxTradeNotes; - late final Box<String> _boxFavoriteWallets; - late final Box<xmr.WalletInfo> _walletInfoSource; - late final Box<dynamic> _boxPrefs; - late final Box<TradeWalletLookup> _boxTradeLookup; - late final Box<dynamic> _boxDBInfo; + Box<dynamic>? _boxAddressBook; + Box<String>? _boxDebugInfo; + Box<NodeModel>? _boxNodeModels; + Box<NodeModel>? _boxPrimaryNodes; + Box<dynamic>? _boxAllWalletsData; + Box<NotificationModel>? _boxNotifications; + Box<NotificationModel>? _boxWatchedTransactions; + Box<NotificationModel>? _boxWatchedTrades; + Box<ExchangeTransaction>? _boxTrades; + Box<Trade>? _boxTradesV2; + Box<String>? _boxTradeNotes; + Box<String>? _boxFavoriteWallets; + Box<xmr.WalletInfo>? _walletInfoSource; + Box<dynamic>? _boxPrefs; + Box<TradeWalletLookup>? _boxTradeLookup; + Box<dynamic>? _boxDBInfo; + Box<String>? _boxDesktopData; final Map<String, Box<dynamic>> _walletBoxes = {}; @@ -66,7 +67,7 @@ class DB { final Map<Coin, Box<dynamic>> _usedSerialsCacheBoxes = {}; // exposed for monero - Box<xmr.WalletInfo> get moneroWalletInfoBox => _walletInfoSource; + Box<xmr.WalletInfo> get moneroWalletInfoBox => _walletInfoSource!; // mutex for stack backup final mutex = Mutex(); @@ -122,6 +123,12 @@ class DB { _boxAllWalletsData = await Hive.openBox<dynamic>(boxNameAllWalletsData); } + if (Hive.isBoxOpen(boxNameDesktopData)) { + _boxDesktopData = Hive.box<String>(boxNameDesktopData); + } else { + _boxDesktopData = await Hive.openBox<String>(boxNameDesktopData); + } + _boxNotifications = await Hive.openBox<NotificationModel>(boxNameNotifications); _boxWatchedTransactions = @@ -143,22 +150,11 @@ 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"); - } } } Future<void> _loadWalletBoxes() async { - final names = _boxAllWalletsData.get("names") as Map? ?? {}; + final names = _boxAllWalletsData!.get("names") as Map? ?? {}; names.removeWhere((name, dyn) { final jsonObject = Map<String, dynamic>.from(dyn as Map); try { diff --git a/lib/main.dart b/lib/main.dart index eb7cd8e64..8136965db 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:cw_core/node.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -10,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:isar/isar.dart'; @@ -30,7 +32,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'; @@ -46,15 +49,16 @@ import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/services/trade_service.dart'; -import 'package:stackwallet/services/wallets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/db_version_migration.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:window_size/window_size.dart'; @@ -68,26 +72,36 @@ final openedFromSWBFileStringStateProvider = void main() async { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); GoogleFonts.config.allowRuntimeFetching = false; + if (Platform.isIOS) { + Util.libraryPath = await getLibraryDirectory(); + } + + Screen? screen; + if (Platform.isLinux || Util.isDesktop) { + screen = await getCurrentScreen(); + Util.screenWidth = screen?.frame.width; + } if (Util.isDesktop) { setWindowTitle('Stack Wallet'); - setWindowMinSize(const Size(1200, 900)); + setWindowMinSize(const Size(1220, 100)); setWindowMaxSize(Size.infinite); + + final screenHeight = screen?.frame.height; + if (screenHeight != null) { + // starting to height be 3/4 screen height or 900, whichever is smaller + final height = min<double>(screenHeight * 0.75, 900); + setWindowFrame( + Rect.fromLTWH(0, 0, 1220, height), + ); + } } - Directory appDirectory = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDirectory = (await getLibraryDirectory()); - } - if (Platform.isLinux || Logging.isArmLinux) { - appDirectory = Directory("${appDirectory.path}/.stackwallet"); - await appDirectory.create(); - } // FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); if (!(Logging.isArmLinux || Logging.isTestEnv)) { final isar = await Isar.open( [LogSchema], - directory: appDirectory.path, + directory: (await StackFileSystem.applicationIsarDirectory()).path, inspector: false, ); await Logging.instance.init(isar); @@ -136,18 +150,29 @@ void main() async { Hive.registerAdapter(WalletTypeAdapter()); Hive.registerAdapter(UnspentCoinsInfoAdapter()); - await Hive.initFlutter(appDirectory.path); + await Hive.initFlutter( + (await StackFileSystem.applicationHiveDirectory()).path); await Hive.openBox<dynamic>(DB.boxNameDBInfo); - int dbVersion = DB.instance.get<dynamic>( - boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ?? - 0; - if (dbVersion < Constants.currentHiveDbVersion) { - try { - await DbVersionMigrator().migrate(dbVersion); - } catch (e, s) { - Logging.instance.log("Cannot migrate database\n$e $s", - level: LogLevel.Error, printFullLength: true); + + // todo: db migrate stuff for desktop needs to be handled eventually + if (!Util.isDesktop) { + int dbVersion = DB.instance.get<dynamic>( + boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ?? + 0; + if (dbVersion < Constants.currentHiveDbVersion) { + try { + await DbVersionMigrator().migrate( + dbVersion, + secureStore: const SecureStorageWrapper( + store: FlutterSecureStorage(), + isDesktop: false, + ), + ); + } catch (e, s) { + Logging.instance.log("Cannot migrate database\n$e $s", + level: LogLevel.Error, printFullLength: true); + } } } @@ -195,8 +220,8 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> static const platform = MethodChannel("STACK_WALLET_RESTORE"); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); - late final Wallets _wallets; - late final Prefs _prefs; + // late final Wallets _wallets; + // late final Prefs _prefs; late final NotificationsService _notificationsService; late final NodeService _nodeService; late final TradesService _tradesService; @@ -204,58 +229,86 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> late final Completer<void> loadingCompleter; bool didLoad = false; + bool didLoadShared = false; + bool _desktopHasPassword = false; - Future<void> load() async { - if (didLoad) { + Future<void> loadShared() async { + if (didLoadShared) { return; } - didLoad = true; + didLoadShared = true; await DB.instance.init(); - await _prefs.init(); + await ref.read(prefsChangeNotifierProvider).init(); - _notificationsService = ref.read(notificationsProvider); - _nodeService = ref.read(nodeServiceChangeNotifierProvider); - _tradesService = ref.read(tradesServiceProvider); + final familiarity = ref.read(prefsChangeNotifierProvider).familiarity + 1; + ref.read(prefsChangeNotifierProvider).familiarity = familiarity; - NotificationApi.prefs = _prefs; - NotificationApi.notificationsService = _notificationsService; + Constants.exchangeForExperiencedUsers(familiarity); - 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 (Util.isDesktop) { + _desktopHasPassword = + await ref.read(storageCryptoHandlerProvider).hasPassword(); } + } - 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; + Future<void> load() async { + try { + if (didLoad) { + return; } + didLoad = true; + + if (!Util.isDesktop) { + await loadShared(); + } + + _notificationsService = ref.read(notificationsProvider); + _nodeService = ref.read(nodeServiceChangeNotifierProvider); + _tradesService = ref.read(tradesServiceProvider); + + NotificationApi.prefs = ref.read(prefsChangeNotifierProvider); + NotificationApi.notificationsService = _notificationsService; + + unawaited(ref.read(baseCurrenciesProvider).update()); + + await _nodeService.updateDefaults(); + await _notificationsService.init( + nodeService: _nodeService, + tradesService: _tradesService, + prefs: ref.read(prefsChangeNotifierProvider), + ); + ref.read(priceAnd24hChangeNotifierProvider).start(true); + await ref + .read(walletsChangeNotifierProvider) + .load(ref.read(prefsChangeNotifierProvider)); + 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 && + ref.read(prefsChangeNotifierProvider).externalCalls && + await ref.read(prefsChangeNotifierProvider).isExternalCallsSet()) { + unawaited(ExchangeDataLoadingService().loadAll(ref)); + } + + if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) { + switch (ref.read(prefsChangeNotifierProvider).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); } } @@ -265,14 +318,17 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> final colorScheme = DB.instance .get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme") as String?; - ThemeType themeType; + StackColorTheme colorTheme; switch (colorScheme) { case "dark": - themeType = ThemeType.dark; + colorTheme = DarkColors(); + break; + case "oceanBreeze": + colorTheme = OceanBreezeColors(); break; case "light": default: - themeType = ThemeType.light; + colorTheme = LightColors(); } loadingCompleter = Completer(); WidgetsBinding.instance.addObserver(this); @@ -281,13 +337,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> .read(localeServiceChangeNotifierProvider.notifier) .loadLocale(notify: false); - _prefs = ref.read(prefsChangeNotifierProvider); - _wallets = ref.read(walletsChangeNotifierProvider); - WidgetsBinding.instance.addPostFrameCallback((_) async { ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - themeType == ThemeType.dark ? DarkColors() : LightColors()); + StackColors.fromStackColorTheme(colorTheme); if (Platform.isAndroid) { // fetch open file if it exists @@ -388,7 +440,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> } Future<void> goToRestoreSWB(String encrypted) async { - if (!_prefs.hasPin) { + if (!ref.read(prefsChangeNotifierProvider).hasPin) { await Navigator.of(navigatorKey.currentContext!) .pushNamed(CreatePinView.routeName, arguments: true) .then((value) { @@ -534,49 +586,70 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), ), ), - home: FutureBuilder( - future: load(), - builder: (BuildContext context, AsyncSnapshot<void> snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - // FlutterNativeSplash.remove(); - if (_wallets.hasWallets || _prefs.hasPin) { - // return HomeView(); + home: Util.isDesktop + ? FutureBuilder( + future: loadShared(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (_desktopHasPassword) { + String? startupWalletId; + if (ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup) { + startupWalletId = + ref.read(prefsChangeNotifierProvider).startupWalletId; + } - String? startupWalletId; - if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { - startupWalletId = - ref.read(prefsChangeNotifierProvider).startupWalletId; - } + return DesktopLoginView( + startupWalletId: startupWalletId, + load: load, + ); + } else { + return const IntroView(); + } + } else { + return const LoadingView(); + } + }, + ) + : FutureBuilder( + future: load(), + builder: (BuildContext context, AsyncSnapshot<void> snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + // FlutterNativeSplash.remove(); + if (ref.read(walletsChangeNotifierProvider).hasWallets || + ref.read(prefsChangeNotifierProvider).hasPin) { + // return HomeView(); - // 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(); - } + String? startupWalletId; + if (ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup) { + startupWalletId = + ref.read(prefsChangeNotifierProvider).startupWalletId; + } - return LockscreenView( - isInitialAppLogin: true, - routeOnSuccess: HomeView.routeName, - routeOnSuccessArguments: startupWalletId, - biometricsAuthenticationTitle: "Unlock Stack", - biometricsLocalizedReason: - "Unlock your stack wallet using biometrics", - biometricsCancelButtonString: "Cancel", - ); - } else { - return const IntroView(); - } - } else { - // CURRENTLY DISABLED as cannot be animated - // technically not needed as FlutterNativeSplash will overlay - // anything returned here until the future completes but - // FutureBuilder requires you to return something - return const LoadingView(); - } - }, - ), + return LockscreenView( + isInitialAppLogin: true, + routeOnSuccess: HomeView.routeName, + routeOnSuccessArguments: startupWalletId, + biometricsAuthenticationTitle: "Unlock Stack", + biometricsLocalizedReason: + "Unlock your stack wallet using biometrics", + biometricsCancelButtonString: "Cancel", + ); + } else { + return const IntroView(); + } + } else { + // CURRENTLY DISABLED as cannot be animated + // technically not needed as FlutterNativeSplash will overlay + // anything returned here until the future completes but + // FutureBuilder requires you to return something + return const LoadingView(); + } + }, + ), ); } } diff --git a/lib/models/isar/models/encrypted_string_value.dart b/lib/models/isar/models/encrypted_string_value.dart new file mode 100644 index 000000000..79e9fcaae --- /dev/null +++ b/lib/models/isar/models/encrypted_string_value.dart @@ -0,0 +1,18 @@ +import 'package:isar/isar.dart'; + +part 'encrypted_string_value.g.dart'; + +@Collection() +class EncryptedStringValue { + Id id = Isar.autoIncrement; + + @Index(unique: true, replace: true) + late String key; + + late String value; + + @override + String toString() { + return "EncryptedStringValue {\n key=$key\n value=$value\n}"; + } +} diff --git a/lib/models/isar/models/encrypted_string_value.g.dart b/lib/models/isar/models/encrypted_string_value.g.dart new file mode 100644 index 000000000..2315c5d85 --- /dev/null +++ b/lib/models/isar/models/encrypted_string_value.g.dart @@ -0,0 +1,748 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'encrypted_string_value.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, join_return_with_assignment, avoid_js_rounded_ints, prefer_final_locals + +extension GetEncryptedStringValueCollection on Isar { + IsarCollection<EncryptedStringValue> get encryptedStringValues => + this.collection(); +} + +const EncryptedStringValueSchema = CollectionSchema( + name: r'EncryptedStringValue', + id: 4826543019451092626, + properties: { + r'key': PropertySchema( + id: 0, + name: r'key', + type: IsarType.string, + ), + r'value': PropertySchema( + id: 1, + name: r'value', + type: IsarType.string, + ) + }, + estimateSize: _encryptedStringValueEstimateSize, + serializeNative: _encryptedStringValueSerializeNative, + deserializeNative: _encryptedStringValueDeserializeNative, + deserializePropNative: _encryptedStringValueDeserializePropNative, + serializeWeb: _encryptedStringValueSerializeWeb, + deserializeWeb: _encryptedStringValueDeserializeWeb, + deserializePropWeb: _encryptedStringValueDeserializePropWeb, + idName: r'id', + indexes: { + r'key': IndexSchema( + id: -4906094122524121629, + name: r'key', + unique: true, + replace: true, + properties: [ + IndexPropertySchema( + name: r'key', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _encryptedStringValueGetId, + getLinks: _encryptedStringValueGetLinks, + attach: _encryptedStringValueAttach, + version: 5, +); + +int _encryptedStringValueEstimateSize( + EncryptedStringValue object, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.key.length * 3; + bytesCount += 3 + object.value.length * 3; + return bytesCount; +} + +int _encryptedStringValueSerializeNative( + EncryptedStringValue object, + IsarBinaryWriter writer, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + writer.writeString(offsets[0], object.key); + writer.writeString(offsets[1], object.value); + return writer.usedBytes; +} + +EncryptedStringValue _encryptedStringValueDeserializeNative( + int id, + IsarBinaryReader reader, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + final object = EncryptedStringValue(); + object.id = id; + object.key = reader.readString(offsets[0]); + object.value = reader.readString(offsets[1]); + return object; +} + +P _encryptedStringValueDeserializePropNative<P>( + Id id, + IsarBinaryReader reader, + int propertyId, + int offset, + Map<Type, List<int>> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readString(offset)) as P; + case 1: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Object _encryptedStringValueSerializeWeb( + IsarCollection<EncryptedStringValue> collection, + EncryptedStringValue object) { + /*final jsObj = IsarNative.newJsObject();*/ throw UnimplementedError(); +} + +EncryptedStringValue _encryptedStringValueDeserializeWeb( + IsarCollection<EncryptedStringValue> collection, Object jsObj) { + /*final object = EncryptedStringValue();object.id = IsarNative.jsObjectGet(jsObj, r'id') ?? (double.negativeInfinity as int);object.key = IsarNative.jsObjectGet(jsObj, r'key') ?? '';object.value = IsarNative.jsObjectGet(jsObj, r'value') ?? '';*/ + //return object; + throw UnimplementedError(); +} + +P _encryptedStringValueDeserializePropWeb<P>( + Object jsObj, String propertyName) { + switch (propertyName) { + default: + throw IsarError('Illegal propertyName'); + } +} + +int? _encryptedStringValueGetId(EncryptedStringValue object) { + if (object.id == Isar.autoIncrement) { + return null; + } else { + return object.id; + } +} + +List<IsarLinkBase<dynamic>> _encryptedStringValueGetLinks( + EncryptedStringValue object) { + return []; +} + +void _encryptedStringValueAttach( + IsarCollection<dynamic> col, Id id, EncryptedStringValue object) { + object.id = id; +} + +extension EncryptedStringValueByIndex on IsarCollection<EncryptedStringValue> { + Future<EncryptedStringValue?> getByKey(String key) { + return getByIndex(r'key', [key]); + } + + EncryptedStringValue? getByKeySync(String key) { + return getByIndexSync(r'key', [key]); + } + + Future<bool> deleteByKey(String key) { + return deleteByIndex(r'key', [key]); + } + + bool deleteByKeySync(String key) { + return deleteByIndexSync(r'key', [key]); + } + + Future<List<EncryptedStringValue?>> getAllByKey(List<String> keyValues) { + final values = keyValues.map((e) => [e]).toList(); + return getAllByIndex(r'key', values); + } + + List<EncryptedStringValue?> getAllByKeySync(List<String> keyValues) { + final values = keyValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'key', values); + } + + Future<int> deleteAllByKey(List<String> keyValues) { + final values = keyValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'key', values); + } + + int deleteAllByKeySync(List<String> keyValues) { + final values = keyValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'key', values); + } + + Future<int> putByKey(EncryptedStringValue object) { + return putByIndex(r'key', object); + } + + int putByKeySync(EncryptedStringValue object, {bool saveLinks = true}) { + return putByIndexSync(r'key', object, saveLinks: saveLinks); + } + + Future<List<int>> putAllByKey(List<EncryptedStringValue> objects) { + return putAllByIndex(r'key', objects); + } + + List<int> putAllByKeySync(List<EncryptedStringValue> objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'key', objects, saveLinks: saveLinks); + } +} + +extension EncryptedStringValueQueryWhereSort + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QWhere> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhere> + anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension EncryptedStringValueQueryWhere + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QWhereClause> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idEqualTo(int id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idNotEqualTo(int id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idGreaterThan(int id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idLessThan(int id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idBetween( + int lowerId, + int upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + keyEqualTo(String key) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'key', + value: [key], + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + keyNotEqualTo(String key) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'key', + lower: [], + upper: [key], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'key', + lower: [key], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'key', + lower: [key], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'key', + lower: [], + upper: [key], + includeUpper: false, + )); + } + }); + } +} + +extension EncryptedStringValueQueryFilter on QueryBuilder<EncryptedStringValue, + EncryptedStringValue, QFilterCondition> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> idEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> idGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> idLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> idBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'key', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> + keyContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> + keyMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'key', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'value', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> + valueContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> + valueMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'value', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } +} + +extension EncryptedStringValueQueryObject on QueryBuilder<EncryptedStringValue, + EncryptedStringValue, QFilterCondition> {} + +extension EncryptedStringValueQueryLinks on QueryBuilder<EncryptedStringValue, + EncryptedStringValue, QFilterCondition> {} + +extension EncryptedStringValueQuerySortBy + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QSortBy> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + sortByKey() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + sortByKeyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.desc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + sortByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + sortByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } +} + +extension EncryptedStringValueQuerySortThenBy + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QSortThenBy> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByKey() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByKeyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.desc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } +} + +extension EncryptedStringValueQueryWhereDistinct + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QDistinct> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QDistinct> + distinctByKey({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'key', caseSensitive: caseSensitive); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QDistinct> + distinctByValue({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'value', caseSensitive: caseSensitive); + }); + } +} + +extension EncryptedStringValueQueryProperty on QueryBuilder< + EncryptedStringValue, EncryptedStringValue, QQueryProperty> { + QueryBuilder<EncryptedStringValue, int, QQueryOperations> idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder<EncryptedStringValue, String, QQueryOperations> keyProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'key'); + }); + } + + QueryBuilder<EncryptedStringValue, String, QQueryOperations> valueProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'value'); + }); + } +} diff --git a/lib/models/node_model.dart b/lib/models/node_model.dart index 342f8b2ef..af5f8cbc1 100644 --- a/lib/models/node_model.dart +++ b/lib/models/node_model.dart @@ -1,4 +1,5 @@ import 'package:hive/hive.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; part 'type_adaptors/node_model.g.dart'; @@ -65,8 +66,7 @@ class NodeModel { } /// convenience getter to retrieve login password - Future<String?> getPassword( - FlutterSecureStorageInterface secureStorage) async { + Future<String?> getPassword(SecureStorageInterface secureStorage) async { return await secureStorage.read(key: "${id}_nodePW"); } @@ -85,7 +85,7 @@ class NodeModel { return map; } - bool get isDefault => id.startsWith("default_"); + bool get isDefault => id.startsWith(DefaultNodes.defaultNodeIdPrefix); @override String toString() { diff --git a/lib/models/paymint/transactions_model.dart b/lib/models/paymint/transactions_model.dart index 08b6eb7e2..382459922 100644 --- a/lib/models/paymint/transactions_model.dart +++ b/lib/models/paymint/transactions_model.dart @@ -2,6 +2,7 @@ import 'package:dart_numerics/dart_numerics.dart'; import 'package:decimal/decimal.dart'; import 'package:hive/hive.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; part '../type_adaptors/transactions_model.g.dart'; @@ -220,14 +221,16 @@ class Transaction { (DateTime.now().millisecondsSinceEpoch ~/ 1000), txType: json['txType'] as String, amount: (Decimal.parse(json["amount"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(Coin + .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt(), aliens: [], worthNow: json['worthNow'] as String, worthAtBlockTimestamp: json['worthAtBlockTimestamp'] as String? ?? "0", fees: (Decimal.parse(json["fees"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(Coin + .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt(), inputSize: json['inputSize'] as int? ?? 0, @@ -386,7 +389,8 @@ class Output { scriptpubkeyType: json['scriptPubKey']['type'] as String?, scriptpubkeyAddress: address, value: (Decimal.parse(json["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(Coin + .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt(), ); diff --git a/lib/models/transaction_filter.dart b/lib/models/transaction_filter.dart index 119ac7015..7ef5f0bff 100644 --- a/lib/models/transaction_filter.dart +++ b/lib/models/transaction_filter.dart @@ -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 }"; } } diff --git a/lib/notifications/notification_card.dart b/lib/notifications/notification_card.dart index 67be236f0..2a181499c 100644 --- a/lib/notifications/notification_card.dart +++ b/lib/notifications/notification_card.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/models/notification_model.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/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -20,22 +22,33 @@ class NotificationCard extends StatelessWidget { return Format.extractDateFrom(date.millisecondsSinceEpoch ~/ 1000); } + static const double mobileIconSize = 24; + static const double desktopIconSize = 30; + @override Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + return Stack( children: [ RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ) + : const EdgeInsets.all(12), child: Row( children: [ notification.changeNowId == null ? SvgPicture.asset( notification.iconAssetName, - width: 24, - height: 24, + width: isDesktop ? desktopIconSize : mobileIconSize, + height: isDesktop ? desktopIconSize : mobileIconSize, ) : Container( - width: 24, - height: 24, + width: isDesktop ? desktopIconSize : mobileIconSize, + height: isDesktop ? desktopIconSize : mobileIconSize, decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(24), @@ -45,8 +58,8 @@ class NotificationCard extends StatelessWidget { color: Theme.of(context) .extension<StackColors>()! .accentColorDark, - width: 24, - height: 24, + width: isDesktop ? desktopIconSize : mobileIconSize, + height: isDesktop ? desktopIconSize : mobileIconSize, ), ), const SizedBox( @@ -56,9 +69,35 @@ class NotificationCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - notification.title, - style: STextStyles.titleBold12(context), + ConditionalParent( + condition: isDesktop && !notification.read, + builder: (child) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + child, + Text( + "New", + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ), + ) + ], + ), + child: Text( + notification.title, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.titleBold12(context), + ), ), const SizedBox( height: 2, @@ -68,11 +107,25 @@ class NotificationCard extends StatelessWidget { children: [ Text( notification.description, - style: STextStyles.label(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ) + : STextStyles.label(context), ), Text( extractPrettyDateString(notification.date), - style: STextStyles.label(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ) + : STextStyles.label(context), ), ], ), diff --git a/lib/notifications/show_flush_bar.dart b/lib/notifications/show_flush_bar.dart index 5320c8a9d..47cea682a 100644 --- a/lib/notifications/show_flush_bar.dart +++ b/lib/notifications/show_flush_bar.dart @@ -6,6 +6,8 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +export 'package:stackwallet/utilities/enums/flush_bar_type.dart'; + Future<dynamic> showFloatingFlushBar({ required FlushBarType type, required String message, diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index ae72e1846..29bae26c1 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -11,6 +11,7 @@ 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/background.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'; @@ -90,6 +91,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) { @@ -180,40 +183,43 @@ class _AddWalletViewState extends State<AddWalletView> { ), ); } else { - return Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), - ), - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const AddWalletText( - isDesktop: false, - ), - const SizedBox( - height: 16, - ), - Expanded( - child: MobileCoinList( - coins: coins, + body: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const AddWalletText( isDesktop: false, ), - ), - const SizedBox( - height: 16, - ), - const AddWalletNextButton( - isDesktop: false, - ), - ], + const SizedBox( + height: 16, + ), + Expanded( + child: MobileCoinList( + coins: coins, + ), + ), + const SizedBox( + height: 16, + ), + const AddWalletNextButton( + isDesktop: false, + ), + ], + ), ), ), ), diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart index 1f36f3b65..fd950963c 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart @@ -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) { diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart index fb443b915..38181b9e1 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart @@ -26,11 +26,17 @@ 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); + // Kidgloves for Wownero on desktop + if(isDesktop) { + _coins.remove(Coin.wownero); + } + return _coins; } diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart index b3be99e25..1dfdc2a53 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/exit_to_my import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.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'; @@ -77,49 +78,53 @@ class CreateOrRestoreWalletView extends StatelessWidget { ), ); } else { - return Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), - ), - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(31), - child: CoinImage( + body: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(31), + child: CoinImage( + coin: coin, + isDesktop: isDesktop, + ), + ), + const Spacer( + flex: 2, + ), + CreateRestoreWalletTitle( coin: coin, isDesktop: isDesktop, ), - ), - const Spacer( - flex: 2, - ), - CreateRestoreWalletTitle( - coin: coin, - isDesktop: isDesktop, - ), - const SizedBox( - height: 8, - ), - CreateRestoreWalletSubTitle( - isDesktop: isDesktop, - ), - const Spacer( - flex: 5, - ), - CreateWalletButtonGroup( - coin: coin, - isDesktop: isDesktop, - ), - ], + const SizedBox( + height: 8, + ), + CreateRestoreWalletSubTitle( + isDesktop: isDesktop, + ), + const Spacer( + flex: 5, + ), + CreateWalletButtonGroup( + coin: coin, + isDesktop: isDesktop, + ), + ], + ), ), ), ), diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index f7dbe3d33..e435e285d 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/utilities/name_generator.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/background.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'; @@ -108,40 +109,44 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> { ), ); } else { - return Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - if (textFieldFocusNode.hasFocus) { - textFieldFocusNode.unfocus(); - Future<void>.delayed(const Duration(milliseconds: 100)) - .then((value) => Navigator.of(context).pop()); - } else { - if (mounted) { - Navigator.of(context).pop(); + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + if (textFieldFocusNode.hasFocus) { + textFieldFocusNode.unfocus(); + Future<void>.delayed(const Duration(milliseconds: 100)) + .then((value) => Navigator.of(context).pop()); + } else { + if (mounted) { + Navigator.of(context).pop(); + } } - } - }, - ), - ), - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (ctx, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: _content(), - ), - ), - ); }, ), ), + body: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (ctx, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: _content(), + ), + ), + ); + }, + ), + ), + ), ), ); } @@ -194,6 +199,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) { diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart index faab6d08c..b3ceb0968 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart @@ -16,7 +16,6 @@ import 'package:stackwallet/services/coins/manager.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/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'; diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart index 946f54d4a..7d21dfec4 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart @@ -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, ), ), ], diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table_item.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table_item.dart index 8928ff3a6..ec103dfc6 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table_item.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table_item.dart @@ -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), diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 83dc43933..24a2e1a44 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -241,6 +242,7 @@ class _NewWalletRecoveryPhraseWarningViewState coin, walletId, walletName, + ref.read(secureStoreProvider), node, txTracker, ref.read(prefsChangeNotifierProvider), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 76e74fa14..a66af63fc 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -23,6 +23,8 @@ import 'package:stackwallet/utilities/util.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/rounded_date_picker/flutter_rounded_date_picker_widget.dart' + as datePicker; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; @@ -152,7 +154,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { await Future<void>.delayed(const Duration(milliseconds: 125)); } - final date = await showRoundedDatePicker( + final date = await datePicker.showRoundedDatePicker( context: context, initialDate: DateTime.now(), height: height * 0.5, @@ -252,7 +254,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { SizedBox( height: isDesktop ? 40 : 24, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) Text( "Choose start date", style: isDesktop @@ -264,11 +270,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) SizedBox( height: isDesktop ? 16 : 8, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) // if (!isDesktop) RestoreFromDatePicker( @@ -278,11 +292,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { // if (isDesktop) // // TODO desktop date picker - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) const SizedBox( height: 8, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) RoundedWhiteContainer( child: Center( child: Text( @@ -299,7 +321,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { ), ), ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) SizedBox( height: isDesktop ? 24 : 16, ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart index 49896e107..4f5b76fab 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart @@ -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, diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart index e5637cfc6..803e9b03b 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart @@ -4,6 +4,8 @@ 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, @@ -35,6 +37,8 @@ class _RestoreFromDatePickerState extends State<RestoreFromDatePicker> { 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), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index def0724b5..9839c82f7 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -8,6 +8,7 @@ import 'package:bip39/src/wordlists/english.dart' as bip39wordlist; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_libmonero/monero/monero.dart'; +import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; @@ -18,6 +19,7 @@ import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widge import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -149,12 +151,18 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { super.dispose(); } + // TODO: check for wownero wordlist? bool _isValidMnemonicWord(String word) { // TODO: get the actual language if (widget.coin == Coin.monero) { var moneroWordList = monero.getMoneroWordList("English"); return moneroWordList.contains(word); } + if (widget.coin == Coin.wownero) { + var wowneroWordList = wownero.getWowneroWordList("English", + seedWordsLength: widget.seedWordsLength); + return wowneroWordList.contains(word); + } return _wordListHashSet.contains(word); } @@ -180,7 +188,13 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { if (widget.coin == Coin.monero) { height = monero.getHeigthByDate(date: widget.restoreFromDate); + } else if (widget.coin == Coin.wownero) { + height = wownero.getHeightByDate(date: widget.restoreFromDate); } + // todo: wait until this implemented + // else if (widget.coin == Coin.wownero) { + // height = wownero.getHeightByDate(date: widget.restoreFromDate); + // } // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index if (widget.coin == Coin.epicCash) { @@ -260,6 +274,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { widget.coin, walletId, widget.walletName, + ref.read(secureStoreProvider), node, txTracker, ref.read(prefsChangeNotifierProvider), diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index b70ef19ed..0fbf334a7 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -13,20 +13,27 @@ 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/address_book_card.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_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'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; class AddressBookView extends ConsumerStatefulWidget { - const AddressBookView({Key? key, this.coin}) : super(key: key); + const AddressBookView({ + Key? key, + this.coin, + this.filterTerm, + }) : super(key: key); static const String routeName = "/addressBook"; final Coin? coin; + final String? filterTerm; @override ConsumerState<AddressBookView> createState() => _AddressBookViewState(); @@ -37,9 +44,6 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { final _searchFocusNode = FocusNode(); - List<Contact>? _cache; - List<Contact>? _cacheFav; - String _searchTerm = ""; @override @@ -48,8 +52,7 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { ref.refresh(addressBookFilterProvider); if (widget.coin == null) { - List<Coin> coins = - Coin.values.where((e) => !(e == Coin.epicCash)).toList(); + List<Coin> coins = Coin.values.toList(); coins.remove(Coin.firoTestNet); bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; @@ -57,8 +60,9 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); } else { - ref.read(addressBookFilterProvider).addAll( - coins.getRange(0, coins.length - kTestNetCoinCount + 1), false); + ref + .read(addressBookFilterProvider) + .addAll(coins.getRange(0, coins.length - kTestNetCoinCount), false); } } else { ref.read(addressBookFilterProvider).add(widget.coin!, false); @@ -98,289 +102,276 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final addressBookEntriesFuture = ref.watch( - addressBookServiceProvider.select((value) => value.addressBookEntries)); + final contacts = + ref.watch(addressBookServiceProvider.select((value) => value.contacts)); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Address book", - 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("addressBookFilterViewButton"), - 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, - ), + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( onPressed: () { - Navigator.of(context).pushNamed( - AddressBookFilterView.routeName, - ); + Navigator.of(context).pop(); }, ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("addressBookAddNewContactViewButton"), - 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( - AddAddressBookEntryView.routeName, - ); - }, + title: Text( + "Address book", + 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, + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addressBookFilterViewButton"), + 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( + AddressBookFilterView.routeName, + ); + }, + ), + ), ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addressBookAddNewContactViewButton"), + 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( + AddAddressBookEntryView.routeName, + ); + }, + ), + ), + ), + ], + ), + 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: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + MediaQuery.of(context).size.height - 271, + ), + child: child, ), - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + ), + ), + ), + ), + ); + }, + ), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: !isDesktop + ? TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 = ""; + }); + }, + ), + ], ), ), - 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 = ""; - }); - }, - ), - ], - ), - ), - ) - : null, + ) + : null, + ), + ) + : null, + ), + if (!isDesktop) const SizedBox(height: 16), + Text( + "Favorites", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + if (contacts.isNotEmpty) + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e)) + .where((element) => element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + const SizedBox( + height: 16, + ), + Text( + "All contacts", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + if (contacts.isNotEmpty) + Column( + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select((value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e)) + .map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), ), - ), - ), - const SizedBox( - height: 16, - ), - Text( - "Favorites", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _cacheFav = snapshot.data!; - } - if (_cacheFav == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cacheFav!.isNotEmpty) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cacheFav! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider - .select((value) => value - .coins - .contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read( - addressBookServiceProvider) - .matches(_searchTerm, e)) - .where( - (element) => element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key( - "favContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), - ); - } - } - }, - ), - const SizedBox( - height: 16, - ), - Text( - "All contacts", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _cache = snapshot.data!; - } - if (_cache == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cache!.isNotEmpty) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cache! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider - .select((value) => value - .coins - .contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where( - (element) => !element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key( - "contactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), - ); - } - } - }, - ), ], ), ), ), + ], + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), ), ), - ); - }, + ], ), ); } diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 4fa89908c..36191e0b7 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -15,8 +15,15 @@ import 'package:stackwallet/utilities/clipboard_interface.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/background.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/blue_text_button.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/emoji_select_sheet.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -106,393 +113,615 @@ class _AddAddressBookEntryViewState 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: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "New contact", - 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("addAddressBookEntryFavoriteButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.star, - color: _isFavorite - ? Theme.of(context) - .extension<StackColors>()! - .favoriteStarActive - : Theme.of(context) - .extension<StackColors>()! - .favoriteStarInactive, - width: 20, - height: 20, + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + 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(); + } + }, ), - onPressed: () { - setState(() { - _isFavorite = !_isFavorite; - }); - }, - ), - ), - ), - ], - ), - body: LayoutBuilder( - builder: (context, constraint) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: SingleChildScrollView( - controller: scrollController, - padding: const EdgeInsets.only( - // top: 8, - left: 4, - right: 4, - bottom: 16, - ), - child: ConstrainedBox( - constraints: BoxConstraints( - // subtract top and bottom padding set in parent - minHeight: constraint.maxHeight - 16, // - 8, + title: Text( + "New contact", + style: STextStyles.navBarTitle(context), ), - child: IntrinsicHeight( - child: Column( - children: [ - const SizedBox( - height: 4, - ), - GestureDetector( - onTap: () { - if (_selectedEmoji != null) { - setState(() { - _selectedEmoji = null; - }); - return; - } - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addAddressBookEntryFavoriteButtonKey"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension<StackColors>()! + .background, + icon: SvgPicture.asset( + Assets.svg.star, + color: _isFavorite + ? Theme.of(context) + .extension<StackColors>()! + .favoriteStarActive + : Theme.of(context) + .extension<StackColors>()! + .favoriteStarInactive, + width: 20, + height: 20, + ), + onPressed: () { + setState(() { + _isFavorite = !_isFavorite; }); }, - child: SizedBox( - height: 48, - width: 48, - child: Stack( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - _selectedEmoji!.char, - style: - STextStyles.pageTitleH1(context), - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 12, - height: 12, - ) - : SvgPicture.asset( - Assets.svg.thickX, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 8, - height: 8, - ), - ), - ), - ) - ], - ), - ), ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + ), + ), + ], + ), + body: child), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Text( + "New contact", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, ), - child: TextField( - controller: nameController, - focusNode: nameFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter contact name", - nameFocusNode, - context, - ).copyWith( - suffixIcon: ref - .read(contactNameIsNotEmptyStateProvider - .state) - .state - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - nameController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - onChanged: (newValue) { - ref - .read(contactNameIsNotEmptyStateProvider.state) - .state = newValue.isNotEmpty; - }, - ), - ), - if (forms.length <= 1) - const SizedBox( - height: 8, - ), - if (forms.length <= 1) forms[0], - if (forms.length > 1) - for (int i = 0; i < forms.length; i++) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 12, - ), - Row( + ], + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 10, + right: 10, + bottom: 32, + ), + child: child, + ), + ), + ], + ); + }, + child: LayoutBuilder( + builder: (context, constraint) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SingleChildScrollView( + controller: scrollController, + padding: EdgeInsets.only( + // top: 8, + left: 4, + right: 4, + bottom: isDesktop ? 0 : 16, + ), + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: + constraint.maxHeight - (isDesktop ? 0 : 16), // - 8, + ), + child: IntrinsicHeight( + child: Column( + children: [ + if (!isDesktop) const SizedBox(height: 4), + isDesktop + ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Address ${i + 1}", - style: STextStyles.smallMed12(context), + SizedBox( + height: 56, + width: 56, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } + + showDialog<dynamic>( + context: context, + builder: (context) { + return const DesktopDialog( + maxHeight: 700, + maxWidth: 600, + child: Padding( + padding: EdgeInsets.only( + left: 32, + right: 20, + top: 32, + bottom: 32, + ), + child: EmojiSelectSheet(), + ), + ); + }).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + }, + child: Stack( + children: [ + Container( + height: 56, + width: 56, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(100), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.user, + height: 30, + width: 30, + ) + : Text( + _selectedEmoji!.char, + style: STextStyles + .pageTitleH1( + context), + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular( + 14), + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of( + context) + .extension< + StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of( + context) + .extension< + StackColors>()! + .textWhite, + width: 8, + height: 8, + ), + ), + ), + ) + ], + ), + ), + ), ), - BlueTextButton( + const SizedBox(width: 8), + SizedBox( + width: isDesktop ? 450 : null, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: + Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: ref + .read( + contactNameIsNotEmptyStateProvider + .state) + .state + ? Padding( + padding: + const EdgeInsets.only( + right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController + .text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + onChanged: (newValue) { + ref + .read( + contactNameIsNotEmptyStateProvider + .state) + .state = newValue.isNotEmpty; + }, + ), + ), + ), + ], + ) + : Column( + children: [ + GestureDetector( onTap: () { - _removeForm(forms[i].id); + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } + + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); }, - text: "Remove", + child: SizedBox( + height: 48, + width: 48, + child: Stack( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + _selectedEmoji!.char, + style: STextStyles + .pageTitleH1(context), + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(14), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension< + StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of(context) + .extension< + StackColors>()! + .textWhite, + width: 8, + height: 8, + ), + ), + ), + ) + ], + ), + ), + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: + Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: ref + .read( + contactNameIsNotEmptyStateProvider + .state) + .state + ? Padding( + padding: const EdgeInsets.only( + right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController + .text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + onChanged: (newValue) { + ref + .read( + contactNameIsNotEmptyStateProvider + .state) + .state = newValue.isNotEmpty; + }, + ), ), ], ), - const SizedBox( - height: 8, - ), - forms[i], - ], - ), - const SizedBox( - height: 16, - ), - BlueTextButton( - onTap: () { - _addForm(); - scrollController.animateTo( - scrollController.position.maxScrollExtent + 500, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); - }, - text: "+ Add another address", - ), - // GestureDetector( - // - // child: Text( - // "+ Add another address", - // style: STextStyles.largeMedium14(context), - // ), - // ), - const SizedBox( - height: 16, - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - 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(); - } - }, - ), - ), + const SizedBox(height: 8), + if (forms.length <= 1) const SizedBox( - width: 16, + height: 8, ), - Expanded( - child: Builder( - builder: (context) { - bool nameExists = ref - .watch(contactNameIsNotEmptyStateProvider - .state) - .state; - - bool validForms = ref.watch( - validContactStateProvider(forms - .map((e) => e.id) - .toList(growable: false))); - - bool shouldEnableSave = - validForms && nameExists; - - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75), - ); - } - List<ContactAddressEntry> entries = - []; - for (int i = 0; - i < forms.length; - i++) { - entries.add(ref - .read(addressEntryDataProvider( - forms[i].id)) - .buildAddressEntry()); - } - Contact contact = Contact( - emojiChar: _selectedEmoji?.char, - name: nameController.text, - addresses: entries, - isFavorite: _isFavorite, - ); - - if (await ref - .read(addressBookServiceProvider) - .addContact(contact)) { - if (mounted) { - Navigator.of(context).pop(); - } - // TODO show success notification - } else { - // TODO show error notification - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context).copyWith( - color: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimaryDisabled, + if (forms.length <= 1) forms[0], + if (forms.length > 1) + for (int i = 0; i < forms.length; i++) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 12, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address ${i + 1}", + style: STextStyles.smallMed12(context), ), - ), - ); - }, + BlueTextButton( + onTap: () { + _removeForm(forms[i].id); + }, + text: "Remove", + ), + ], + ), + const SizedBox( + height: 8, + ), + forms[i], + ], ), - ), - ], - ), - ], + const SizedBox( + height: 16, + ), + BlueTextButton( + onTap: () { + _addForm(); + scrollController.animateTo( + scrollController.position.maxScrollExtent + 500, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + }, + text: "+ Add another address", + ), + // GestureDetector( + // + // child: Text( + // "+ Add another address", + // style: STextStyles.largeMedium14(context), + // ), + // ), + const SizedBox( + height: 16, + ), + const Spacer(), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.m : null, + onPressed: () async { + if (!isDesktop && + FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Builder( + builder: (context) { + bool nameExists = ref + .watch(contactNameIsNotEmptyStateProvider + .state) + .state; + + bool validForms = ref.watch( + validContactStateProvider(forms + .map((e) => e.id) + .toList(growable: false))); + + bool shouldEnableSave = + validForms && nameExists; + + return PrimaryButton( + label: "Save", + buttonHeight: + isDesktop ? ButtonHeight.m : null, + enabled: shouldEnableSave, + onPressed: shouldEnableSave + ? () async { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration( + milliseconds: 75), + ); + } + List<ContactAddressEntry> entries = + []; + for (int i = 0; + i < forms.length; + i++) { + entries.add(ref + .read( + addressEntryDataProvider( + forms[i].id)) + .buildAddressEntry()); + } + Contact contact = Contact( + emojiChar: _selectedEmoji?.char, + name: nameController.text, + addresses: entries, + isFavorite: _isFavorite, + ); + + if (await ref + .read( + addressBookServiceProvider) + .addContact(contact)) { + if (mounted) { + Navigator.of(context).pop(); + } + // TODO show success notification + } else { + // TODO show error notification + } + } + : null, + ); + }, + ), + ), + ], + ), + ], + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart index e5dbaa7b9..de2dfe90c 100644 --- a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart @@ -12,7 +12,12 @@ import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.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/background.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'; class AddNewContactAddressView extends ConsumerStatefulWidget { const AddNewContactAddressView({ @@ -55,190 +60,173 @@ class _AddNewContactAddressViewState final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); - 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( - "Add new address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + 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(); + } + }, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, + title: Text( + "Add new address", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - Row( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - contact.name, - style: STextStyles.pageTitleH2(context), - ), - ), - ), - ], - ), - const SizedBox( - height: 16, - ), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), - const SizedBox( - height: 16, - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - 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(); - } - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Builder( - builder: (context) { - bool shouldEnableSave = - ref.watch(validContactStateProvider([0])); - - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75), - ); - } - List<ContactAddressEntry> entries = - contact.addresses; - - entries.add(ref - .read( - addressEntryDataProvider(0)) - .buildAddressEntry()); - - Contact editedContact = contact - .copyWith(addresses: entries); - - if (await ref - .read( - addressBookServiceProvider) - .editContact(editedContact)) { - if (mounted) { - Navigator.of(context).pop(); - } - // TODO show success notification - } else { - // TODO show error notification - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ); - }, - ), - ), - ], - ) - ], + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), + ); + }, + ), + ), + ), + child: Column( + children: [ + Row( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), + ), ), - ), - ); - }, + const SizedBox( + width: 16, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + if (!isDesktop) + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + NewContactAddressEntryForm( + id: 0, + barcodeScanner: barcodeScanner, + clipboard: clipboard, + ), + const SizedBox( + height: 16, + ), + const Spacer(), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: ref.watch(validContactStateProvider([0])), + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + List<ContactAddressEntry> entries = contact.addresses; + + entries.add(ref + .read(addressEntryDataProvider(0)) + .buildAddressEntry()); + + Contact editedContact = + contact.copyWith(addresses: entries); + + if (await ref + .read(addressBookServiceProvider) + .editContact(editedContact)) { + if (mounted) { + Navigator.of(context).pop(); + } + // TODO show success notification + } else { + // TODO show error notification + } + }, + ), + ), + ], + ) + ], ), ); } diff --git a/lib/pages/address_book_views/subviews/address_book_filter_view.dart b/lib/pages/address_book_views/subviews/address_book_filter_view.dart index 35968621a..9f410aae7 100644 --- a/lib/pages/address_book_views/subviews/address_book_filter_view.dart +++ b/lib/pages/address_book_views/subviews/address_book_filter_view.dart @@ -5,7 +5,13 @@ import 'package:stackwallet/providers/ui/address_book_providers/address_book_fil 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/background.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_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddressBookFilterView extends ConsumerStatefulWidget { @@ -33,7 +39,7 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { } else { _coins = coins .toList(growable: false) - .getRange(0, coins.length - kTestNetCoinCount + 1) + .getRange(0, coins.length - kTestNetCoinCount) .toList(growable: false); } super.initState(); @@ -41,167 +47,226 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { @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 { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Filter addresses", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder(builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Text( - "Only selected cryptocurrency addresses will be displayed.", - style: STextStyles.itemSubtitle(context), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Select cryptocurrency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Wrap( + title: Text( + "Filter addresses", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder(builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ..._coins.map( - (coin) => Row( - children: [ - GestureDetector( - onTap: () { - if (ref - .read(addressBookFilterProvider) - .coins - .contains(coin)) { - ref - .read(addressBookFilterProvider) - .remove(coin, true); - } else { - ref - .read(addressBookFilterProvider) - .add(coin, true); - } - setState(() {}); - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - height: 20, - width: 20, - child: Checkbox( - value: ref - .watch( - addressBookFilterProvider - .select((value) => - value.coins)) - .contains(coin), - onChanged: (value) { - if (value is bool) { - if (value) { - ref - .read( - addressBookFilterProvider) - .add(coin, true); - } else { - ref - .read( - addressBookFilterProvider) - .remove(coin, true); - } - setState(() {}); - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - coin.prettyName, - style: - STextStyles.largeMedium14( - context), - ), - const SizedBox( - height: 2, - ), - Text( - coin.ticker, - style: - STextStyles.itemSubtitle( - context), - ), - ], - ) - ], - ), - ), - ), - ), - ], + RoundedWhiteContainer( + child: Text( + "Only selected cryptocurrency addresses will be displayed.", + style: STextStyles.itemSubtitle(context), ), ), + const SizedBox( + height: 12, + ), + Text( + "Select cryptocurrency", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + child, ], ), ), - const Spacer(), - // Row( - // children: [ - // TextButton( - // onPressed: () {}, - // child: Text("Cancel"), - // ), - // SizedBox( - // width: 16, - // ), - // TextButton( - // onPressed: () {}, - // child: Text("Cancel"), - // ), - // ], - // ) - ], + ), ), + ); + }), + ), + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Select cryptocurrency", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32), + child: child, + ), + ], + ), + ), + ), + ); + }, ), ), - ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SecondaryButton( + width: 248, + buttonHeight: ButtonHeight.l, + enabled: true, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + // const SizedBox(width: 16), + PrimaryButton( + width: 248, + buttonHeight: ButtonHeight.l, + enabled: true, + label: "Apply", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ), + ], ); - }), + }, + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Wrap( + children: [ + ..._coins.map( + (coin) => Row( + children: [ + GestureDetector( + onTap: () { + if (ref + .read(addressBookFilterProvider) + .coins + .contains(coin)) { + ref + .read(addressBookFilterProvider) + .remove(coin, true); + } else { + ref.read(addressBookFilterProvider).add(coin, true); + } + setState(() {}); + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 20, + width: 20, + child: Checkbox( + value: ref + .watch(addressBookFilterProvider + .select((value) => value.coins)) + .contains(coin), + onChanged: (value) { + if (value is bool) { + if (value) { + ref + .read(addressBookFilterProvider) + .add(coin, true); + } else { + ref + .read(addressBookFilterProvider) + .remove(coin, true); + } + setState(() {}); + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + coin.prettyName, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + coin.ticker, + style: STextStyles.itemSubtitle(context), + ), + ], + ) + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), ), ); } diff --git a/lib/pages/address_book_views/subviews/contact_details_view.dart b/lib/pages/address_book_views/subviews/contact_details_view.dart index c0c10b3b1..0ab6ebba9 100644 --- a/lib/pages/address_book_views/subviews/contact_details_view.dart +++ b/lib/pages/address_book_views/subviews/contact_details_view.dart @@ -18,6 +18,7 @@ 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/widgets/background.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/loading_indicator.dart'; @@ -104,335 +105,203 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> { final _contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(_contactId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Contact details", - 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("contactDetails"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.star, - color: _contact.isFavorite - ? Theme.of(context) - .extension<StackColors>()! - .favoriteStarActive - : Theme.of(context) - .extension<StackColors>()! - .favoriteStarInactive, - width: 20, - height: 20, - ), - onPressed: () { - bool isFavorite = _contact.isFavorite; + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Contact details", + 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("contactDetails"), + size: 36, + shadows: const [], + color: Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.star, + color: _contact.isFavorite + ? Theme.of(context) + .extension<StackColors>()! + .favoriteStarActive + : Theme.of(context) + .extension<StackColors>()! + .favoriteStarInactive, + width: 20, + height: 20, + ), + onPressed: () { + bool isFavorite = _contact.isFavorite; - ref - .read(addressBookServiceProvider) - .editContact(_contact.copyWith(isFavorite: !isFavorite)); - }, + ref.read(addressBookServiceProvider).editContact( + _contact.copyWith(isFavorite: !isFavorite)); + }, + ), ), ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("contactDetailsViewDeleteContactButtonKey"), - 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: () { - showDialog<dynamic>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Delete ${_contact.name}?", - message: "Contact will be deleted permanently!", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("contactDetailsViewDeleteContactButtonKey"), + 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: () { + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => StackDialog( + title: "Delete ${_contact.name}?", + message: "Contact will be deleted permanently!", + 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( + "Delete", + style: STextStyles.button(context), + ), + onPressed: () { + ref + .read(addressBookServiceProvider) + .removeContact(_contact.id); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.success, + message: "${_contact.name} deleted", + context: context, + ); + }, ), - onPressed: () { - Navigator.of(context).pop(); - }, ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Delete", - style: STextStyles.button(context), + ); + }, + ), + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 12, + ), + Row( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, ), + child: Center( + child: _contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + _contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), + ), + ), + const SizedBox( + width: 16, + ), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + _contact.name, + textAlign: TextAlign.left, + style: STextStyles.pageTitleH2(context), + ), + ), + const Spacer(), + TextButton( onPressed: () { - ref - .read(addressBookServiceProvider) - .removeContact(_contact.id); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.success, - message: "${_contact.name} deleted", - context: context, + Navigator.of(context).pushNamed( + EditContactNameEmojiView.routeName, + arguments: _contact.id, ); }, - ), - ), - ); - }, - ), - ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 12, - ), - Row( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) + style: Theme.of(context) .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - _contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), - ), - ), - const SizedBox( - width: 16, - ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - _contact.name, - textAlign: TextAlign.left, - style: STextStyles.pageTitleH2(context), - ), - ), - const Spacer(), - TextButton( - onPressed: () { - Navigator.of(context).pushNamed( - EditContactNameEmojiView.routeName, - arguments: _contact.id, - ); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context)! - .copyWith( - minimumSize: MaterialStateProperty.all<Size>( - const Size(46, 32)), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - SvgPicture.asset(Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - const SizedBox( - width: 4, + .getSecondaryEnabledButtonColor(context)! + .copyWith( + minimumSize: MaterialStateProperty.all<Size>( + const Size(46, 32)), ), - Text( - "Edit", - style: STextStyles.buttonSmall(context), - ), - ], - ), - ), - ), - ], - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Addresses", - style: STextStyles.itemSubtitle(context), - ), - BlueTextButton( - text: "Add new", - onTap: () { - Navigator.of(context).pushNamed( - AddNewContactAddressView.routeName, - arguments: _contact.id, - ); - }, - ), - ], - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._contact.addresses.map( - (e) => Padding( - padding: const EdgeInsets.all(12), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: e.coin), - height: 24, - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${e.label} (${e.coin.ticker})", - style: - STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 2, - ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - e.address, - style: STextStyles.itemSubtitle(context) - .copyWith( - fontSize: 8, - ), - ), - ), - ], - ), - ), - GestureDetector( - onTap: () { - ref - .read(addressEntryDataProvider(0)) - .address = e.address; - ref - .read(addressEntryDataProvider(0)) - .addressLabel = e.label; - ref.read(addressEntryDataProvider(0)).coin = - e.coin; - - Navigator.of(context).pushNamed( - EditContactAddressView.routeName, - arguments: Tuple2(_contact.id, e), - ); - }, - child: RoundedContainer( + SvgPicture.asset(Assets.svg.pencil, + width: 10, + height: 10, color: Theme.of(context) .extension<StackColors>()! - .textFieldDefaultBG, - padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.pencil, - width: 14, - height: 14, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), + .accentColorDark), const SizedBox( width: 4, ), - GestureDetector( - onTap: () { - clipboard.setData( - ClipboardData(text: e.address), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.copy, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), + Text( + "Edit", + style: STextStyles.buttonSmall(context), ), ], ), @@ -440,81 +309,216 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> { ), ], ), - ), - const SizedBox( - height: 24, - ), - Text( - "Transaction history", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: _filteredTransactionsByContact( - ref.watch(walletsChangeNotifierProvider).managers), - builder: (_, - AsyncSnapshot<List<Tuple2<String, Transaction>>> - snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - _cachedTransactions = snapshot.data!; - - if (_cachedTransactions.isNotEmpty) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cachedTransactions.map( - (e) => TransactionCard( - key: Key( - "contactDetailsTransaction_${e.item2.txid}_cardKey"), - transaction: e.item2, - walletId: e.item1, + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Addresses", + style: STextStyles.itemSubtitle(context), + ), + BlueTextButton( + text: "Add new", + onTap: () { + Navigator.of(context).pushNamed( + AddNewContactAddressView.routeName, + arguments: _contact.id, + ); + }, + ), + ], + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ..._contact.addresses.map( + (e) => Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: e.coin), + height: 24, ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "No transactions found", - style: STextStyles.itemSubtitle(context), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${e.label} (${e.coin.ticker})", + style: + STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 2, + ), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + e.address, + style: + STextStyles.itemSubtitle(context) + .copyWith( + fontSize: 8, + ), + ), + ), + ], + ), + ), + GestureDetector( + onTap: () { + ref + .read(addressEntryDataProvider(0)) + .address = e.address; + ref + .read(addressEntryDataProvider(0)) + .addressLabel = e.label; + ref.read(addressEntryDataProvider(0)).coin = + e.coin; + + Navigator.of(context).pushNamed( + EditContactAddressView.routeName, + arguments: Tuple2(_contact.id, e), + ); + }, + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + Assets.svg.pencil, + width: 14, + height: 14, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + const SizedBox( + width: 4, + ), + GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: e.address), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + Assets.svg.copy, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ], ), ), - ); - } - } else { - // TODO: proper loading animation - if (_cachedTransactions.isEmpty) { - return const LoadingIndicator(); - } else { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cachedTransactions.map( - (e) => TransactionCard( - key: Key( - "contactDetailsTransaction_${e.item2.txid}_cardKey"), - transaction: e.item2, - walletId: e.item1, + ), + ], + ), + ), + const SizedBox( + height: 24, + ), + Text( + "Transaction history", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: _filteredTransactionsByContact( + ref.watch(walletsChangeNotifierProvider).managers), + builder: (_, + AsyncSnapshot<List<Tuple2<String, Transaction>>> + snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _cachedTransactions = snapshot.data!; + + if (_cachedTransactions.isNotEmpty) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, + ), ), + ], + ), + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "No transactions found", + style: STextStyles.itemSubtitle(context), ), - ], - ), - ); + ), + ); + } + } else { + // TODO: proper loading animation + if (_cachedTransactions.isEmpty) { + return const LoadingIndicator(); + } else { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, + ), + ), + ], + ), + ); + } } - } - }, - ), - const SizedBox( - height: 16, - ), - ], + }, + ), + const SizedBox( + height: 16, + ), + ], + ), ), ), ), diff --git a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart index 618a41982..0454903c3 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart @@ -12,7 +12,12 @@ import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.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/background.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'; class EditContactAddressView extends ConsumerStatefulWidget { const EditContactAddressView({ @@ -44,6 +49,42 @@ class _EditContactAddressViewState late final BarcodeScannerInterface barcodeScanner; late final ClipboardInterface clipboard; + Future<void> save(Contact contact) async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + List<ContactAddressEntry> entries = contact.addresses.toList(); + + final entry = entries.firstWhere( + (e) => + e.label == addressEntry.label && + e.address == addressEntry.address && + e.coin == addressEntry.coin, + ); + + final index = entries.indexOf(entry); + entries.remove(entry); + + ContactAddressEntry editedEntry = + ref.read(addressEntryDataProvider(0)).buildAddressEntry(); + + entries.insert(index, editedEntry); + + Contact editedContact = contact.copyWith(addresses: entries); + + if (await ref.read(addressBookServiceProvider).editContact(editedContact)) { + if (mounted) { + Navigator.of(context).pop(); + } + // TODO show success notification + } else { + // TODO show error notification + } + } + @override void initState() { contactId = widget.contactId; @@ -59,236 +100,184 @@ class _EditContactAddressViewState final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); - 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( - "Edit address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + final bool isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + 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(); + } + }, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, + title: Text( + "Edit address", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - Row( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - contact.name, - style: STextStyles.pageTitleH2(context), - ), - ), - ), - ], - ), - const SizedBox( - height: 16, - ), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), - const SizedBox( - height: 24, - ), - GestureDetector( - onTap: () async { - // delete address - final _addresses = contact.addresses; - final entry = _addresses.firstWhere( - (e) => - e.label == addressEntry.label && - e.address == addressEntry.address && - e.coin == addressEntry.coin, - ); - - _addresses.remove(entry); - Contact editedContact = - contact.copyWith(addresses: _addresses); - if (await ref - .read(addressBookServiceProvider) - .editContact(editedContact)) { - Navigator.of(context).pop(); - // TODO show success notification - } else { - // TODO show error notification - } - }, - child: Text( - "Delete address", - style: STextStyles.link(context), - ), - ), - const Spacer(), - const SizedBox( - height: 16, - ), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - 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(); - } - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Builder( - builder: (context) { - bool shouldEnableSave = - ref.watch(validContactStateProvider([0])); - - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75), - ); - } - List<ContactAddressEntry> entries = - contact.addresses.toList(); - - final entry = entries.firstWhere( - (e) => - e.label == - addressEntry.label && - e.address == - addressEntry.address && - e.coin == addressEntry.coin, - ); - - final index = - entries.indexOf(entry); - entries.remove(entry); - - ContactAddressEntry editedEntry = ref - .read( - addressEntryDataProvider(0)) - .buildAddressEntry(); - - entries.insert(index, editedEntry); - - Contact editedContact = contact - .copyWith(addresses: entries); - - if (await ref - .read( - addressBookServiceProvider) - .editContact(editedContact)) { - if (mounted) { - Navigator.of(context).pop(); - } - // TODO show success notification - } else { - // TODO show error notification - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ); - }, - ), - ), - ], - ), - ], + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), + ); + }, + ), + ), + ), + child: Column( + children: [ + Row( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), + ), + ), + const SizedBox( + width: 16, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + if (!isDesktop) + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + NewContactAddressEntryForm( + id: 0, + barcodeScanner: barcodeScanner, + clipboard: clipboard, + ), + const SizedBox( + height: 24, + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () async { + // delete address + final _addresses = contact.addresses; + final entry = _addresses.firstWhere( + (e) => + e.label == addressEntry.label && + e.address == addressEntry.address && + e.coin == addressEntry.coin, + ); + + _addresses.remove(entry); + Contact editedContact = contact.copyWith(addresses: _addresses); + if (await ref + .read(addressBookServiceProvider) + .editContact(editedContact)) { + Navigator.of(context).pop(); + // TODO show success notification + } else { + // TODO show error notification + } + }, + child: Text( + "Delete address", + style: STextStyles.link(context), ), ), - ); - }, + ), + const Spacer(), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: ref.watch(validContactStateProvider([0])), + onPressed: () => save(contact), + buttonHeight: isDesktop ? ButtonHeight.l : null, + ), + ), + ], + ), + ], ), ); } diff --git a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart index 45c23b13c..99638ad2c 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,7 +9,13 @@ 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/background.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/emoji_select_sheet.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -67,266 +75,326 @@ class _EditContactNameEmojiViewState final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); - 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( - "Edit contact", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, 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( - children: [ - GestureDetector( - onTap: () { - if (_selectedEmoji != null) { - setState(() { - _selectedEmoji = null; - }); - return; - } - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); - }, - child: SizedBox( - height: 48, - width: 48, - child: Stack( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - _selectedEmoji!.char, - style: STextStyles.pageTitleH1( - context), - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 12, - height: 12, - ) - : SvgPicture.asset( - Assets.svg.thickX, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 8, - height: 8, - ), - ), - ), - ) - ], - ), - ), - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: nameController, - focusNode: nameFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Enter contact name", - nameFocusNode, - context, - ).copyWith( - suffixIcon: nameController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - nameController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const Spacer(), - const SizedBox( - height: 16, - ), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - 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(); - } - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Builder( - builder: (context) { - bool shouldEnableSave = - nameController.text.isNotEmpty; + final isDesktop = Util.isDesktop; + final double emojiSize = isDesktop ? 56 : 48; - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75), - ); - } - final editedContact = - contact.copyWith( - shouldCopyEmojiWithNull: true, - name: nameController.text, - emojiChar: _selectedEmoji == null - ? null - : _selectedEmoji!.char, - ); - ref - .read( - addressBookServiceProvider) - .editContact( - editedContact, - ); - if (mounted) { - Navigator.of(context).pop(); - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ); - }, - ), - ), - ], - ) - ], + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + 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( + "Edit contact", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (context, 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: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } + if (isDesktop) { + showDialog<dynamic>( + barrierColor: Colors.transparent, + context: context, + builder: (context) { + return const DesktopDialog( + maxHeight: 700, + maxWidth: 600, + child: Padding( + padding: EdgeInsets.only( + left: 32, + right: 20, + top: 32, + bottom: 32, + ), + child: EmojiSelectSheet(), + ), + ); + }).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + } else { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + } + }, + child: SizedBox( + height: emojiSize, + width: emojiSize, + child: Stack( + children: [ + Container( + height: emojiSize, + width: emojiSize, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(emojiSize / 2), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.user, + height: emojiSize / 2, + width: emojiSize / 2, + ) + : Text( + _selectedEmoji!.char, + style: isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH1(context), + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + width: 8, + height: 8, + ), + ), + ), + ) + ], + ), + ), + ), + if (isDesktop) + const SizedBox( + width: 8, + ), + if (isDesktop) + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ], + ), + if (!isDesktop) + const SizedBox( + height: 8, + ), + if (!isDesktop) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), ), ), - ); - }, + const Spacer(), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: nameController.text.isNotEmpty, + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + final editedContact = contact.copyWith( + shouldCopyEmojiWithNull: true, + name: nameController.text, + emojiChar: + _selectedEmoji == null ? null : _selectedEmoji!.char, + ); + unawaited( + ref.read(addressBookServiceProvider).editContact( + editedContact, + ), + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + ], + ) + ], ), ); } diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index 73de8b0aa..25cff073b 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -1,8 +1,10 @@ +import 'package:dropdown_button2/dropdown_button2.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:stackwallet/pages/address_book_views/subviews/coin_select_sheet.dart'; +import 'package:stackwallet/providers/providers.dart'; // import 'package:stackwallet/providers/global/should_show_lockscreen_on_resume_state_provider.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; import 'package:stackwallet/utilities/address_utils.dart'; @@ -14,6 +16,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.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/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; @@ -46,6 +49,8 @@ class _NewContactAddressEntryFormState late final FocusNode addressLabelFocusNode; late final FocusNode addressFocusNode; + List<Coin> coins = []; + @override void initState() { addressLabelController = TextEditingController() @@ -54,6 +59,7 @@ class _NewContactAddressEntryFormState ..text = ref.read(addressEntryDataProvider(widget.id)).address ?? ""; addressLabelFocusNode = FocusNode(); addressFocusNode = FocusNode(); + coins = [...Coin.values]; super.initState(); } @@ -68,84 +74,180 @@ class _NewContactAddressEntryFormState @override Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + bool showTestNet = ref.watch( + prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), + ); + if (isDesktop) { + coins = [...Coin.values]; + + coins.remove(Coin.firoTestNet); + if (showTestNet) { + coins = coins.sublist(0, coins.length - kTestNetCoinCount); + } + } + return Column( children: [ - TextField( - readOnly: true, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Select cryptocurrency", - hintStyle: STextStyles.fieldLabel(context), - prefixIcon: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: RawMaterialButton( - splashColor: - Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (isDesktop) + DropdownButtonHideUnderline( + child: DropdownButton2<Coin>( + hint: Text( + "Select cryptocurrency", + style: STextStyles.fieldLabel(context), + ), + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: ref.watch(addressEntryDataProvider(widget.id) + .select((value) => value.coin)), + onChanged: (value) { + if (value is Coin) { + ref.read(addressEntryDataProvider(widget.id)).coin = value; + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + items: [ + ...coins.map( + (coin) => DropdownMenuItem<Coin>( + value: coin, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + height: 24, + width: 24, + ), + const SizedBox( + width: 12, + ), + Text( + coin.prettyName, + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), ), ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - builder: (_) => const CoinSelectSheet(), - ).then((value) { - if (value is Coin) { - ref.read(addressEntryDataProvider(widget.id)).coin = - value; - } - }); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ref.watch(addressEntryDataProvider(widget.id) - .select((value) => value.coin)) == - null - ? Text( - "Select cryptocurrency", - style: STextStyles.fieldLabel(context), - ) - : Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor( - coin: ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.coin))!), - height: 20, - width: 20, - ), - const SizedBox( - width: 12, - ), - Text( - ref - .watch(addressEntryDataProvider(widget.id) - .select((value) => value.coin))! - .prettyName, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, + ), + ], + ), + ), + if (!isDesktop) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + readOnly: true, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Select cryptocurrency", + hintStyle: STextStyles.fieldLabel(context), + prefixIcon: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: RawMaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ], + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + builder: (_) => const CoinSelectSheet(), + ).then((value) { + if (value is Coin) { + ref.read(addressEntryDataProvider(widget.id)).coin = + value; + } + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ref.watch(addressEntryDataProvider(widget.id) + .select((value) => value.coin)) == + null + ? Text( + "Select cryptocurrency", + style: STextStyles.fieldLabel(context), + ) + : Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor( + coin: ref.watch( + addressEntryDataProvider(widget.id) + .select( + (value) => value.coin))!), + height: 20, + width: 20, + ), + const SizedBox( + width: 12, + ), + Text( + ref + .watch( + addressEntryDataProvider(widget.id) + .select((value) => value.coin))! + .prettyName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + if (!isDesktop) + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ], + ), ), ), ), ), ), - ), const SizedBox( height: 8, ), @@ -154,6 +256,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), @@ -162,6 +266,7 @@ class _NewContactAddressEntryFormState addressLabelFocusNode, context, ).copyWith( + labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: addressLabelController.text.isNotEmpty ? Padding( padding: const EdgeInsets.only(right: 0), @@ -197,6 +302,7 @@ class _NewContactAddressEntryFormState Constants.size.circularBorderRadius, ), child: TextField( + enableSuggestions: Util.isDesktop ? false : true, focusNode: addressFocusNode, controller: addressController, style: STextStyles.field(context), @@ -205,6 +311,7 @@ class _NewContactAddressEntryFormState addressFocusNode, context, ).copyWith( + labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ @@ -244,9 +351,10 @@ class _NewContactAddressEntryFormState }, child: const ClipboardIcon(), ), - if (ref.watch(addressEntryDataProvider(widget.id) - .select((value) => value.address)) == - null) + if (!Util.isDesktop && + ref.watch(addressEntryDataProvider(widget.id) + .select((value) => value.address)) == + null) TextFieldIconButton( key: const Key("addAddressBookEntryScanQrButtonKey"), onTap: () async { @@ -324,7 +432,6 @@ class _NewContactAddressEntryFormState key: const Key("addAddressBookEntryViewAddressField"), readOnly: false, autocorrect: false, - enableSuggestions: false, // inputFormatters: <TextInputFormatter>[ // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), // ], diff --git a/lib/pages/exchange_view/choose_from_stack_view.dart b/lib/pages/exchange_view/choose_from_stack_view.dart index f54a7552c..7c7669430 100644 --- a/lib/pages/exchange_view/choose_from_stack_view.dart +++ b/lib/pages/exchange_view/choose_from_stack_view.dart @@ -5,6 +5,7 @@ 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/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart'; @@ -39,89 +40,92 @@ class _ChooseFromStackViewState extends ConsumerState<ChooseFromStackView> { final walletIds = ref.watch(walletsChangeNotifierProvider .select((value) => value.getWalletIdsFor(coin: coin))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: const AppBarBackButton(), - title: Text( - "Choose your ${coin.ticker.toUpperCase()} wallet", - style: STextStyles.navBarTitle(context), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: const AppBarBackButton(), + title: Text( + "Choose your ${coin.ticker.toUpperCase()} wallet", + style: STextStyles.navBarTitle(context), + ), ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: walletIds.isEmpty - ? Column( - children: [ - RoundedWhiteContainer( - child: Center( - child: Text( - "No ${coin.ticker.toUpperCase()} wallets", - style: STextStyles.itemSubtitle(context), + body: Padding( + padding: const EdgeInsets.all(16), + child: walletIds.isEmpty + ? Column( + children: [ + RoundedWhiteContainer( + child: Center( + child: Text( + "No ${coin.ticker.toUpperCase()} wallets", + style: STextStyles.itemSubtitle(context), + ), ), ), - ), - ], - ) - : ListView.builder( - itemCount: walletIds.length, - itemBuilder: (context, index) { - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletIds[index]))); + ], + ) + : ListView.builder( + itemCount: walletIds.length, + itemBuilder: (context, index) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletIds[index]))); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: RawMaterialButton( - splashColor: - Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: RawMaterialButton( + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - padding: const EdgeInsets.all(0), - // color: Theme.of(context).extension<StackColors>()!.popupBG, - elevation: 0, - onPressed: () async { - if (mounted) { - Navigator.of(context).pop(manager.walletId); - } - }, - child: RoundedWhiteContainer( - // color: Colors.transparent, - child: Row( - children: [ - WalletInfoCoinIcon(coin: coin), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), - overflow: TextOverflow.ellipsis, - ), - const SizedBox( - height: 2, - ), - WalletInfoRowBalanceFuture( - walletId: walletIds[index], - ), - ], + padding: const EdgeInsets.all(0), + // color: Theme.of(context).extension<StackColors>()!.popupBG, + elevation: 0, + onPressed: () async { + if (mounted) { + Navigator.of(context).pop(manager.walletId); + } + }, + child: RoundedWhiteContainer( + // color: Colors.transparent, + child: Row( + children: [ + WalletInfoCoinIcon(coin: coin), + const SizedBox( + width: 12, ), - ) - ], + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12(context), + overflow: TextOverflow.ellipsis, + ), + const SizedBox( + height: 2, + ), + WalletInfoRowBalanceFuture( + walletId: walletIds[index], + ), + ], + ), + ) + ], + ), ), ), - ), - ); - }, - ), + ); + }, + ), + ), ), ); } diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index d77ad6b8c..25ae51ecf 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -7,14 +7,24 @@ import 'package:stackwallet/models/trade_wallet_lookup.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/constants.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/utilities/util.dart'; +import 'package:stackwallet/widgets/background.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/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -27,6 +37,8 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { required this.walletId, this.routeOnSuccessName = WalletView.routeName, required this.trade, + this.shouldSendPublicFiroFunds, + this.fromDesktopStep4 = false, }) : super(key: key); static const String routeName = "/confirmChangeNowSend"; @@ -35,6 +47,8 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { final String walletId; final String routeOnSuccessName; final Trade trade; + final bool? shouldSendPublicFiroFunds; + final bool fromDesktopStep4; @override ConsumerState<ConfirmChangeNowSendView> createState() => @@ -49,21 +63,31 @@ class _ConfirmChangeNowSendViewState late final Trade trade; Future<void> _attemptSend(BuildContext context) async { - unawaited(showDialog<void>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return const SendingTransactionDialog(); - }, - )); + unawaited( + showDialog<void>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return const SendingTransactionDialog(); + }, + ), + ); final String note = transactionInfo["note"] as String? ?? ""; final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); try { - final txid = await manager.confirmSend(txData: transactionInfo); + late final String txid; + + if (widget.shouldSendPublicFiroFunds == true) { + txid = await (manager.wallet as FiroWallet) + .confirmSendPublic(txData: transactionInfo); + } else { + txid = await manager.confirmSend(txData: transactionInfo); + } + unawaited(manager.refresh()); // save note @@ -82,6 +106,19 @@ class _ConfirmChangeNowSendViewState // pop back to wallet if (mounted) { + if (Util.isDesktop) { + Navigator.of(context, rootNavigator: true).pop(); + + // stupid hack + if (widget.fromDesktopStep4) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + } + } + Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); } } catch (e) { @@ -118,6 +155,65 @@ class _ConfirmChangeNowSendViewState } } + Future<void> _confirmSend() async { + final dynamic unlocked; + + final coin = + ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; + + if (Util.isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend( + coin: coin, + ), + ), + ], + ), + ), + ); + } else { + 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) { + await _attemptSend(context); + } + } + @override void initState() { transactionInfo = widget.transactionInfo; @@ -131,280 +227,507 @@ class _ConfirmChangeNowSendViewState 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, + + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + 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), + ), ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, + 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) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 6, + ), + const AppBarBackButton( + isCompact: true, + iconSize: 23, + ), + const SizedBox( + width: 12, + ), + Text( + "Confirm ${ref.watch(managerProvider.select((value) => value.coin)).ticker} transaction", + style: STextStyles.desktopH3(context), + ) + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: child, + ), + const SizedBox( + height: 16, + ), + Row( 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( - "Send from", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - ref - .watch(walletsChangeNotifierProvider) - .getManager(walletId) - .walletName, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "${trade.exchangeName} address", - 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 SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade ID", - style: STextStyles.smallMed12(context), - ), - Text( - trade.tradeId, - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, - ), - ], - ), - ), - 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, - ), - const Spacer(), - 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) { - await _attemptSend(context); - } - }, - child: Text( - "Send", - style: STextStyles.button(context), - ), + "Transaction fee", + style: + STextStyles.desktopTextExtraExtraSmall(context), ), ], ), + const SizedBox( + height: 10, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + "${Format.satoshiAmountToPrettyString( + (transactionInfo["fee"] as int), + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + ref.watch( + managerProvider.select((value) => value.coin), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + 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), + ), + )} ${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, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: _confirmSend, + ), + ), + ], + ) + ], + ), + ), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ConditionalParent( + condition: isDesktop, + builder: (child) => Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + child, + ], ), ), ), + child: Text( + "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", + style: isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.pageTitleH1(context), + ), ), - ); - }, + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Send from", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + ref + .watch(walletsChangeNotifierProvider) + .getManager(walletId) + .walletName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "${trade.exchangeName} address", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + "${transactionInfo["address"] ?? "ERROR"}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => Row( + children: [ + child, + Builder(builder: (context) { + final coin = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).coin)); + final price = ref.watch( + priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); + final amount = Format.satoshisToAmount( + transactionInfo["recipientAmt"] as int, + coin: coin, + ); + final value = price.item1 * amount; + final currency = ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)); + + return Text( + " | ${value.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} $currency", + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ); + }) + ], + ), + child: Text( + "${Format.satoshiAmountToPrettyString(transactionInfo["recipientAmt"] as int, ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), ref.watch( + managerProvider.select((value) => value.coin), + ))} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : 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), + ))} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : 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), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade ID", + style: STextStyles.smallMed12(context), + ), + Text( + trade.tradeId, + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + if (!isDesktop) + 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), + ))} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + if (!isDesktop) const Spacer(), + if (!isDesktop) + PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: _confirmSend, + ), + ], + ), ), ); } diff --git a/lib/pages/exchange_view/edit_trade_note_view.dart b/lib/pages/exchange_view/edit_trade_note_view.dart index 5e1571b73..19804b7c5 100644 --- a/lib/pages/exchange_view/edit_trade_note_view.dart +++ b/lib/pages/exchange_view/edit_trade_note_view.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/providers/exchange/trade_note_service_provider.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -45,103 +47,109 @@ class _EditNoteViewState extends ConsumerState<EditTradeNoteView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( + return Background( + child: Scaffold( 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(); - } - }, + 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 trade note", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit trade note", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - 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, - onChanged: (_) => setState(() {}), - 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 = ""; - }); - }, - ), - ], + body: Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _noteController, + style: STextStyles.field(context), + focusNode: noteFieldFocusNode, + onChanged: (_) => setState(() {}), + 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, + ) + : null, + ), ), ), - ), - const Spacer(), - TextButton( - onPressed: () async { - await ref.read(tradeNoteServiceProvider).set( - tradeId: widget.tradeId, - note: _noteController.text, - ); - if (mounted) { - Navigator.of(context).pop(); - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Save", - style: STextStyles.button(context), - ), - ) - ], + const Spacer(), + TextButton( + onPressed: () async { + await ref.read(tradeNoteServiceProvider).set( + tradeId: widget.tradeId, + note: _noteController.text, + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Save", + style: STextStyles.button(context), + ), + ) + ], + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart index d7577e960..fdc1e027a 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart @@ -8,6 +8,9 @@ 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/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -118,93 +121,109 @@ class _FixedRateMarketPairCoinSelectionViewState @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: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Choose a coin to exchange", - style: STextStyles.pageTitleH2(context), - ), - ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + 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: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a coin to exchange", + style: STextStyles.pageTitleH2(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, + ), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isDesktop) const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: filter, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: filter, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 = ""; - }); - }, - ), - ], - ), - ), - ) - : 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 = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), - const SizedBox( - height: 10, - ), - Text( - "Popular coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Builder(builder: (context) { + ), + const SizedBox( + height: 10, + ), + Text( + "Popular coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: Builder(builder: (context) { final items = _markets .where((e) => Coin.values .where((coin) => @@ -217,6 +236,7 @@ class _FixedRateMarketPairCoinSelectionViewState padding: const EdgeInsets.all(0), child: ListView.builder( shrinkWrap: true, + primary: isDesktop ? false : null, itemCount: items.length, itemBuilder: (builderContext, index) { final String ticker = @@ -278,84 +298,85 @@ class _FixedRateMarketPairCoinSelectionViewState ), ); }), - const SizedBox( - height: 20, - ), - Text( - "All coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Flexible( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: ListView.builder( - shrinkWrap: true, - itemCount: _markets.length, - itemBuilder: (builderContext, index) { - final String ticker = - isFrom ? _markets[index].from : _markets[index].to; + ), + const SizedBox( + height: 20, + ), + Text( + "All coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: _markets.length, + itemBuilder: (builderContext, index) { + final String ticker = + isFrom ? _markets[index].from : _markets[index].to; - final tuple = _imageUrlAndNameFor(ticker); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(ticker); - }, - child: RoundedWhiteContainer( - child: Row( - children: [ - SizedBox( + final tuple = _imageUrlAndNameFor(ticker); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(ticker); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: SvgPicture.network( + tuple.item1, width: 24, height: 24, - child: SvgPicture.network( - tuple.item1, - width: 24, - height: 24, - placeholderBuilder: (_) => - const LoadingIndicator(), - ), + placeholderBuilder: (_) => + const LoadingIndicator(), ), - const SizedBox( - width: 10, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tuple.item2, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tuple.item2, - style: STextStyles.largeMedium14(context), - ), - const SizedBox( - height: 2, - ), - Text( - ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), - ), - ], - ), + ), + ], ), ), - ); - }, - ), + ), + ); + }, ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart index 7c3b935b7..1a82e5ec5 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart @@ -6,6 +6,9 @@ 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/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -74,94 +77,112 @@ class _FloatingRateCurrencySelectionViewState @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: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Choose a coin to exchange", - style: STextStyles.pageTitleH2(context), - ), - ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + 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: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a coin to exchange", + style: STextStyles.pageTitleH2(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, + ), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: filter, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: filter, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 = ""; - }); - filter(""); - }, - ), - ], - ), - ), - ) - : 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 = ""; + }); + filter(""); + }, + ), + ], + ), + ), + ) + : null, ), ), - const SizedBox( - height: 10, - ), - Text( - "Popular coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Builder(builder: (context) { + ), + const SizedBox( + height: 10, + ), + Text( + "Popular coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: Builder(builder: (context) { final items = _currencies .where((e) => Coin.values .where((coin) => @@ -173,6 +194,7 @@ class _FloatingRateCurrencySelectionViewState padding: const EdgeInsets.all(0), child: ListView.builder( shrinkWrap: true, + primary: isDesktop ? false : null, itemCount: items.length, itemBuilder: (builderContext, index) { return Padding( @@ -230,80 +252,81 @@ class _FloatingRateCurrencySelectionViewState ), ); }), - const SizedBox( - height: 20, - ), - Text( - "All coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Flexible( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: ListView.builder( - shrinkWrap: true, - itemCount: _currencies.length, - itemBuilder: (builderContext, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(_currencies[index]); - }, - child: RoundedWhiteContainer( - child: Row( - children: [ - SizedBox( + ), + const SizedBox( + height: 20, + ), + Text( + "All coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: _currencies.length, + itemBuilder: (builderContext, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(_currencies[index]); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: SvgPicture.network( + _currencies[index].image, width: 24, height: 24, - child: SvgPicture.network( - _currencies[index].image, - width: 24, - height: 24, - placeholderBuilder: (_) => - const LoadingIndicator(), - ), + placeholderBuilder: (_) => + const LoadingIndicator(), ), - const SizedBox( - width: 10, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _currencies[index].name, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + _currencies[index].ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _currencies[index].name, - style: STextStyles.largeMedium14(context), - ), - const SizedBox( - height: 2, - ), - Text( - _currencies[index].ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), - ), - ], - ), + ), + ], ), ), - ); - }, - ), + ), + ); + }, ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 7b04f90b3..d818a73ff 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; 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:flutter_svg/svg.dart'; @@ -18,19 +17,30 @@ import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_2_view. import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_options.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/rate_type_toggle.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.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/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_loading_overlay.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/loading_indicator.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/desktop/simple_desktop_dialog.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/exchange_textfield.dart'; import 'package:tuple/tuple.dart'; class ExchangeForm extends ConsumerStatefulWidget { @@ -54,6 +64,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { late final TextEditingController _sendController; late final TextEditingController _receiveController; + final isDesktop = Util.isDesktop; final FocusNode _sendFocusNode = FocusNode(); final FocusNode _receiveFocusNode = FocusNode(); @@ -135,14 +146,27 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { .read(exchangeFormStateProvider) .updateMarket(market, true); } catch (e) { - unawaited(showDialog<dynamic>( - context: context, - builder: (_) => const StackDialog( - title: "Fixed rate market error", - message: - "Could not find the specified fixed rate trade pair", + unawaited( + showDialog<dynamic>( + context: context, + builder: (_) { + if (isDesktop) { + return const SimpleDesktopDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } else { + return const StackDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } + }, ), - )); + ); + return; } }, @@ -225,14 +249,26 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { .read(exchangeFormStateProvider) .updateMarket(market, true); } catch (e) { - unawaited(showDialog<dynamic>( - context: context, - builder: (_) => const StackDialog( - title: "Fixed rate market error", - message: - "Could not find the specified fixed rate trade pair", + unawaited( + showDialog<dynamic>( + context: context, + builder: (_) { + if (isDesktop) { + return const SimpleDesktopDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } else { + return const StackDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } + }, ), - )); + ); return; } }, @@ -320,7 +356,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { await ref.read(exchangeFormStateProvider).swap(); } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } _swapLock = false; } @@ -375,13 +411,65 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } }).toList(growable: false); - final result = await Navigator.of(context).push( - MaterialPageRoute<dynamic>( - builder: (_) => FloatingRateCurrencySelectionView( - currencies: tickers, - ), - ), - ); + final result = isDesktop + ? await showDialog<Currency?>( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Choose a coin to exchange", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: FloatingRateCurrencySelectionView( + currencies: tickers, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute<dynamic>( + builder: (_) => FloatingRateCurrencySelectionView( + currencies: tickers, + ), + ), + ); if (mounted && result is Currency) { onSelected(result); @@ -455,15 +543,73 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { .toList(growable: false); } - final result = await Navigator.of(context).push( - MaterialPageRoute<dynamic>( - builder: (_) => FixedRateMarketPairCoinSelectionView( - markets: marketsThatPairWithExcludedTicker, - currencies: ref.read(availableChangeNowCurrenciesProvider).currencies, - isFrom: excludedTicker != fromTicker, - ), - ), - ); + final result = isDesktop + ? await showDialog<String?>( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Choose a coin to exchange", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: FixedRateMarketPairCoinSelectionView( + markets: marketsThatPairWithExcludedTicker, + currencies: ref + .read( + availableChangeNowCurrenciesProvider) + .currencies, + isFrom: excludedTicker != fromTicker, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute<dynamic>( + builder: (_) => FixedRateMarketPairCoinSelectionView( + markets: marketsThatPairWithExcludedTicker, + currencies: + ref.read(availableChangeNowCurrenciesProvider).currencies, + isFrom: excludedTicker != fromTicker, + ), + ), + ); if (mounted && result is String) { onSelected(result); @@ -563,14 +709,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ? "-" : ref.read(exchangeFormStateProvider).toAmountString; if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } return; } } } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } if (!(fromTicker == "-" || toTicker == "-")) { unawaited( @@ -616,7 +762,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { true, ); if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } return; case SimpleSwapExchange.exchangeName: @@ -653,7 +799,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ? "-" : ref.read(exchangeFormStateProvider).toAmountString; if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } return; } @@ -665,7 +811,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } unawaited( showFloatingFlushBar( @@ -718,15 +864,27 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } if (!isAvailable) { - unawaited(showDialog<dynamic>( - context: context, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Selected trade pair unavailable", - message: - "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", + unawaited( + showDialog<dynamic>( + context: context, + barrierDismissible: true, + builder: (_) { + if (isDesktop) { + return SimpleDesktopDialog( + title: "Selected trade pair unavailable", + message: + "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", + ); + } else { + return StackDialog( + title: "Selected trade pair unavailable", + message: + "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", + ); + } + }, ), - )); + ); return; } rate = @@ -740,37 +898,101 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { shouldCancel = await showDialog<bool?>( context: context, barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to update trade estimate", - message: - "${estimate.warningMessage!}\n\nDo you want to attempt trade anyways?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - // notify return to cancel - Navigator.of(context).pop(true); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Attempt", - style: STextStyles.button(context), - ), - onPressed: () { - // continue and try to attempt trade - Navigator.of(context).pop(false); - }, - ), - ), + builder: (_) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Failed to update trade estimate", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const Spacer(), + Text( + estimate.warningMessage!, + style: STextStyles.desktopTextSmall(context), + ), + const Spacer(), + Text( + "Do you want to attempt trade anyways?", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Attempt", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), + ), + ), + ], + ) + ], + ), + ); + } else { + return StackDialog( + title: "Failed to update trade estimate", + message: + "${estimate.warningMessage!}\n\nDo you want to attempt trade anyways?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + // notify return to cancel + Navigator.of(context).pop(true); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Attempt", + style: STextStyles.button(context), + ), + onPressed: () { + // continue and try to attempt trade + Navigator.of(context).pop(false); + }, + ), + ); + } + }, ); } @@ -799,20 +1021,61 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { if (walletInitiated) { ref.read(exchangeSendFromWalletIdStateProvider.state).state = Tuple2(walletId!, coin!); - unawaited( - Navigator.of(context).pushNamed( - Step2View.routeName, - arguments: model, - ), - ); + if (isDesktop) { + await showDialog<void>( + context: context, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 2, + model: model, + body: DesktopStep2( + model: model, + ), + ), + ); + }, + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + Step2View.routeName, + arguments: model, + ), + ); + } } else { ref.read(exchangeSendFromWalletIdStateProvider.state).state = null; - unawaited( - Navigator.of(context).pushNamed( - Step1View.routeName, - arguments: model, - ), - ); + + if (isDesktop) { + await showDialog<void>( + context: context, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 1, + model: model, + body: DesktopStep1( + model: model, + ), + ), + ); + }, + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + Step1View.routeName, + arguments: model, + ), + ); + } } } } @@ -960,220 +1223,108 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { color: Theme.of(context).extension<StackColors>()!.textDark3, ), ), - const SizedBox( - height: 4, + SizedBox( + height: isDesktop ? 10 : 4, ), - TextFormField( - style: STextStyles.smallMed14(context).copyWith( + ExchangeTextField( + controller: _sendController, + focusNode: _sendFocusNode, + textStyle: STextStyles.smallMed14(context).copyWith( color: Theme.of(context).extension<StackColors>()!.textDark, ), - focusNode: _sendFocusNode, - controller: _sendController, - textAlign: TextAlign.right, + buttonColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + borderRadius: Constants.size.circularBorderRadius, + background: + Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, onTap: () { if (_sendController.text == "-") { _sendController.text = ""; } }, onChanged: sendFieldOnChanged, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: selectSendCurrency, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - ), - child: Builder( - builder: (context) { - final image = _fetchIconUrlFromTicker(ref.watch( - exchangeFormStateProvider - .select((value) => value.fromTicker))); - - if (image != null && image.isNotEmpty) { - return Center( - child: SvgPicture.network( - image, - height: 18, - placeholderBuilder: (_) => Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - 18, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - 18, - ), - child: const LoadingIndicator(), - ), - ), - ), - ); - } else { - return Container( - width: 18, - height: 18, - decoration: BoxDecoration( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - borderRadius: BorderRadius.circular(18), - ), - child: SvgPicture.asset( - Assets.svg.circleQuestion, - width: 18, - height: 18, - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - ), - ); - } - }, - ), - ), - const SizedBox( - width: 6, - ), - Text( - ref.watch(exchangeFormStateProvider.select((value) => - value.fromTicker?.toUpperCase())) ?? - "-", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - if (!isWalletCoin(coin, true)) - const SizedBox( - width: 6, - ), - if (!isWalletCoin(coin, true)) - SvgPicture.asset( - Assets.svg.chevronDown, - width: 5, - height: 2.5, - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ], - ), - ), - ), - ), - ), - ), + onButtonTap: selectSendCurrency, + isWalletCoin: isWalletCoin(coin, true), + image: _fetchIconUrlFromTicker(ref.watch( + exchangeFormStateProvider.select((value) => value.fromTicker))), + ticker: ref.watch( + exchangeFormStateProvider.select((value) => value.fromTicker)), ), - const SizedBox( - height: 4, + SizedBox( + height: isDesktop ? 10 : 4, ), - Stack( + SizedBox( + height: isDesktop ? 10 : 4, + ), + if (ref + .watch( + exchangeFormStateProvider.select((value) => value.warning)) + .isNotEmpty && + !ref.watch( + exchangeFormStateProvider.select((value) => value.reversed))) + Text( + ref.watch( + exchangeFormStateProvider.select((value) => value.warning)), + style: STextStyles.errorSmall(context), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Positioned.fill( - child: Align( - alignment: Alignment.bottomLeft, - child: Text( - "You will receive", - style: STextStyles.itemSubtitle(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, - ), - ), + Text( + "You will receive", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, ), ), - Center( - child: Column( - children: [ - const SizedBox( - height: 6, - ), - GestureDetector( - onTap: () async { - await _swap(); - }, - child: Padding( - padding: const EdgeInsets.all(4), - child: SvgPicture.asset( - Assets.svg.swap, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(6) + : const EdgeInsets.all(2), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + radiusMultiplier: 0.75, + child: GestureDetector( + onTap: () async { + await _swap(); + }, + child: Padding( + padding: const EdgeInsets.all(4), + child: SvgPicture.asset( + Assets.svg.swap, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, ), ), - const SizedBox( - height: 6, - ), - ], - ), - ), - Positioned.fill( - child: Align( - alignment: ref.watch(exchangeFormStateProvider - .select((value) => value.reversed)) - ? Alignment.bottomRight - : Alignment.topRight, - child: Text( - ref.watch(exchangeFormStateProvider - .select((value) => value.warning)), - style: STextStyles.errorSmall(context), ), ), ), ], ), - const SizedBox( - height: 4, + SizedBox( + height: isDesktop ? 10 : 7, ), - TextFormField( - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark, - ), + ExchangeTextField( focusNode: _receiveFocusNode, controller: _receiveController, - readOnly: ref.watch(prefsChangeNotifierProvider - .select((value) => value.exchangeRateType)) == - ExchangeRateType.estimated || - ref.watch(exchangeProvider).name == - SimpleSwapExchange.exchangeName, + textStyle: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + buttonColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + borderRadius: Constants.size.circularBorderRadius, + background: + Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, onTap: () { if (!(ref.read(prefsChangeNotifierProvider).exchangeRateType == ExchangeRateType.estimated) && @@ -1182,138 +1333,39 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } }, onChanged: receiveFieldOnChanged, - textAlign: TextAlign.right, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - child: GestureDetector( - onTap: selectReceiveCurrency, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - ), - child: Builder( - builder: (context) { - final image = _fetchIconUrlFromTicker(ref.watch( - exchangeFormStateProvider - .select((value) => value.toTicker))); - - if (image != null && image.isNotEmpty) { - return Center( - child: SvgPicture.network( - image, - height: 18, - placeholderBuilder: (_) => Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(18), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - 18, - ), - child: const LoadingIndicator(), - ), - ), - ), - ); - } else { - return Container( - width: 18, - height: 18, - decoration: BoxDecoration( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - borderRadius: BorderRadius.circular(18), - ), - child: SvgPicture.asset( - Assets.svg.circleQuestion, - width: 18, - height: 18, - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - ), - ); - } - }, - ), - ), - const SizedBox( - width: 6, - ), - Text( - ref.watch(exchangeFormStateProvider.select( - (value) => value.toTicker?.toUpperCase())) ?? - "-", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - if (!isWalletCoin(coin, false)) - const SizedBox( - width: 6, - ), - if (!isWalletCoin(coin, false)) - SvgPicture.asset( - Assets.svg.chevronDown, - width: 5, - height: 2.5, - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ], - ), - ), - ), - ), - ), - ), + onButtonTap: selectReceiveCurrency, + isWalletCoin: isWalletCoin(coin, true), + image: _fetchIconUrlFromTicker(ref.watch( + exchangeFormStateProvider.select((value) => value.toTicker))), + ticker: ref.watch( + exchangeFormStateProvider.select((value) => value.toTicker)), + readOnly: ref.watch(prefsChangeNotifierProvider + .select((value) => value.exchangeRateType)) == + ExchangeRateType.estimated || + ref.watch(exchangeProvider).name == + SimpleSwapExchange.exchangeName, ), - const SizedBox( - height: 12, + if (ref + .watch( + exchangeFormStateProvider.select((value) => value.warning)) + .isNotEmpty && + ref.watch( + exchangeFormStateProvider.select((value) => value.reversed))) + Text( + ref.watch( + exchangeFormStateProvider.select((value) => value.warning)), + style: STextStyles.errorSmall(context), + ), + SizedBox( + height: isDesktop ? 20 : 12, ), RateTypeToggle( onChanged: onRateTypeChanged, ), if (ref.read(exchangeFormStateProvider).fromAmount != null && ref.read(exchangeFormStateProvider).fromAmount != Decimal.zero) - const SizedBox( - height: 8, + SizedBox( + height: isDesktop ? 20 : 12, ), if (ref.read(exchangeFormStateProvider).fromAmount != null && ref.read(exchangeFormStateProvider).fromAmount != Decimal.zero) @@ -1328,10 +1380,11 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { reversed: ref.watch( exchangeFormStateProvider.select((value) => value.reversed)), ), - const SizedBox( - height: 12, + SizedBox( + height: isDesktop ? 20 : 12, ), PrimaryButton( + buttonHeight: isDesktop ? ButtonHeight.l : null, enabled: ref.watch( exchangeFormStateProvider.select((value) => value.canExchange)), onPressed: ref.watch(exchangeFormStateProvider diff --git a/lib/pages/exchange_view/exchange_step_views/step_1_view.dart b/lib/pages/exchange_view/exchange_step_views/step_1_view.dart index 95f086003..b51bccc55 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_1_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_1_view.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -39,166 +40,169 @@ class _Step1ViewState extends State<Step1View> { @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(); - } - }, + return Background( + 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( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(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: [ - StepRow( - count: 4, - current: 0, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Confirm amount", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Network fees and other exchange charges are included in the rate.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 24, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "You send", - style: STextStyles.itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - Text( - "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - ], + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(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: [ + StepRow( + count: 4, + current: 0, + width: width, ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "You receive", - style: STextStyles.itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - Text( - "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - ], + const SizedBox( + height: 14, ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - model.rateType == ExchangeRateType.estimated - ? "Estimated rate" - : "Fixed rate", - style: - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemLabel, + Text( + "Confirm amount", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 24, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "You send", + style: STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), ), - ), - Text( - model.rateInfo, - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - ], + Text( + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), + ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - const Spacer(), - TextButton( - onPressed: () { - Navigator.of(context).pushNamed(Step2View.routeName, - arguments: model); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Next", - style: STextStyles.button(context), + const SizedBox( + height: 12, ), - ), - ], + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "You receive", + style: STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), + ), + Text( + "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + model.rateType == ExchangeRateType.estimated + ? "Estimated rate" + : "Fixed rate", + style: STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemLabel, + ), + ), + Text( + model.rateInfo, + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + const Spacer(), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed( + Step2View.routeName, + arguments: model); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Next", + style: STextStyles.button(context), + ), + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 800d1e146..d943adcfa 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -7,8 +7,6 @@ import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_3_view.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart'; -import 'package:stackwallet/providers/exchange/exchange_flow_is_active_state_provider.dart'; -import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; @@ -18,8 +16,10 @@ import 'package:stackwallet/utilities/enums/coin_enum.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/background.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/primary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; @@ -57,6 +57,8 @@ class _Step2ViewState extends ConsumerState<Step2View> { late final FocusNode _toFocusNode; late final FocusNode _refundFocusNode; + bool enableNext = false; + bool isStackCoin(String ticker) { try { coinFromTickerCaseInsensitive(ticker); @@ -121,548 +123,618 @@ class _Step2ViewState extends ConsumerState<Step2View> { @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(); - } - }, + return Background( + 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( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(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: [ - StepRow( - count: 4, - current: 1, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Exchange details", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Enter your recipient and refund addresses", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recipient Wallet", - style: STextStyles.smallMed12(context), - ), - if (isStackCoin(model.receiveTicker)) - BlueTextButton( - text: "Choose from stack", - onTap: () { - try { - final coin = coinFromTickerCaseInsensitive( - model.receiveTicker, - ); - Navigator.of(context) - .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) - .then((value) async { - if (value is String) { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(value); - - _toController.text = manager.walletName; - model.recipientAddress = await manager - .currentReceivingAddress; - } - }); - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Info); - } - }, - ), - ], - ), - const SizedBox( - height: 4, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(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: [ + StepRow( + count: 4, + current: 1, + width: width, ), - child: TextField( - onTap: () {}, - key: const Key( - "recipientExchangeStep2ViewAddressFieldKey"), - controller: _toController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: <TextInputFormatter>[ - // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, - ), - focusNode: _toFocusNode, - style: STextStyles.field(context), - onChanged: (value) { - setState(() {}); - }, - decoration: standardInputDecoration( - "Enter the ${model.receiveTicker.toUpperCase()} payout address", - _toFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, + const SizedBox( + height: 14, + ), + Text( + "Exchange details", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Enter your recipient and refund addresses", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipient Wallet", + style: STextStyles.smallMed12(context), ), - suffixIcon: Padding( - padding: _toController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _toController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey"), - onTap: () { - _toController.text = ""; - model.recipientAddress = - _toController.text; + if (isStackCoin(model.receiveTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: () { + try { + final coin = + coinFromTickerCaseInsensitive( + model.receiveTicker, + ); + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final manager = ref + .read( + walletsChangeNotifierProvider) + .getManager(value); - setState(() {}); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey"), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = - data.text!.trim(); + _toController.text = + manager.walletName; + model.recipientAddress = await manager + .currentReceivingAddress; - _toController.text = content; + setState(() { + enableNext = + _toController.text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } + }); + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Info); + } + }, + ), + ], + ), + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + onTap: () {}, + key: const Key( + "recipientExchangeStep2ViewAddressFieldKey"), + controller: _toController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _toFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() {}); + }, + decoration: standardInputDecoration( + "Enter the ${model.receiveTicker.toUpperCase()} payout address", + _toFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _toController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _toController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _toController.text = ""; model.recipientAddress = _toController.text; - setState(() {}); - } - }, - child: _toController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_toController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey"), - onTap: () { - ref - .read( - exchangeFlowIsActiveStateProvider - .state) - .state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then((_) { + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = + data.text!.trim(); + + _toController.text = + content; + model.recipientAddress = + _toController.text; + + setState(() { + enableNext = _toController + .text + .isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } + }, + child: + _toController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey"), + onTap: () { ref .read( exchangeFlowIsActiveStateProvider .state) - .state = false; - - final address = ref - .read( - exchangeFromAddressBookAddressStateProvider - .state) - .state; - if (address.isNotEmpty) { - _toController.text = address; - model.recipientAddress = - _toController.text; + .state = true; + Navigator.of(context) + .pushNamed( + AddressBookView.routeName, + ) + .then((_) { ref + .read( + exchangeFlowIsActiveStateProvider + .state) + .state = false; + + final address = ref .read( exchangeFromAddressBookAddressStateProvider .state) - .state = ""; + .state; + if (address.isNotEmpty) { + _toController.text = address; + model.recipientAddress = + _toController.text; + ref + .read( + exchangeFromAddressBookAddressStateProvider + .state) + .state = ""; + } + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + }); + }, + child: const AddressBookIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey"), + onTap: () async { + try { + final qrResult = + await scanner.scan(); + + final results = + AddressUtils.parseUri( + qrResult.rawContent); + if (results.isNotEmpty) { + // auto fill address + _toController.text = + results["address"] ?? ""; + model.recipientAddress = + _toController.text; + + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } else { + _toController.text = + qrResult.rawContent; + model.recipientAddress = + _toController.text; + + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } - }); - }, - child: const AddressBookIcon(), - ), - if (_toController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey"), - onTap: () async { - try { - final qrResult = - await scanner.scan(); - - final results = - AddressUtils.parseUri( - qrResult.rawContent); - if (results.isNotEmpty) { - // auto fill address - _toController.text = - results["address"] ?? ""; - model.recipientAddress = - _toController.text; - - setState(() {}); - } else { - _toController.text = - qrResult.rawContent; - model.recipientAddress = - _toController.text; - - setState(() {}); - } - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ), - ], + }, + child: const QrCodeIcon(), + ), + ], + ), ), ), ), ), ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Text( - "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", - style: STextStyles.label(context), + const SizedBox( + height: 6, ), - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Refund Wallet (required)", - style: STextStyles.smallMed12(context), + RoundedWhiteContainer( + child: Text( + "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", + style: STextStyles.label(context), ), - if (isStackCoin(model.sendTicker)) - BlueTextButton( - text: "Choose from stack", - onTap: () { - try { - final coin = coinFromTickerCaseInsensitive( - model.sendTicker, - ); - Navigator.of(context) - .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) - .then((value) async { - if (value is String) { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(value); - - _refundController.text = - manager.walletName; - model.refundAddress = await manager - .currentReceivingAddress; - } - }); - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Info); - } - }, - ), - ], - ), - const SizedBox( - height: 4, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, ), - child: TextField( - key: const Key( - "refundExchangeStep2ViewAddressFieldKey"), - controller: _refundController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: <TextInputFormatter>[ - // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, - ), - focusNode: _refundFocusNode, - style: STextStyles.field(context), - onChanged: (value) { - setState(() {}); - }, - decoration: standardInputDecoration( - "Enter ${model.sendTicker.toUpperCase()} refund address", - _refundFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Refund Wallet (required)", + style: STextStyles.smallMed12(context), ), - suffixIcon: Padding( - padding: _refundController.text.isEmpty - ? const EdgeInsets.only(right: 16) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _refundController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey"), - onTap: () { - _refundController.text = ""; - model.refundAddress = - _refundController.text; + if (isStackCoin(model.sendTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: () { + try { + final coin = + coinFromTickerCaseInsensitive( + model.sendTicker, + ); + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final manager = ref + .read( + walletsChangeNotifierProvider) + .getManager(value); - setState(() {}); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey"), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = - data.text!.trim(); - - _refundController.text = - content; + _refundController.text = + manager.walletName; + model.refundAddress = await manager + .currentReceivingAddress; + } + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + }); + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Info); + } + }, + ), + ], + ), + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "refundExchangeStep2ViewAddressFieldKey"), + controller: _refundController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _refundFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() {}); + }, + decoration: standardInputDecoration( + "Enter ${model.sendTicker.toUpperCase()} refund address", + _refundFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _refundController.text.isEmpty + ? const EdgeInsets.only(right: 16) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _refundController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _refundController.text = ""; model.refundAddress = _refundController.text; - setState(() {}); - } - }, - child: - _refundController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_refundController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey"), - onTap: () { - ref - .read( - exchangeFlowIsActiveStateProvider - .state) - .state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then((_) { + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = + data.text!.trim(); + + _refundController.text = + content; + model.refundAddress = + _refundController.text; + + setState(() { + enableNext = _toController + .text + .isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } + }, + child: _refundController + .text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_refundController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey"), + onTap: () { ref .read( exchangeFlowIsActiveStateProvider .state) - .state = false; - final address = ref - .read( - exchangeFromAddressBookAddressStateProvider - .state) - .state; - if (address.isNotEmpty) { - _refundController.text = - address; - model.refundAddress = - _refundController.text; + .state = true; + Navigator.of(context) + .pushNamed( + AddressBookView.routeName, + ) + .then((_) { + ref + .read( + exchangeFlowIsActiveStateProvider + .state) + .state = false; + final address = ref + .read( + exchangeFromAddressBookAddressStateProvider + .state) + .state; + if (address.isNotEmpty) { + _refundController.text = + address; + model.refundAddress = + _refundController.text; + } + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + }); + }, + child: const AddressBookIcon(), + ), + if (_refundController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey"), + onTap: () async { + try { + final qrResult = + await scanner.scan(); + + final results = + AddressUtils.parseUri( + qrResult.rawContent); + if (results.isNotEmpty) { + // auto fill address + _refundController.text = + results["address"] ?? ""; + model.refundAddress = + _refundController.text; + + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } else { + _refundController.text = + qrResult.rawContent; + model.refundAddress = + _refundController.text; + + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } - }); - }, - child: const AddressBookIcon(), - ), - if (_refundController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey"), - onTap: () async { - try { - final qrResult = - await scanner.scan(); - - final results = - AddressUtils.parseUri( - qrResult.rawContent); - if (results.isNotEmpty) { - // auto fill address - _refundController.text = - results["address"] ?? ""; - model.refundAddress = - _refundController.text; - - setState(() {}); - } else { - _refundController.text = - qrResult.rawContent; - model.refundAddress = - _refundController.text; - - setState(() {}); - } - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ), - ], + }, + child: const QrCodeIcon(), + ), + ], + ), ), ), ), ), ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Text( - "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", - style: STextStyles.label(context), + const SizedBox( + height: 6, ), - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Back", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, + RoundedWhiteContainer( + child: Text( + "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", + style: STextStyles.label(context), + ), + ), + const Spacer(), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), ), ), ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: TextButton( - onPressed: () { - Navigator.of(context).pushNamed( - Step3View.routeName, - arguments: model, - ); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Next", - style: STextStyles.button(context), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + enabled: enableNext, + onPressed: () { + Navigator.of(context).pushNamed( + Step3View.routeName, + arguments: model, + ); + }, ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart index 0f03d4216..467a3b9e7 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -50,290 +51,295 @@ class _Step3ViewState extends ConsumerState<Step3View> { @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(); - } - }, + return Background( + 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( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(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: [ - StepRow( - count: 4, - current: 2, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Confirm exchange details", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 24, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "You send", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Text( - "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ) - ], + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(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: [ + StepRow( + count: 4, + current: 2, + width: width, ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "You receive", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Text( - "${model.receiveAmount.toString()} ${model.receiveTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ) - ], + const SizedBox( + height: 14, ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "Estimated rate", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Text( - model.rateInfo, - style: STextStyles.itemSubtitle12(context), - ) - ], + Text( + "Confirm exchange details", + style: STextStyles.pageTitleH1(context), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Recipient ${model.receiveTicker.toUpperCase()} address", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - Text( - model.recipientAddress!, - style: STextStyles.itemSubtitle12(context), - ) - ], + const SizedBox( + height: 24, ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Refund ${model.sendTicker.toUpperCase()} address", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - Text( - model.refundAddress!, - style: STextStyles.itemSubtitle12(context), - ) - ], + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "You send", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ) + ], + ), ), - ), - const SizedBox( - height: 8, - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Back", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "You receive", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + "${model.receiveAmount.toString()} ${model.receiveTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Estimated rate", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + model.rateInfo, + style: STextStyles.itemSubtitle12(context), + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Recipient ${model.receiveTicker.toUpperCase()} address", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + Text( + model.recipientAddress!, + style: STextStyles.itemSubtitle12(context), + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Refund ${model.sendTicker.toUpperCase()} address", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + Text( + model.refundAddress!, + style: STextStyles.itemSubtitle12(context), + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + const Spacer(), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), ), ), ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: TextButton( - onPressed: () async { - unawaited( - showDialog<void>( - context: context, - barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension<StackColors>()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Creating a trade", - eventBus: null, + const SizedBox( + width: 16, + ), + Expanded( + child: TextButton( + onPressed: () async { + unawaited( + showDialog<void>( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension<StackColors>()! + .overlay + .withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Creating a trade", + eventBus: null, + ), ), ), ), - ), - ); + ); - final ExchangeResponse<Trade> response = - await ref - .read(exchangeProvider) - .createTrade( - from: model.sendTicker, - to: model.receiveTicker, - fixedRate: model.rateType != - ExchangeRateType.estimated, - amount: model.reversed - ? model.receiveAmount - : model.sendAmount, - addressTo: model.recipientAddress!, - extraId: null, - addressRefund: model.refundAddress!, - refundExtraId: "", - rateId: model.rateId, - reversed: model.reversed, - ); + final ExchangeResponse<Trade> response = + await ref + .read(exchangeProvider) + .createTrade( + from: model.sendTicker, + to: model.receiveTicker, + fixedRate: model.rateType != + ExchangeRateType.estimated, + amount: model.reversed + ? model.receiveAmount + : model.sendAmount, + addressTo: + model.recipientAddress!, + extraId: null, + addressRefund: + model.refundAddress!, + refundExtraId: "", + rateId: model.rateId, + reversed: model.reversed, + ); + + if (response.value == null) { + if (mounted) { + Navigator.of(context).pop(); + } + + unawaited(showDialog<void>( + context: context, + barrierDismissible: true, + builder: (_) => StackDialog( + title: "Failed to create trade", + message: + response.exception?.toString(), + ), + )); + return; + } + + // save trade to hive + await ref.read(tradesServiceProvider).add( + trade: response.value!, + shouldNotifyListeners: true, + ); + + String status = response.value!.status; + + model.trade = response.value!; + + // extra info if status is waiting + if (status == "Waiting") { + status += " for deposit"; + } - if (response.value == null) { if (mounted) { Navigator.of(context).pop(); } - unawaited(showDialog<void>( - context: context, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to create trade", - message: response.exception?.toString(), - ), + unawaited(NotificationApi.showNotification( + changeNowId: model.trade!.tradeId, + title: status, + body: "Trade ID ${model.trade!.tradeId}", + walletId: "", + iconAssetName: Assets.svg.arrowRotate, + date: model.trade!.timestamp, + shouldWatchForUpdates: true, + coinName: "coinName", )); - return; - } - // save trade to hive - await ref.read(tradesServiceProvider).add( - trade: response.value!, - shouldNotifyListeners: true, - ); - - String status = response.value!.status; - - model.trade = response.value!; - - // extra info if status is waiting - if (status == "Waiting") { - status += " for deposit"; - } - - if (mounted) { - Navigator.of(context).pop(); - } - - unawaited(NotificationApi.showNotification( - changeNowId: model.trade!.tradeId, - title: status, - body: "Trade ID ${model.trade!.tradeId}", - walletId: "", - iconAssetName: Assets.svg.arrowRotate, - date: model.trade!.timestamp, - shouldWatchForUpdates: true, - coinName: "coinName", - )); - - if (mounted) { - unawaited(Navigator.of(context).pushNamed( - Step4View.routeName, - arguments: model, - )); - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Next", - style: STextStyles.button(context), + if (mounted) { + unawaited(Navigator.of(context).pushNamed( + Step4View.routeName, + arguments: model, + )); + } + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Next", + style: STextStyles.button(context), + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 0921f68e0..101ac637f 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -18,10 +18,10 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_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/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -107,539 +107,548 @@ class _Step4ViewState extends ConsumerState<Step4View> { Widget build(BuildContext context) { final bool isWalletCoin = _isWalletCoinAndHasWallet(model.trade!.payInCurrency, ref); - 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(); - } - }, + return Background( + 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( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(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: [ - StepRow( - count: 4, - current: 3, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Send ${model.sendTicker.toUpperCase()} to the address below", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .warningBackground, - child: RichText( - text: TextSpan( - text: - "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", - style: STextStyles.label700(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(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: [ + StepRow( + count: 4, + current: 3, + width: width, + ), + const SizedBox( + height: 14, + ), + Text( + "Send ${model.sendTicker.toUpperCase()} to the address below", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 12, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .warningBackground, + child: RichText( + text: TextSpan( + text: + "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", + style: STextStyles.label700(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + children: [ + TextSpan( + text: + "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", + style: STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + ), + ], ), + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan( - text: - "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", - style: STextStyles.label(context).copyWith( + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.itemSubtitle(context), + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.sendAmount.toString()); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 10, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Send ${model.sendTicker.toUpperCase()} to this address", + style: STextStyles.itemSubtitle(context), + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.trade!.payInAddress); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 10, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + model.trade!.payInAddress, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Trade ID", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + Text( + model.trade!.tradeId, + style: + STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.trade!.tradeId); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 12, + ), + ) + ], + ) + ], + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.itemSubtitle(context), + ), + Text( + _statusString, + style: STextStyles.itemSubtitle(context) + .copyWith( color: Theme.of(context) .extension<StackColors>()! - .warningForeground, + .colorForStatus(_statusString), ), ), ], ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.sendAmount.toString()); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - width: 10, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 4, - ), - Text( - "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Send ${model.sendTicker.toUpperCase()} to this address", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.trade!.payInAddress); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - width: 10, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 4, - ), - Text( - model.trade!.payInAddress, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "Trade ID", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - Text( - model.trade!.tradeId, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.trade!.tradeId); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - width: 12, - ), - ) - ], - ) - ], - ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), - Text( - _statusString, - style: - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .colorForStatus(_statusString), - ), - ), - ], - ), - ), - const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: () { - showDialog<dynamic>( - context: context, - barrierDismissible: true, - builder: (_) { - return StackDialogBase( - child: Column( - children: [ - const SizedBox( - height: 8, - ), - Center( - child: Text( - "Send ${model.sendTicker} to this address", - style: - STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 24, - ), - Center( - child: QrImage( - // TODO: grab coin uri scheme from somewhere - // data: "${coin.uriScheme}:$receivingAddress", - data: model.trade!.payInAddress, - size: MediaQuery.of(context) - .size - .width / - 2, - foregroundColor: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - const SizedBox( - height: 24, - ), - Row( - children: [ - const Spacer(), - Expanded( - child: TextButton( - onPressed: () => - Navigator.of(context).pop(), - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Cancel", - style: - STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ), - ), - ), - ), - ], - ) - ], - ), - ); - }, - ); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Show QR Code", - style: STextStyles.button(context), - ), - ), - if (isWalletCoin) + const Spacer(), const SizedBox( height: 12, ), - if (isWalletCoin) - Builder( - builder: (context) { - String buttonTitle = "Send from Stack Wallet"; - - final tuple = ref - .read(exchangeSendFromWalletIdStateProvider - .state) - .state; - if (tuple != null && - model.sendTicker.toLowerCase() == - tuple.item2.ticker.toLowerCase()) { - final walletName = ref - .read(walletsChangeNotifierProvider) - .getManager(tuple.item1) - .walletName; - buttonTitle = "Send from $walletName"; - } - - return TextButton( - onPressed: tuple != null && - model.sendTicker.toLowerCase() == - tuple.item2.ticker.toLowerCase() - ? () async { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(tuple.item1); - - final amount = - Format.decimalAmountToSatoshis( - model.sendAmount); - final address = - model.trade!.payInAddress; - - try { - bool wasCancelled = false; - - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; - - Navigator.of(context).pop(); - }, - ); - }, - )); - - final txData = - await manager.prepareSend( - address: address, - satoshiAmount: amount, - args: { - "feeRate": FeeRateType.average, - // ref.read(feeRateTypeStateProvider) - }, - ); - - if (!wasCancelled) { - // pop building dialog - - if (mounted) { - Navigator.of(context).pop(); - } - - txData["note"] = - "${model.trade!.payInCurrency.toUpperCase()}/${model.trade!.payOutCurrency.toUpperCase()} exchange"; - txData["address"] = address; - - if (mounted) { - unawaited( - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => - ConfirmChangeNowSendView( - transactionInfo: txData, - walletId: tuple.item1, - routeOnSuccessName: - HomeView.routeName, - trade: model.trade!, - ), - settings: const RouteSettings( - name: - ConfirmChangeNowSendView - .routeName, - ), - ), - )); - } - } - } catch (e) { - // if (mounted) { - // pop building dialog - Navigator.of(context).pop(); - - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Ok", - style: STextStyles.button( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .buttonTextSecondary, - ), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, - )); - // } - } - } - : () { - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (BuildContext context) { - return SendFromView( - coin: - coinFromTickerCaseInsensitive( - model.trade! - .payInCurrency), - amount: model.sendAmount, - address: - model.trade!.payInAddress, - trade: model.trade!, - ); - }, - settings: const RouteSettings( - name: SendFromView.routeName, - ), + TextButton( + onPressed: () { + showDialog<dynamic>( + context: context, + barrierDismissible: true, + builder: (_) { + return StackDialogBase( + child: Column( + children: [ + const SizedBox( + height: 8, + ), + Center( + child: Text( + "Send ${model.sendTicker} to this address", + style: STextStyles.pageTitleH2( + context), ), - ); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - buttonTitle, - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ), - ), + ), + const SizedBox( + height: 24, + ), + Center( + child: QrImage( + // TODO: grab coin uri scheme from somewhere + // data: "${coin.uriScheme}:$receivingAddress", + data: model.trade!.payInAddress, + size: MediaQuery.of(context) + .size + .width / + 2, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: TextButton( + onPressed: () => + Navigator.of(context).pop(), + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Cancel", + style: STextStyles.button( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .buttonTextSecondary, + ), + ), + ), + ), + ], + ) + ], + ), + ); + }, ); }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Show QR Code", + style: STextStyles.button(context), + ), ), - ], + if (isWalletCoin) + const SizedBox( + height: 12, + ), + if (isWalletCoin) + Builder( + builder: (context) { + String buttonTitle = "Send from Stack Wallet"; + + final tuple = ref + .read(exchangeSendFromWalletIdStateProvider + .state) + .state; + if (tuple != null && + model.sendTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase()) { + final walletName = ref + .read(walletsChangeNotifierProvider) + .getManager(tuple.item1) + .walletName; + buttonTitle = "Send from $walletName"; + } + + return TextButton( + onPressed: tuple != null && + model.sendTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase() + ? () async { + final manager = ref + .read( + walletsChangeNotifierProvider) + .getManager(tuple.item1); + + final amount = + Format.decimalAmountToSatoshis( + model.sendAmount, + manager.coin); + final address = + model.trade!.payInAddress; + + try { + bool wasCancelled = false; + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, + )); + + final txData = + await manager.prepareSend( + address: address, + satoshiAmount: amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + + if (!wasCancelled) { + // pop building dialog + + if (mounted) { + Navigator.of(context).pop(); + } + + txData["note"] = + "${model.trade!.payInCurrency.toUpperCase()}/${model.trade!.payOutCurrency.toUpperCase()} exchange"; + txData["address"] = address; + + if (mounted) { + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (_) => + ConfirmChangeNowSendView( + transactionInfo: txData, + walletId: tuple.item1, + routeOnSuccessName: + HomeView.routeName, + trade: model.trade!, + ), + settings: + const RouteSettings( + name: + ConfirmChangeNowSendView + .routeName, + ), + ), + )); + } + } + } catch (e) { + // if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension< + StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Ok", + style: STextStyles.button( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .buttonTextSecondary, + ), + ), + onPressed: () { + Navigator.of(context) + .pop(); + }, + ), + ); + }, + )); + // } + } + } + : () { + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (BuildContext context) { + return SendFromView( + coin: + coinFromTickerCaseInsensitive( + model.trade! + .payInCurrency), + amount: model.sendAmount, + address: + model.trade!.payInAddress, + trade: model.trade!, + ); + }, + settings: const RouteSettings( + name: SendFromView.routeName, + ), + ), + ); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + buttonTitle, + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), + ); + }, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/exchange_view/exchange_view.dart b/lib/pages/exchange_view/exchange_view.dart index 84e054eac..ed99047b2 100644 --- a/lib/pages/exchange_view/exchange_view.dart +++ b/lib/pages/exchange_view/exchange_view.dart @@ -43,7 +43,11 @@ class _ExchangeViewState extends ConsumerState<ExchangeView> { handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: const SliverToBoxAdapter( child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + ), child: ExchangeForm(), ), ), diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 271e2b349..6a7fe5285 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -8,8 +8,11 @@ import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/pages/exchange_view/confirm_change_now_send.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -17,8 +20,14 @@ import 'package:stackwallet/utilities/enums/fee_rate_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/background.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/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -29,6 +38,8 @@ class SendFromView extends ConsumerStatefulWidget { required this.trade, required this.amount, required this.address, + this.shouldPopRoot = false, + this.fromDesktopStep4 = false, }) : super(key: key); static const String routeName = "/sendFrom"; @@ -37,6 +48,8 @@ class SendFromView extends ConsumerStatefulWidget { final Decimal amount; final String address; final Trade trade; + final bool shouldPopRoot; + final bool fromDesktopStep4; @override ConsumerState<SendFromView> createState() => _SendFromViewState(); @@ -49,24 +62,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> { late final Trade trade; String formatAmount(Decimal amount, Coin coin) { - switch (coin) { - case Coin.bitcoin: - case Coin.bitcoincash: - case Coin.dogecoin: - case Coin.epicCash: - case Coin.firo: - case Coin.namecoin: - case Coin.particl: - case Coin.bitcoinTestNet: - case Coin.bitcoincashTestnet: - case Coin.dogecoinTestNet: - case Coin.firoTestNet: - return amount.toStringAsFixed(Constants.decimalPlaces); - case Coin.monero: - return amount.toStringAsFixed(Constants.decimalPlacesMonero); - case Coin.wownero: - return amount.toStringAsFixed(Constants.decimalPlacesWownero); - } + return amount.toStringAsFixed(Constants.decimalPlacesForCoin(coin)); } @override @@ -85,21 +81,70 @@ class _SendFromViewState extends ConsumerState<SendFromView> { final walletIds = ref.watch(walletsChangeNotifierProvider .select((value) => value.getWalletIdsFor(coin: coin))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Send from", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Send from Stack", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: widget.shouldPopRoot, + ).pop, + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: child, + ), + ], + ), ), - title: Text( - "Send from", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -107,15 +152,23 @@ class _SendFromViewState extends ConsumerState<SendFromView> { children: [ Text( "You need to send ${formatAmount(amount, coin)} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), ), ], ), const SizedBox( height: 16, ), - Expanded( + ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: child, + ), child: ListView.builder( + primary: isDesktop ? false : null, + shrinkWrap: isDesktop, itemCount: walletIds.length, itemBuilder: (context, index) { return Padding( @@ -125,6 +178,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> { amount: amount, address: address, trade: trade, + fromDesktopStep4: widget.fromDesktopStep4, ), ); }, @@ -144,12 +198,14 @@ class SendFromCard extends ConsumerStatefulWidget { required this.amount, required this.address, required this.trade, + this.fromDesktopStep4 = false, }) : super(key: key); final String walletId; final Decimal amount; final String address; final Trade trade; + final bool fromDesktopStep4; @override ConsumerState<SendFromCard> createState() => _SendFromCardState(); @@ -161,6 +217,147 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { late final String address; late final Trade trade; + Future<void> _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async { + final _amount = Format.decimalAmountToSatoshis(amount, manager.coin); + + try { + bool wasCancelled = false; + + unawaited( + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: child, + ), + ), + child: BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ), + ); + }, + ), + ); + + late Map<String, dynamic> txData; + + // if not firo then do normal send + if (shouldSendPublicFiroFunds == null) { + txData = await manager.prepareSend( + address: address, + satoshiAmount: _amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + } else { + final firoWallet = manager.wallet as FiroWallet; + // otherwise do firo send based on balance selected + if (shouldSendPublicFiroFunds) { + txData = await firoWallet.prepareSendPublic( + address: address, + satoshiAmount: _amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + } else { + txData = await firoWallet.prepareSend( + address: address, + satoshiAmount: _amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + } + } + + if (!wasCancelled) { + // pop building dialog + + if (mounted) { + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop(); + } + + txData["note"] = + "${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange"; + txData["address"] = address; + + if (mounted) { + await Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => ConfirmChangeNowSendView( + transactionInfo: txData, + walletId: walletId, + routeOnSuccessName: Util.isDesktop + ? DesktopExchangeView.routeName + : HomeView.routeName, + trade: trade, + shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, + fromDesktopStep4: widget.fromDesktopStep4, + ), + settings: const RouteSettings( + name: ConfirmChangeNowSendView.routeName, + ), + ), + ); + } + } + } catch (e) { + // if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + ); + // } + } + } + @override void initState() { walletId = widget.walletId; @@ -181,181 +378,295 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { final coin = manager.coin; + final isFiro = coin == Coin.firoTestNet || coin == Coin.firo; + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: MaterialButton( - splashColor: Theme.of(context).extension<StackColors>()!.highlight, - key: Key("walletsSheetItemButtonKey_$walletId"), - padding: const EdgeInsets.all(8), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: ConditionalParent( + condition: isFiro, + builder: (child) => Expandable( + header: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(12), + child: child, + ), + ), + body: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () async { + if (mounted) { + unawaited( + _send( + manager, + shouldSendPublicFiroFunds: false, + ), + ); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use private balance", + style: STextStyles.itemSubtitle(context), + ), + FutureBuilder( + future: (manager.wallet as FiroWallet) + .availablePrivateBalance(), + builder: (builderContext, + AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${Format.localizedStringAsFixed( + value: snapshot.data!, + locale: locale, + decimalPlaces: + Constants.decimalPlacesForCoin(coin), + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemLabel, + ), + ], + ), + ), + ), + ), + MaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () async { + if (mounted) { + unawaited( + _send( + manager, + shouldSendPublicFiroFunds: true, + ), + ); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use public balance", + style: STextStyles.itemSubtitle(context), + ), + FutureBuilder( + future: (manager.wallet as FiroWallet) + .availablePublicBalance(), + builder: (builderContext, + AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${Format.localizedStringAsFixed( + value: snapshot.data!, + locale: locale, + decimalPlaces: + Constants.decimalPlacesForCoin(coin), + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemLabel, + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 6, + ), + ], ), ), - onPressed: () async { - final _amount = Format.decimalAmountToSatoshis(amount); - - try { - bool wasCancelled = false; - - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; - - Navigator.of(context).pop(); - }, + child: ConditionalParent( + condition: !isFiro, + builder: (child) => MaterialButton( + splashColor: Theme.of(context).extension<StackColors>()!.highlight, + key: Key("walletsSheetItemButtonKey_$walletId"), + padding: const EdgeInsets.all(8), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () async { + if (mounted) { + unawaited( + _send(manager), ); - }, - )); - - final txData = await manager.prepareSend( - address: address, - satoshiAmount: _amount, - args: { - "feeRate": FeeRateType.average, - // ref.read(feeRateTypeStateProvider) - }, - ); - - if (!wasCancelled) { - // pop building dialog - - if (mounted) { - Navigator.of(context).pop(); } - - txData["note"] = - "${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange"; - txData["address"] = address; - - if (mounted) { - await Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmChangeNowSendView( - transactionInfo: txData, - walletId: walletId, - routeOnSuccessName: HomeView.routeName, - trade: trade, - ), - settings: const RouteSettings( - name: ConfirmChangeNowSendView.routeName, - ), + }, + child: child, + ), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .colorForCoin(manager.coin) + .withOpacity(0.5), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - } - } - } catch (e) { - // if (mounted) { - // pop building dialog - Navigator.of(context).pop(); - - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, + ), + child: Padding( + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12(context), + ), + if (!isFiro) + const SizedBox( + height: 2, ), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, - ); - // } - } - }, - child: Row( - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .colorForCoin(manager.coin) - .withOpacity(0.5), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (!isFiro) + FutureBuilder( + future: manager.totalBalance, + builder: + (builderContext, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${Format.localizedStringAsFixed( + value: snapshot.data!, + locale: locale, + decimalPlaces: + Constants.decimalPlacesForCoin(coin), + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], ), ), - child: Padding( - padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - FutureBuilder( - future: manager.totalBalance, - builder: (builderContext, AsyncSnapshot<Decimal> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: locale, - decimalPlaces: coin == Coin.monero - ? Constants.decimalPlacesMonero - : coin == Coin.wownero - ? Constants.decimalPlacesWownero - : Constants.decimalPlaces, - )} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, - ), - ], - ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index 2113e199c..ebaf68066 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -15,7 +15,9 @@ import 'package:stackwallet/utilities/format.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/animated_text.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class ExchangeProviderOptions extends ConsumerWidget { @@ -38,353 +40,414 @@ class ExchangeProviderOptions extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final isDesktop = Util.isDesktop; return RoundedWhiteContainer( + padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, child: Column( children: [ - GestureDetector( - onTap: () { - if (ref.read(currentExchangeNameStateProvider.state).state != - ChangeNowExchange.exchangeName) { - ref.read(currentExchangeNameStateProvider.state).state = - ChangeNowExchange.exchangeName; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName( - ref.read(currentExchangeNameStateProvider.state).state); - } - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: ChangeNowExchange.exchangeName, - groupValue: ref - .watch(currentExchangeNameStateProvider.state) - .state, - onChanged: (value) { - if (value is String) { - ref - .read(currentExchangeNameStateProvider.state) - .state = value; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName(ref + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (ref.read(currentExchangeNameStateProvider.state).state != + ChangeNowExchange.exchangeName) { + ref.read(currentExchangeNameStateProvider.state).state = + ChangeNowExchange.exchangeName; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider.state) + .state); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: ChangeNowExchange.exchangeName, + groupValue: ref + .watch(currentExchangeNameStateProvider.state) + .state, + onChanged: (value) { + if (value is String) { + ref .read(currentExchangeNameStateProvider.state) - .state); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - SvgPicture.asset( - Assets.exchange.changeNow, - width: 24, - height: 24, - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ChangeNowExchange.exchangeName, - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark2, - ), + .state = value; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider + .state) + .state); + } + }, ), - if (from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero) - FutureBuilder( - future: ChangeNowExchange().getEstimate( - from!, - to!, - reversed ? toAmount! : fromAmount!, - fixedRate, - reversed, + ), + const SizedBox( + width: 14, + ), + SvgPicture.asset( + Assets.exchange.changeNow, + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ChangeNowExchange.exchangeName, + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), ), - builder: (context, - AsyncSnapshot<ExchangeResponse<Estimate>> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimate = snapshot.data?.value; - if (estimate != null) { - Decimal rate; - if (estimate.reversed) { - rate = - (toAmount! / estimate.estimatedAmount) + if (from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero) + FutureBuilder( + future: ChangeNowExchange().getEstimate( + from!, + to!, + reversed ? toAmount! : fromAmount!, + fixedRate, + reversed, + ), + builder: (context, + AsyncSnapshot<ExchangeResponse<Estimate>> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + final estimate = snapshot.data?.value; + if (estimate != null) { + Decimal rate; + if (estimate.reversed) { + rate = (toAmount! / + estimate.estimatedAmount) .toDecimal( scaleOnInfinitePrecision: 12); + } else { + rate = (estimate.estimatedAmount / + fromAmount!) + .toDecimal( + scaleOnInfinitePrecision: 12); + } + Coin coin; + try { + coin = + coinFromTickerCaseInsensitive(to!); + } catch (_) { + coin = Coin.bitcoin; + } + + return Text( + "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( + value: rate, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + decimalPlaces: + Constants.decimalPlacesForCoin( + coin), + )} ${to!.toUpperCase()}", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } else { + Logging.instance.log( + "$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}", + level: LogLevel.Warning, + ); + return Text( + "Failed to fetch rate", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } } else { - rate = - (estimate.estimatedAmount / fromAmount!) - .toDecimal( - scaleOnInfinitePrecision: 12); - } - return Text( - "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + return AnimatedText( + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), - decimalPlaces: to!.toUpperCase() == - Coin.monero.ticker.toUpperCase() - ? Constants.decimalPlacesMonero - : Constants.decimalPlaces, - )} ${to!.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - }, - ), - if (!(from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), + ); + } + }, + ), + if (!(from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero)) + Text( + "n/a", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), + ), + ], ), - ], + ), ), ), ), - const SizedBox( - height: 16, - ), - GestureDetector( - onTap: () { - if (ref.read(currentExchangeNameStateProvider.state).state != - SimpleSwapExchange.exchangeName) { - ref.read(currentExchangeNameStateProvider.state).state = - SimpleSwapExchange.exchangeName; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName( - ref.read(currentExchangeNameStateProvider.state).state); - } - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: SimpleSwapExchange.exchangeName, - groupValue: ref - .watch(currentExchangeNameStateProvider.state) - .state, - onChanged: (value) { - if (value is String) { - ref - .read(currentExchangeNameStateProvider.state) - .state = value; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName(ref + if (isDesktop) + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (ref.read(currentExchangeNameStateProvider.state).state != + SimpleSwapExchange.exchangeName) { + ref.read(currentExchangeNameStateProvider.state).state = + SimpleSwapExchange.exchangeName; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider.state) + .state); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: SimpleSwapExchange.exchangeName, + groupValue: ref + .watch(currentExchangeNameStateProvider.state) + .state, + onChanged: (value) { + if (value is String) { + ref .read(currentExchangeNameStateProvider.state) - .state); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - SvgPicture.asset( - Assets.exchange.simpleSwap, - width: 24, - height: 24, - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - SimpleSwapExchange.exchangeName, - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark2, - ), + .state = value; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider + .state) + .state); + } + }, ), - if (from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero) - FutureBuilder( - future: SimpleSwapExchange().getEstimate( - from!, - to!, - // reversed ? toAmount! : fromAmount!, - fromAmount!, - fixedRate, - // reversed, - false, + ), + const SizedBox( + width: 14, + ), + SvgPicture.asset( + Assets.exchange.simpleSwap, + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + SimpleSwapExchange.exchangeName, + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), ), - builder: (context, - AsyncSnapshot<ExchangeResponse<Estimate>> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimate = snapshot.data?.value; - if (estimate != null) { - Decimal rate = (estimate.estimatedAmount / - fromAmount!) - .toDecimal(scaleOnInfinitePrecision: 12); + if (from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero) + FutureBuilder( + future: SimpleSwapExchange().getEstimate( + from!, + to!, + // reversed ? toAmount! : fromAmount!, + fromAmount!, + fixedRate, + // reversed, + false, + ), + builder: (context, + AsyncSnapshot<ExchangeResponse<Estimate>> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + final estimate = snapshot.data?.value; + if (estimate != null) { + Decimal rate = (estimate.estimatedAmount / + fromAmount!) + .toDecimal( + scaleOnInfinitePrecision: 12); - return Text( - "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + Coin coin; + try { + coin = + coinFromTickerCaseInsensitive(to!); + } catch (_) { + coin = Coin.bitcoin; + } + return Text( + "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( + value: rate, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + decimalPlaces: + Constants.decimalPlacesForCoin( + coin), + )} ${to!.toUpperCase()}", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } else { + Logging.instance.log( + "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}", + level: LogLevel.Warning, + ); + return Text( + "Failed to fetch rate", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), - decimalPlaces: to!.toUpperCase() == - Coin.monero.ticker.toUpperCase() - ? Constants.decimalPlacesMonero - : Constants.decimalPlaces, - )} ${to!.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - }, - ), - // if (!(from != null && - // to != null && - // (reversed - // ? toAmount != null && toAmount! > Decimal.zero - // : fromAmount != null && - // fromAmount! > Decimal.zero))) - if (!(from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), + ); + } + }, + ), + // if (!(from != null && + // to != null && + // (reversed + // ? toAmount != null && toAmount! > Decimal.zero + // : fromAmount != null && + // fromAmount! > Decimal.zero))) + if (!(from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero)) + Text( + "n/a", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), + ), + ], ), - ], + ), ), ), ), diff --git a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart index 31460c75f..361d49e65 100644 --- a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart +++ b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart @@ -7,8 +7,9 @@ import 'package:stackwallet/providers/providers.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/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; -import 'package:stackwallet/widgets/rounded_white_container.dart'; class RateTypeToggle extends ConsumerWidget { const RateTypeToggle({ @@ -21,106 +22,177 @@ class RateTypeToggle extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + final estimated = ref.watch(prefsChangeNotifierProvider .select((value) => value.exchangeRateType)) == ExchangeRateType.estimated; - return RoundedWhiteContainer( + return RoundedContainer( padding: const EdgeInsets.all(0), + color: isDesktop + ? Theme.of(context).extension<StackColors>()!.buttonBackSecondary + : Theme.of(context).extension<StackColors>()!.popupBG, child: Row( children: [ Expanded( - child: GestureDetector( - onTap: () { - if (!estimated) { - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.estimated; - onChanged?.call(ExchangeRateType.estimated); - } - }, - child: RoundedContainer( - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG - : Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.lock, - width: 12, - height: 14, - color: estimated - ? Theme.of(context).extension<StackColors>()!.textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - const SizedBox( - width: 5, - ), - Text( - "Estimate rate", - style: STextStyles.smallMed12(context).copyWith( - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: estimated + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (!estimated) { + ref.read(prefsChangeNotifierProvider).exchangeRateType = + ExchangeRateType.estimated; + onChanged?.call(ExchangeRateType.estimated); + } + }, + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(12), + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG + : Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.lockOpen, + width: 12, + height: 14, + color: isDesktop + ? estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), - ), - ], + const SizedBox( + width: 5, + ), + Text( + "Estimate rate", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), ), ), ), ), Expanded( - child: GestureDetector( - onTap: () { - if (estimated) { - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.fixed; - onChanged?.call(ExchangeRateType.fixed); - } - }, - child: RoundedContainer( - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG - : Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.lock, - width: 12, - height: 14, - color: !estimated - ? Theme.of(context).extension<StackColors>()!.textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - const SizedBox( - width: 5, - ), - Text( - "Fixed rate", - style: STextStyles.smallMed12(context).copyWith( - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: !estimated + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (estimated) { + ref.read(prefsChangeNotifierProvider).exchangeRateType = + ExchangeRateType.fixed; + onChanged?.call(ExchangeRateType.fixed); + } + }, + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(12), + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG + : Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.lock, + width: 12, + height: 14, + color: isDesktop + ? !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), - ), - ], + const SizedBox( + width: 5, + ), + Text( + "Fixed rate", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), ), ), ), diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 76c845027..5679d5be5 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -15,17 +15,24 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.d import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.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/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/background.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/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -157,34 +164,159 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { final sendAmount = Decimal.tryParse(trade.payInAmount) ?? Decimal.parse("-1"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Trade details", - style: STextStyles.navBarTitle(context), + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Trade details", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), ), ), - body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( + child: Padding( + padding: isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(0), + child: BranchedParent( + condition: isDesktop, + conditionBranchBuilder: (children) => Padding( + padding: const EdgeInsets.only( + right: 20, + ), + child: Padding( + padding: const EdgeInsets.only( + right: 12, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RoundedWhiteContainer( + borderColor: + Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: ListView( + primary: false, + shrinkWrap: true, + children: children, + ), + ), + if (!hasTx && + isStackCoin(trade.payInCurrency) && + (trade.status == "New" || + trade.status == "new" || + trade.status == "waiting" || + trade.status == "Waiting")) + const SizedBox( + height: 32, + ), + if (!hasTx && + isStackCoin(trade.payInCurrency) && + (trade.status == "New" || + trade.status == "new" || + trade.status == "waiting" || + trade.status == "Waiting")) + SecondaryButton( + label: "Send from Stack", + buttonHeight: ButtonHeight.l, + onPressed: () { + final amount = sendAmount; + final address = trade.payInAddress; + + final coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + Navigator.of(context).pushNamed( + SendFromView.routeName, + arguments: Tuple4( + coin, + amount, + address, + trade, + ), + ); + }, + ), + const SizedBox( + height: 32, + ), + ], + ), + ), + ), + otherBranchBuilder: (children) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: children, + ), + children: [ + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Container( + decoration: isDesktop + ? BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ) + : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + if (isDesktop) + Row( + children: [ + SvgPicture.asset( + _fetchIconAssetForStatus(trade.status), + width: 32, + height: 32, + ), + const SizedBox( + width: 16, + ), + SelectableText( + "Exchange", + style: STextStyles.desktopTextMedium(context), + ), + ], + ), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ SelectableText( "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", @@ -194,7 +326,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { height: 4, ), SelectableText( - "${Format.localizedStringAsFixed(value: sendAmount, locale: ref.watch( + "-${Format.localizedStringAsFixed(value: sendAmount, locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), decimalPlaces: trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}", @@ -202,136 +334,239 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { ), ], ), - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - ), - child: Center( - child: SvgPicture.asset( - _fetchIconAssetForStatus(trade.status), - width: 32, - height: 32, + if (!isDesktop) + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + ), + child: Center( + child: SvgPicture.asset( + _fetchIconAssetForStatus(trade.status), + width: 32, + height: 32, + ), ), ), - ), ], ), ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - trade.status, - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .colorForStatus(trade.status), - ), - ), - // ), - // ), - ], - ), - ), - if (!sentFromStack && !hasTx) - const SizedBox( + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( height: 12, ), - if (!sentFromStack && !hasTx) - RoundedContainer( - color: Theme.of(context) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + trade.status, + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .colorForStatus(trade.status), + ), + ), + // ), + // ), + ], + ), + ), + if (!sentFromStack && !hasTx) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (!sentFromStack && !hasTx) + RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: isDesktop + ? Theme.of(context).extension<StackColors>()!.popupBG + : Theme.of(context) .extension<StackColors>()! .warningBackground, - child: RichText( - text: TextSpan( - text: - "You must send at least ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}. ", - style: STextStyles.label700(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Amount", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 2, + ), + Text( + "${trade.payInAmount} ${trade.payInCurrency.toUpperCase()}", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], ), - children: [ - TextSpan( - text: - "If you send less than ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" - ? 12 - : 8, - )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", - style: STextStyles.label(context).copyWith( + IconCopyButton( + data: trade.payInAmount, + ), + ], + ), + const SizedBox( + height: 6, + ), + child, + ], + ), + child: RichText( + text: TextSpan( + text: + "You must send at least ${sendAmount.toStringAsFixed( + trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, + )} ${trade.payInCurrency.toUpperCase()}. ", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed) + : STextStyles.label(context).copyWith( color: Theme.of(context) .extension<StackColors>()! .warningForeground, ), - ), - ]), - ), - ), - if (sentFromStack) - const SizedBox( - height: 12, - ), - if (sentFromStack) - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Sent from", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - widget.walletName!, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 10, - ), - GestureDetector( - onTap: () { - final Coin coin = coinFromTickerCaseInsensitive( - trade.payInCurrency); - - Navigator.of(context).pushNamed( - TransactionDetailsView.routeName, - arguments: Tuple3( - transactionIfSentFromStack!, coin, walletId!), - ); - }, - child: Text( - "View transaction", - style: STextStyles.link2(context), + children: [ + TextSpan( + text: + "If you send less than ${sendAmount.toStringAsFixed( + trade.payInCurrency.toLowerCase() == "xmr" + ? 12 + : 8, + )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed) + : STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), ), - ), - ], + ]), + ), + ), + ), + if (sentFromStack) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, ), - ), - if (sentFromStack) - const SizedBox( - height: 12, - ), - if (sentFromStack) - RoundedWhiteContainer( - child: Column( + if (sentFromStack) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sent from", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + widget.walletName!, + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 10, + ), + BlueTextButton( + text: "View transaction", + onTap: () { + final Coin coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + if (isDesktop) { + Navigator.of(context).push( + FadePageRoute<void>( + DesktopDialog( + maxHeight: + MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionDetailsView( + coin: coin, + transaction: transactionIfSentFromStack!, + walletId: walletId!, + ), + ), + const RouteSettings( + name: TransactionDetailsView.routeName, + ), + ), + ); + } else { + Navigator.of(context).pushNamed( + TransactionDetailsView.routeName, + arguments: Tuple3( + transactionIfSentFromStack!, coin, walletId!), + ); + } + }, + ), + ], + ), + ), + if (sentFromStack) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (sentFromStack) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -347,252 +582,224 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { ), ], ), - ), - if (!sentFromStack && !hasTx) - const SizedBox( - height: 12, - ), - if (!sentFromStack && !hasTx) - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (isDesktop) + IconCopyButton( + data: trade.payInAddress, + ), + ], + ), + ), + if (!sentFromStack && !hasTx) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (!sentFromStack && !hasTx) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Send ${trade.payInCurrency.toUpperCase()} to this address", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final address = trade.payInAddress; - await Clipboard.setData( - ClipboardData( - text: address, - ), - ); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: Row( + Text( + "Send ${trade.payInCurrency.toUpperCase()} to this address", + style: STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconCopyButton( + data: trade.payInAddress, + ) + : GestureDetector( + onTap: () async { + final address = trade.payInAddress; + await Clipboard.setData( + ClipboardData( + text: address, + ), + ); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 12, + height: 12, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + trade.payInAddress, + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 10, + ), + GestureDetector( + onTap: () { + 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: [ - SvgPicture.asset( - Assets.svg.copy, - width: 12, - height: 12, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, + Center( + child: Text( + "Send ${trade.payInCurrency.toUpperCase()} to this address", + style: STextStyles.pageTitleH2(context), + ), ), const SizedBox( - width: 4, + height: 12, ), - Text( - "Copy", - style: STextStyles.link2(context), + Center( + child: RepaintBoundary( + // key: _qrKey, + child: SizedBox( + width: width + 20, + height: width + 20, + child: QrImage( + data: trade.payInAddress, + 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 { + // await _capturePng(true); + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Cancel", + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), ), ], ), - ), - ], - ), - const SizedBox( - height: 4, - ), - SelectableText( - trade.payInAddress, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 10, - ), - GestureDetector( - onTap: () { - 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( - "Send ${trade.payInCurrency.toUpperCase()} to this address", - style: - STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: RepaintBoundary( - // key: _qrKey, - child: SizedBox( - width: width + 20, - height: width + 20, - child: QrImage( - data: trade.payInAddress, - 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 { - // await _capturePng(true); - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorDark), - ), - ), - ), - ), - ], - ), - ); - }, ); }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.qrcode, - width: 12, - height: 12, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Show QR code", - style: STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ); + }, + child: Row( children: [ - Text( - "Trade note", - style: STextStyles.itemSubtitle(context), + SvgPicture.asset( + Assets.svg.qrcode, + width: 12, + height: 12, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, ), - GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditTradeNoteView.routeName, - arguments: Tuple2( - tradeId, - ref - .read(tradeNoteServiceProvider) - .getNote(tradeId: tradeId), - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.link2(context), - ), - ], - ), + const SizedBox( + width: 4, + ), + Text( + "Show QR code", + style: STextStyles.link2(context), ), ], ), - const SizedBox( - height: 4, - ), - SelectableText( - ref.watch(tradeNoteServiceProvider.select( - (value) => value.getNote(tradeId: tradeId))), - style: STextStyles.itemSubtitle12(context), - ), - ], - ), + ), + ], ), - if (sentFromStack) - const SizedBox( + ), + isDesktop + ? const _Divider() + : const SizedBox( height: 12, ), - if (sentFromStack) - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction note", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade note", + style: STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconPencilButton( + onPressed: () { + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditTradeNoteView( + tradeId: tradeId, + note: _note, + ), + ); + }, + ); + }, + ) + : GestureDetector( onTap: () { Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple3( - transactionIfSentFromStack!.txid, - walletId!, - _note, + EditTradeNoteView.routeName, + arguments: Tuple2( + tradeId, + ref + .read(tradeNoteServiceProvider) + .getNote(tradeId: tradeId), ), ); }, @@ -616,193 +823,372 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { ], ), ), - ], - ), - const SizedBox( - height: 4, - ), - FutureBuilder( - future: ref.watch( - notesServiceChangeNotifierProvider(walletId!) - .select((value) => value.getNoteFor( - txid: transactionIfSentFromStack!.txid))), - builder: - (builderContext, AsyncSnapshot<String> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _note = snapshot.data ?? ""; - } - return SelectableText( - _note, - style: STextStyles.itemSubtitle12(context), - ); - }, + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + ref.watch(tradeNoteServiceProvider + .select((value) => value.getNote(tradeId: tradeId))), + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + if (sentFromStack) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (sentFromStack) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction note", + style: STextStyles.itemSubtitle(context), ), + isDesktop + ? IconPencilButton( + onPressed: () { + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditNoteView( + txid: + transactionIfSentFromStack!.txid, + walletId: walletId!, + note: _note, + ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditNoteView.routeName, + arguments: Tuple3( + transactionIfSentFromStack!.txid, + walletId!, + _note, + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Edit", + style: STextStyles.link2(context), + ), + ], + ), + ), ], ), - ), - const SizedBox( - height: 12, + const SizedBox( + height: 4, + ), + FutureBuilder( + future: ref.watch( + notesServiceChangeNotifierProvider(walletId!).select( + (value) => value.getNoteFor( + txid: transactionIfSentFromStack!.txid))), + builder: + (builderContext, AsyncSnapshot<String> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _note = snapshot.data ?? ""; + } + return SelectableText( + _note, + style: STextStyles.itemSubtitle12(context), + ); + }, + ), + ], ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Date", style: STextStyles.itemSubtitle(context), ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - Format.extractDateFrom( - trade.timestamp.millisecondsSinceEpoch ~/ 1000), - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), ], ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + if (!isDesktop) + SelectableText( + Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + style: STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + IconCopyButton( + data: Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Exchange", style: STextStyles.itemSubtitle(context), ), - SelectableText( - trade.exchangeName, - style: STextStyles.itemSubtitle12(context), - ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + trade.exchangeName, + style: STextStyles.itemSubtitle12(context), + ), ], ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( + if (isDesktop) + IconCopyButton( + data: trade.exchangeName, + ), + if (!isDesktop) + SelectableText( + trade.exchangeName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Trade ID", style: STextStyles.itemSubtitle(context), ), - const Spacer(), - Row( - children: [ - Text( - trade.tradeId, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, - ), - GestureDetector( - onTap: () async { - final data = ClipboardData(text: trade.tradeId); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - width: 12, - ), - ) - ], - ) - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Tracking", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - Builder(builder: (context) { - late final String url; - switch (trade.exchangeName) { - case ChangeNowExchange.exchangeName: - url = - "https://changenow.io/exchange/txs/${trade.tradeId}"; - break; - case SimpleSwapExchange.exchangeName: - url = - "https://simpleswap.io/exchange?id=${trade.tradeId}"; - break; - } - return GestureDetector( - onTap: () { - launchUrl( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ); - }, - child: Text( - url, - style: STextStyles.link2(context), - ), - ); - }), - ], - ), - ), - const SizedBox( - height: 12, - ), - if (isStackCoin(trade.payInCurrency) && - (trade.status == "New" || - trade.status == "new" || - trade.status == "waiting" || - trade.status == "Waiting")) - SecondaryButton( - label: "Send from Stack", - onPressed: () { - final amount = sendAmount; - final address = trade.payInAddress; - - final coin = - coinFromTickerCaseInsensitive(trade.payInCurrency); - - Navigator.of(context).pushNamed( - SendFromView.routeName, - arguments: Tuple4( - coin, - amount, - address, - trade, + if (isDesktop) + const SizedBox( + height: 2, ), - ); - }, + if (isDesktop) + Text( + trade.tradeId, + style: STextStyles.itemSubtitle12(context), + ), + ], ), - ], + if (isDesktop) + IconCopyButton( + data: trade.tradeId, + ), + if (!isDesktop) + Row( + children: [ + Text( + trade.tradeId, + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + GestureDetector( + onTap: () async { + final data = ClipboardData(text: trade.tradeId); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 12, + ), + ) + ], + ), + ], + ), ), - ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Tracking", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + Builder(builder: (context) { + late final String url; + switch (trade.exchangeName) { + case ChangeNowExchange.exchangeName: + url = + "https://changenow.io/exchange/txs/${trade.tradeId}"; + break; + case SimpleSwapExchange.exchangeName: + url = + "https://simpleswap.io/exchange?id=${trade.tradeId}"; + break; + } + return GestureDetector( + onTap: () { + launchUrl( + Uri.parse(url), + mode: LaunchMode.externalApplication, + ); + }, + child: Text( + url, + style: STextStyles.link2(context), + ), + ); + }), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + if (!isDesktop && + !hasTx && + isStackCoin(trade.payInCurrency) && + (trade.status == "New" || + trade.status == "new" || + trade.status == "waiting" || + trade.status == "Waiting")) + SecondaryButton( + label: "Send from Stack", + onPressed: () { + final amount = sendAmount; + final address = trade.payInAddress; + + final coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + Navigator.of(context).pushNamed( + SendFromView.routeName, + arguments: Tuple4( + coin, + amount, + address, + trade, + ), + ); + }, + ), + ], ), ), ); } } + +class _Divider extends StatelessWidget { + const _Divider({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ); + } +} diff --git a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart index c816d4fe8..915fb33ba 100644 --- a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart +++ b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; class WalletInitiatedExchangeView extends ConsumerStatefulWidget { @@ -47,75 +48,77 @@ class _WalletInitiatedExchangeViewState 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: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + 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( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(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: [ - StepRow( - count: 4, - current: 0, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Exchange amount", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Network fees and other exchange charges are included in the rate.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 24, - ), - ExchangeForm( - walletId: walletId, - coin: coin, - ), - ], + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(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: [ + StepRow( + count: 4, + current: 0, + width: width, + ), + const SizedBox( + height: 14, + ), + Text( + "Exchange amount", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 24, + ), + ExchangeForm( + walletId: walletId, + coin: coin, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index 33744570b..5f41bfb16 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -20,6 +20,7 @@ import 'package:stackwallet/utilities/constants.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -141,129 +142,138 @@ class _HomeViewState extends ConsumerState<HomeView> { debugPrint("BUILD: $runtimeType"); return WillPopScope( onWillPop: _onWillPop, - child: Scaffold( - key: _key, - appBar: AppBar( - automaticallyImplyLeading: false, - title: Row( - children: [ - GestureDetector( - onTap: _hiddenOptions, - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 24, - height: 24, - ), - ), - const SizedBox( - width: 16, - ), - Text( - "My Stack", - 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("walletsViewAlertsButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - ref.watch(notificationsProvider - .select((value) => value.hasUnreadNotifications)) - ? Assets.svg.bellNew(context) - : Assets.svg.bell, - width: 20, - height: 20, - color: ref.watch(notificationsProvider - .select((value) => value.hasUnreadNotifications)) - ? null - : Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, + child: Background( + child: Scaffold( + backgroundColor: Colors.transparent, + key: _key, + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: + Theme.of(context).extension<StackColors>()!.backgroundAppBar, + title: Row( + children: [ + GestureDetector( + onTap: _hiddenOptions, + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 24, + height: 24, ), - onPressed: () { - // reset unread state - ref.refresh(unreadNotificationsStateProvider); - - Navigator.of(context) - .pushNamed(NotificationsView.routeName) - .then((_) { - final Set<int> unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - List<Future<void>> futures = []; - for (int i = 0; - i < unreadNotificationIds.length - 1; - i++) { - futures.add(ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.elementAt(i), false)); - } - - // wait for multiple to update if any - Future.wait(futures).then((_) { - // only notify listeners once - ref - .read(notificationsProvider) - .markAsRead(unreadNotificationIds.last, true); - }); - }); - }, ), - ), + const SizedBox( + width: 16, + ), + Text( + "My Stack", + style: STextStyles.navBarTitle(context), + ) + ], ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletsViewSettingsButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.gear, + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletsViewAlertsButton"), + size: 36, + shadows: const [], color: Theme.of(context) .extension<StackColors>()! - .topNavIconPrimary, - width: 20, - height: 20, + .backgroundAppBar, + icon: SvgPicture.asset( + ref.watch(notificationsProvider + .select((value) => value.hasUnreadNotifications)) + ? Assets.svg.bellNew(context) + : Assets.svg.bell, + width: 20, + height: 20, + color: ref.watch(notificationsProvider + .select((value) => value.hasUnreadNotifications)) + ? null + : Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + ), + onPressed: () { + // reset unread state + ref.refresh(unreadNotificationsStateProvider); + + Navigator.of(context) + .pushNamed(NotificationsView.routeName) + .then((_) { + final Set<int> unreadNotificationIds = ref + .read(unreadNotificationsStateProvider.state) + .state; + if (unreadNotificationIds.isEmpty) return; + + List<Future<void>> futures = []; + for (int i = 0; + i < unreadNotificationIds.length - 1; + i++) { + futures.add(ref + .read(notificationsProvider) + .markAsRead( + unreadNotificationIds.elementAt(i), false)); + } + + // wait for multiple to update if any + Future.wait(futures).then((_) { + // only notify listeners once + ref + .read(notificationsProvider) + .markAsRead(unreadNotificationIds.last, true); + }); + }); + }, ), - onPressed: () { - debugPrint("main view settings tapped"); - Navigator.of(context) - .pushNamed(GlobalSettingsView.routeName); - }, ), ), - ), - ], - ), - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Column( + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletsViewSettingsButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension<StackColors>()! + .backgroundAppBar, + icon: SvgPicture.asset( + Assets.svg.gear, + color: Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + width: 20, + height: 20, + ), + onPressed: () { + debugPrint("main view settings tapped"); + Navigator.of(context) + .pushNamed(GlobalSettingsView.routeName); + }, + ), + ), + ), + ], + ), + body: Column( children: [ if (Constants.enableExchange) Container( decoration: BoxDecoration( - color: - Theme.of(context).extension<StackColors>()!.background, + color: Theme.of(context) + .extension<StackColors>()! + .backgroundAppBar, boxShadow: [ Theme.of(context) .extension<StackColors>()! diff --git a/lib/pages/intro_view.dart b/lib/pages/intro_view.dart index 494f23973..be0a9b82a 100644 --- a/lib/pages/intro_view.dart +++ b/lib/pages/intro_view.dart @@ -3,14 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/prefs.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'; +import 'package:stackwallet/widgets/background.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:stackwallet/utilities/prefs.dart'; - class IntroView extends StatefulWidget { const IntroView({Key? key}) : super(key: key); @@ -32,118 +31,120 @@ class _IntroViewState extends State<IntroView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType "); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - body: Center( - child: !isDesktop - ? Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer( - flex: 2, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 300, - ), - child: Image( - image: AssetImage( - Assets.png.stack, - ), - ), - ), - ), - const Spacer( - flex: 1, - ), - AppNameText( - isDesktop: isDesktop, - ), - const SizedBox( - height: 8, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 48, - ), - child: IntroAboutText( - isDesktop: isDesktop, - ), - ), - const Spacer( - flex: 4, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: PrivacyAndTOSText( - isDesktop: isDesktop, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: Row( - children: [ - Expanded( - child: GetStartedButton( - isDesktop: isDesktop, - ), - ), - ], - ), - ), - ], - ) - : SizedBox( - width: 350, - height: 540, - child: Column( + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + body: Center( + child: !isDesktop + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ const Spacer( flex: 2, ), - SizedBox( - width: 130, - height: 130, - child: SvgPicture.asset( - Assets.svg.stackIcon(context), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 300, + ), + child: Image( + image: AssetImage( + Assets.png.stack, + ), + ), ), ), const Spacer( - flex: 42, + flex: 1, ), AppNameText( isDesktop: isDesktop, ), - const Spacer( - flex: 24, + const SizedBox( + height: 8, ), - IntroAboutText( - isDesktop: isDesktop, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 48, + ), + child: IntroAboutText( + isDesktop: isDesktop, + ), ), const Spacer( - flex: 42, + flex: 4, ), - GetStartedButton( - isDesktop: isDesktop, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: PrivacyAndTOSText( + isDesktop: isDesktop, + ), ), - const Spacer( - flex: 65, - ), - PrivacyAndTOSText( - isDesktop: isDesktop, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Row( + children: [ + Expanded( + child: GetStartedButton( + isDesktop: isDesktop, + ), + ), + ], + ), ), ], + ) + : SizedBox( + width: 350, + height: 540, + child: Column( + children: [ + const Spacer( + flex: 2, + ), + SizedBox( + width: 130, + height: 130, + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + ), + ), + const Spacer( + flex: 42, + ), + AppNameText( + isDesktop: isDesktop, + ), + const Spacer( + flex: 24, + ), + IntroAboutText( + isDesktop: isDesktop, + ), + const Spacer( + flex: 42, + ), + GetStartedButton( + isDesktop: isDesktop, + ), + const Spacer( + flex: 65, + ), + PrivacyAndTOSText( + isDesktop: isDesktop, + ), + ], + ), ), - ), + ), ), ); } diff --git a/lib/pages/loading_view.dart b/lib/pages/loading_view.dart index c252913df..b7db1aa58 100644 --- a/lib/pages/loading_view.dart +++ b/lib/pages/loading_view.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; class LoadingView extends StatelessWidget { const LoadingView({Key? key}) : super(key: key); @@ -11,25 +12,27 @@ class LoadingView extends StatelessWidget { @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Center( - child: SizedBox( - width: min(size.width, size.height) * 0.5, - child: Lottie.asset( - Assets.lottie.test2, - animate: true, - repeat: true, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + body: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Center( + child: SizedBox( + width: min(size.width, size.height) * 0.5, + child: Lottie.asset( + Assets.lottie.test2, + animate: true, + repeat: true, + ), ), + // child: Image( + // image: AssetImage( + // Assets.png.splash, + // ), + // width: MediaQuery.of(context).size.width * 0.5, + // ), ), - // child: Image( - // image: AssetImage( - // Assets.png.splash, - // ), - // width: MediaQuery.of(context).size.width * 0.5, - // ), ), ), ); diff --git a/lib/pages/notification_views/notifications_view.dart b/lib/pages/notification_views/notifications_view.dart index a034a34e4..a574e083d 100644 --- a/lib/pages/notification_views/notifications_view.dart +++ b/lib/pages/notification_views/notifications_view.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -43,66 +44,68 @@ class _NotificationsViewState extends ConsumerState<NotificationsView> { .where((element) => element.walletId == widget.walletId) .toList(growable: false); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - title: Text( - "Notifications", - style: STextStyles.navBarTitle(context), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + title: Text( + "Notifications", + style: STextStyles.navBarTitle(context), + ), + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), ), - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: notifications.isNotEmpty - ? Column( - children: [ - Expanded( - child: ListView.builder( - shrinkWrap: true, - itemCount: notifications.length, - itemBuilder: (builderContext, index) { - final notification = notifications[index]; - if (notification.read == false) { - ref - .read(unreadNotificationsStateProvider.state) - .state - .add(notification.id); - } - return Padding( - padding: const EdgeInsets.all(4), - child: NotificationCard( - notification: notifications[index], - ), - ); - }, + body: Padding( + padding: const EdgeInsets.all(12), + child: notifications.isNotEmpty + ? Column( + children: [ + Expanded( + child: ListView.builder( + shrinkWrap: true, + itemCount: notifications.length, + itemBuilder: (builderContext, index) { + final notification = notifications[index]; + if (notification.read == false) { + ref + .read(unreadNotificationsStateProvider.state) + .state + .add(notification.id); + } + return Padding( + padding: const EdgeInsets.all(4), + child: NotificationCard( + notification: notifications[index], + ), + ); + }, + ), ), - ), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: RoundedWhiteContainer( - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "Notifications will appear here", - style: STextStyles.itemSubtitle(context), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "Notifications will appear here", + style: STextStyles.itemSubtitle(context), + ), ), ), ), - ), - ) - ], - ), + ) + ], + ), + ), ), ); } diff --git a/lib/pages/pinpad_views/create_pin_view.dart b/lib/pages/pinpad_views/create_pin_view.dart index f8b84cfb4..3180689c2 100644 --- a/lib/pages/pinpad_views/create_pin_view.dart +++ b/lib/pages/pinpad_views/create_pin_view.dart @@ -2,10 +2,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/biometrics.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_pin_put/custom_pin_put.dart'; @@ -20,15 +21,11 @@ class CreatePinView extends ConsumerStatefulWidget { const CreatePinView({ Key? key, this.popOnSuccess = false, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), this.biometrics = const Biometrics(), }) : super(key: key); static const String routeName = "/createPin"; - final FlutterSecureStorageInterface secureStore; final Biometrics biometrics; final bool popOnSuccess; @@ -58,12 +55,12 @@ class _CreatePinViewState extends ConsumerState<CreatePinView> { final TextEditingController _pinPutController2 = TextEditingController(); final FocusNode _pinPutFocusNode2 = FocusNode(); - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late Biometrics biometrics; @override initState() { - _secureStore = widget.secureStore; + _secureStore = ref.read(secureStoreProvider); biometrics = widget.biometrics; super.initState(); } @@ -80,215 +77,219 @@ class _CreatePinViewState extends ConsumerState<CreatePinView> { @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: 70)); - } - if (mounted) { - Navigator.of(context).pop(widget.popOnSuccess); - } - }, + return Background( + 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(widget.popOnSuccess); + } + }, + ), ), - ), - body: SafeArea( - child: PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - children: [ - // page 1 - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Create a PIN", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "This PIN protects access to your wallet.", - style: STextStyles.subtitle(context), - ), - const SizedBox( - height: 36, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, + body: SafeArea( + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + // page 1 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Create a PIN", + style: STextStyles.pageTitleH1(context), ), - focusNode: _pinPutFocusNode1, - controller: _pinPutController1, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: - Theme.of(context).extension<StackColors>()!.background, - counterText: "", + const SizedBox( + height: 8, ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, + Text( + "This PIN protects access to your wallet.", + style: STextStyles.subtitle(context), + ), + const SizedBox( + height: 36, + ), + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label(context).copyWith( + fontSize: 1, + ), + focusNode: _pinPutFocusNode1, + controller: _pinPutController1, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( color: Theme.of(context) .extension<StackColors>()! .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) { - if (pin.length == Constants.pinLength) { - _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.linear, - ); - } - }, - ), - ], - ), - - // page 2 - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Confirm PIN", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "This PIN protects access to your wallet.", - style: STextStyles.subtitle(context), - ), - const SizedBox( - height: 36, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle3, - fontSize: 1, - ), - focusNode: _pinPutFocusNode2, - controller: _pinPutController2, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: - Theme.of(context).extension<StackColors>()!.background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) async { - // _onSubmitCount++; - // if (_onSubmitCount - _onSubmitFailCount > 1) return; - - if (_pinPutController1.text == _pinPutController2.text) { - // ask if want to use biometrics - final bool useBiometrics = (Platform.isLinux) - ? false - : await biometrics.authenticate( - cancelButtonText: "SKIP", - localizedReason: - "You can use your fingerprint to unlock the wallet and confirm transactions.", - title: "Enable fingerprint authentication", - ); - - //TODO investigate why this crashes IOS, maybe ios persists securestorage even after an uninstall? - // This should never fail as we are writing a new pin - // assert( - // (await _secureStore.read(key: "stack_pin")) == null); - // possible alternative to the above but it does not guarantee we aren't overwriting a pin - // if (!Platform.isLinux) - // assert((await _secureStore.read(key: "stack_pin")) == - // null); - assert(ref.read(prefsChangeNotifierProvider).hasPin == - false); - - await _secureStore.write(key: "stack_pin", value: pin); - - ref.read(prefsChangeNotifierProvider).useBiometrics = - useBiometrics; - ref.read(prefsChangeNotifierProvider).hasPin = true; - - await Future<void>.delayed( - const Duration(milliseconds: 200)); - - if (mounted) { - if (!widget.popOnSuccess) { - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ); - } else { - Navigator.of(context).pop(); - } + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) { + if (pin.length == Constants.pinLength) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); } - } else { - // _onSubmitFailCount++; - _pageController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.linear, - ); + }, + ), + ], + ), - showFloatingFlushBar( - type: FlushBarType.warning, - message: "PIN codes do not match. Try again.", - context: context, - iconAsset: Assets.svg.alertCircle, - ); + // page 2 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Confirm PIN", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "This PIN protects access to your wallet.", + style: STextStyles.subtitle(context), + ), + const SizedBox( + height: 36, + ), + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle3, + fontSize: 1, + ), + focusNode: _pinPutFocusNode2, + controller: _pinPutController2, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) async { + // _onSubmitCount++; + // if (_onSubmitCount - _onSubmitFailCount > 1) return; - _pinPutController1.text = ''; - _pinPutController2.text = ''; - } - }, - ), - ], - ), - ], + if (_pinPutController1.text == _pinPutController2.text) { + // ask if want to use biometrics + final bool useBiometrics = (Platform.isLinux) + ? false + : await biometrics.authenticate( + cancelButtonText: "SKIP", + localizedReason: + "You can use your fingerprint to unlock the wallet and confirm transactions.", + title: "Enable fingerprint authentication", + ); + + //TODO investigate why this crashes IOS, maybe ios persists securestorage even after an uninstall? + // This should never fail as we are writing a new pin + // assert( + // (await _secureStore.read(key: "stack_pin")) == null); + // possible alternative to the above but it does not guarantee we aren't overwriting a pin + // if (!Platform.isLinux) + // assert((await _secureStore.read(key: "stack_pin")) == + // null); + assert(ref.read(prefsChangeNotifierProvider).hasPin == + false); + + await _secureStore.write(key: "stack_pin", value: pin); + + ref.read(prefsChangeNotifierProvider).useBiometrics = + useBiometrics; + ref.read(prefsChangeNotifierProvider).hasPin = true; + + await Future<void>.delayed( + const Duration(milliseconds: 200)); + + if (mounted) { + if (!widget.popOnSuccess) { + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ); + } else { + Navigator.of(context).pop(); + } + } + } else { + // _onSubmitFailCount++; + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); + + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ); + + _pinPutController1.text = ''; + _pinPutController2.text = ''; + } + }, + ), + ], + ), + ], + ), ), ), ); diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index 00d8b1914..455d5ee3b 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -2,12 +2,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:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; // import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; // import 'package:stackwallet/providers/global/should_show_lockscreen_on_resume_state_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -17,6 +17,7 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_pin_put/custom_pin_put.dart'; import 'package:stackwallet/widgets/shake/shake.dart'; @@ -33,9 +34,6 @@ class LockscreenView extends ConsumerStatefulWidget { this.popOnSuccess = false, this.isInitialAppLogin = false, this.routeOnSuccessArguments, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), this.biometrics = const Biometrics(), this.onSuccess, }) : super(key: key); @@ -50,7 +48,6 @@ class LockscreenView extends ConsumerStatefulWidget { final String biometricsAuthenticationTitle; final String biometricsLocalizedReason; final String biometricsCancelButtonString; - final FlutterSecureStorageInterface secureStore; final Biometrics biometrics; final VoidCallback? onSuccess; @@ -134,7 +131,7 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { void initState() { _shakeController = ShakeController(); - _secureStore = widget.secureStore; + _secureStore = ref.read(secureStoreProvider); biometrics = widget.biometrics; _attempts = 0; _timeout = Duration.zero; @@ -162,176 +159,180 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { final _pinTextController = TextEditingController(); final FocusNode _pinFocusNode = FocusNode(); - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late Biometrics biometrics; - Scaffold get _body => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: widget.showBackButton - ? 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(); - } - }, - ) - : Container(), - ), - body: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Shake( - animationDuration: const Duration(milliseconds: 700), - animationRange: 12, - controller: _shakeController, - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Enter PIN", - style: STextStyles.pageTitleH1(context), + Widget get _body => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: widget.showBackButton + ? 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(); + } + }, + ) + : Container(), + ), + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Shake( + animationDuration: const Duration(milliseconds: 700), + animationRange: 12, + controller: _shakeController, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Enter PIN", + style: STextStyles.pageTitleH1(context), + ), ), - ), - const SizedBox( - height: 52, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, + const SizedBox( + height: 52, ), - focusNode: _pinFocusNode, - controller: _pinTextController, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension<StackColors>()! - .background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label(context).copyWith( + fontSize: 1, + ), + focusNode: _pinFocusNode, + controller: _pinTextController, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( color: Theme.of(context) .extension<StackColors>()! .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) async { - _attempts++; + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) async { + _attempts++; - if (_attempts > maxAttemptsBeforeThrottling) { - _attemptLock = true; - switch (_attempts) { - case 4: - _timeout = const Duration(seconds: 30); - break; + if (_attempts > maxAttemptsBeforeThrottling) { + _attemptLock = true; + switch (_attempts) { + case 4: + _timeout = const Duration(seconds: 30); + break; - case 5: - _timeout = const Duration(seconds: 60); - break; + case 5: + _timeout = const Duration(seconds: 60); + break; - case 6: - _timeout = const Duration(minutes: 5); - break; + case 6: + _timeout = const Duration(minutes: 5); + break; - case 7: - _timeout = const Duration(minutes: 10); - break; + case 7: + _timeout = const Duration(minutes: 10); + break; - case 8: - _timeout = const Duration(minutes: 20); - break; + case 8: + _timeout = const Duration(minutes: 20); + break; - case 9: - _timeout = const Duration(minutes: 30); - break; + case 9: + _timeout = const Duration(minutes: 30); + break; - default: - _timeout = const Duration(minutes: 60); + default: + _timeout = const Duration(minutes: 60); + } + + unawaited( + Future<void>.delayed(_timeout).then((_) { + _attemptLock = false; + _attempts = 0; + })); } - unawaited(Future<void>.delayed(_timeout).then((_) { - _attemptLock = false; - _attempts = 0; - })); - } + if (_attemptLock) { + String prettyTime = ""; + if (_timeout.inSeconds >= 60) { + prettyTime += "${_timeout.inMinutes} minutes"; + } else { + prettyTime += "${_timeout.inSeconds} seconds"; + } - if (_attemptLock) { - String prettyTime = ""; - if (_timeout.inSeconds >= 60) { - prettyTime += "${_timeout.inMinutes} minutes"; + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Incorrect PIN entered too many times. Please wait $prettyTime", + context: context, + iconAsset: Assets.svg.alertCircle, + )); + + await Future<void>.delayed( + const Duration(milliseconds: 100)); + + _pinTextController.text = ''; + + return; + } + + final storedPin = + await _secureStore.read(key: 'stack_pin'); + + if (storedPin == pin) { + await Future<void>.delayed( + const Duration(milliseconds: 200)); + unawaited(_onUnlock()); } else { - prettyTime += "${_timeout.inSeconds} seconds"; + unawaited(_shakeController.shake()); + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Incorrect PIN. Please try again", + context: context, + iconAsset: Assets.svg.alertCircle, + )); + + await Future<void>.delayed( + const Duration(milliseconds: 100)); + + _pinTextController.text = ''; } - - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: - "Incorrect PIN entered too many times. Please wait $prettyTime", - context: context, - iconAsset: Assets.svg.alertCircle, - )); - - await Future<void>.delayed( - const Duration(milliseconds: 100)); - - _pinTextController.text = ''; - - return; - } - - final storedPin = - await _secureStore.read(key: 'stack_pin'); - - if (storedPin == pin) { - await Future<void>.delayed( - const Duration(milliseconds: 200)); - unawaited(_onUnlock()); - } else { - unawaited(_shakeController.shake()); - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Incorrect PIN. Please try again", - context: context, - iconAsset: Assets.svg.alertCircle, - )); - - await Future<void>.delayed( - const Duration(milliseconds: 100)); - - _pinTextController.text = ''; - } - }, - ), - ], + }, + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 744d8b53c..46b48bb42 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; // import 'package:document_file_save_plus/document_file_save_plus.dart'; import 'package:decimal/decimal.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_svg/svg.dart'; @@ -11,6 +13,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -19,7 +22,12 @@ 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/background.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 +58,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(); @@ -62,26 +74,217 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { await image.toByteData(format: ui.ImageByteFormat.png); Uint8List pngBytes = byteData!.buffer.asUint8List(); - // if (shouldSaveInsteadOfShare) { - // await DocumentFileSavePlus.saveFile( - // pngBytes, - // "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png", - // "image/png"); - // } else { - final tempDir = await getTemporaryDirectory(); - final file = await File("${tempDir.path}/qrcode.png").create(); - await file.writeAsBytes(pngBytes); + if (shouldSaveInsteadOfShare) { + if (Util.isDesktop) { + final dir = Directory("${Platform.environment['HOME']}"); + if (!dir.existsSync()) { + throw Exception( + "Home dir not found while trying to open filepicker on QR image save"); + } + final path = await FilePicker.platform.saveFile( + fileName: "qrcode.png", + initialDirectory: dir.path, + ); - await Share.shareFiles(["${tempDir.path}/qrcode.png"], - text: "Receive URI QR Code"); - // } + if (path != null) { + final file = File(path); + if (file.existsSync()) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$path already exists!", + context: context, + ), + ); + } else { + await file.writeAsBytes(pngBytes); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "$path saved!", + context: context, + ), + ); + } + } + } else { + // await DocumentFileSavePlus.saveFile( + // pngBytes, + // "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png", + // "image/png"); + } + } else { + final tempDir = await getTemporaryDirectory(); + final file = await File("${tempDir.path}/qrcode.png").create(); + await file.writeAsBytes(pngBytes); + + await Share.shareFiles(["${tempDir.path}/qrcode.png"], + text: "Receive URI QR Code"); + } } catch (e) { debugPrint(e.toString()); } } + 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; + } + + Map<String, String> queryParams = {}; + + if (amountString.isNotEmpty) { + queryParams["amount"] = amountString; + } + if (noteString.isNotEmpty) { + queryParams["message"] = noteString; + } + + String receivingAddress = widget.receivingAddress; + if ((widget.coin == Coin.bitcoincash || + widget.coin == Coin.bitcoincashTestnet) && + receivingAddress.contains(":")) { + // remove cash addr prefix + receivingAddress = receivingAddress.split(":").sublist(1).join(); + } + + final uriString = AddressUtils.buildUriString( + widget.coin, + receivingAddress, + queryParams, + ); + + 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; + + String receivingAddress = widget.receivingAddress; + if ((widget.coin == Coin.bitcoincash || + widget.coin == Coin.bitcoincashTestnet) && + receivingAddress.contains(":")) { + // remove cash addr prefix + receivingAddress = receivingAddress.split(":").sublist(1).join(); + } + + _uriString = AddressUtils.buildUriString( + widget.coin, + receivingAddress, + {}, + ); + amountController = TextEditingController(); noteController = TextEditingController(); super.initState(); @@ -100,311 +303,342 @@ 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(); - } - }, - ), - 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, + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + 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(); + } + }, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, + 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: 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), - ), - ), - ], + 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, + buttonHeight: isDesktop ? ButtonHeight.l : null, + ), + 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, + width: isDesktop ? 370 : null, + 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( + mainAxisAlignment: isDesktop + ? MainAxisAlignment.center + : MainAxisAlignment.start, + children: [ + if (!isDesktop) + SecondaryButton( + width: 170, + buttonHeight: + isDesktop ? ButtonHeight.l : null, + onPressed: () async { + await _capturePng(false); + }, + label: "Share", + icon: SvgPicture.asset( + Assets.svg.share, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), + if (!isDesktop) + const SizedBox( + width: 16, + ), + PrimaryButton( + width: 170, + buttonHeight: + isDesktop ? ButtonHeight.l : null, + onPressed: () async { + // TODO: add save functionality instead of share + // save works on linux at the moment + await _capturePng(true); + }, + label: "Save", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + ), + ], + ) + ], + ), + ), + ], + ), + ], + ), + ], + ), ), ); } diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 50e0ffd5e..1ba00f8ee 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -12,9 +12,9 @@ import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.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/widgets/background.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/custom_loading_overlay.dart'; @@ -115,147 +115,149 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> { } }); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Receive ${coin.ticker}", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Receive ${coin.ticker}", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: 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: 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: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ], - ), - const SizedBox( - height: 4, - ), - Row( - children: [ - Expanded( - child: Text( - receivingAddress, - style: STextStyles.itemSubtitle12(context), - ), - ), - ], - ), - ], - ), - ), - ), - if (coin != Coin.epicCash) - const SizedBox( - height: 12, - ), - if (coin != Coin.epicCash) - TextButton( - onPressed: generateNewAddress, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Generate new address", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - const SizedBox( - height: 30, - ), - RoundedWhiteContainer( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Center( + body: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: 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: RoundedWhiteContainer( child: Column( children: [ - QrImage( - data: "${coin.uriScheme}:$receivingAddress", - size: MediaQuery.of(context).size.width / 2, - foregroundColor: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - const SizedBox( - height: 20, + Row( + children: [ + Text( + "Your ${coin.ticker} address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 10, + height: 10, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], ), - BlueTextButton( - text: "Create new QR code", - onTap: () async { - unawaited(Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: receivingAddress, - ), - settings: const RouteSettings( - name: GenerateUriQrCodeView.routeName, - ), + const SizedBox( + height: 4, + ), + Row( + children: [ + Expanded( + child: Text( + receivingAddress, + style: STextStyles.itemSubtitle12(context), ), - )); - }, + ), + ], ), ], ), ), ), - ), - ], + if (coin != Coin.epicCash) + const SizedBox( + height: 12, + ), + if (coin != Coin.epicCash) + TextButton( + onPressed: generateNewAddress, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Generate new address", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + const SizedBox( + height: 30, + ), + RoundedWhiteContainer( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Column( + children: [ + QrImage( + data: "${coin.uriScheme}:$receivingAddress", + size: MediaQuery.of(context).size.width / 2, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + const SizedBox( + height: 20, + ), + BlueTextButton( + text: "Create new QR code", + onTap: () async { + unawaited(Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: receivingAddress, + ), + settings: const RouteSettings( + name: GenerateUriQrCodeView.routeName, + ), + ), + )); + }, + ), + ], + ), + ), + ), + ), + ], + ), ), ), ), diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 65537f20e..f1075b6e6 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -1,22 +1,33 @@ 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'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; 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/background.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/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'; @@ -27,6 +38,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { required this.transactionInfo, required this.walletId, this.routeOnSuccessName = WalletView.routeName, + this.isTradeTransaction = false, }) : super(key: key); static const String routeName = "/confirmTransactionView"; @@ -34,6 +46,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { final Map<String, dynamic> transactionInfo; final String walletId; final String routeOnSuccessName; + final bool isTradeTransaction; @override ConsumerState<ConfirmTransactionView> createState() => @@ -45,16 +58,19 @@ class _ConfirmTransactionViewState late final Map<String, dynamic> transactionInfo; late final String walletId; late final String routeOnSuccessName; + late final bool isDesktop; Future<void> _attemptSend(BuildContext context) async { - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return const SendingTransactionDialog(); - }, - )); + unawaited( + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return const SendingTransactionDialog(); + }, + ), + ); final note = transactionInfo["note"] as String? ?? ""; final manager = @@ -72,13 +88,13 @@ class _ConfirmTransactionViewState txid = await manager.confirmSend(txData: transactionInfo); } - unawaited(manager.refresh()); - // save note await ref .read(notesServiceChangeNotifierProvider(walletId)) .editOrAddNote(txid: txid, note: note); + unawaited(manager.refresh()); + // pop back to wallet if (mounted) { Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); @@ -107,25 +123,66 @@ class _ConfirmTransactionViewState useSafeArea: false, barrierDismissible: true, builder: (context) { - return StackDialog( - title: "Broadcast transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + if (isDesktop) { + return DesktopDialog( + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Broadcast transaction failed", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 24, + ), + Text( + e.toString(), + style: STextStyles.smallMed14(context), + ), + const SizedBox( + height: 56, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ) + ], + ), ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); + ); + } else { + return StackDialog( + title: "Broadcast transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + } }, ); } @@ -133,6 +190,7 @@ class _ConfirmTransactionViewState @override void initState() { + isDesktop = Util.isDesktop; transactionInfo = widget.transactionInfo; walletId = widget.walletId; routeOnSuccessName = widget.routeOnSuccessName; @@ -143,234 +201,672 @@ 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), + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + 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, + ), + ), + ), + ), + ); + }, + ), ), ), - 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: 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(), ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), + 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( - "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", - style: STextStyles.pageTitleH1(context), + "Recipient", + style: STextStyles.smallMed12(context), ), const SizedBox( - height: 12, + height: 4, ), - 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), - ), + 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), + ))} ${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), + ))} ${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, + )} ${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: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + child: Builder(builder: (context) { + final coin = ref + .watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))) + .coin; + + final fee = Format.satoshisToAmount( + transactionInfo["fee"] as int, + coin: coin, + ); + + return Text( + "${Format.localizedStringAsFixed( + value: fee, + locale: ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)), + decimalPlaces: Constants.decimalPlacesForCoin(coin), + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + }), + ) + // DropdownButtonHideUnderline( + // child: DropdownButton2( + // offset: const Offset(0, -10), + // isExpanded: true, + // + // dropdownElevation: 0, + // value: _fee, + // items: [ + // ..._dropDownItems.map( + // (e) { + // String message = _fee.toString(); + // + // return DropdownMenuItem( + // value: e, + // child: Text(message), + // ); + // }, + // ), + // ], + // onChanged: (value) { + // if (value is int) { + // setState(() { + // _fee = value; + // }); + // } + // }, + // icon: SvgPicture.asset( + // Assets.svg.chevronDown, + // width: 12, + // height: 6, + // color: + // Theme.of(context).extension<StackColors>()!.textDark3, + // ), + // buttonPadding: const EdgeInsets.symmetric( + // horizontal: 16, + // vertical: 8, + // ), + // buttonDecoration: BoxDecoration( + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFieldDefaultBG, + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // ), + // dropdownDecoration: BoxDecoration( + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFieldDefaultBG, + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // ), + // ), + // ), + ), + 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), + ), + )} ${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", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + final dynamic unlocked; + + final coin = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .coin; + + if (isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend( + coin: coin, + ), + ), + ], + ), + ), + ); + } else { + 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 (mounted) { + if (unlocked == true) { + unawaited(_attemptSend(context)); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", + context: context), + ); + } + } + }, + ), + ), + if (isDesktop) + const SizedBox( + height: 32, + ), + ], + ), ), ); } diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 5e689a580..a89a20d85 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:cw_core/monero_transaction_priority.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -30,7 +31,9 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.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/background.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/icon_widgets/addressbook_icon.dart'; @@ -209,29 +212,47 @@ class _SendViewState extends ConsumerState<SendView> { } int fee; + if (coin == Coin.monero) { + MoneroTransactionPriority specialMoneroId; + switch (ref.read(feeRateTypeStateProvider.state).state) { + case FeeRateType.fast: + specialMoneroId = MoneroTransactionPriority.fast; + break; + case FeeRateType.average: + specialMoneroId = MoneroTransactionPriority.regular; + break; + case FeeRateType.slow: + specialMoneroId = MoneroTransactionPriority.slow; + break; + } - if (coin == Coin.firo || coin == Coin.firoTestNet) { + fee = await manager.estimateFeeFor(amount, specialMoneroId.raw!); + cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + + return cachedFees[amount]!; + } else if (coin == Coin.firo || coin == Coin.firoTestNet) { if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { fee = await manager.estimateFeeFor(amount, feeRate); - cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee) - .toStringAsFixed(Constants.decimalPlaces); + cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee, coin: coin) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); return cachedFiroPrivateFees[amount]!; } else { fee = await (manager.wallet as FiroWallet) .estimateFeeForPublic(amount, feeRate); - cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee) - .toStringAsFixed(Constants.decimalPlaces); + cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee, coin: coin) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); return cachedFiroPublicFees[amount]!; } } else { fee = await manager.estimateFeeFor(amount, feeRate); - cachedFees[amount] = - Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces); + cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); return cachedFees[amount]!; } @@ -294,8 +315,8 @@ class _SendViewState extends ConsumerState<SendView> { }); } else { setState(() { - _calculateFeesFuture = - calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + _calculateFeesFuture = calculateFees( + Format.decimalAmountToSatoshis(_amountToSend!, coin)); }); } } @@ -309,8 +330,8 @@ class _SendViewState extends ConsumerState<SendView> { }); } else { setState(() { - _calculateFeesFuture = - calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + _calculateFeesFuture = calculateFees( + Format.decimalAmountToSatoshis(_amountToSend!, coin)); }); } } @@ -352,330 +373,471 @@ class _SendViewState extends ConsumerState<SendView> { }); } else { setState(() { - _calculateFeesFuture = - calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + _calculateFeesFuture = calculateFees( + Format.decimalAmountToSatoshis(_amountToSend!, coin)); }); } }); } - 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: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + 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: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Send ${coin.ticker}", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Send ${coin.ticker}", - 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( - // subtract top and bottom padding set in parent - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 22, + height: 22, + ), + const SizedBox( + width: 6, + ), + if (coin != Coin.firo && + coin != Coin.firoTestNet) + Expanded( + child: Text( + ref.watch(provider.select( + (value) => value.walletName)), + style: STextStyles.titleBold12(context), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + if (coin == Coin.firo || + coin == Coin.firoTestNet) + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + ref.watch(provider.select( + (value) => value.walletName)), + style: + STextStyles.titleBold12(context) + .copyWith(fontSize: 14), + ), + // const SizedBox( + // height: 2, + // ), + Text( + "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + style: STextStyles.label(context) + .copyWith(fontSize: 10), + ), + ], + ), + if (coin != Coin.firo && + coin != Coin.firoTestNet) + const SizedBox( + width: 10, + ), + if (coin == Coin.firo || + coin == Coin.firoTestNet) + const Spacer(), + FutureBuilder( + future: (coin != Coin.firo && + coin != Coin.firoTestNet) + ? ref.watch(provider.select( + (value) => value.availableBalance)) + : ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" + ? (ref.watch(provider).wallet + as FiroWallet) + .availablePrivateBalance() + : (ref.watch(provider).wallet + as FiroWallet) + .availablePublicBalance(), + builder: + (_, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + _cachedBalance = snapshot.data!; + } + + if (_cachedBalance != null) { + return GestureDetector( + onTap: () { + cryptoAmountController.text = + _cachedBalance!.toStringAsFixed( + Constants + .decimalPlacesForCoin( + coin)); + }, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + "${Format.localizedStringAsFixed( + value: _cachedBalance!, + locale: locale, + decimalPlaces: 8, + )} ${coin.ticker}", + style: + STextStyles.titleBold12( + context) + .copyWith( + fontSize: 10, + ), + textAlign: TextAlign.right, + ), + Text( + "${Format.localizedStringAsFixed( + value: _cachedBalance! * + ref.watch(priceAnd24hChangeNotifierProvider + .select((value) => + value + .getPrice( + coin) + .item1)), + locale: locale, + decimalPlaces: 2, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles + .titleBold12_400( + context) + .copyWith( + fontSize: 8, + ), + textAlign: TextAlign.right, + ) + ], + ), + ), + ); + } else { + return Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance...", + ], + style: STextStyles.itemSubtitle( + context) + .copyWith( + fontSize: 10, + ), + ), + const SizedBox( + height: 2, + ), + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance...", + ], + style: STextStyles.itemSubtitle( + context) + .copyWith( + fontSize: 8, + ), + ) + ], + ); + } + }, + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + Text( + "Send to", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 22, - height: 22, - ), - const SizedBox( - width: 6, - ), - if (coin != Coin.firo && - coin != Coin.firoTestNet) - Expanded( - child: Text( - ref.watch(provider - .select((value) => value.walletName)), - style: STextStyles.titleBold12(context), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - if (coin == Coin.firo || - coin == Coin.firoTestNet) - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - ref.watch(provider.select( - (value) => value.walletName)), - style: STextStyles.titleBold12(context) - .copyWith(fontSize: 14), - ), - // const SizedBox( - // height: 2, - // ), - Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", - style: STextStyles.label(context) - .copyWith(fontSize: 10), - ), - ], - ), - if (coin != Coin.firo && - coin != Coin.firoTestNet) - const SizedBox( - width: 10, - ), - if (coin == Coin.firo || - coin == Coin.firoTestNet) - const Spacer(), - FutureBuilder( - future: (coin != Coin.firo && - coin != Coin.firoTestNet) - ? ref.watch(provider.select( - (value) => value.availableBalance)) - : ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" - ? (ref.watch(provider).wallet - as FiroWallet) - .availablePrivateBalance() - : (ref.watch(provider).wallet - as FiroWallet) - .availablePublicBalance(), - builder: - (_, AsyncSnapshot<Decimal> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _cachedBalance = snapshot.data!; - } - - if (_cachedBalance != null) { - return GestureDetector( - onTap: () { - cryptoAmountController.text = - _cachedBalance!.toStringAsFixed( - Constants.decimalPlaces); - }, - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance!, - locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", - style: STextStyles.titleBold12( - context) - .copyWith( - fontSize: 10, - ), - textAlign: TextAlign.right, - ), - Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance! * - ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => - value - .getPrice( - coin) - .item1)), - locale: locale, - decimalPlaces: 2, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", - style: - STextStyles.titleBold12_400( - context) - .copyWith( - fontSize: 8, - ), - textAlign: TextAlign.right, - ) - ], - ), - ), - ); - } else { - return Column( - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance...", - ], - style: STextStyles.itemSubtitle( - context) - .copyWith( - fontSize: 10, - ), - ), - const SizedBox( - height: 2, - ), - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance...", - ], - style: STextStyles.itemSubtitle( - context) - .copyWith( - fontSize: 8, - ), - ) - ], - ); - } - }, - ), - ], - ), - ), - ), - const SizedBox( - height: 16, - ), - Text( - "Send to", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("sendViewAddressFieldKey"), - controller: sendToController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: <TextInputFormatter>[ - // FilteringTextInputFormatter.allow( - // RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, - ), - onChanged: (newValue) { - _address = newValue; - _updatePreviewButtonState( - _address, _amountToSend); - - setState(() { - _addressToggleFlag = newValue.isNotEmpty; - }); - }, - focusNode: _addressFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${coin.ticker} address", - _addressFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, + child: TextField( + key: const Key("sendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow( + // RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, ), - suffixIcon: Padding( - padding: sendToController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _addressToggleFlag - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey"), - onTap: () { - sendToController.text = ""; - _address = ""; - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey"), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = - data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf("\n")); + onChanged: (newValue) { + _address = newValue; + _updatePreviewButtonState( + _address, _amountToSend); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${coin.ticker} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = + data.text!.trim(); + if (content + .contains("\n")) { + content = + content.substring( + 0, + content.indexOf( + "\n")); + } + + sendToController.text = + content; + _address = content; + + _updatePreviewButtonState( + _address, + _amountToSend); + setState(() { + _addressToggleFlag = + sendToController + .text.isNotEmpty; + }); + } + }, + child: sendToController + .text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey"), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: widget.coin, + ); + }, + child: const AddressBookIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey"), + onTap: () async { + try { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future<void>.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await scanner.scan(); + + // Future<void>.delayed( + // const Duration(seconds: 2), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = + AddressUtils.parseUri( + qrResult.rawContent); + + Logging.instance.log( + "qrResult parsed: $results", + level: LogLevel.Info); + + if (results.isNotEmpty && + results["scheme"] == + coin.uriScheme) { + // auto fill address + _address = + results["address"] ?? ""; + sendToController.text = + _address!; + + // autofill notes field + if (results["message"] != + null) { + noteController.text = + results["message"]!; + } else if (results["label"] != + null) { + noteController.text = + results["label"]!; } + // autofill amount field + if (results["amount"] != + null) { + final amount = + Decimal.parse( + results["amount"]!); + cryptoAmountController + .text = + Format + .localizedStringAsFixed( + value: amount, + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + decimalPlaces: Constants + .decimalPlacesForCoin( + coin), + ); + amount.toString(); + _amountToSend = amount; + } + + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = + sendToController + .text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read( + walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress( + qrResult.rawContent)) { + _address = + qrResult.rawContent; sendToController.text = - content; - _address = content; + _address ?? ""; _updatePreviewButtonState( _address, _amountToSend); @@ -685,207 +847,498 @@ class _SendViewState extends ConsumerState<SendView> { .text.isNotEmpty; }); } - }, - child: - sendToController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey"), - onTap: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: widget.coin, - ); - }, - child: const AddressBookIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey"), - onTap: () async { - try { - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75)); + } on PlatformException catch (e, s) { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true; + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); } - - final qrResult = - await scanner.scan(); - - // Future<void>.delayed( - // const Duration(seconds: 2), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - - Logging.instance.log( - "qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); - - final results = - AddressUtils.parseUri( - qrResult.rawContent); - - Logging.instance.log( - "qrResult parsed: $results", - level: LogLevel.Info); - - if (results.isNotEmpty && - results["scheme"] == - coin.uriScheme) { - // auto fill address - _address = - results["address"] ?? ""; - sendToController.text = - _address!; - - // autofill notes field - if (results["message"] != - null) { - noteController.text = - results["message"]!; - } else if (results["label"] != - null) { - noteController.text = - results["label"]!; - } - - // autofill amount field - if (results["amount"] != null) { - final amount = Decimal.parse( - results["amount"]!); - cryptoAmountController.text = - Format - .localizedStringAsFixed( - value: amount, - locale: ref - .read( - localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: - Constants.decimalPlaces, - ); - amount.toString(); - _amountToSend = amount; - } - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController - .text.isNotEmpty; - }); - - // now check for non standard encoded basic address - } else if (ref - .read( - walletsChangeNotifierProvider) - .getManager(walletId) - .validateAddress( - qrResult.rawContent)) { - _address = qrResult.rawContent; - sendToController.text = - _address ?? ""; - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController - .text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true; - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); - } - }, - child: const QrCodeIcon(), - ) - ], + }, + child: const QrCodeIcon(), + ) + ], + ), ), ), ), ), ), - ), - Builder( - builder: (_) { - final error = _updateInvalidAddressText( - _address ?? "", - ref - .read(walletsChangeNotifierProvider) - .getManager(walletId), - ); + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ref + .read(walletsChangeNotifierProvider) + .getManager(walletId), + ); - if (error == null || error.isEmpty) { - return Container(); - } else { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - top: 4.0, - ), - child: Text( - error, - textAlign: TextAlign.left, - style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textError, + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: + STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textError, + ), ), ), + ); + } + }, + ), + if (coin == Coin.firo) + const SizedBox( + height: 12, + ), + if (coin == Coin.firo) + Text( + "Send from", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + if (coin == Coin.firo) + const SizedBox( + height: 8, + ), + if (coin == Coin.firo) + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, ), - ); - } - }, - ), - if (coin == Coin.firo) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: RawMaterialButton( + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + FiroBalanceSelectionSheet( + walletId: walletId, + ), + ); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + style: STextStyles.itemSubtitle12( + context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _firoBalanceFuture( + provider, locale), + builder: (context, + AsyncSnapshot<String?> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + _privateBalanceString = + snapshot.data!; + } else { + _publicBalanceString = + snapshot.data!; + } + } + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" && + _privateBalanceString != + null) { + return Text( + "$_privateBalanceString ${coin.ticker}", + style: STextStyles + .itemSubtitle(context), + ); + } else if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Public" && + _publicBalanceString != + null) { + return Text( + "$_publicBalanceString ${coin.ticker}", + style: STextStyles + .itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance...", + ], + style: STextStyles + .itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ], + ), + ), + ) + ], + ), const SizedBox( height: 12, ), - if (coin == Coin.firo) - Text( - "Send from", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + BlueTextButton( + text: "Send all ${coin.ticker}", + onTap: () async { + if (coin == Coin.firo || + coin == Coin.firoTestNet) { + final firoWallet = + ref.read(provider).wallet as FiroWallet; + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + cryptoAmountController.text = + (await firoWallet + .availablePrivateBalance()) + .toStringAsFixed(Constants + .decimalPlacesForCoin(coin)); + } else { + cryptoAmountController.text = + (await firoWallet + .availablePublicBalance()) + .toStringAsFixed(Constants + .decimalPlacesForCoin(coin)); + } + } else { + cryptoAmountController.text = (await ref + .read(provider) + .availableBalance) + .toStringAsFixed( + Constants.decimalPlacesForCoin( + coin)); + } + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + key: + const Key("amountInputFieldCryptoTextFieldKey"), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, + newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: + STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + coin.ticker, + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ), + if (Prefs.instance.externalCalls) + const SizedBox( + height: 8, + ), + 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>()! + .textDark, + ), + key: + const Key("amountInputFieldFiatTextFieldKey"), + controller: baseAmountController, + focusNode: _baseFocus, + keyboardType: + const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a fiat amount with 2 decimal places + TextInputFormatter.withFunction((oldValue, + newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: (baseAmountString) { + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = + baseAmountString.contains(",") + ? Decimal.parse(baseAmountString + .replaceFirst(",", ".")) + : Decimal.parse(baseAmountString); + + var _price = ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + .item1; + + if (_price == Decimal.zero) { + _amountToSend = Decimal.zero; + } else { + _amountToSend = baseAmount <= Decimal.zero + ? Decimal.zero + : (baseAmount / _price).toDecimal( + scaleOnInfinitePrecision: + Constants.decimalPlacesForCoin( + coin)); + } + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final amountString = + Format.localizedStringAsFixed( + value: _amountToSend!, + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + decimalPlaces: + Constants.decimalPlacesForCoin(coin), + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Decimal.zero; + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis( + // _amountToSend!)); + // }); + _updatePreviewButtonState( + _address, _amountToSend); + }, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: + STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)), + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ), + 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( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _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, + ), + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Transaction fee (estimated)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), - if (coin == Coin.firo) const SizedBox( height: 8, ), - if (coin == Coin.firo) Stack( children: [ - const TextField( + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: feeController, readOnly: true, textInputAction: TextInputAction.none, ), @@ -902,764 +1355,172 @@ class _SendViewState extends ConsumerState<SendView> { Constants.size.circularBorderRadius, ), ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => FiroBalanceSelectionSheet( - walletId: walletId, - ), - ); - }, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", - style: STextStyles.itemSubtitle12( - context), - ), - const SizedBox( - width: 10, - ), - FutureBuilder( - future: _firoBalanceFuture( - provider, locale), - builder: (context, - AsyncSnapshot<String?> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - _privateBalanceString = - snapshot.data!; + onPressed: (coin == Coin.firo || + coin == Coin.firoTestNet) && + ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" + ? null + : () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + TransactionFeeSelectionSheet( + walletId: walletId, + amount: Decimal.tryParse( + cryptoAmountController + .text) ?? + Decimal.zero, + updateChosen: (String fee) { + setState(() { + _calculateFeesFuture = + Future(() => fee); + }); + }, + ), + ); + }, + child: ((coin == Coin.firo || + coin == Coin.firoTestNet) && + ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") + ? Row( + children: [ + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "~${snapshot.data! as String} ${coin.ticker}", + style: STextStyles + .itemSubtitle(context), + ); } else { - _publicBalanceString = - snapshot.data!; + return AnimatedText( + stringsToLoopThrough: const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: STextStyles + .itemSubtitle(context), + ); } - } - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" && - _privateBalanceString != - null) { - return Text( - "$_privateBalanceString ${coin.ticker}", - style: - STextStyles.itemSubtitle( - context), - ); - } else if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Public" && - _publicBalanceString != - null) { - return Text( - "$_publicBalanceString ${coin.ticker}", - style: - STextStyles.itemSubtitle( - context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance...", - ], - style: - STextStyles.itemSubtitle( - context), - ); - } - }, - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - ), - ], - ), + }, + ), + ], + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch( + feeRateTypeStateProvider + .state) + .state + .prettyName, + style: STextStyles + .itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + return Text( + "~${snapshot.data! as String} ${coin.ticker}", + style: STextStyles + .itemSubtitle( + context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: STextStyles + .itemSubtitle( + context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ], + ), ), ) ], ), - const SizedBox( - height: 12, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - BlueTextButton( - text: "Send all ${coin.ticker}", - onTap: () async { - if (coin == Coin.firo || - coin == Coin.firoTestNet) { - final firoWallet = - ref.read(provider).wallet as FiroWallet; - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - cryptoAmountController.text = - (await firoWallet - .availablePrivateBalance()) - .toStringAsFixed( - Constants.decimalPlaces); - } else { - cryptoAmountController.text = - (await firoWallet - .availablePublicBalance()) - .toStringAsFixed( - Constants.decimalPlaces); - } - } else { - cryptoAmountController.text = (await ref - .read(provider) - .availableBalance) - .toStringAsFixed(Constants.decimalPlaces); - } - }, - ), - ], - ), - const SizedBox( - height: 8, - ), - TextField( - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - key: const Key("amountInputFieldCryptoTextFieldKey"), - controller: cryptoAmountController, - focusNode: _cryptoFocus, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - textAlign: TextAlign.right, - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, - newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - coin.ticker, - style: STextStyles.smallMed14(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ), - ), - ), - if (Prefs.instance.externalCalls) + const Spacer(), const SizedBox( - height: 8, + height: 12, ), - if (Prefs.instance.externalCalls) - TextField( - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - key: const Key("amountInputFieldFiatTextFieldKey"), - controller: baseAmountController, - focusNode: _baseFocus, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - textAlign: TextAlign.right, - inputFormatters: [ - // regex to validate a fiat amount with 2 decimal places - TextInputFormatter.withFunction((oldValue, - newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - onChanged: (baseAmountString) { - if (baseAmountString.isNotEmpty && - baseAmountString != "." && - baseAmountString != ",") { - final baseAmount = baseAmountString - .contains(",") - ? Decimal.parse( - baseAmountString.replaceFirst(",", ".")) - : Decimal.parse(baseAmountString); - - var _price = ref - .read(priceAnd24hChangeNotifierProvider) - .getPrice(coin) - .item1; - - if (_price == Decimal.zero) { - _amountToSend = Decimal.zero; - } else { - _amountToSend = baseAmount <= Decimal.zero - ? Decimal.zero - : (baseAmount / _price).toDecimal( - scaleOnInfinitePrecision: - Constants.decimalPlaces); - } - if (_cachedAmountToSend != null && - _cachedAmountToSend == _amountToSend) { - return; - } - _cachedAmountToSend = _amountToSend; - Logging.instance.log( - "it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); - - final amountString = - Format.localizedStringAsFixed( - value: _amountToSend!, - locale: ref - .read(localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: Constants.decimalPlaces, - ); - - _cryptoAmountChangeLock = true; - cryptoAmountController.text = amountString; - _cryptoAmountChangeLock = false; - } else { - _amountToSend = Decimal.zero; - _cryptoAmountChangeLock = true; - cryptoAmountController.text = ""; - _cryptoAmountChangeLock = false; - } - // setState(() { - // _calculateFeesFuture = calculateFees( - // Format.decimalAmountToSatoshis( - // _amountToSend!)); - // }); - _updatePreviewButtonState( - _address, _amountToSend); - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: - STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), - style: STextStyles.smallMed14(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ), - ), - ), - 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( - "Type something...", - _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, - ), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Transaction fee (estimated)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - Stack( - children: [ - TextField( - controller: feeController, - readOnly: true, - textInputAction: TextInputAction.none, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: (coin == Coin.firo || - coin == Coin.firoTestNet) && - ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" - ? null - : () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - TransactionFeeSelectionSheet( - walletId: walletId, - amount: Decimal.tryParse( - cryptoAmountController - .text) ?? - Decimal.zero, - updateChosen: (String fee) { - setState(() { - _calculateFeesFuture = - Future(() => fee); - }); - }, - ), - ); - }, - child: ((coin == Coin.firo || - coin == Coin.firoTestNet) && - ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") - ? Row( - children: [ - FutureBuilder( - future: _calculateFeesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "~${snapshot.data! as String} ${coin.ticker}", - style: - STextStyles.itemSubtitle( - context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: - STextStyles.itemSubtitle( - context), - ); - } - }, - ), - ], - ) - : Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - ref - .watch( - feeRateTypeStateProvider - .state) - .state - .prettyName, - style: - STextStyles.itemSubtitle12( - context), - ), - const SizedBox( - width: 10, - ), - FutureBuilder( - future: _calculateFeesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - return Text( - "~${snapshot.data! as String} ${coin.ticker}", - style: STextStyles - .itemSubtitle( - context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: STextStyles - .itemSubtitle( - context), - ); - } - }, - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - ), - ], - ), - ), - ) - ], - ), - const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: ref - .watch(previewTxButtonStateProvider.state) - .state - ? () async { - // wait for keyboard to disappear - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 100), - ); - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(walletId); - - // TODO: remove the need for this!! - final bool isOwnAddress = - await manager.isOwnAddress(_address!); - if (isOwnAddress) { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: - "Sending to self is currently disabled", - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Ok", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorDark), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, + TextButton( + onPressed: ref + .watch(previewTxButtonStateProvider.state) + .state + ? () async { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 100), ); - return; - } + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId); - final amount = Format.decimalAmountToSatoshis( - _amountToSend!); - int availableBalance; - if ((coin == Coin.firo || - coin == Coin.firoTestNet)) { - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - availableBalance = - Format.decimalAmountToSatoshis( - await (manager.wallet - as FiroWallet) - .availablePrivateBalance()); - } else { - availableBalance = - Format.decimalAmountToSatoshis( - await (manager.wallet - as FiroWallet) - .availablePublicBalance()); - } - } else { - availableBalance = - Format.decimalAmountToSatoshis( - await manager.availableBalance); - } - - // confirm send all - if (amount == availableBalance) { - final bool? shouldSendAll = - await showDialog<bool>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Confirm send all", - message: - "You are about to send your entire balance. Would you like to continue?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorDark), - ), - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context), - child: Text( - "Yes", - style: - STextStyles.button(context), - ), - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ); - }, - ); - - if (shouldSendAll == null || - shouldSendAll == false) { - // cancel preview - return; - } - } - - try { - bool wasCancelled = false; - - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; - - Navigator.of(context).pop(); - }, - ); - }, - )); - - Map<String, dynamic> txData; - - if ((coin == Coin.firo || - coin == Coin.firoTestNet) && - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state != - "Private") { - txData = - await (manager.wallet as FiroWallet) - .prepareSendPublic( - address: _address!, - satoshiAmount: amount, - args: { - "feeRate": - ref.read(feeRateTypeStateProvider) - }, - ); - } else { - txData = await manager.prepareSend( - address: _address!, - satoshiAmount: amount, - args: { - "feeRate": - ref.read(feeRateTypeStateProvider) - }, - ); - } - - if (!wasCancelled && mounted) { - // pop building dialog - Navigator.of(context).pop(); - txData["note"] = noteController.text; - txData["address"] = _address; - - unawaited(Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator - .useMaterialPageRoute, - builder: (_) => - ConfirmTransactionView( - transactionInfo: txData, - walletId: walletId, - ), - settings: const RouteSettings( - name: ConfirmTransactionView - .routeName, - ), - ), - )); - } - } catch (e) { - if (mounted) { - // pop building dialog - Navigator.of(context).pop(); - - unawaited(showDialog<dynamic>( + // TODO: remove the need for this!! + final bool isOwnAddress = + await manager.isOwnAddress(_address!); + if (isOwnAddress) { + await showDialog<dynamic>( context: context, useSafeArea: false, barrierDismissible: true, builder: (context) { return StackDialog( title: "Transaction failed", - message: e.toString(), + message: + "Sending to self is currently disabled", rightButton: TextButton( style: Theme.of(context) .extension<StackColors>()! @@ -1681,36 +1542,238 @@ class _SendViewState extends ConsumerState<SendView> { ), ); }, + ); + return; + } + + final amount = + Format.decimalAmountToSatoshis( + _amountToSend!, coin); + int availableBalance; + if ((coin == Coin.firo || + coin == Coin.firoTestNet)) { + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + availableBalance = + Format.decimalAmountToSatoshis( + await (manager.wallet + as FiroWallet) + .availablePrivateBalance(), + coin); + } else { + availableBalance = + Format.decimalAmountToSatoshis( + await (manager.wallet + as FiroWallet) + .availablePublicBalance(), + coin); + } + } else { + availableBalance = + Format.decimalAmountToSatoshis( + await manager.availableBalance, + coin); + } + + // confirm send all + if (amount == availableBalance) { + final bool? shouldSendAll = + await showDialog<bool>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Confirm send all", + message: + "You are about to send your entire balance. Would you like to continue?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Cancel", + style: STextStyles.button( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context) + .pop(false); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor( + context), + child: Text( + "Yes", + style: + STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ); + + if (shouldSendAll == null || + shouldSendAll == false) { + // cancel preview + return; + } + } + + try { + bool wasCancelled = false; + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, )); + + Map<String, dynamic> txData; + + if ((coin == Coin.firo || + coin == Coin.firoTestNet) && + ref + .read( + publicPrivateBalanceStateProvider + .state) + .state != + "Private") { + txData = + await (manager.wallet as FiroWallet) + .prepareSendPublic( + address: _address!, + satoshiAmount: amount, + args: { + "feeRate": ref + .read(feeRateTypeStateProvider) + }, + ); + } else { + txData = await manager.prepareSend( + address: _address!, + satoshiAmount: amount, + args: { + "feeRate": ref + .read(feeRateTypeStateProvider) + }, + ); + } + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context).pop(); + txData["note"] = noteController.text; + txData["address"] = _address; + + unawaited(Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (_) => + ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + ), + settings: const RouteSettings( + name: ConfirmTransactionView + .routeName, + ), + ), + )); + } + } catch (e) { + if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Ok", + style: STextStyles.button( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + )); + } } } - } - : null, - style: ref - .watch(previewTxButtonStateProvider.state) - .state - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - child: Text( - "Preview", - style: STextStyles.button(context), + : null, + style: ref + .watch(previewTxButtonStateProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + child: Text( + "Preview", + style: STextStyles.button(context), + ), ), - ), - const SizedBox( - height: 4, - ), - ], + const SizedBox( + height: 4, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart index 0b6786915..1f6c95df6 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -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( + buttonHeight: ButtonHeight.l, + 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(); - }, - ), - ), - ); + ); + } } } diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart index e639a8cf8..d6de3c6ee 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart @@ -161,7 +161,7 @@ class _FiroBalanceSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "${snapshot.data!} ${manager.coin.ticker}", + "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), textAlign: TextAlign.left, ); @@ -251,7 +251,7 @@ class _FiroBalanceSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "${snapshot.data!} ${manager.coin.ticker}", + "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), textAlign: TextAlign.left, ); diff --git a/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart index 1eb106b53..e5c86fe2e 100644 --- a/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; 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/desktop_dialog.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class SendingTransactionDialog extends StatefulWidget { @@ -43,24 +46,56 @@ class _RestoringDialogState extends State<SendingTransactionDialog> @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return false; - }, - child: StackDialog( - title: "Sending 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 DesktopDialog( + child: Padding( + padding: const EdgeInsets.all(40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Sending 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, + ), + ), + ], ), ), - ), - ); + ); + } else { + return WillPopScope( + onWillPop: () async { + return false; + }, + child: StackDialog( + title: "Sending 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, + ), + ), + ), + ); + } } } diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index 982086d3c..2f5ce2f3e 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/monero_transaction_priority.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -70,16 +71,27 @@ class _TransactionFeeSelectionSheetState final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - if ((coin == Coin.firo || coin == Coin.firoTestNet) && + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.fast.raw!); + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount(await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate)); + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); } else { ref.read(feeSheetSessionCacheProvider).fast[amount] = Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate)); + await manager.estimateFeeFor(amount, feeRate), + coin: coin); } } return ref.read(feeSheetSessionCacheProvider).fast[amount]!; @@ -88,17 +100,27 @@ class _TransactionFeeSelectionSheetState if (ref.read(feeSheetSessionCacheProvider).average[amount] == null) { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - - if ((coin == Coin.firo || coin == Coin.firoTestNet) && + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.regular.raw!); + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount(await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate)); + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); } else { ref.read(feeSheetSessionCacheProvider).average[amount] = Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate)); + await manager.estimateFeeFor(amount, feeRate), + coin: coin); } } return ref.read(feeSheetSessionCacheProvider).average[amount]!; @@ -107,17 +129,27 @@ class _TransactionFeeSelectionSheetState if (ref.read(feeSheetSessionCacheProvider).slow[amount] == null) { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - - if ((coin == Coin.firo || coin == Coin.firoTestNet) && + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.slow.raw!); + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount(await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate)); + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); } else { ref.read(feeSheetSessionCacheProvider).slow[amount] = Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate)); + await manager.estimateFeeFor(amount, feeRate), + coin: coin); } } return ref.read(feeSheetSessionCacheProvider).slow[amount]!; @@ -225,7 +257,7 @@ class _TransactionFeeSelectionSheetState ref.read(feeRateTypeStateProvider.state).state = FeeRateType.fast; } - String? fee = getAmount(FeeRateType.fast); + String? fee = getAmount(FeeRateType.fast, manager.coin); if (fee != null) { widget.updateChosen(fee); } @@ -293,7 +325,7 @@ class _TransactionFeeSelectionSheetState feeRate: feeObject!.fast, amount: Format .decimalAmountToSatoshis( - amount)), + amount, manager.coin)), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), @@ -358,7 +390,8 @@ class _TransactionFeeSelectionSheetState ref.read(feeRateTypeStateProvider.state).state = FeeRateType.average; } - String? fee = getAmount(FeeRateType.average); + String? fee = + getAmount(FeeRateType.average, manager.coin); if (fee != null) { widget.updateChosen(fee); } @@ -424,7 +457,7 @@ class _TransactionFeeSelectionSheetState feeRate: feeObject!.medium, amount: Format .decimalAmountToSatoshis( - amount)), + amount, manager.coin)), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), @@ -489,7 +522,7 @@ class _TransactionFeeSelectionSheetState ref.read(feeRateTypeStateProvider.state).state = FeeRateType.slow; } - String? fee = getAmount(FeeRateType.slow); + String? fee = getAmount(FeeRateType.slow, manager.coin); print("fee $fee"); if (fee != null) { widget.updateChosen(fee); @@ -557,7 +590,7 @@ class _TransactionFeeSelectionSheetState feeRate: feeObject!.slow, amount: Format .decimalAmountToSatoshis( - amount)), + amount, manager.coin)), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), @@ -624,10 +657,10 @@ class _TransactionFeeSelectionSheetState ); } - String? getAmount(FeeRateType feeRateType) { + String? getAmount(FeeRateType feeRateType, Coin coin) { try { print(feeRateType); - var amount = Format.decimalAmountToSatoshis(this.amount); + var amount = Format.decimalAmountToSatoshis(this.amount, coin); print(amount); print(ref.read(feeSheetSessionCacheProvider).fast); print(ref.read(feeSheetSessionCacheProvider).average); diff --git a/lib/pages/settings_views/global_settings_view/about_view.dart b/lib/pages/settings_views/global_settings_view/about_view.dart index dc2da2488..a1e78ba7d 100644 --- a/lib/pages/settings_views/global_settings_view/about_view.dart +++ b/lib/pages/settings_views/global_settings_view/about_view.dart @@ -11,6 +11,7 @@ import 'package:package_info_plus/package_info_plus.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/background.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/rounded_white_container.dart'; @@ -117,408 +118,431 @@ class AboutView extends ConsumerWidget { ]; Future commitMoneroFuture = Future.wait(futureMoneroList); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "About", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "About", - 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: [ - FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: - (context, AsyncSnapshot<PackageInfo> snapshot) { - String version = ""; - String signature = ""; - String appName = ""; - String build = ""; + 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: [ + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: + (context, AsyncSnapshot<PackageInfo> snapshot) { + String version = ""; + String signature = ""; + String appName = ""; + String build = ""; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - version = snapshot.data!.version; - build = snapshot.data!.buildNumber; - signature = snapshot.data!.buildSignature; - appName = snapshot.data!.appName; - } + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + version = snapshot.data!.version; + build = snapshot.data!.buildNumber; + signature = snapshot.data!.buildSignature; + appName = snapshot.data!.appName; + } - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + appName, + style: STextStyles.pageTitleH2(context), + ), + ), + const SizedBox( + height: 24, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Version", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + version, + style: + STextStyles.itemSubtitle(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Build number", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + build, + style: + STextStyles.itemSubtitle(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Build signature", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + signature, + style: + STextStyles.itemSubtitle(context), + ), + ], + ), + ), + ], + ); + }, + ), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: commitFiroFuture, + builder: + (context, AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = snapshot.data![0] as bool; + isHead = snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = CommitStatus.isOldCommit; + } else { + stateOfCommit = CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle(context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed); + break; + default: + break; + } + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Firo Build Commit", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + firoCommit, + style: indicationStyle, + ), + ], + ), + ); + }), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: commitEpicFuture, + builder: + (context, AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = snapshot.data![0] as bool; + isHead = snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = CommitStatus.isOldCommit; + } else { + stateOfCommit = CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle(context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed); + break; + default: + break; + } + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Epic Cash Build Commit", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + epicCashCommit, + style: indicationStyle, + ), + ], + ), + ); + }), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: commitMoneroFuture, + builder: + (context, AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = snapshot.data![0] as bool; + isHead = snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = CommitStatus.isOldCommit; + } else { + stateOfCommit = CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle(context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed); + break; + default: + break; + } + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Monero Build Commit", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + moneroCommit, + style: indicationStyle, + ), + ], + ), + ); + }), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: Text( - appName, - style: STextStyles.pageTitleH2(context), - ), + Text( + "Website", + style: STextStyles.titleBold12(context), ), const SizedBox( - height: 24, + height: 4, ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Version", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - version, - style: STextStyles.itemSubtitle(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Build number", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - build, - style: STextStyles.itemSubtitle(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Build signature", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - signature, - style: STextStyles.itemSubtitle(context), - ), - ], - ), + BlueTextButton( + text: "https://stackwallet.com", + onTap: () { + launchUrl( + Uri.parse("https://stackwallet.com"), + mode: LaunchMode.externalApplication, + ); + }, ), ], - ); - }, - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: commitFiroFuture, - builder: (context, AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = snapshot.data![0] as bool; - isHead = snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = CommitStatus.isOldCommit; - } else { - stateOfCommit = CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle(context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed); - break; - default: - break; - } - return RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Firo Build Commit", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - firoCommit, - style: indicationStyle, - ), - ], - ), - ); - }), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: commitEpicFuture, - builder: (context, AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = snapshot.data![0] as bool; - isHead = snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = CommitStatus.isOldCommit; - } else { - stateOfCommit = CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle(context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed); - break; - default: - break; - } - return RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Epic Cash Build Commit", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - epicCashCommit, - style: indicationStyle, - ), - ], - ), - ); - }), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: commitMoneroFuture, - builder: (context, AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = snapshot.data![0] as bool; - isHead = snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = CommitStatus.isOldCommit; - } else { - stateOfCommit = CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle(context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed); - break; - default: - break; - } - return RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Monero Build Commit", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - moneroCommit, - style: indicationStyle, - ), - ], - ), - ); - }), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Website", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - BlueTextButton( - text: "https://stackwallet.com", - onTap: () { - launchUrl( - Uri.parse("https://stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], + ), ), - ), - const SizedBox( - height: 12, - ), - const Spacer(), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: STextStyles.label(context), - children: [ - const TextSpan( - text: - "By using Stack Wallet, you agree to the "), - TextSpan( - text: "Terms of service", - style: STextStyles.richLink(context), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/terms-of-service.html"), - mode: LaunchMode.externalApplication, - ); - }, - ), - const TextSpan(text: " and "), - TextSpan( - text: "Privacy policy", - style: STextStyles.richLink(context), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/privacy-policy.html"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], + const SizedBox( + height: 12, ), - ), - ], + const Spacer(), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: STextStyles.label(context), + children: [ + const TextSpan( + text: + "By using Stack Wallet, you agree to the "), + TextSpan( + text: "Terms of service", + style: STextStyles.richLink(context), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html"), + mode: LaunchMode.externalApplication, + ); + }, + ), + const TextSpan(text: " and "), + TextSpan( + text: "Privacy policy", + style: STextStyles.richLink(context), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/privacy-policy.html"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart index 86212e0c7..06b798c36 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; +import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/providers/global/prefs_provider.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/background.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'; -import 'package:tuple/tuple.dart'; - -import 'package:stackwallet/pages/stack_privacy_calls.dart'; class AdvancedSettingsView extends StatelessWidget { const AdvancedSettingsView({ @@ -23,158 +22,160 @@ class AdvancedSettingsView extends StatelessWidget { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Advanced", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Advanced", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + 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(DebugView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - Text( - "Debug info", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of(context).pushNamed(DebugView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Debug info", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Toggle testnet coins", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.showTestNetCoins), - ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .showTestNetCoins = newValue; - }, + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Toggle testnet coins", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - ), - ], - ), - ), - ); - }, - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Consumer( - builder: (_, ref, __) { - final externalCalls = ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - ); - return RawMaterialButton( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context).pushNamed( - StackPrivacyCalls.routeName, - arguments: true, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - RichText( - textAlign: TextAlign.left, - text: TextSpan( - children: [ - TextSpan( - text: "Stack Experience", - style: STextStyles.titleBold12(context), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.showTestNetCoins), ), - TextSpan( - text: externalCalls - ? "\nEasy crypto" - : "\nIncognito", - style: STextStyles.label(context) - .copyWith(fontSize: 15.0), - ) - ], + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .showTestNetCoins = newValue; + }, + ), ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - ], + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Consumer( + builder: (_, ref, __) { + final externalCalls = ref.watch( + prefsChangeNotifierProvider + .select((value) => value.externalCalls), + ); + return RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + Navigator.of(context).pushNamed( + StackPrivacyCalls.routeName, + arguments: true, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + RichText( + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: "Stack Experience", + style: STextStyles.titleBold12(context), + ), + TextSpan( + text: externalCalls + ? "\nEasy crypto" + : "\nIncognito", + style: STextStyles.label(context) + .copyWith(fontSize: 15.0), + ) + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart index 06815bd41..33f21759e 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart @@ -7,19 +7,26 @@ 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_libepiccash/git_versions.dart' as EPIC_VERSIONS; +import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS; 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'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; import 'package:stackwallet/providers/global/debug_service_provider.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/flush_bar_type.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/stack_file_system.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/background.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/custom_loading_overlay.dart'; @@ -28,13 +35,6 @@ 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'; class DebugView extends ConsumerStatefulWidget { const DebugView({Key? key}) : super(key: key); @@ -100,472 +100,484 @@ class _DebugViewState extends ConsumerState<DebugView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Debug", - 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("deleteLogsAppBarButtonKey"), - 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 { - await showDialog<void>( - context: context, - builder: (_) => StackDialog( - title: "Delete logs?", - message: - "You are about to delete all logs permanently. Are you sure?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Debug", + 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("deleteLogsAppBarButtonKey"), + 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 { + await showDialog<void>( + context: context, + builder: (_) => StackDialog( + title: "Delete logs?", + message: + "You are about to delete all logs permanently. Are you sure?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(); + }, ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Delete logs", - style: STextStyles.button(context), - ), - onPressed: () async { - Navigator.of(context).pop(); + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Delete logs", + style: STextStyles.button(context), + ), + onPressed: () async { + Navigator.of(context).pop(); - bool shouldPop = false; - unawaited(showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: const CustomLoadingOverlay( - message: "Deleting logs...", - eventBus: null, + bool shouldPop = false; + unawaited(showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: const CustomLoadingOverlay( + message: "Deleting logs...", + eventBus: null, + ), ), - ), - )); + )); - await ref - .read(debugServiceProvider) - .deleteAllMessages(); - await ref - .read(debugServiceProvider) - .updateRecentLogs(); + await ref + .read(debugServiceProvider) + .deleteAllMessages(); + await ref + .read(debugServiceProvider) + .updateRecentLogs(); - shouldPop = true; + shouldPop = true; - if (mounted) { - Navigator.pop(context); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - context: context, - message: 'Logs cleared!')); - } - }, + if (mounted) { + Navigator.pop(context); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + context: context, + message: 'Logs cleared!')); + } + }, + ), ), - ), - ); - }, + ); + }, + ), ), ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, + ], ), - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (newString) { - setState(() => _searchTerm = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 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, + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ), - ), - const SizedBox( - height: 12, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - 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); - } + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => _searchTerm = newString); }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 Spacer(), - BlueTextButton( - text: "Save logs to file", - onTap: () async { - final systemfile = StackFileSystem(); - await systemfile.prepareStorage(); - Directory rootPath = - (await getApplicationDocumentsDirectory()); - - if (Platform.isAndroid) { - rootPath = Directory("/storage/emulated/0/"); - } - - Directory dir = - Directory('${rootPath.path}/Documents'); - if (Platform.isIOS) { - dir = Directory(rootPath.path); - } - try { - if (!dir.existsSync()) { - dir.createSync(); - } - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Error); - } - String? path; - if (Platform.isAndroid) { - path = dir.path; - } else { - path = await FilePicker.platform - .getDirectoryPath( - dialogTitle: "Choose Log Save Location", - initialDirectory: - systemfile.startPath!.path, - lockParentWindow: true, - ); - } - - if (path != null) { - final eventBus = EventBus(); - bool shouldPop = false; - unawaited(showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: CustomLoadingOverlay( - message: "Generating Stack logs file", - eventBus: eventBus, - ), - ), - )); - - bool logssaved = true; - var filename; + ), + const SizedBox( + height: 12, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BlueTextButton( + text: "Save Debug Info to clipboard", + onTap: () async { try { - filename = await ref - .read(debugServiceProvider) - .exportToFile(path, eventBus); + 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) { - logssaved = false; Logging.instance .log("$e $s", level: LogLevel.Error); } + }, + ), + const Spacer(), + BlueTextButton( + text: "Save logs to file", + onTap: () async { + final systemfile = SWBFileSystem(); + await systemfile.prepareStorage(); + Directory rootPath = await StackFileSystem + .applicationRootDirectory(); - shouldPop = true; + if (Platform.isAndroid) { + rootPath = + Directory("/storage/emulated/0/"); + } - if (mounted) { - Navigator.pop(context); + Directory dir = + Directory('${rootPath.path}/Documents'); + if (Platform.isIOS) { + dir = Directory(rootPath.path); + } + try { + if (!dir.existsSync()) { + dir.createSync(); + } + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + } + String? path; + if (Platform.isAndroid) { + path = dir.path; + } else { + path = await FilePicker.platform + .getDirectoryPath( + dialogTitle: "Choose Log Save Location", + initialDirectory: + systemfile.startPath!.path, + lockParentWindow: true, + ); + } - if (Platform.isAndroid) { - unawaited( - showDialog( - context: context, - builder: (context) => StackOkDialog( - title: logssaved - ? "Logs saved to" - : "Error Saving Logs", - message: "${path!}/$filename", + if (path != null) { + final eventBus = EventBus(); + bool shouldPop = false; + unawaited(showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: CustomLoadingOverlay( + message: "Generating Stack logs file", + eventBus: 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; + + if (mounted) { + Navigator.pop(context); + + if (Platform.isAndroid) { + unawaited( + showDialog( + context: context, + builder: (context) => StackOkDialog( + title: logssaved + ? "Logs saved to" + : "Error Saving Logs", + message: "${path!}/$filename", + ), ), - ), - ); - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - context: context, - message: logssaved - ? 'Logs file saved' - : "Error Saving Logs", - ), - ); + ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + context: context, + message: logssaved + ? 'Logs file saved' + : "Error Saving Logs", + ), + ); + } } } - } - }, - ), - ], - ) - ], + }, + ), + ], + ) + ], + ), ), ), ), - ), - ]; - }, - body: Builder( - builder: (context) { - final logs = filtered( - ref.watch(debugServiceProvider - .select((value) => value.recentLogs)), - _searchTerm) - .reversed - .toList(growable: false); + ]; + }, + body: Builder( + builder: (context) { + final logs = filtered( + ref.watch(debugServiceProvider + .select((value) => value.recentLogs)), + _searchTerm) + .reversed + .toList(growable: false); - return CustomScrollView( - reverse: true, - // shrinkWrap: true, - controller: scrollController, - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, + return CustomScrollView( + reverse: true, + // shrinkWrap: true, + controller: scrollController, + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final log = logs[index]; + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final log = logs[index]; - return Container( - key: Key("log_${log.id}_${log.timestampInMillisUTC}"), - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, - borderRadius: _borderRadius(index, logs.length), - ), - child: Padding( - padding: const EdgeInsets.all(4), - child: RoundedContainer( - padding: const EdgeInsets.all(0), + return Container( + key: Key( + "log_${log.id}_${log.timestampInMillisUTC}"), + decoration: BoxDecoration( color: Theme.of(context) .extension<StackColors>()! .popupBG, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - " [${log.logLevel.name}]", - style: STextStyles.baseXS(context) - .copyWith( - fontSize: 8, - color: (log.logLevel == LogLevel.Info - ? Theme.of(context) - .extension<StackColors>()! - .topNavIconGreen - : (log.logLevel == - LogLevel.Warning - ? Theme.of(context) - .extension<StackColors>()! - .topNavIconYellow - : (log.logLevel == - LogLevel.Error - ? Colors.orange - : Theme.of(context) - .extension< - StackColors>()! - .topNavIconRed))), + borderRadius: _borderRadius(index, logs.length), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + " [${log.logLevel.name}]", + style: STextStyles.baseXS(context) + .copyWith( + fontSize: 8, + color: (log.logLevel == + LogLevel.Info + ? Theme.of(context) + .extension<StackColors>()! + .topNavIconGreen + : (log.logLevel == + LogLevel.Warning + ? Theme.of(context) + .extension< + StackColors>()! + .topNavIconYellow + : (log.logLevel == + LogLevel.Error + ? Colors.orange + : Theme.of(context) + .extension< + StackColors>()! + .topNavIconRed))), + ), ), - ), - Text( - "[${DateTime.fromMillisecondsSinceEpoch(log.timestampInMillisUTC, isUtc: true)}]: ", - style: STextStyles.baseXS(context) - .copyWith( - fontSize: 8, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, + Text( + "[${DateTime.fromMillisecondsSinceEpoch(log.timestampInMillisUTC, isUtc: true)}]: ", + style: STextStyles.baseXS(context) + .copyWith( + fontSize: 8, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), ), - ), - ], - ), - Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const SizedBox( - width: 20, - ), - Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SelectableText( - log.message, - style: STextStyles.baseXS(context) - .copyWith(fontSize: 8), - ), - ], + ], + ), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 20, ), - ), - ], - ), - ], + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SelectableText( + log.message, + style: + STextStyles.baseXS(context) + .copyWith(fontSize: 8), + ), + ], + ), + ), + ], + ), + ], + ), ), ), - ), - ); - }, - childCount: logs.length, + ); + }, + childCount: logs.length, + ), ), - ), - ], - ); - }, + ], + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart index b0cf35a84..693f39f02 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart @@ -8,7 +8,9 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.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'; @@ -18,157 +20,417 @@ class AppearanceSettingsView extends ConsumerWidget { static const String routeName = "/appearanceSettings"; + String chooseThemeType(ThemeType type) { + switch (type) { + case ThemeType.light: + return "Light theme"; + case ThemeType.oceanBreeze: + return "Ocean theme"; + case ThemeType.dark: + return "Dark theme"; + } + } + @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(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Appearance", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Appearance", - 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: [ - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + 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: [ + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Display favorite wallets", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.showFavoriteWallets), + onPressed: null, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Display favorite wallets", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => + value.showFavoriteWallets), + ), + onValueChanged: (newValue) { + ref + .read( + prefsChangeNotifierProvider) + .showFavoriteWallets = newValue; + }, ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .showFavoriteWallets = newValue; - }, - ), - ) - ], + ) + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - const SizedBox( - height: 10, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: null, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Enable dark mode", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: (DB.instance.get<dynamic>( - boxName: DB.boxNameTheme, - key: "colorScheme") - as String?) == - "dark", - onValueChanged: (newValue) { - DB.instance.put<dynamic>( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: (newValue - ? ThemeType.dark - : ThemeType.light) - .name, - ); - ref - .read(colorThemeProvider.state) - .state = - StackColors.fromStackColorTheme( - newValue - ? DarkColors() - : LightColors()); - }, - ), - ) - ], - ), - ), - ); - }, + const SizedBox( + height: 10, ), - ), - ], + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Choose Theme", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + const Padding( + padding: EdgeInsets.all(10), + child: ThemeOptionsView(), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); } } + +class ThemeOptionsView extends ConsumerStatefulWidget { + const ThemeOptionsView({ + Key? key, + }) : super(key: key); + + @override + ConsumerState<ThemeOptionsView> createState() => _ThemeOptionsView(); +} + +class _ThemeOptionsView extends ConsumerState<ThemeOptionsView> { + late String _selectedTheme; + + @override + void initState() { + _selectedTheme = + DB.instance.get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme") + as String? ?? + "light"; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "light", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "light") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Light", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "oceanBreeze", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "oceanBreeze") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Ocean Breeze", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "dark", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "dark") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Dark", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/currency_view.dart b/lib/pages/settings_views/global_settings_view/currency_view.dart index cae947caa..5bcf7fb7f 100644 --- a/lib/pages/settings_views/global_settings_view/currency_view.dart +++ b/lib/pages/settings_views/global_settings_view/currency_view.dart @@ -7,9 +7,15 @@ 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/background.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/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -31,14 +37,20 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { final _searchFocusNode = FocusNode(); void onTap(int index) { - if (currenciesWithoutSelected[index] == current || current.isEmpty) { - // ignore if already selected currency - return; + if (Util.isDesktop) { + setState(() { + current = currenciesWithoutSelected[index]; + }); + } else { + if (currenciesWithoutSelected[index] == current || current.isEmpty) { + // ignore if already selected currency + return; + } + current = currenciesWithoutSelected[index]; + currenciesWithoutSelected.remove(current); + currenciesWithoutSelected.insert(0, current); + ref.read(prefsChangeNotifierProvider).currency = current; } - current = currenciesWithoutSelected[index]; - currenciesWithoutSelected.remove(current); - currenciesWithoutSelected.insert(0, current); - ref.read(prefsChangeNotifierProvider).currency = current; } BorderRadius? _borderRadius(int index) { @@ -76,6 +88,15 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { @override void initState() { _searchController = TextEditingController(); + if (Util.isDesktop) { + currenciesWithoutSelected = + ref.read(baseCurrenciesProvider).map.keys.toList(); + current = ref.read(prefsChangeNotifierProvider).currency; + if (current.isNotEmpty) { + currenciesWithoutSelected.remove(current); + currenciesWithoutSelected.insert(0, current); + } + } super.initState(); } @@ -88,43 +109,123 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { @override Widget build(BuildContext context) { - current = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final isDesktop = Util.isDesktop; - currenciesWithoutSelected = ref - .watch(baseCurrenciesProvider.select((value) => value.map)) - .keys - .toList(); - if (current.isNotEmpty) { - currenciesWithoutSelected.remove(current); - currenciesWithoutSelected.insert(0, current); + if (!isDesktop) { + current = ref + .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + + currenciesWithoutSelected = ref + .watch(baseCurrenciesProvider.select((value) => value.map)) + .keys + .toList(); + + if (current.isNotEmpty) { + currenciesWithoutSelected.remove(current); + 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, - ), + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + 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( + "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", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save changes", + buttonHeight: ButtonHeight.l, + onPressed: () { + ref.read(prefsChangeNotifierProvider).currency = + current; + + if (ref + .read(prefsChangeNotifierProvider) + .externalCalls) { + ref + .read(priceAnd24hChangeNotifierProvider) + .updatePrice(); + } + + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ], + ), + ); + }, child: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) { @@ -140,6 +241,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) { diff --git a/lib/pages/settings_views/global_settings_view/global_settings_view.dart b/lib/pages/settings_views/global_settings_view/global_settings_view.dart index fe6529c20..49e7ea36c 100644 --- a/lib/pages/settings_views/global_settings_view/global_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/global_settings_view.dart @@ -20,11 +20,10 @@ 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'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:stackwallet/utilities/delete_everything.dart'; - class GlobalSettingsView extends StatelessWidget { const GlobalSettingsView({ Key? key, @@ -35,254 +34,257 @@ class GlobalSettingsView extends StatelessWidget { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Settings", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Settings", - 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: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - SettingsListButton( - iconAssetName: Assets.svg.addressBook, - iconSize: 16, - title: "Address book", - onPressed: () { - Navigator.of(context) - .pushNamed(AddressBookView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.downloadFolder, - iconSize: 14, - title: "Stack backup & restore", - onPressed: () { - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: - StackBackupView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to access Stack backup & restore settings", - biometricsAuthenticationTitle: - "Stack backup", - ), - settings: const RouteSettings( - name: "/swblockscreen"), - ), - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.lock, - iconSize: 16, - title: "Security", - onPressed: () { - Navigator.of(context) - .pushNamed(SecurityView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.dollarSign, - iconSize: 18, - title: "Currency", - onPressed: () { - Navigator.of(context).pushNamed( - BaseCurrencySettingsView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.language, - iconSize: 18, - title: "Language", - onPressed: () { - Navigator.of(context).pushNamed( - LanguageSettingsView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.node, - iconSize: 16, - title: "Manage nodes", - onPressed: () { - Navigator.of(context) - .pushNamed(ManageNodesView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.arrowRotate3, - iconSize: 18, - title: "Syncing preferences", - onPressed: () { - Navigator.of(context).pushNamed( - SyncingPreferencesView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.arrowUpRight, - iconSize: 16, - title: "Startup", - onPressed: () { - Navigator.of(context).pushNamed( - StartupPreferencesView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.sun, - iconSize: 18, - title: "Appearance", - onPressed: () { - Navigator.of(context).pushNamed( - AppearanceSettingsView.routeName); - }, - ), - if (Platform.isIOS) + 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: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: Column( + children: [ + SettingsListButton( + iconAssetName: Assets.svg.addressBook, + iconSize: 16, + title: "Address book", + onPressed: () { + Navigator.of(context) + .pushNamed(AddressBookView.routeName); + }, + ), const SizedBox( height: 8, ), - if (Platform.isIOS) SettingsListButton( - iconAssetName: Assets.svg.circleAlert, - iconSize: 16, - title: "Delete account", - onPressed: () async { - await Navigator.of(context) - .pushNamed(DeleteAccountView.routeName); + iconAssetName: Assets.svg.downloadFolder, + iconSize: 14, + title: "Stack backup & restore", + onPressed: () { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: + StackBackupView.routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to access Stack backup & restore settings", + biometricsAuthenticationTitle: + "Stack backup", + ), + settings: const RouteSettings( + name: "/swblockscreen"), + ), + ); }, ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.ellipsis, - iconSize: 18, - title: "About", - onPressed: () { - Navigator.of(context) - .pushNamed(AboutView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.solidSliders, - iconSize: 16, - title: "Advanced", - onPressed: () { - Navigator.of(context).pushNamed( - AdvancedSettingsView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.questionMessage, - iconSize: 16, - title: "Support", - onPressed: () { - Navigator.of(context) - .pushNamed(SupportView.routeName); - }, - ), - // TextButton( - // style: Theme.of(context) - // .textButtonTheme - // .style - // ?.copyWith( - // backgroundColor: - // MaterialStateProperty.all<Color>( - // Theme.of(context).extension<StackColors>()!.accentColorDark - // ), - // ), - // child: Text( - // "fire test notification", - // style: STextStyles.button(context), - // ), - // onPressed: () async { - // NotificationApi.showNotification2( - // title: "Test notification", - // body: "My doggy wallet", - // walletId: - // "3c5e2d70-fcc3-11ec-86a3-31a106a81c3b", - // iconAssetName: - // Assets.svg.iconFor(coin: Coin.dogecoin), - // date: DateTime.now(), - // ); - // }, - // ), - ], + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.lock, + iconSize: 16, + title: "Security", + onPressed: () { + Navigator.of(context) + .pushNamed(SecurityView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.dollarSign, + iconSize: 18, + title: "Currency", + onPressed: () { + Navigator.of(context).pushNamed( + BaseCurrencySettingsView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.language, + iconSize: 18, + title: "Language", + onPressed: () { + Navigator.of(context).pushNamed( + LanguageSettingsView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.node, + iconSize: 16, + title: "Manage nodes", + onPressed: () { + Navigator.of(context) + .pushNamed(ManageNodesView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.arrowRotate3, + iconSize: 18, + title: "Syncing preferences", + onPressed: () { + Navigator.of(context).pushNamed( + SyncingPreferencesView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.arrowUpRight, + iconSize: 16, + title: "Startup", + onPressed: () { + Navigator.of(context).pushNamed( + StartupPreferencesView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.sun, + iconSize: 18, + title: "Appearance", + onPressed: () { + Navigator.of(context).pushNamed( + AppearanceSettingsView.routeName); + }, + ), + if (Platform.isIOS) + const SizedBox( + height: 8, + ), + if (Platform.isIOS) + SettingsListButton( + iconAssetName: Assets.svg.circleAlert, + iconSize: 16, + title: "Delete account", + onPressed: () async { + await Navigator.of(context).pushNamed( + DeleteAccountView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.ellipsis, + iconSize: 18, + title: "About", + onPressed: () { + Navigator.of(context) + .pushNamed(AboutView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.solidSliders, + iconSize: 16, + title: "Advanced", + onPressed: () { + Navigator.of(context).pushNamed( + AdvancedSettingsView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.questionMessage, + iconSize: 16, + title: "Support", + onPressed: () { + Navigator.of(context) + .pushNamed(SupportView.routeName); + }, + ), + // TextButton( + // style: Theme.of(context) + // .textButtonTheme + // .style + // ?.copyWith( + // backgroundColor: + // MaterialStateProperty.all<Color>( + // Theme.of(context).extension<StackColors>()!.accentColorDark + // ), + // ), + // child: Text( + // "fire test notification", + // style: STextStyles.button(context), + // ), + // onPressed: () async { + // NotificationApi.showNotification2( + // title: "Test notification", + // body: "My doggy wallet", + // walletId: + // "3c5e2d70-fcc3-11ec-86a3-31a106a81c3b", + // iconAssetName: + // Assets.svg.iconFor(coin: Coin.dogecoin), + // date: DateTime.now(), + // ); + // }, + // ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - ], + const SizedBox( + height: 12, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 6b6377ab6..d92b166d7 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -5,9 +5,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.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/background.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class HiddenSettings extends StatelessWidget { @@ -17,149 +18,187 @@ class HiddenSettings extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: Container(), - title: Text( - "Not so secret anymore", - style: STextStyles.navBarTitle(context), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: Container(), + title: Text( + "Not so secret anymore", + 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: [ - Consumer(builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - final notifs = - ref.read(notificationsProvider).notifications; + 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: [ + Consumer(builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + final notifs = + ref.read(notificationsProvider).notifications; - for (final n in notifs) { + for (final n in notifs) { + await ref + .read(notificationsProvider) + .delete(n, false); + } await ref .read(notificationsProvider) - .delete(n, false); + .delete(notifs[0], true); + + unawaited(showFloatingFlushBar( + type: FlushBarType.success, + message: "Notification history deleted", + context: context, + )); + }, + child: RoundedWhiteContainer( + child: Text( + "Delete notifications", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ); + }), + // const SizedBox( + // height: 12, + // ), + // Consumer(builder: (_, ref, __) { + // return GestureDetector( + // onTap: () async { + // final trades = + // ref.read(tradesServiceProvider).trades; + // + // for (final trade in trades) { + // ref.read(tradesServiceProvider).delete( + // trade: trade, shouldNotifyListeners: false); + // } + // ref.read(tradesServiceProvider).delete( + // trade: trades[0], shouldNotifyListeners: true); + // + // // ref.read(notificationsProvider).DELETE_EVERYTHING(); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Delete trade history", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context).extension<StackColors>()!.accentColorDark + // ), + // ), + // ), + // ); + // }), + const SizedBox( + height: 12, + ), + Consumer(builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + await ref + .read(debugServiceProvider) + .deleteAllMessages(); + + unawaited(showFloatingFlushBar( + type: FlushBarType.success, + message: "Debug Logs deleted", + context: context, + )); + }, + child: RoundedWhiteContainer( + child: Text( + "Delete Debug Logs", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ); + }), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + if (ref.watch(prefsChangeNotifierProvider + .select((value) => value.familiarity)) < + 6) { + return GestureDetector( + onTap: () async { + final familiarity = ref + .read(prefsChangeNotifierProvider) + .familiarity; + if (familiarity < 6) { + ref + .read(prefsChangeNotifierProvider) + .familiarity = 6; + + Constants.exchangeForExperiencedUsers(6); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Enable exchange", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ); + } else { + return Container(); } - await ref - .read(notificationsProvider) - .delete(notifs[0], true); - - unawaited(showFloatingFlushBar( - type: FlushBarType.success, - message: "Notification history deleted", - context: context, - )); }, - child: RoundedWhiteContainer( - child: Text( - "Delete notifications", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ); - }), - // const SizedBox( - // height: 12, - // ), - // Consumer(builder: (_, ref, __) { - // return GestureDetector( - // onTap: () async { - // final trades = - // ref.read(tradesServiceProvider).trades; - // - // for (final trade in trades) { - // ref.read(tradesServiceProvider).delete( - // trade: trade, shouldNotifyListeners: false); - // } - // ref.read(tradesServiceProvider).delete( - // trade: trades[0], shouldNotifyListeners: true); - // - // // ref.read(notificationsProvider).DELETE_EVERYTHING(); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Delete trade history", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - // ), - // ), - // ), - // ); - // }), - const SizedBox( - height: 12, - ), - Consumer(builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - await ref - .read(debugServiceProvider) - .deleteAllMessages(); - - unawaited(showFloatingFlushBar( - type: FlushBarType.success, - message: "Debug Logs deleted", - context: context, - )); - }, - child: RoundedWhiteContainer( - child: Text( - "Delete Debug Logs", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ); - }), - // const SizedBox( - // height: 12, - // ), - // GestureDetector( - // onTap: () async { - // showDialog<void>( - // context: context, - // builder: (_) { - // return StackDialogBase( - // child: SizedBox( - // width: 200, - // child: Lottie.asset( - // Assets.lottie.test2, - // ), - // ), - // ); - // }, - // ); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Lottie test", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - // ), - // ), - // ), - // ), - ], + ), + // const SizedBox( + // height: 12, + // ), + // GestureDetector( + // onTap: () async { + // showDialog<void>( + // context: context, + // builder: (_) { + // return StackDialogBase( + // child: SizedBox( + // width: 200, + // child: Lottie.asset( + // Assets.lottie.test2, + // ), + // ), + // ); + // }, + // ); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Lottie test", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context).extension<StackColors>()!.accentColorDark + // ), + // ), + // ), + // ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/language_view.dart b/lib/pages/settings_views/global_settings_view/language_view.dart index 75a2751a2..9ba2d6dd2 100644 --- a/lib/pages/settings_views/global_settings_view/language_view.dart +++ b/lib/pages/settings_views/global_settings_view/language_view.dart @@ -7,6 +7,8 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/languages_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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; @@ -98,201 +100,205 @@ class _LanguageViewState extends ConsumerState<LanguageSettingsView> { listWithoutSelected.insert(0, current); } listWithoutSelected = _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(); - } - }, + return Background( + 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( + "Language", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Language", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (newString) { - setState(() => filter = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => filter = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 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 = ""; - filter = ""; - }); - }, - ), - ], + 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 = ""; + filter = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, + ), ), ), ), ), ), - ), - ]; - }, - body: Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, + ]; + }, + body: Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, - borderRadius: _borderRadius(index), - ), - child: Padding( - padding: const EdgeInsets.all(4), - key: Key( - "languageSelect_${listWithoutSelected[index]}"), - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: index == 0 - ? Theme.of(context) - .extension<StackColors>()! - .currencyListItemBG - : Theme.of(context) - .extension<StackColors>()! - .popupBG, - child: RawMaterialButton( - onPressed: () async { - onTap(index); - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: _borderRadius(index), + ), + child: Padding( + padding: const EdgeInsets.all(4), + key: Key( + "languageSelect_${listWithoutSelected[index]}"), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: index == 0 + ? Theme.of(context) + .extension<StackColors>()! + .currencyListItemBG + : Theme.of(context) + .extension<StackColors>()! + .popupBG, + child: RawMaterialButton( + onPressed: () async { + onTap(index); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: true, - groupValue: index == 0, - onChanged: (_) { - onTap(index); - }, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: true, + groupValue: index == 0, + onChanged: (_) { + onTap(index); + }, + ), ), - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - listWithoutSelected[index], - key: (index == 0) - ? const Key( - "selectedLanguageSettingsLanguageText") - : null, - style: STextStyles.largeMedium14( - context), - ), - const SizedBox( - height: 2, - ), - Text( - listWithoutSelected[index], - key: (index == 0) - ? const Key( - "selectedLanguageSettingsLanguageTextDescription") - : null, - style: STextStyles.itemSubtitle( - context), - ), - ], - ), - ], + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + listWithoutSelected[index], + key: (index == 0) + ? const Key( + "selectedLanguageSettingsLanguageText") + : null, + style: STextStyles.largeMedium14( + context), + ), + const SizedBox( + height: 2, + ), + Text( + listWithoutSelected[index], + key: (index == 0) + ? const Key( + "selectedLanguageSettingsLanguageTextDescription") + : null, + style: STextStyles.itemSubtitle( + context), + ), + ], + ), + ], + ), ), ), ), ), - ), - ); - }, - childCount: listWithoutSelected.length, + ); + }, + childCount: listWithoutSelected.length, + ), ), - ), - ], - ); - }, + ], + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index df4921af9..6c515b468 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -3,24 +3,28 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/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/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.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/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; 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/background.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'; @@ -36,9 +40,6 @@ class AddEditNodeView extends ConsumerStatefulWidget { required this.coin, required this.nodeId, required this.routeOnSuccessOrDelete, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/addEditNode"; @@ -47,7 +48,6 @@ class AddEditNodeView extends ConsumerStatefulWidget { final Coin coin; final String routeOnSuccessOrDelete; final String? nodeId; - final FlutterSecureStorageInterface secureStore; @override ConsumerState<AddEditNodeView> createState() => _AddEditNodeViewState(); @@ -57,6 +57,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; @@ -105,7 +106,29 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { ref.read(nodeFormDataProvider).useSSL = false; } - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = await testMoneroNodeConnection( + Uri.parse(uriString), true); + testPassed = response.success; + } + } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); @@ -115,11 +138,13 @@ 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.particl: case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: @@ -159,8 +184,200 @@ 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", + buttonHeight: + isDesktop ? ButtonHeight.l : null, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + buttonHeight: + isDesktop ? ButtonHeight.l : null, + 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; @@ -193,279 +410,215 @@ 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)); - - await ref.read(nodeServiceChangeNotifierProvider).delete( - nodeId!, - true, - ); - }, - ), - ), + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + 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(); + } + }, ), - ], - ), - 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); + title: Text( + viewType == AddEditNodeViewType.edit ? "Edit node" : "Add node", + style: STextStyles.navBarTitle(context), + ), + actions: [ + if (viewType == AddEditNodeViewType.edit && + ref + .watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))) + .length > + 1) + 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)); - 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), - ), - ), - ], + 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: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxWidth: 580, + maxHeight: 500, + 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: ref.read(secureStoreProvider), + 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, + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: testConnectionEnabled + ? () async { + await _testConnection(); + } + : null, + ), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + if (isDesktop) + Expanded( + child: PrimaryButton( + label: "Save", + enabled: saveEnabled, + buttonHeight: ButtonHeight.l, + 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), + ), + ), + ], ), ), ); @@ -496,7 +649,7 @@ class NodeForm extends ConsumerStatefulWidget { }) : super(key: key); final NodeModel? node; - final FlutterSecureStorageInterface secureStore; + final SecureStorageInterface secureStore; final bool readOnly; final Coin coin; final void Function(bool canSave, bool canTestConnection)? onChanged; @@ -530,12 +683,14 @@ 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.particl: case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: @@ -650,6 +805,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), @@ -689,106 +846,100 @@ class _NodeFormState extends ConsumerState<NodeForm> { const SizedBox( height: 8, ), - Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("addCustomNodeNodeAddressFieldKey"), - readOnly: widget.readOnly, - enabled: enableField(_hostController), - controller: _hostController, - focusNode: _hostFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - (widget.coin != Coin.monero && - widget.coin != Coin.wownero && - widget.coin != Coin.epicCash) - ? "IP address" - : "Url", - _hostFocusNode, - context, - ).copyWith( - suffixIcon: - !widget.readOnly && _hostController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _hostController.text = ""; - _updateState(); - }, - ), - ], - ), - ), - ) - : null, - ), - onChanged: (newValue) { - _updateState(); - setState(() {}); - }, - ), - ), + ClipRRect( + borderRadius: BorderRadius.circular( + 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), + controller: _hostController, + focusNode: _hostFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + (widget.coin != Coin.monero && + widget.coin != Coin.wownero && + widget.coin != Coin.epicCash) + ? "IP address" + : "Url", + _hostFocusNode, + context, + ).copyWith( + suffixIcon: !widget.readOnly && _hostController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _hostController.text = ""; + _updateState(); + }, + ), + ], + ), + ), + ) + : null, ), - const SizedBox( - width: 12, + onChanged: (newValue) { + _updateState(); + setState(() {}); + }, + ), + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + 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), + controller: _portController, + focusNode: _portFocusNode, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + keyboardType: TextInputType.number, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Port", + _portFocusNode, + context, + ).copyWith( + suffixIcon: !widget.readOnly && _portController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _portController.text = ""; + _updateState(); + }, + ), + ], + ), + ), + ) + : null, ), - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("addCustomNodeNodePortFieldKey"), - readOnly: widget.readOnly, - enabled: enableField(_portController), - controller: _portController, - focusNode: _portFocusNode, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - keyboardType: TextInputType.number, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Port", - _portFocusNode, - context, - ).copyWith( - suffixIcon: - !widget.readOnly && _portController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _portController.text = ""; - _updateState(); - }, - ), - ], - ), - ), - ) - : null, - ), - onChanged: (newValue) { - _updateState(); - setState(() {}); - }, - ), - ), - ), - ], + onChanged: (newValue) { + _updateState(); + setState(() {}); + }, + ), ), const SizedBox( height: 8, @@ -799,6 +950,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), @@ -846,6 +999,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), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart index 12573042e..91e6871f3 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart @@ -7,18 +7,25 @@ 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/background.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 +44,153 @@ 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, + ), + ); + }, + ), + ], + ), + ), + const SizedBox( + width: 12, + ), + Padding( + padding: const EdgeInsets.all(20), + child: NodesList( + coin: widget.coin, + popBackToRoute: CoinNodesView.routeName, + ), + ), + ], + ), + ); + } else { + return Background( + child: 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, ), ), ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, ), - child: SingleChildScrollView( - child: NodesList( - coin: widget.coin, - popBackToRoute: CoinNodesView.routeName, - ), - ), - ), - ); + ); + } } } diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart index 22f239232..743fc6957 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart @@ -8,6 +8,7 @@ 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/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -47,88 +48,91 @@ class _ManageNodesViewState extends ConsumerState<ManageNodesView> { ? _coins : _coins.sublist(0, _coins.length - kTestNetCoinCount); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Manage nodes", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Manage nodes", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ...coins.map( - (coin) { - final count = ref - .watch(nodeServiceChangeNotifierProvider - .select((value) => value.getNodesFor(coin))) - .length; + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...coins.map( + (coin) { + final count = ref + .watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))) + .length; - return Padding( - padding: const EdgeInsets.all(4), - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + return Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - CoinNodesView.routeName, - arguments: coin, - ); - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${coin.prettyName} nodes", - style: STextStyles.titleBold12(context), - ), - Text( - count > 1 ? "$count nodes" : "Default", - style: STextStyles.label(context), - ), - ], - ) - ], + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + CoinNodesView.routeName, + arguments: coin, + ); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${coin.prettyName} nodes", + style: STextStyles.titleBold12(context), + ), + Text( + count > 1 ? "$count nodes" : "Default", + style: STextStyles.label(context), + ), + ], + ) + ], + ), ), ), ), - ), - ); - }, - ), - ], + ); + }, + ), + ], + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 8baddb700..24af0e78e 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -2,22 +2,28 @@ 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/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; 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/background.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 { @@ -26,14 +32,10 @@ class NodeDetailsView extends ConsumerStatefulWidget { required this.coin, required this.nodeId, required this.popRouteName, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/nodeDetails"; - final FlutterSecureStorageInterface secureStore; final Coin coin; final String nodeId; final String popRouteName; @@ -43,14 +45,16 @@ class NodeDetailsView extends ConsumerStatefulWidget { } class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { - late final FlutterSecureStorageInterface secureStore; + late final SecureStorageInterface secureStore; late final Coin coin; late final String nodeId; late final String popRouteName; + bool _desktopReadOnly = true; + @override initState() { - secureStore = widget.secureStore; + secureStore = ref.read(secureStoreProvider); coin = widget.coin; nodeId = widget.nodeId; popRouteName = widget.popRouteName; @@ -89,7 +93,29 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = await testMoneroNodeConnection( + Uri.parse(uriString), true); + testPassed = response.success; + } + } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); @@ -98,6 +124,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { break; case Coin.bitcoin: + case Coin.litecoin: case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: @@ -105,6 +132,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 +152,266 @@ 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))); + + final nodesForCoin = ref.watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))); + + final canDelete = nodesForCoin.length > 1; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + 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(DefaultNodes.defaultNodeIdPrefix)) + 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, ), - ], - ), - 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), - ], + 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 && canDelete) + 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 && canDelete) + const SizedBox( + height: 45, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Test connection", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + await _testConnection(ref, context); + }, + ), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + if (isDesktop) + Expanded( + child: + // !nodeId.startsWith(DefaultNodes.defaultNodeIdPrefix) + // ? + PrimaryButton( + label: _desktopReadOnly ? "Edit" : "Save", + buttonHeight: ButtonHeight.l, + onPressed: () async { + final shouldSave = _desktopReadOnly == false; + setState(() { + _desktopReadOnly = !_desktopReadOnly; + }); + + if (shouldSave) { + final editedNode = node!.copyWith( + host: ref.read(nodeFormDataProvider).host, + port: ref.read(nodeFormDataProvider).port, + name: ref.read(nodeFormDataProvider).name, + useSSL: ref.read(nodeFormDataProvider).useSSL, + loginName: ref.read(nodeFormDataProvider).login, + isFailover: + ref.read(nodeFormDataProvider).isFailover, + ); + + await ref + .read(nodeServiceChangeNotifierProvider) + .edit( + editedNode, + ref.read(nodeFormDataProvider).password, + true, + ); + } + }, + ) + // : Container() + , + ), + ], + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + ], ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index 39c95cad7..fb5722594 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -1,33 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/security_views/security_view.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_pin_put/custom_pin_put.dart'; -class ChangePinView extends StatefulWidget { +class ChangePinView extends ConsumerStatefulWidget { const ChangePinView({ Key? key, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/changePin"; - final FlutterSecureStorageInterface secureStore; - @override - State<ChangePinView> createState() => _ChangePinViewState(); + ConsumerState<ChangePinView> createState() => _ChangePinViewState(); } -class _ChangePinViewState extends State<ChangePinView> { +class _ChangePinViewState extends ConsumerState<ChangePinView> { BoxDecoration get _pinPutDecoration { return BoxDecoration( color: Theme.of(context).extension<StackColors>()!.textSubtitle2, @@ -49,11 +46,11 @@ class _ChangePinViewState extends State<ChangePinView> { final TextEditingController _pinPutController2 = TextEditingController(); final FocusNode _pinPutFocusNode2 = FocusNode(); - late final FlutterSecureStorageInterface _secureStore; + late final SecureStorageInterface _secureStore; @override void initState() { - _secureStore = widget.secureStore; + _secureStore = ref.read(secureStoreProvider); super.initState(); } @@ -69,182 +66,186 @@ class _ChangePinViewState extends State<ChangePinView> { @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: 70)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + 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(); + } + }, + ), ), - ), - body: SafeArea( - child: PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - children: [ - // page 1 - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: Text( - "Create new PIN", - style: STextStyles.pageTitleH1(context), + body: SafeArea( + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + // page 1 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + "Create new PIN", + style: STextStyles.pageTitleH1(context), + ), ), - ), - const SizedBox( - height: 52, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, + const SizedBox( + height: 52, ), - focusNode: _pinPutFocusNode1, - controller: _pinPutController1, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: - Theme.of(context).extension<StackColors>()!.background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label(context).copyWith( + fontSize: 1, + ), + focusNode: _pinPutFocusNode1, + controller: _pinPutController1, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( color: Theme.of(context) .extension<StackColors>()! .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) { - if (pin.length == Constants.pinLength) { - _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.linear, - ); - } - }, - ), - ], - ), - - // page 2 - - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: Text( - "Confirm new PIN", - style: STextStyles.pageTitleH1(context), - ), - ), - const SizedBox( - height: 52, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle3, - fontSize: 1, - ), - focusNode: _pinPutFocusNode2, - controller: _pinPutController2, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: - Theme.of(context).extension<StackColors>()!.background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) async { - if (_pinPutController1.text == _pinPutController2.text) { - // This should never fail as we are overwriting the existing pin - assert( - (await _secureStore.read(key: "stack_pin")) != null); - await _secureStore.write(key: "stack_pin", value: pin); - - showFloatingFlushBar( - type: FlushBarType.success, - message: "New PIN is set up", - context: context, - iconAsset: Assets.svg.check, - ); - - await Future<void>.delayed( - const Duration(milliseconds: 1200)); - - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(SecurityView.routeName), + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) { + if (pin.length == Constants.pinLength) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.linear, ); } - } else { - _pageController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.linear, - ); + }, + ), + ], + ), - showFloatingFlushBar( - type: FlushBarType.warning, - message: "PIN codes do not match. Try again.", - context: context, - iconAsset: Assets.svg.alertCircle, - ); + // page 2 - _pinPutController1.text = ''; - _pinPutController2.text = ''; - } - }, - ), - ], - ), - ], + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + "Confirm new PIN", + style: STextStyles.pageTitleH1(context), + ), + ), + const SizedBox( + height: 52, + ), + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle3, + fontSize: 1, + ), + focusNode: _pinPutFocusNode2, + controller: _pinPutController2, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) async { + if (_pinPutController1.text == _pinPutController2.text) { + // This should never fail as we are overwriting the existing pin + assert((await _secureStore.read(key: "stack_pin")) != + null); + await _secureStore.write(key: "stack_pin", value: pin); + + showFloatingFlushBar( + type: FlushBarType.success, + message: "New PIN is set up", + context: context, + iconAsset: Assets.svg.check, + ); + + await Future<void>.delayed( + const Duration(milliseconds: 1200)); + + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(SecurityView.routeName), + ); + } + } else { + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); + + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ); + + _pinPutController1.text = ''; + _pinPutController2.text = ''; + } + }, + ), + ], + ), + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart index 24fce5cd8..c2a64bb50 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/route_generator.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/background.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'; @@ -22,128 +23,131 @@ class SecurityView extends StatelessWidget { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Security", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Security", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: ChangePinView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: "Authenticate to change PIN", - biometricsAuthenticationTitle: "Change PIN", - ), - settings: - const RouteSettings(name: "/changepinlockscreen"), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, ), - child: Row( - children: [ - Text( - "Change PIN", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, + onPressed: () { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: ChangePinView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to change PIN", + biometricsAuthenticationTitle: "Change PIN", + ), + settings: + const RouteSettings(name: "/changepinlockscreen"), ), - ], + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Change PIN", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - // () { - // final useBio = - // ref.read(prefsChangeNotifierProvider).useBiometrics; - // - // debugPrint("useBio: $useBio"); - // ref.read(prefsChangeNotifierProvider).useBiometrics = - // !useBio; - // - // debugPrint( - // "useBio set to: ${ref.read(prefsChangeNotifierProvider).useBiometrics}"); - // }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Enable biometric authentication", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.useBiometrics), - ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .useBiometrics = newValue; - }, + onPressed: null, + // () { + // final useBio = + // ref.read(prefsChangeNotifierProvider).useBiometrics; + // + // debugPrint("useBio: $useBio"); + // ref.read(prefsChangeNotifierProvider).useBiometrics = + // !useBio; + // + // debugPrint( + // "useBio set to: ${ref.read(prefsChangeNotifierProvider).useBiometrics}"); + // }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable biometric authentication", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - ), - ], + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.useBiometrics), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .useBiometrics = newValue; + }, + ), + ), + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart index 3f832a4af..15b5a12fa 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart @@ -11,6 +11,8 @@ import 'package:stackwallet/utilities/enums/backup_frequency_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/background.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/custom_buttons/draggable_switch_button.dart'; @@ -223,237 +225,241 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> { frequencyController.text = Format.prettyFrequencyType(next); }); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Auto Backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Auto Backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Auto Backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - key: const Key("autoBackupToggleButtonKey"), - isOn: _toggle, - controller: toggleController, - onValueChanged: (newValue) async { - _toggle = newValue; - - if (_toggle) { - attemptEnable(); - } else { - attemptDisable(); - } - }, - ), - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - if (!isEnabledAutoBackup) + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ RoundedWhiteContainer( - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - style: STextStyles.label(context), - children: [ - const TextSpan( - text: - "Auto Backup is a custom Stack Wallet feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website "), - TextSpan( - text: "stackwallet.com.", - style: STextStyles.richLink(context), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse("https://stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - ), - if (isEnabledAutoBackup) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RoundedWhiteContainer( + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - BlueTextButton( - text: "Back up now", - onTap: () { - ref.read(autoSWBServiceProvider).doBackup(); - }, - ), Text( - "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", - style: STextStyles.itemSubtitle(context), - ) + "Auto Backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + key: const Key("autoBackupToggleButtonKey"), + isOn: _toggle, + controller: toggleController, + onValueChanged: (newValue) async { + _toggle = newValue; + + if (_toggle) { + attemptEnable(); + } else { + attemptDisable(); + } + }, + ), + ), ], ), ), - const SizedBox( - height: 32, - ), - Text( - "Auto Backup file", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("backupSavedToFileLocationTextFieldKey"), - focusNode: fileLocationFocusNode, - controller: fileLocationController, - enabled: false, - style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark - .withOpacity(0.5), - ), - readOnly: true, - enableSuggestions: false, - autocorrect: false, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: true, - ), - decoration: standardInputDecoration( - "Saved to", - fileLocationFocusNode, - context, - ), - ), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("backupPasswordFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, - enabled: false, - style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark - .withOpacity(0.5), - ), - obscureText: true, - enableSuggestions: false, - autocorrect: false, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: true, - ), - decoration: standardInputDecoration( - "Passphrase", - passwordFocusNode, - context, - ), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Auto Backup frequency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - TextField( - key: const Key("backupFrequencyFieldKey"), - controller: frequencyController, - enabled: false, - style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark - .withOpacity(0.5), - ), - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: true, - ), - ), - const SizedBox( - height: 20, - ), - Center( - child: BlueTextButton( - text: "Edit Auto Backup", - onTap: () async { - Navigator.of(context) - .pushNamed(EditAutoBackupView.routeName); - }, - ), - ) - ], + ), ), - ], + const SizedBox( + height: 8, + ), + if (!isEnabledAutoBackup) + RoundedWhiteContainer( + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: STextStyles.label(context), + children: [ + const TextSpan( + text: + "Auto Backup is a custom Stack Wallet feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website "), + TextSpan( + text: "stackwallet.com.", + style: STextStyles.richLink(context), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse("https://stackwallet.com"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ), + if (isEnabledAutoBackup) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BlueTextButton( + text: "Back up now", + onTap: () { + ref.read(autoSWBServiceProvider).doBackup(); + }, + ), + Text( + "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", + style: STextStyles.itemSubtitle(context), + ) + ], + ), + ), + const SizedBox( + height: 32, + ), + Text( + "Auto Backup file", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("backupSavedToFileLocationTextFieldKey"), + focusNode: fileLocationFocusNode, + controller: fileLocationController, + enabled: false, + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.5), + ), + readOnly: true, + enableSuggestions: false, + autocorrect: false, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: true, + ), + decoration: standardInputDecoration( + "Saved to", + fileLocationFocusNode, + context, + ), + ), + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("backupPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + enabled: false, + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.5), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: true, + ), + decoration: standardInputDecoration( + "Passphrase", + passwordFocusNode, + context, + ), + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Auto Backup frequency", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 10, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("backupFrequencyFieldKey"), + controller: frequencyController, + enabled: false, + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.5), + ), + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: true, + ), + ), + const SizedBox( + height: 20, + ), + Center( + child: BlueTextButton( + text: "Edit Auto Backup", + onTap: () async { + Navigator.of(context) + .pushNamed(EditAutoBackupView.routeName); + }, + ), + ) + ], + ), + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index b44a473b4..8e8731105 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -4,15 +4,15 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; 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:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.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/helpers/swb_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; @@ -21,6 +21,8 @@ import 'package:stackwallet/utilities/format.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -30,22 +32,17 @@ import 'package:zxcvbn/zxcvbn.dart'; class CreateAutoBackupView extends ConsumerStatefulWidget { const CreateAutoBackupView({ Key? key, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/createAutoBackup"; - final FlutterSecureStorageInterface secureStore; - @override ConsumerState<CreateAutoBackupView> createState() => _EnableAutoBackupViewState(); } class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { - late final FlutterSecureStorageInterface secureStore; + late final SecureStorageInterface secureStore; late final TextEditingController fileLocationController; late final TextEditingController passwordController; @@ -53,7 +50,7 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { late final FocusNode passwordFocusNode; late final FocusNode passwordRepeatFocusNode; - late final StackFileSystem stackFileSystem; + late final SWBFileSystem stackFileSystem; final zxcvbn = Zxcvbn(); String passwordFeedback = @@ -73,8 +70,8 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { @override void initState() { - secureStore = widget.secureStore; - stackFileSystem = StackFileSystem(); + secureStore = ref.read(secureStoreProvider); + stackFileSystem = SWBFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); passwordRepeatController = TextEditingController(); @@ -112,543 +109,559 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Create Auto Backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Create Auto 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: [ - Text( - "Create your backup file", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - if (!Platform.isAndroid) - TextField( - 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) {}, + 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: [ + Text( + "Create your backup file", + style: STextStyles.smallMed12(context), ), - if (!Platform.isAndroid) const SizedBox( height: 10, ), - 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; + if (!Platform.isAndroid) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + onTap: Platform.isAndroid + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); - passwordStrength = result.score! / 4; + if (mounted) { + await stackFileSystem.pickDir(context); + } - // 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: 32, - ), - Text( - "Auto Backup frequency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - Stack( - children: [ - const TextField( - readOnly: true, - textInputAction: TextInputAction.none, - ), - Positioned.fill( - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const BackupFrequencyTypeSelectSheet(), - ); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0), + 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( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, children: [ - Text( - Format.prettyFrequencyType(ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.backupFrequencyType))), - style: STextStyles.itemSubtitle12(context), + const SizedBox( + width: 16, ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: SvgPicture.asset( - Assets.svg.chevronDown, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - width: 12, - height: 6, - ), + SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + const SizedBox( + width: 12, ), ], ), ), ), - ) - ], - ), - const Spacer(), - const SizedBox( - height: 10, - ), - 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; + key: const Key( + "createBackupSaveToFileLocationTextFieldKey"), + readOnly: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: false, + ), + onChanged: (newValue) {}, + ), + if (!Platform.isAndroid) + const SizedBox( + height: 10, + ), + 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; - if (pathToSave.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ); - return; - } - if (!(await Directory(pathToSave).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ); - return; - } - if (passphrase.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ); - return; - } - if (passphrase != repeatPassphrase) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ); - return; - } + passwordStrength = result.score! / 4; - showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting initial backup", - message: "This shouldn't take long", - ), - ); + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", "phrases\nNo need"); + } - // make sure the dialog is able to be displayed for at least some time - final fut = Future<void>.delayed( - const Duration(milliseconds: 300)); + if (feedback.endsWith("\n")) { + feedback = + feedback.substring(0, feedback.length - 2); + } - String adkString; - int adkVersion; - try { - final adk = - await compute(generateAdk, passphrase); - adkString = Format.uint8listToString(adk.item2); - adkVersion = adk.item1; - } on Exception catch (e, s) { - String err = getErrorMessageFromSWBException(e); - Logging.instance - .log("$err\n$s", level: LogLevel.Error); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: err, - context: context, - ); - return; - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Error); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "$e", - context: context, - ); - return; - } - - await secureStore.write( - key: "auto_adk_string", value: adkString); - await secureStore.write( - key: "auto_adk_version_string", - value: adkVersion.toString()); - - final DateTime now = DateTime.now(); - final String fileToSave = - createAutoBackupFilename(pathToSave, now); - - final backup = await SWB.createStackWalletJSON(); - - bool result = await SWB.encryptStackWalletWithADK( - fileToSave, - adkString, - jsonEncode(backup), - adkVersion: adkVersion, - ); - - // this future should already be complete unless there was an error encrypting - await Future.wait([fut]); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - ref - .read(prefsChangeNotifierProvider) - .autoBackupLocation = pathToSave; - ref - .read(prefsChangeNotifierProvider) - .lastAutoBackup = now; - - ref - .read(prefsChangeNotifierProvider) - .isAutoBackupEnabled = true; - - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: - "Stack Auto Backup enabled and saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: - "Stack Auto Backup enabled!"), - ); - if (mounted) { - passwordController.text = ""; - passwordRepeatController.text = ""; - - Navigator.of(context).popUntil( - ModalRoute.withName( - AutoBackupView.routeName)); - } - } else { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Failed to enable Auto Backup"), - ); - } - } - }, - child: Text( - "Enable Auto Backup", - style: STextStyles.button(context), + 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: 32, + ), + Text( + "Auto Backup frequency", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 10, + ), + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, + ), + Positioned.fill( + child: RawMaterialButton( + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + const BackupFrequencyTypeSelectSheet(), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + Format.prettyFrequencyType(ref.watch( + prefsChangeNotifierProvider.select( + (value) => + value.backupFrequencyType))), + style: + STextStyles.itemSubtitle12(context), + ), + Padding( + padding: + const EdgeInsets.only(right: 4.0), + child: SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + width: 12, + height: 6, + ), + ), + ], + ), + ), + ), + ) + ], + ), + const Spacer(), + const SizedBox( + height: 10, + ), + 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) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + ); + return; + } + if (!(await Directory(pathToSave).exists())) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + ); + return; + } + if (passphrase.isEmpty) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + ); + return; + } + if (passphrase != repeatPassphrase) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + ); + return; + } + + showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting initial backup", + message: "This shouldn't take long", + ), + ); + + // make sure the dialog is able to be displayed for at least some time + final fut = Future<void>.delayed( + const Duration(milliseconds: 300)); + + String adkString; + int adkVersion; + try { + final adk = + await compute(generateAdk, passphrase); + adkString = + Format.uint8listToString(adk.item2); + adkVersion = adk.item1; + } on Exception catch (e, s) { + String err = + getErrorMessageFromSWBException(e); + Logging.instance + .log("$err\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.warning, + message: err, + context: context, + ); + return; + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$e", + context: context, + ); + return; + } + + await secureStore.write( + key: "auto_adk_string", value: adkString); + await secureStore.write( + key: "auto_adk_version_string", + value: adkVersion.toString()); + + final DateTime now = DateTime.now(); + final String fileToSave = + createAutoBackupFilename(pathToSave, now); + + final backup = await SWB.createStackWalletJSON( + secureStorage: secureStore, + ); + + bool result = + await SWB.encryptStackWalletWithADK( + fileToSave, + adkString, + jsonEncode(backup), + adkVersion: adkVersion, + ); + + // this future should already be complete unless there was an error encrypting + await Future.wait([fut]); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + ref + .read(prefsChangeNotifierProvider) + .autoBackupLocation = pathToSave; + ref + .read(prefsChangeNotifierProvider) + .lastAutoBackup = now; + + ref + .read(prefsChangeNotifierProvider) + .isAutoBackupEnabled = true; + + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: + "Stack Auto Backup enabled and saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: + "Stack Auto Backup enabled!"), + ); + if (mounted) { + passwordController.text = ""; + passwordRepeatController.text = ""; + + Navigator.of(context).popUntil( + ModalRoute.withName( + AutoBackupView.routeName)); + } + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: + "Failed to enable Auto Backup"), + ); + } + } + }, + child: Text( + "Enable Auto Backup", + style: STextStyles.button(context), + ), + ), + ], + ), ), ), - ), - ); - }), + ); + }), + ), ), ); } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart index 772c446f2..012477a5b 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -12,75 +13,77 @@ class CreateBackupInfoView extends StatelessWidget { @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)); - } - Navigator.of(context).pop(); - }, + return Background( + 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)); + } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Create backup", + style: STextStyles.navBarTitle(context), + ), ), - 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: [ - Center( - child: Text( - "Info", - style: STextStyles.pageTitleH2(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: [ + Center( + child: Text( + "Info", + style: STextStyles.pageTitleH2(context), + ), ), - ), - const SizedBox( - height: 16, - ), - RoundedWhiteContainer( - child: Text( - // TODO: need info - "{lorem ipsum}", - style: STextStyles.baseXS(context), + const SizedBox( + height: 16, ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - Navigator.of(context) - .pushNamed(CreateBackupView.routeName); - }, - child: Text( - "Next", - style: STextStyles.button(context), + RoundedWhiteContainer( + child: Text( + // TODO: need info + "{lorem ipsum}", + style: STextStyles.baseXS(context), + ), ), - ), - ], + const SizedBox( + height: 16, + ), + const Spacer(), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + Navigator.of(context) + .pushNamed(CreateBackupView.routeName); + }, + child: Text( + "Next", + style: STextStyles.button(context), + ), + ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 8dfc7588c..4d69ce4e9 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -7,14 +7,21 @@ 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/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/helpers/swb_file_system.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.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/utilities/util.dart'; +import 'package:stackwallet/widgets/background.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/progress_bar.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -36,7 +43,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { late final FocusNode passwordFocusNode; late final FocusNode passwordRepeatFocusNode; - late final StackFileSystem stackFileSystem; + late final SWBFileSystem stackFileSystem; final zxcvbn = Zxcvbn(); String passwordFeedback = @@ -56,7 +63,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { @override void initState() { - stackFileSystem = StackFileSystem(); + stackFileSystem = SWBFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); passwordRepeatController = TextEditingController(); @@ -92,424 +99,698 @@ 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, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + 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( + "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.only(bottom: 10), + child: Text( + "Choose file location", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), ), - 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(); + ), + child, + ], + ); + }, + 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) { + 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 (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, ), - ); - }), - if (!Platform.isAndroid) + 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) + SizedBox( + height: !isDesktop ? 8 : 24, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Text( + "Create a passphrase", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + ), + 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( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ const SizedBox( - height: 8, + width: 16, ), - 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); - } - + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { setState(() { - passwordFeedback = feedback; + hidePassword = !hidePassword; }); }, - ), - ), - 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) + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) .extension<StackColors>()! - .buttonBackSecondary, - percent: passwordStrength < 0.25 - ? 0.03 - : passwordStrength, + .textDark3, + width: 16, + height: 16, ), ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + width: 12, ), - 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? + ], + ), + ), + ), + 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( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, + 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( - 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; + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + // TODO: ? check if passwords match? + }, + ), + ), + const SizedBox( + height: 16, + ), + if (!isDesktop) const Spacer(), + !isDesktop + ? Consumer(builder: (context, ref, __) { + return 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>( + if (pathToSave.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", 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)); + 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; + } - final DateTime now = DateTime.now(); - final String fileToSave = - "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; + 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 backup = - await SWB.createStackWalletJSON(); + final DateTime now = DateTime.now(); + final String fileToSave = + "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; - bool result = - await SWB.encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); + final backup = await SWB.createStackWalletJSON( + secureStorage: ref.read(secureStoreProvider)); - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); + bool result = + await SWB.encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); - 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"), - ); - } + if (mounted) { + // pop encryption progress dialog + if (!isDesktop) 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), - ), + } + }, + child: Text( + "Create backup", + style: STextStyles.button(context), + ), + ); + }) + : Row( + children: [ + Consumer(builder: (context, ref, __) { + return PrimaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Create backup", + enabled: shouldEnableCreate, + 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: (_) { + if (Util.isDesktop) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all( + 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Encrypting initial backup", + style: + STextStyles.desktopH3( + context), + ), + const SizedBox( + height: 40, + ), + Text( + "This shouldn't take long", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ); + } else { + return const StackDialog( + title: "Encrypting initial backup", + message: "This shouldn't take long", + ); + } + }, + ), + ); + + await Future<void>.delayed( + const Duration(seconds: 1)); + + // make sure the dialog is able to be displayed for at least 1 second + final fut = 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( + secureStorage: + ref.read(secureStoreProvider)); + + bool result = await SWB + .encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); + + await Future.wait([fut]); + + if (mounted) { + // pop encryption progress dialog + if (!isDesktop) Navigator.of(context).pop(); + + if (result) { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (context) { + if (Platform.isAndroid) { + return StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ); + } else if (isDesktop) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 500, + child: Padding( + padding: + const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + const SizedBox( + height: 26), + Text( + "Stack backup saved to: \n", + style: STextStyles + .desktopH3(context), + ), + Text( + fileToSave, + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 40, + ), + Row( + children: [ + // const Spacer(), + Expanded( + child: + PrimaryButton( + label: "Ok", + buttonHeight: + ButtonHeight + .l, + onPressed: () { + int count = 0; + Navigator.of( + context) + .popUntil((_) => + count++ >= + 2); + }, + ), + ), + ], + ) + ], + ), + ), + ); + } else { + return 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"), + ); + } + } + }, + ); + }), + const SizedBox( + width: 16, + ), + SecondaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Cancel", + onPressed: () {}, ), ], ), - ), - ), - ); - }, + ], ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart index 16e51a35d..905cdea72 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart @@ -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/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class CancelStackRestoreDialog extends StatelessWidget { @@ -14,38 +19,95 @@ class CancelStackRestoreDialog extends StatelessWidget { onWillPop: () async { return false; }, - child: StackDialog( - title: "Cancel restore process", - message: - "Cancelling will revert any changes that may have been applied", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Back", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Yes, cancel", - style: STextStyles.itemSubtitle12(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.buttonTextPrimary, + child: !Util.isDesktop + ? StackDialog( + title: "Cancel restore process", + message: + "Cancelling will revert any changes that may have been applied", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Yes, cancel", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ) + : DesktopDialog( + maxHeight: 250, + maxWidth: 600, + child: Padding( + padding: const EdgeInsets.only( + top: 20, left: 32, right: 32, bottom: 20), + child: Column( + children: [ + Text( + "Cancel Restore Process", + style: STextStyles.desktopH3(context), + ), + const SizedBox(height: 24), + SizedBox( + width: 500, + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackError, + child: Text( + "If you cancel, the restore will not complete, and " + "the wallets will not appear in your Stack.", + style: STextStyles.desktopTextMedium(context), + ), + ), + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 248, + buttonHeight: ButtonHeight.l, + enabled: true, + label: "Keep restoring", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 248, + buttonHeight: ButtonHeight.l, + enabled: true, + label: "Cancel anyway", + onPressed: () { + Navigator.of(context).pop(true); + }, + ) + ], + ), + ], + ), + ), ), - ), - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ), ); } } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 9368d3b77..7187c5311 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -1,27 +1,35 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/foundation.dart'; 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:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.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/helpers/swb_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.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/background.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'; @@ -30,21 +38,16 @@ import 'package:zxcvbn/zxcvbn.dart'; class EditAutoBackupView extends ConsumerStatefulWidget { const EditAutoBackupView({ Key? key, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/editAutoBackup"; - final FlutterSecureStorageInterface secureStore; - @override ConsumerState<EditAutoBackupView> createState() => _EditAutoBackupViewState(); } class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { - late final FlutterSecureStorageInterface secureStore; + late final SecureStorageInterface secureStore; late final TextEditingController fileLocationController; late final TextEditingController passwordController; @@ -52,9 +55,17 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { late final FocusNode passwordFocusNode; late final FocusNode passwordRepeatFocusNode; - late final StackFileSystem stackFileSystem; + late final SWBFileSystem stackFileSystem; final zxcvbn = Zxcvbn(); + late BackupFrequencyType _currentDropDownValue; + + final List<BackupFrequencyType> _dropDownItems = [ + BackupFrequencyType.everyTenMinutes, + BackupFrequencyType.everyAppStart, + BackupFrequencyType.afterClosingAWallet, + ]; + String passwordFeedback = "Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters."; @@ -70,10 +81,161 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { passwordRepeatController.text.isNotEmpty; } + void onSavePressed() 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: "Updating Auto Backup", + message: "This shouldn't take long", + ), + ), + ); + // make sure the dialog is able to be displayed for at least 1 second + final fut = Future<void>.delayed(const Duration(seconds: 1)); + + String adkString; + int adkVersion; + try { + final adk = await compute(generateAdk, passphrase); + adkString = Format.uint8listToString(adk.item2); + adkVersion = adk.item1; + } on Exception catch (e, s) { + String err = getErrorMessageFromSWBException(e); + Logging.instance.log("$err\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: err, + context: context, + ), + ); + return; + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$e", + context: context, + ), + ); + return; + } + + await secureStore.write(key: "auto_adk_string", value: adkString); + await secureStore.write( + key: "auto_adk_version_string", value: adkVersion.toString()); + + final DateTime now = DateTime.now(); + final String fileToSave = createAutoBackupFilename(pathToSave, now); + + final backup = await SWB.createStackWalletJSON( + secureStorage: ref.read(secureStoreProvider), + ); + + bool result = await SWB.encryptStackWalletWithADK( + fileToSave, + adkString, + jsonEncode(backup), + adkVersion: adkVersion, + ); + + // this future should already be complete unless there was an error encrypting + await Future.wait([fut]); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + ref.read(prefsChangeNotifierProvider).autoBackupLocation = pathToSave; + ref.read(prefsChangeNotifierProvider).lastAutoBackup = now; + + ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = true; + + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Stack Auto Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog(title: "Stack Auto Backup saved"), + ); + if (mounted) { + passwordController.text = ""; + passwordRepeatController.text = ""; + + Navigator.of(context) + .popUntil(ModalRoute.withName(AutoBackupView.routeName)); + } + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => + const StackOkDialog(title: "Failed to update Auto Backup"), + ); + } + } + } + @override void initState() { - secureStore = widget.secureStore; - stackFileSystem = StackFileSystem(); + secureStore = ref.read(secureStoreProvider); + stackFileSystem = SWBFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); passwordRepeatController = TextEditingController(); @@ -81,6 +243,9 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { fileLocationController.text = ref.read(prefsChangeNotifierProvider).autoBackupLocation ?? ""; + _currentDropDownValue = + ref.read(prefsChangeNotifierProvider).backupFrequencyType; + passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); @@ -114,541 +279,518 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Edit Auto Backup", - style: STextStyles.navBarTitle(context), + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Edit Auto 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, + ), + ), + ); + }), + ), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder(builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, + child: Column( + crossAxisAlignment: + isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.stretch, + children: [ + if (!isDesktop) + Text( + "Create your backup", + style: STextStyles.smallMed12(context), + ), + if (isDesktop) + Text( + "Choose file location", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Create your backup", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - if (!Platform.isAndroid) - TextField( - onTap: Platform.isAndroid - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + if (!Platform.isAndroid) + 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) {}, - ), - if (!Platform.isAndroid) - const SizedBox( - height: 10, - ), - 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); - } + if (mounted) { + await stackFileSystem.pickDir(context); + } + if (mounted) { setState(() { - passwordFeedback = feedback; + 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) {}, + ), + if (isDesktop) + const SizedBox( + height: 24, + ), + if (isDesktop) + Text( + "Create a passphrase", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.left, + ), + if (!Platform.isAndroid) + const SizedBox( + height: 10, + ), + 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( + labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; }); }, - ), - ), - 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) + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) .extension<StackColors>()! - .buttonBackSecondary, - percent: - passwordStrength < 0.25 ? 0.03 : passwordStrength, + .textDark3, + width: 16, + height: 16, ), ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + width: 12, ), - 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: 32, - ), - Text( - "Auto Backup frequency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - Stack( - children: [ - const TextField( - readOnly: true, - textInputAction: TextInputAction.none, - ), - Positioned.fill( - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const BackupFrequencyTypeSelectSheet(), - ); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - Format.prettyFrequencyType(ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.backupFrequencyType))), - style: STextStyles.itemSubtitle12(context), - ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: SvgPicture.asset( - Assets.svg.chevronDown, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - width: 12, - height: 6, - ), - ), - ], - ), - ), - ), - ) - ], - ), - const Spacer(), - const SizedBox( - height: 10, - ), - 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; + ], + ), + ), + ), + 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; - if (pathToSave.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ); - return; - } - if (!(await Directory(pathToSave).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ); - return; - } - if (passphrase.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ); - return; - } - if (passphrase != repeatPassphrase) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ); - return; - } + passwordStrength = result.score! / 4; - showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Updating Auto Backup", - message: "This shouldn't take long", - ), - ); - // make sure the dialog is able to be displayed for at least 1 second - final fut = Future<void>.delayed( - const Duration(seconds: 1)); + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", "phrases\nNo need"); + } - String adkString; - int adkVersion; - try { - final adk = - await compute(generateAdk, passphrase); - adkString = Format.uint8listToString(adk.item2); - adkVersion = adk.item1; - } on Exception catch (e, s) { - String err = getErrorMessageFromSWBException(e); - Logging.instance - .log("$err\n$s", level: LogLevel.Error); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: err, - context: context, - ); - return; - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Error); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "$e", - context: context, - ); - return; - } + if (feedback.endsWith("\n")) { + feedback = feedback.substring(0, feedback.length - 2); + } - await secureStore.write( - key: "auto_adk_string", value: adkString); - await secureStore.write( - key: "auto_adk_version_string", - value: adkVersion.toString()); - - final DateTime now = DateTime.now(); - final String fileToSave = - createAutoBackupFilename(pathToSave, now); - - final backup = await SWB.createStackWalletJSON(); - - bool result = await SWB.encryptStackWalletWithADK( - fileToSave, - adkString, - jsonEncode(backup), - adkVersion: adkVersion, - ); - - // this future should already be complete unless there was an error encrypting - await Future.wait([fut]); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - ref - .read(prefsChangeNotifierProvider) - .autoBackupLocation = pathToSave; - ref - .read(prefsChangeNotifierProvider) - .lastAutoBackup = now; - - ref - .read(prefsChangeNotifierProvider) - .isAutoBackupEnabled = true; - - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: - "Stack Auto Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: "Stack Auto Backup saved"), - ); - if (mounted) { - passwordController.text = ""; - passwordRepeatController.text = ""; - - Navigator.of(context).popUntil( - ModalRoute.withName( - AutoBackupView.routeName)); - } - } else { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Failed to update Auto Backup"), - ); - } - } - }, - child: Text( - "Save", - style: STextStyles.button(context), - ), + 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: isDesktop + ? 492 + : 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, + ), + ), + SizedBox( + height: isDesktop ? 16 : 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( + labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, + 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? + }, + ), + ), + SizedBox( + height: isDesktop ? 24 : 32, + ), + Text( + "Auto Backup frequency", + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3, + ) + : STextStyles.smallMed12(context), + ), + const SizedBox( + height: 10, + ), + if (isDesktop) + DropdownButtonHideUnderline( + child: DropdownButton2( + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: _currentDropDownValue, + items: [ + ..._dropDownItems.map( + (e) { + String message = ""; + switch (e) { + case BackupFrequencyType.everyTenMinutes: + message = "Every 10 minutes"; + break; + case BackupFrequencyType.everyAppStart: + message = "Every app startup"; + break; + case BackupFrequencyType.afterClosingAWallet: + message = "After closing a cryptocurrency wallet"; + break; + } + + return DropdownMenuItem( + value: e, + child: Text( + message, + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ); + }, + ), + ], + onChanged: (value) { + if (value is BackupFrequencyType) { + if (ref + .read(prefsChangeNotifierProvider) + .backupFrequencyType != + value) { + ref + .read(prefsChangeNotifierProvider) + .backupFrequencyType = value; + } + setState(() { + _currentDropDownValue = value; + }); + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), ), ), - ); - }), + if (!isDesktop) + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, + ), + Positioned.fill( + child: RawMaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => const BackupFrequencyTypeSelectSheet(), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + Format.prettyFrequencyType(ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.backupFrequencyType))), + style: STextStyles.itemSubtitle12(context), + ), + Padding( + padding: const EdgeInsets.only(right: 4.0), + child: SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + width: 12, + height: 6, + ), + ), + ], + ), + ), + ), + ) + ], + ), + if (!isDesktop) const Spacer(), + SizedBox( + height: isDesktop ? 24 : 10, + ), + if (isDesktop) + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + buttonHeight: ButtonHeight.l, + enabled: shouldEnableCreate, + onPressed: onSavePressed, + ), + ), + ], + ), + if (!isDesktop) + TextButton( + style: shouldEnableCreate + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: !shouldEnableCreate ? null : onSavePressed, + child: Text( + "Save", + style: STextStyles.button(context), + ), + ) + ], ), ); } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 6d803eb6d..3d599f717 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -3,10 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; -import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; @@ -91,10 +88,13 @@ abstract class SWB { static bool _shouldCancelRestore = false; - static bool _checkShouldCancel(PreRestoreState? revertToState) { + static bool _checkShouldCancel( + PreRestoreState? revertToState, + SecureStorageInterface secureStorageInterface, + ) { if (_shouldCancelRestore) { if (revertToState != null) { - _revert(revertToState); + _revert(revertToState, secureStorageInterface); } else { _cancelCompleter!.complete(); _shouldCancelRestore = false; @@ -193,15 +193,15 @@ abstract class SWB { /// [secureStorage] parameter exposed for testing purposes static Future<Map<String, dynamic>> createStackWalletJSON({ - FlutterSecureStorageInterface? secureStorage, + required SecureStorageInterface secureStorage, }) async { Logging.instance .log("Starting createStackWalletJSON...", level: LogLevel.Info); final _wallets = Wallets.sharedInstance; Map<String, dynamic> backupJson = {}; - NodeService nodeService = NodeService(); - final _secureStore = - secureStorage ?? const SecureStorageWrapper(FlutterSecureStorage()); + NodeService nodeService = + NodeService(secureStorageInterface: secureStorage); + final _secureStore = secureStorage; Logging.instance.log("createStackWalletJSON awaiting DB.instance.mutex...", level: LogLevel.Info); @@ -448,6 +448,7 @@ abstract class SWB { Map<String, dynamic> validJSON, StackRestoringUIState? uiState, Map<String, String> oldToNewWalletIdMap, + SecureStorageInterface secureStorageInterface, ) async { Map<String, dynamic> prefs = validJSON["prefs"] as Map<String, dynamic>; List<dynamic>? addressBookEntries = @@ -486,7 +487,11 @@ abstract class SWB { "SWB restoring nodes", level: LogLevel.Warning, ); - await _restoreNodes(nodes, primaryNodes); + await _restoreNodes( + nodes, + primaryNodes, + secureStorageInterface, + ); uiState?.nodes = StackRestoringStatus.success; uiState?.trades = StackRestoringStatus.restoring; @@ -543,6 +548,7 @@ abstract class SWB { static Future<bool?> restoreStackWalletJSON( String jsonBackup, StackRestoringUIState? uiState, + SecureStorageInterface secureStorageInterface, ) async { if (!Platform.isLinux) await Wakelock.enable(); @@ -550,7 +556,8 @@ abstract class SWB { "SWB creating temp backup", level: LogLevel.Warning, ); - final preRestoreJSON = await createStackWalletJSON(); + final preRestoreJSON = + await createStackWalletJSON(secureStorage: secureStorageInterface); Logging.instance.log( "SWB temp backup created", level: LogLevel.Warning, @@ -587,19 +594,34 @@ abstract class SWB { // basic cancel check here // no reverting required yet as nothing has been written to store - if (_checkShouldCancel(null)) { + if (_checkShouldCancel( + null, + secureStorageInterface, + )) { return false; } - await _restoreEverythingButWallets(validJSON, uiState, oldToNewWalletIdMap); + await _restoreEverythingButWallets( + validJSON, + uiState, + oldToNewWalletIdMap, + secureStorageInterface, + ); // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } - final nodeService = NodeService(); - final walletsService = WalletsService(); + final nodeService = NodeService( + secureStorageInterface: secureStorageInterface, + ); + final walletsService = WalletsService( + secureStorageInterface: secureStorageInterface, + ); final _prefs = Prefs.instance; await _prefs.init(); @@ -609,7 +631,10 @@ abstract class SWB { for (var walletbackup in wallets) { // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -647,7 +672,10 @@ abstract class SWB { final failovers = nodeService.failoverNodesFor(coin: coin); // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -655,6 +683,7 @@ abstract class SWB { coin, walletId, walletName, + secureStorageInterface, node, txTracker, _prefs, @@ -665,7 +694,10 @@ abstract class SWB { managers.add(Tuple2(walletbackup, manager)); // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -679,7 +711,10 @@ abstract class SWB { } // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -690,7 +725,10 @@ abstract class SWB { // start restoring wallets for (final tuple in managers) { // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } final bools = await asyncRestore(tuple, uiState, walletsService); @@ -698,13 +736,19 @@ abstract class SWB { } // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } for (Future<bool> status in restoreStatuses) { // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } await status; @@ -712,7 +756,10 @@ abstract class SWB { if (!Platform.isLinux) await Wakelock.disable(); // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -720,7 +767,10 @@ abstract class SWB { return true; } - static Future<void> _revert(PreRestoreState revertToState) async { + static Future<void> _revert( + PreRestoreState revertToState, + SecureStorageInterface secureStorageInterface, + ) async { Map<String, dynamic> prefs = revertToState.validJSON["prefs"] as Map<String, dynamic>; List<dynamic>? addressBookEntries = @@ -788,7 +838,9 @@ abstract class SWB { } // nodes - NodeService nodeService = NodeService(); + NodeService nodeService = NodeService( + secureStorageInterface: secureStorageInterface, + ); final currentNodes = nodeService.nodes; if (nodes == null) { // no pre nodes found so we delete all but defaults @@ -914,7 +966,8 @@ abstract class SWB { } // finally remove any added wallets - final walletsService = WalletsService(); + final walletsService = + WalletsService(secureStorageInterface: secureStorageInterface); final namesData = await walletsService.walletNames; for (final entry in namesData.entries) { if (!revertToState.walletIds.contains(entry.value.walletId)) { @@ -989,8 +1042,11 @@ abstract class SWB { static Future<void> _restoreNodes( List<dynamic>? nodes, List<dynamic>? primaryNodes, + SecureStorageInterface secureStorageInterface, ) async { - NodeService nodeService = NodeService(); + NodeService nodeService = NodeService( + secureStorageInterface: secureStorageInterface, + ); if (nodes != null) { for (var node in nodes) { await nodeService.add( diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart similarity index 81% rename from lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart rename to lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart index e57c5493f..e88e11dfe 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart @@ -4,15 +4,16 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:stackwallet/utilities/util.dart'; -class StackFileSystem { +class SWBFileSystem { Directory? rootPath; Directory? startPath; String? filePath; String? dirPath; - final bool isDesktop = !(Platform.isAndroid || Platform.isIOS); + final bool isDesktop = Util.isDesktop; Future<Directory> prepareStorage() async { if (Platform.isAndroid) { @@ -25,11 +26,20 @@ class StackFileSystem { } debugPrint(rootPath!.absolute.toString()); - Directory sampleFolder = - Directory('${rootPath!.path}Documents/Stack_backups'); + late Directory sampleFolder; + if (Platform.isIOS) { sampleFolder = Directory(rootPath!.path); + } else if (Platform.isAndroid) { + sampleFolder = Directory('${rootPath!.path}Documents/Stack_backups'); + } else if (Platform.isLinux) { + sampleFolder = Directory('${rootPath!.path}/Stack_backups'); + } else if (Platform.isWindows) { + sampleFolder = Directory('${rootPath!.path}/Stack_backups'); + } else if (Platform.isMacOS) { + sampleFolder = Directory('${rootPath!.path}/Stack_backups'); } + try { if (!sampleFolder.existsSync()) { sampleFolder.createSync(recursive: true); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart index 3173bc402..14a262d99 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart @@ -9,9 +9,9 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/stack_back import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.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/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -62,204 +62,209 @@ class _RestoreFromEncryptedStringViewState Widget build(BuildContext context) { return WillPopScope( onWillPop: _onWillPop, - 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) { - _onWillPop(); - } - }, + child: Background( + 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) { + _onWillPop(); + } + }, + ), + title: Text( + "Restore from file", + style: STextStyles.navBarTitle(context), + ), ), - 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: [ - 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, + 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: [ + 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, - height: 16, ), - ), - const SizedBox( - width: 12, - ), - ], + 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, + ), + ], + ), ), ), + onChanged: (newValue) { + setState(() {}); + }, ), - onChanged: (newValue) { - setState(() {}); - }, ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: passwordController.text.isEmpty - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - onPressed: passwordController.text.isEmpty - ? null - : () async { - final String passphrase = - passwordController.text; + const SizedBox( + height: 16, + ), + const Spacer(), + TextButton( + style: passwordController.text.isEmpty + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: passwordController.text.isEmpty + ? null + : () async { + final String passphrase = + passwordController.text; - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } - 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, + 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, + const SizedBox( + height: 64, ), - ), - ], - ), - ), - ); - - final String? jsonString = await compute( - SWB.decryptStackWalletStringWithPassphrase, - Tuple2(widget.encrypted, 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, - fromFile: true, + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], ), ), ); - } - }, - child: Text( - "Restore", - style: STextStyles.button(context), + + final String? jsonString = await compute( + SWB.decryptStackWalletStringWithPassphrase, + Tuple2(widget.encrypted, 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, + fromFile: true, + ), + ), + ); + } + }, + child: Text( + "Restore", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index cec114023..6350feb52 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -6,7 +7,7 @@ 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/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/helpers/swb_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/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -15,7 +16,14 @@ 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/background.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/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'; @@ -36,13 +44,13 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { late final FocusNode passwordFocusNode; - late final StackFileSystem stackFileSystem; + late final SWBFileSystem stackFileSystem; bool hidePassword = true; @override void initState() { - stackFileSystem = StackFileSystem(); + stackFileSystem = SWBFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); @@ -63,188 +71,245 @@ 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, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + 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(); + } + }, ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextField( - onTap: () async { - try { - await stackFileSystem.prepareStorage(); - if (mounted) { - await stackFileSystem.openFile(context); - } + 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.only(bottom: 10.0), + child: Text( + "Choose file location", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + ), + child, + ], + ); + }, + 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) { + 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) {}, + ), + SizedBox( + height: !isDesktop ? 8 : 24, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Text( + "Enter passphrase", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + ), + 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 passphrase", + passwordFocusNode, + context, + ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, + 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( - 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, - ), - ], + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, ), ), - ), - 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, - ), - ], - ), - ), + 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; + ), + ), + onChanged: (newValue) { + setState(() {}); + }, + ), + ), + const SizedBox( + height: 16, + ), + if (!isDesktop) const Spacer(), + !isDesktop + ? 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 (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; - } + if (!(await File(fileToRestore).exists())) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Backup file does not exist", + context: context, + ); + return; + } - bool shouldPop = false; + bool shouldPop = false; + unawaited( showDialog<dynamic>( barrierDismissible: false, context: context, @@ -284,52 +349,234 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { ], ), ), + ), + ); + + 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) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to decrypt backup file", + context: context, + ); + return; + } + + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => StackRestoreProgressView( + jsonString: jsonString, + ), + ), ); + } + }, + child: Text( + "Restore", + style: STextStyles.button(context), + ), + ) + : Row( + children: [ + PrimaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Restore", + enabled: !(passwordController.text.isEmpty || + fileLocationController.text.isEmpty), + onPressed: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { + final String fileToRestore = + fileLocationController.text; + final String passphrase = + passwordController.text; - final String? jsonString = await compute( - SWB.decryptStackWalletWithPassphrase, - Tuple2(fileToRestore, passphrase), - debugLabel: "stack wallet decryption compute", - ); + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } - if (mounted) { - // pop LoadingIndicator - shouldPop = true; - Navigator.of(context).pop(); - - passwordController.text = ""; - - if (jsonString == null) { - showFloatingFlushBar( + if (!(await File(fileToRestore).exists())) { + await showFloatingFlushBar( type: FlushBarType.warning, - message: "Failed to decrypt backup file", + message: "Backup file does not exist", context: context, ); return; } - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => StackRestoreProgressView( - jsonString: jsonString, + bool shouldPop = false; + unawaited( + 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, + ), + ), + ], + ), ), ), ); - } - }, - child: Text( - "Restore", - style: STextStyles.button(context), + + final String? jsonString = await compute( + SWB.decryptStackWalletWithPassphrase, + Tuple2(fileToRestore, passphrase), + debugLabel: + "stack wallet decryption compute", + ); + + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of( + context, + rootNavigator: true, + ).pop(); + + passwordController.text = ""; + + if (jsonString == null) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Failed to decrypt backup file", + context: context, + ); + return; + } + + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: 750, + maxWidth: 600, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + mainAxisAlignment: + MainAxisAlignment + .start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets + .all(32), + child: Text( + "Restore Stack Wallet", + style: STextStyles + .desktopH3( + context), + textAlign: + TextAlign + .center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: + const EdgeInsets + .symmetric( + horizontal: + 32), + child: + StackRestoreProgressView( + jsonString: + jsonString, + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ), + ), + ); + }, + ), + ); + }); + } + }, ), - ), - ], - ), - ), - ), - ); - }, - ), - ), - ); + const SizedBox( + width: 16, + ), + SecondaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Cancel", + onPressed: () {}, + ), + ], + ), + ], + ), + )); } } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart index 679043fe7..fe163cb66 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart @@ -7,6 +7,7 @@ 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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -21,147 +22,149 @@ class StackBackupView extends StatelessWidget { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Stack backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Stack backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + 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(AutoBackupView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.backupAuto, - height: 28, - width: 28, - ), - const SizedBox( - width: 12, - ), - Text( - "Auto Backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of(context).pushNamed(AutoBackupView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.backupAuto, + height: 28, + width: 28, + ), + const SizedBox( + width: 12, + ), + Text( + "Auto Backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + 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(CreateBackupView.routeName); - // .pushNamed(CreateBackupInfoView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.backupAdd, - height: 28, - width: 28, - ), - const SizedBox( - width: 12, - ), - Text( - "Create manual backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of(context).pushNamed(CreateBackupView.routeName); + // .pushNamed(CreateBackupInfoView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.backupAdd, + height: 28, + width: 28, + ), + const SizedBox( + width: 12, + ), + Text( + "Create manual backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + 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(RestoreFromFileView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.backupRestore, - height: 28, - width: 28, - ), - const SizedBox( - width: 12, - ), - Text( - "Restore backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of(context) + .pushNamed(RestoreFromFileView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.backupRestore, + height: 28, + width: 28, + ), + const SizedBox( + width: 12, + ), + Text( + "Restore backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart index 5e5142425..92e7742e1 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart @@ -3,13 +3,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.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/restore_from_encrypted_string_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_item_card.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/stack_restore/stack_restoring_ui_state_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -17,11 +17,19 @@ import 'package:stackwallet/utilities/enums/stack_restoring_status.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/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; +import '../../../../../pages_desktop_specific/home/desktop_home_view.dart'; +import '../../../../../pages_desktop_specific/home/desktop_menu.dart'; +import '../../../../../providers/desktop/current_desktop_menu_item.dart'; +import '../../../../../widgets/desktop/primary_button.dart'; + class StackRestoreProgressView extends ConsumerStatefulWidget { const StackRestoreProgressView({ Key? key, @@ -39,6 +47,8 @@ class StackRestoreProgressView extends ConsumerStatefulWidget { class _StackRestoreProgressViewState extends ConsumerState<StackRestoreProgressView> { + bool isDesktop = Util.isDesktop; + Future<void> _cancel() async { bool shouldPop = false; unawaited(showDialog<void>( @@ -79,10 +89,15 @@ class _StackRestoreProgressViewState await SWB.cancelRestore(); shouldPop = true; + + int count = 0; + if (mounted) { - Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile - ? RestoreFromEncryptedStringView.routeName - : StackBackupView.routeName)); + !isDesktop + ? Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile + ? RestoreFromEncryptedStringView.routeName + : StackBackupView.routeName)) + : Navigator.of(context).popUntil((_) => count++ >= 2); } } @@ -92,6 +107,30 @@ class _StackRestoreProgressViewState context: context, builder: (_) => const CancelStackRestoreDialog(), ); + // : await Row( + // children: [ + // SecondaryButton( + // width: 248, + // buttonHeight: ButtonHeight.l, + // enabled: true, + // label: "Keep restoring", + // onPressed: () { + // false; + // }, + // ), + // const SizedBox(width: 16), + // PrimaryButton( + // width: 248, + // buttonHeight: ButtonHeight.l, + // enabled: true, + // label: "Cancel anyway", + // onPressed: () { + // true; + // }, + // ) + // ], + // ); + if (result is bool && result) { return true; } @@ -107,6 +146,7 @@ class _StackRestoreProgressViewState finished = await SWB.restoreStackWalletJSON( widget.jsonString, uiState, + ref.read(secureStoreProvider), ); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); @@ -120,6 +160,7 @@ class _StackRestoreProgressViewState } bool _success = false; + bool pendingCancel = false; Future<bool> _onWillPop() async { if (_success) { @@ -179,281 +220,504 @@ class _StackRestoreProgressViewState @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, - 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 (_success) { - _addWalletsToHomeView(); - if (mounted) { - Navigator.of(context).pop(); - } - } else { - if (await _requestCancel()) { - await _cancel(); - } - } - }, - ), - title: Text( - "Restoring Stack wallet", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: Padding( + bool isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return WillPopScope( + onWillPop: _onWillPop, + 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 (_success) { + _addWalletsToHomeView(); + if (mounted) { + Navigator.of(context).pop(); + } + } else { + if (await _requestCancel()) { + await _cancel(); + } + } + }, + ), + title: Text( + "Restoring Stack wallet", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( padding: const EdgeInsets.only( - left: 4, - top: 4, - right: 4, - bottom: 0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Settings", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - final state = ref.watch(stackRestoringUIStateProvider - .select((value) => value.preferences)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.gear, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Preferences", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - final state = ref.watch(stackRestoringUIStateProvider - .select((value) => value.addressBook)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: AddressBookIcon( - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Address book", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - final state = ref.watch(stackRestoringUIStateProvider - .select((value) => value.nodes)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.node, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Nodes", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - final state = ref.watch(stackRestoringUIStateProvider - .select((value) => value.trades)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.arrowRotate2, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Exchange history", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); - }, - ), - const SizedBox( - height: 16, - ), - Text( - "Wallets", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), - ...ref - .watch(stackRestoringUIStateProvider - .select((value) => value.walletStateProviders)) - .values - .map( - (provider) => Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: RestoringWalletCard( - provider: provider, - ), - ), - ), - const SizedBox( - height: 80, - ), - ], + left: 12, + top: 12, + right: 12, ), + child: child, ), ), - ), - floatingActionButton: SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: TextButton( - onPressed: () async { - if (_success) { - _addWalletsToHomeView(); - Navigator.of(context) - .popUntil(ModalRoute.withName(HomeView.routeName)); - } else { - if (await _requestCancel()) { - await _cancel(); - } - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - _success ? "OK" : "Cancel restore process", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary, + ); + }, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only( + left: 4, + top: 4, + right: 4, + bottom: 4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Settings", + style: STextStyles.itemSubtitle(context), ), - ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + final state = ref.watch(stackRestoringUIStateProvider + .select((value) => value.preferences)); + return !isDesktop + ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.gear, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Preferences", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.gear, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Preferences", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ), + ); + }, + ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + final state = ref.watch(stackRestoringUIStateProvider + .select((value) => value.addressBook)); + return !isDesktop + ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: AddressBookIcon( + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Address book", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: AddressBookIcon( + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Address book", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ), + ); + }, + ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + final state = ref.watch(stackRestoringUIStateProvider + .select((value) => value.nodes)); + return !isDesktop + ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Nodes", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Nodes", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + )); + }, + ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + final state = ref.watch(stackRestoringUIStateProvider + .select((value) => value.trades)); + return !isDesktop + ? Container( + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.arrowRotate2, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Exchange history", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ), + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.arrowRotate2, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Exchange history", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ), + ); + }, + ), + const SizedBox( + height: 16, + ), + Text( + "Wallets", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 8, + ), + ...ref + .watch(stackRestoringUIStateProvider + .select((value) => value.walletStateProviders)) + .values + .map( + (provider) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: RestoringWalletCard( + provider: provider, + ), + ), + ), + const SizedBox( + height: 30, + ), + SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: !isDesktop + ? TextButton( + onPressed: () async { + if (_success) { + Navigator.of(context).pop(); + } else { + if (await _requestCancel()) { + await _cancel(); + } + } + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + _success ? "OK" : "Cancel restore process", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _success + ? PrimaryButton( + width: 248, + buttonHeight: ButtonHeight.l, + enabled: true, + label: "Done", + onPressed: () async { + DesktopMenuItemId keyID = + DesktopMenuItemId.myStack; + + ref + .read(currentDesktopMenuItemProvider + .state) + .state = keyID; + + Navigator.of(context, rootNavigator: true) + .popUntil( + ModalRoute.withName( + DesktopHomeView.routeName), + ); + }, + ) + : SecondaryButton( + width: 248, + buttonHeight: ButtonHeight.l, + enabled: true, + label: "Cancel restore process", + onPressed: () async { + if (await _requestCancel()) { + await _cancel(); + } + }, + ), + ], + ), + ), + ], ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart index 55f0588d2..2239eee71 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart @@ -11,6 +11,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/stack_restoring_status.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/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; @@ -68,140 +69,287 @@ class _RestoringWalletCardState extends ConsumerState<RestoringWalletCard> { final coin = ref.watch(provider.select((value) => value.coin)); final restoringStatus = ref.watch(provider.select((value) => value.restoringState)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context).extension<StackColors>()!.colorForCoin(coin), - child: Center( - child: SvgPicture.asset( - Assets.svg.iconFor( - coin: coin, - ), - height: 20, - width: 20, - ), - ), - ), - ), - onRightTapped: restoringStatus == StackRestoringStatus.failed - ? () async { - final manager = ref.read(provider).manager!; - - ref.read(stackRestoringUIStateProvider).update( - walletId: manager.walletId, - restoringStatus: StackRestoringStatus.restoring); - - try { - final mnemonicList = await manager.mnemonic; - int maxUnusedAddressGap = 20; - if (coin == Coin.firo) { - maxUnusedAddressGap = 50; - } - const maxNumberOfIndexesToCheck = 1000; - - if (mnemonicList.isEmpty) { - await manager.recoverFromMnemonic( - mnemonic: ref.read(provider).mnemonic!, - maxUnusedAddressGap: maxUnusedAddressGap, - maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, - height: ref.read(provider).height ?? 0, - ); - } else { - await manager.fullRescan( - maxUnusedAddressGap, - maxNumberOfIndexesToCheck, - ); - } - - if (mounted) { - final address = await manager.currentReceivingAddress; - - ref.read(stackRestoringUIStateProvider).update( - walletId: manager.walletId, - restoringStatus: StackRestoringStatus.success, - address: address, - ); - } - } catch (_) { - if (mounted) { - ref.read(stackRestoringUIStateProvider).update( - walletId: manager.walletId, - restoringStatus: StackRestoringStatus.failed, - ); - } - } - } - : null, - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState( - ref.watch(provider.select((value) => value.restoringState)), - ), - ), - title: - "${ref.watch(provider.select((value) => value.walletName))} (${coin.ticker})", - subTitle: restoringStatus == StackRestoringStatus.failed - ? Text( - "Unable to restore. Tap icon to retry.", - style: STextStyles.errorSmall(context), - ) - : ref.watch(provider.select((value) => value.address)) != null - ? Text( - ref.watch(provider.select((value) => value.address))!, - style: STextStyles.infoSmall(context), - ) - : null, - button: restoringStatus == StackRestoringStatus.failed - ? Container( - height: 20, - decoration: BoxDecoration( + return !Util.isDesktop + ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), color: Theme.of(context) .extension<StackColors>()! - .buttonBackSecondary, - borderRadius: BorderRadius.circular( - 1000, - ), - ), - child: RawMaterialButton( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - splashColor: - Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, + .colorForCoin(coin), + child: Center( + child: SvgPicture.asset( + Assets.svg.iconFor( + coin: coin, + ), + height: 20, + width: 20, ), ), - onPressed: () async { - final mnemonic = ref.read(provider).mnemonic; + ), + ), + onRightTapped: restoringStatus == StackRestoringStatus.failed + ? () async { + final manager = ref.read(provider).manager!; - if (mnemonic != null) { - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => RecoverPhraseView( - walletName: ref.read(provider).walletName, - mnemonic: mnemonic.split(" "), + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.restoring); + + try { + final mnemonicList = await manager.mnemonic; + int maxUnusedAddressGap = 20; + if (coin == Coin.firo) { + maxUnusedAddressGap = 50; + } + const maxNumberOfIndexesToCheck = 1000; + + if (mnemonicList.isEmpty) { + await manager.recoverFromMnemonic( + mnemonic: ref.read(provider).mnemonic!, + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, + height: ref.read(provider).height ?? 0, + ); + } else { + await manager.fullRescan( + maxUnusedAddressGap, + maxNumberOfIndexesToCheck, + ); + } + + if (mounted) { + final address = await manager.currentReceivingAddress; + + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.success, + address: address, + ); + } + } catch (_) { + if (mounted) { + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.failed, + ); + } + } + } + : null, + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState( + ref.watch(provider.select((value) => value.restoringState)), + ), + ), + title: + "${ref.watch(provider.select((value) => value.walletName))} (${coin.ticker})", + subTitle: restoringStatus == StackRestoringStatus.failed + ? Text( + "Unable to restore. Tap icon to retry.", + style: STextStyles.errorSmall(context), + ) + : ref.watch(provider.select((value) => value.address)) != null + ? Text( + ref.watch(provider.select((value) => value.address))!, + style: STextStyles.infoSmall(context), + ) + : null, + button: restoringStatus == StackRestoringStatus.failed + ? Container( + height: 20, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + borderRadius: BorderRadius.circular( + 1000, + ), + ), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, ), ), - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - "Show recovery phrase", - style: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + onPressed: () async { + final mnemonic = ref.read(provider).mnemonic; + + if (mnemonic != null) { + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => RecoverPhraseView( + walletName: ref.read(provider).walletName, + mnemonic: mnemonic.split(" "), + ), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + "Show recovery phrase", + style: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderColor: Theme.of(context).extension<StackColors>()!.background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .colorForCoin(coin), + child: Center( + child: SvgPicture.asset( + Assets.svg.iconFor( + coin: coin, + ), + height: 20, + width: 20, + ), ), ), ), - ) - : null, - ); + onRightTapped: restoringStatus == StackRestoringStatus.failed + ? () async { + final manager = ref.read(provider).manager!; + + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.restoring); + + try { + final mnemonicList = await manager.mnemonic; + int maxUnusedAddressGap = 20; + if (coin == Coin.firo) { + maxUnusedAddressGap = 50; + } + const maxNumberOfIndexesToCheck = 1000; + + if (mnemonicList.isEmpty) { + await manager.recoverFromMnemonic( + mnemonic: ref.read(provider).mnemonic!, + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: + maxNumberOfIndexesToCheck, + height: ref.read(provider).height ?? 0, + ); + } else { + await manager.fullRescan( + maxUnusedAddressGap, + maxNumberOfIndexesToCheck, + ); + } + + if (mounted) { + final address = await manager.currentReceivingAddress; + + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.success, + address: address, + ); + } + } catch (_) { + if (mounted) { + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.failed, + ); + } + } + } + : null, + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState( + ref.watch(provider.select((value) => value.restoringState)), + ), + ), + title: + "${ref.watch(provider.select((value) => value.walletName))} (${coin.ticker})", + subTitle: restoringStatus == StackRestoringStatus.failed + ? Text( + "Unable to restore. Tap icon to retry.", + style: STextStyles.errorSmall(context), + ) + : ref.watch(provider.select((value) => value.address)) != null + ? Text( + ref.watch(provider.select((value) => value.address))!, + style: STextStyles.infoSmall(context), + ) + : null, + button: restoringStatus == StackRestoringStatus.failed + ? Container( + height: 20, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + borderRadius: BorderRadius.circular( + 1000, + ), + ), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () async { + final mnemonic = ref.read(provider).mnemonic; + + if (mnemonic != null) { + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => RecoverPhraseView( + walletName: ref.read(provider).walletName, + mnemonic: mnemonic.split(" "), + ), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + "Show recovery phrase", + style: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ) + : null, + ), + ); } } diff --git a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart index baf649ba2..186b9b293 100644 --- a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart +++ b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/providers/global/prefs_provider.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -22,255 +23,263 @@ class _StartupPreferencesViewState extends ConsumerState<StartupPreferencesView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Startup preferences", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Startup preferences", - 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: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - 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, + 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: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + 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: () { + ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup = 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: false, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value + .gotoWalletOnStartup), + ), + onChanged: (value) { + if (value is bool) { + ref + .read( + prefsChangeNotifierProvider) + .gotoWalletOnStartup = value; + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Home screen", + style: + STextStyles.titleBold12( + context), + textAlign: TextAlign.left, + ), + Text( + "Stack Wallet home screen", + style: + STextStyles.itemSubtitle( + context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ], + ), + ), ), ), - onPressed: () { - ref - .read(prefsChangeNotifierProvider) - .gotoWalletOnStartup = false; - }, - child: Container( + ), + 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: () { + ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup = true; + }, + 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: true, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value + .gotoWalletOnStartup), + ), + onChanged: (value) { + if (value is bool) { + ref + .read( + prefsChangeNotifierProvider) + .gotoWalletOnStartup = value; + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Specific wallet", + style: + STextStyles.titleBold12( + context), + textAlign: TextAlign.left, + ), + Text( + "Select a specific wallet to load into on startup", + style: + STextStyles.itemSubtitle( + context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + if (!ref.watch(prefsChangeNotifierProvider.select( + (value) => value.gotoWalletOnStartup))) + const SizedBox( + height: 12, + ), + if (ref.watch(prefsChangeNotifierProvider.select( + (value) => value.gotoWalletOnStartup))) + Container( color: Colors.transparent, child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.only( + left: 12.0, + right: 12, + bottom: 12, + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: false, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value - .gotoWalletOnStartup), - ), - onChanged: (value) { - if (value is bool) { - ref - .read( - prefsChangeNotifierProvider) - .gotoWalletOnStartup = value; - } - }, - ), - ), const SizedBox( - width: 12, + width: 12 + 20, + height: 12, ), Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Home screen", - style: STextStyles.titleBold12( - context), - textAlign: TextAlign.left, + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize + .shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + Constants + .size.circularBorderRadius, ), - Text( - "Stack Wallet home screen", - style: STextStyles.itemSubtitle( - context), - textAlign: TextAlign.left, - ), - ], + ), + onPressed: () { + Navigator.of(context).pushNamed( + StartupWalletSelectionView + .routeName); + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Select wallet...", + style: STextStyles.link2( + 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: () { - ref - .read(prefsChangeNotifierProvider) - .gotoWalletOnStartup = true; - }, - 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: true, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value - .gotoWalletOnStartup), - ), - onChanged: (value) { - if (value is bool) { - ref - .read( - prefsChangeNotifierProvider) - .gotoWalletOnStartup = value; - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Specific wallet", - style: STextStyles.titleBold12( - context), - textAlign: TextAlign.left, - ), - Text( - "Select a specific wallet to load into on startup", - style: STextStyles.itemSubtitle( - context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - if (!ref.watch(prefsChangeNotifierProvider - .select((value) => value.gotoWalletOnStartup))) - const SizedBox( - height: 12, - ), - if (ref.watch(prefsChangeNotifierProvider - .select((value) => value.gotoWalletOnStartup))) - 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: () { - Navigator.of(context).pushNamed( - StartupWalletSelectionView - .routeName); - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Select wallet...", - style: - STextStyles.link2(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ), - ], - ), - ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart index 5d9f2edb1..975e8394d 100644 --- a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart +++ b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart @@ -6,6 +6,7 @@ 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/background.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'; @@ -33,170 +34,173 @@ class _StartupWalletSelectionViewState _controllers[manager.walletId] = DSBController(); } - 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( - "Select startup wallet", - style: STextStyles.navBarTitle(context), + return Background( + child: 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( + "Select startup wallet", + style: STextStyles.navBarTitle(context), + ), ), ), - ), - body: LayoutBuilder(builder: (context, 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.start, - children: [ - const SizedBox( - height: 4, - ), - Text( - "Select a wallet to load into immediately on startup", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ...managers.map( - (manager) => Padding( - padding: const EdgeInsets.all(12), - child: Row( - key: Key( - "startupWalletSelectionGroupKey_${manager.walletId}"), - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .colorForCoin(manager.coin) - .withOpacity(0.5), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Padding( - padding: const EdgeInsets.all(4), - child: SvgPicture.asset( - Assets.svg - .iconFor(coin: manager.coin), - width: 20, - height: 20, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12( - context), + body: LayoutBuilder(builder: (context, 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.start, + children: [ + const SizedBox( + height: 4, + ), + Text( + "Select a wallet to load into immediately on startup", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ...managers.map( + (manager) => Padding( + padding: const EdgeInsets.all(12), + child: Row( + key: Key( + "startupWalletSelectionGroupKey_${manager.walletId}"), + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .colorForCoin(manager.coin) + .withOpacity(0.5), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: SvgPicture.asset( + Assets.svg + .iconFor(coin: manager.coin), + width: 20, + height: 20, ), - // const SizedBox( - // height: 2, - // ), - // FutureBuilder( - // future: manager.totalBalance, - // builder: (builderContext, - // AsyncSnapshot<Decimal> snapshot) { - // if (snapshot.connectionState == - // ConnectionState.done && - // snapshot.hasData) { - // return Text( - // "${Format.localizedStringAsFixed( - // value: snapshot.data!, - // locale: ref.watch( - // localeServiceChangeNotifierProvider - // .select((value) => - // value.locale)), - // decimalPlaces: 8, - // )} ${manager.coin.ticker}", - // style: STextStyles.itemSubtitle(context), - // ); - // } else { - // return AnimatedText( - // stringsToLoopThrough: const [ - // "Loading balance", - // "Loading balance.", - // "Loading balance..", - // "Loading balance..." - // ], - // style: STextStyles.itemSubtitle(context), - // ); - // } - // }, - // ), - ], - ), - ), - SizedBox( - height: 20, - width: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: manager.walletId, - groupValue: ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.startupWalletId), ), - onChanged: (value) { - if (value is String) { - ref - .read( - prefsChangeNotifierProvider) - .startupWalletId = value; - } - }, ), - ), - ], + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12( + context), + ), + // const SizedBox( + // height: 2, + // ), + // FutureBuilder( + // future: manager.totalBalance, + // builder: (builderContext, + // AsyncSnapshot<Decimal> snapshot) { + // if (snapshot.connectionState == + // ConnectionState.done && + // snapshot.hasData) { + // return Text( + // "${Format.localizedStringAsFixed( + // value: snapshot.data!, + // locale: ref.watch( + // localeServiceChangeNotifierProvider + // .select((value) => + // value.locale)), + // decimalPlaces: 8, + // )} ${manager.coin.ticker}", + // style: STextStyles.itemSubtitle(context), + // ); + // } else { + // return AnimatedText( + // stringsToLoopThrough: const [ + // "Loading balance", + // "Loading balance.", + // "Loading balance..", + // "Loading balance..." + // ], + // style: STextStyles.itemSubtitle(context), + // ); + // } + // }, + // ), + ], + ), + ), + SizedBox( + height: 20, + width: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: manager.walletId, + groupValue: ref.watch( + prefsChangeNotifierProvider.select( + (value) => + value.startupWalletId), + ), + onChanged: (value) { + if (value is String) { + ref + .read( + prefsChangeNotifierProvider) + .startupWalletId = value; + } + }, + ), + ), + ], + ), ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ), ), - ), - ); - }), + ); + }), + ), ); } } diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index fdfa6f404..1cc3d35a1 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -4,6 +4,9 @@ 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/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -14,272 +17,208 @@ class SupportView extends StatelessWidget { }) : super(key: key); static const String routeName = "/support"; - final double iconSize = 28; @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Support", - style: STextStyles.navBarTitle(context), - ), + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Support", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "If you need support or want to report a bug, reach out to us on any of our socials!", + style: STextStyles.smallMed12(context), + ), + ), + isDesktop + ? const SizedBox( + height: 24, + ) + : const SizedBox( + height: 12, + ), + AboutItem( + linkUrl: "https://t.me/stackwallet", + label: "Telegram", + buttonText: "@stackwallet", + iconAsset: Assets.socials.telegram, + isDesktop: isDesktop, + ), + const SizedBox( + height: 8, + ), + AboutItem( + linkUrl: "https://discord.com/invite/mRPZuXx3At", + label: "Discord", + buttonText: "Stack Wallet", + iconAsset: Assets.socials.discord, + isDesktop: isDesktop, + ), + const SizedBox( + height: 8, + ), + AboutItem( + linkUrl: "https://www.reddit.com/r/stackwallet/", + label: "Reddit", + buttonText: "r/stackwallet", + iconAsset: Assets.socials.reddit, + isDesktop: isDesktop, + ), + const SizedBox( + height: 8, + ), + AboutItem( + linkUrl: "https://twitter.com/stack_wallet", + label: "Twitter", + buttonText: "@stack_wallet", + iconAsset: Assets.socials.twitter, + isDesktop: isDesktop, + ), + const SizedBox( + height: 8, + ), + AboutItem( + linkUrl: "mailto:support@stackwallet.com", + label: "Email", + buttonText: "support@stackwallet.com", + iconAsset: Assets.svg.envelope, + isDesktop: isDesktop, + ), + ], ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Text( - "If you need support or want to report a bug, reach out to us on any of our socials!", - style: STextStyles.smallMed12(context), - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ); + } +} + +class AboutItem extends StatelessWidget { + const AboutItem({ + Key? key, + required this.linkUrl, + required this.label, + required this.buttonText, + required this.iconAsset, + required this.isDesktop, + }) : super(key: key); + + final String linkUrl; + final String label; + final String buttonText; + final String iconAsset; + final bool isDesktop; + + @override + Widget build(BuildContext context) { + final double iconSize = isDesktop ? 20 : 28; + + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + launchUrl( + Uri.parse(linkUrl), + mode: LaunchMode.externalApplication, + ); + }, + child: Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 20, + vertical: 15, + ) + : const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, ), - onPressed: () { - launchUrl( - Uri.parse("https://t.me/stackwallet"), - mode: LaunchMode.externalApplication, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.telegram, - width: iconSize, - height: iconSize, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ConditionalParent( + condition: isDesktop, + builder: (child) => Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10000), color: Theme.of(context) .extension<StackColors>()! - .accentColorDark, + .buttonBackSecondary, ), - const SizedBox( - width: 12, + child: Center( + child: child, ), - Text( - "Telegram", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + ), + child: SvgPicture.asset( + iconAsset, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension<StackColors>()! + .bottomNavIconIcon, + ), ), - ), + const SizedBox( + width: 12, + ), + Text( + label, + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - launchUrl( - Uri.parse("https://discord.gg/RZMG3yUm"), - mode: LaunchMode.externalApplication, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.discord, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Discord", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - launchUrl( - Uri.parse("https://www.reddit.com/r/stackwallet/"), - mode: LaunchMode.externalApplication, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.reddit, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Reddit", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - launchUrl( - Uri.parse("https://twitter.com/stack_wallet"), - mode: LaunchMode.externalApplication, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.twitter, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Twitter", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - launchUrl( - Uri.parse("mailto://support@stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.envelope, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Email", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ), - ), - ], + if (isDesktop) + Text( + buttonText, + style: STextStyles.desktopTextExtraExtraSmall(context), + ) + // BlueTextButton( + // text: buttonText, + // onTap: () { + // launchUrl( + // Uri.parse(linkUrl), + // mode: LaunchMode.externalApplication, + // ); + // }, + // ), + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart index bada67353..10a93d7e8 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart @@ -6,7 +6,12 @@ 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/background.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 +21,392 @@ 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 Background( + child: 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, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], ), ); } diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart index e48ebb342..6ce84627e 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart @@ -6,6 +6,7 @@ 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/widgets/background.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'; @@ -28,131 +29,136 @@ class SyncingPreferencesView 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(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Syncing preferences", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Syncing preferences", - 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: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + 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: [ + RoundedWhiteContainer( padding: const EdgeInsets.all(0), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context) - .pushNamed(SyncingOptionsView.routeName); - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Syncing", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - Text( - _currentTypeDescription(ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.syncType))), - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ) - ], - ), - const Spacer(), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: null, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "AutoSync only on Wi-Fi", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.wifiOnly), - ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .wifiOnly = newValue; - }, + ), + onPressed: () { + Navigator.of(context) + .pushNamed(SyncingOptionsView.routeName); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Syncing", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - ), - ], - ), + Text( + _currentTypeDescription(ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.syncType))), + style: + STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ) + ], + ), + const Spacer(), + ], ), - ); - }, + ), + ), ), - ), - ], + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "AutoSync only on Wi-Fi", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.wifiOnly), + ), + onValueChanged: (newValue) { + ref + .read( + prefsChangeNotifierProvider) + .wifiOnly = newValue; + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart index 1e302cf12..7cbc86b7a 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart @@ -10,7 +10,10 @@ 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/background.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 +28,49 @@ 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 Background( + child: 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: Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: child, + ), ), - ), - ), - body: LayoutBuilder(builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - 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 +93,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 +235,9 @@ class WalletSyncingOptionsView extends ConsumerWidget { ), ), ), - ), - ); - }), + ); + }), + ), ); } } diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart index 3d557d245..a9235172f 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/utilities/constants.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/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -35,186 +36,188 @@ class WalletBackupView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Wallet backup", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.all(10), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - color: Theme.of(context).extension<StackColors>()!.background, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, - ), - onPressed: () async { - await clipboardInterface - .setData(ClipboardData(text: mnemonic.join(" "))); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - )); - }, - ), - ), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - ], - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 4, - ), - Text( - ref - .watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId))) - .walletName, - textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - const SizedBox( - height: 4, - ), - Text( - "Recovery Phrase", - textAlign: TextAlign.center, - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", - style: STextStyles.label(context), + title: Text( + "Wallet backup", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: Theme.of(context).extension<StackColors>()!.background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + ), + onPressed: () async { + await clipboardInterface + .setData(ClipboardData(text: mnemonic.join(" "))); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + )); + }, ), ), ), - const SizedBox( - height: 8, - ), - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: mnemonic, - isDesktop: false, + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 4, + ), + Text( + ref + .watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))) + .walletName, + textAlign: TextAlign.center, + style: STextStyles.label(context).copyWith( + fontSize: 12, ), ), - ), - const SizedBox( - height: 12, - ), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - String data = AddressUtils.encodeQRSeedData(mnemonic); + const SizedBox( + height: 4, + ), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", + style: STextStyles.label(context), + ), + ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: SingleChildScrollView( + child: MnemonicTable( + words: mnemonic, + isDesktop: false, + ), + ), + ), + const SizedBox( + height: 12, + ), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + String data = AddressUtils.encodeQRSeedData(mnemonic); - 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( - "Recovery phrase 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: data, - size: width, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .popupBG, - foregroundColor: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + 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( + "Recovery phrase QR code", + style: STextStyles.pageTitleH2(context), ), ), - ), - const SizedBox( - height: 12, - ), - Center( - child: SizedBox( - width: width, - child: TextButton( - onPressed: () async { - // await _capturePng(true); - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) + const SizedBox( + height: 12, + ), + Center( + child: RepaintBoundary( + // key: _qrKey, + child: SizedBox( + width: width + 20, + height: width + 20, + child: QrImage( + data: data, + size: width, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .popupBG, + foregroundColor: Theme.of(context) .extension<StackColors>()! .accentColorDark), ), ), ), - ), - ], - ), - ); - }, - ); - }, - child: Text( - "Show QR Code", - style: STextStyles.button(context), + const SizedBox( + height: 12, + ), + Center( + child: SizedBox( + width: width, + child: TextButton( + onPressed: () async { + // await _capturePng(true); + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + child: Text( + "Show QR Code", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart index 141eb4c99..150af6ac5 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart @@ -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( + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + label: "Cancel", + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + 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(); - }, ), - ), - ); + ); + } } } diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 5b1c57214..7e29010b1 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/pages/settings_views/sub_widgets/nodes_list.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; @@ -23,9 +24,13 @@ 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/animated_text.dart'; +import 'package:stackwallet/widgets/background.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/blue_text_button.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -59,7 +64,7 @@ class _WalletNetworkSettingsViewState extends ConsumerState<WalletNetworkSettingsView> { final double _padding = 16; final double _boxPadding = 12; - final double _iconSize = 28; + final double _iconSize = Util.isDesktop ? 40 : 28; late final EventBus eventBus; @@ -73,19 +78,22 @@ class _WalletNetworkSettingsViewState late double _percent; late int _blocksRemaining; + bool _advancedIsExpanded = false; Future<void> _attemptRescan() async { - if (!Platform.isLinux) Wakelock.enable(); + if (!Platform.isLinux) await Wakelock.enable(); int maxUnusedAddressGap = 20; const int maxNumberOfIndexesToCheck = 1000; - showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) => const RescanningDialog(), + unawaited( + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) => const RescanningDialog(), + ), ); try { @@ -131,7 +139,7 @@ class _WalletNetworkSettingsViewState ); } } catch (e) { - if (!Platform.isLinux) Wakelock.disable(); + if (!Platform.isLinux) await Wakelock.disable(); if (mounted) { // pop rescanning dialog @@ -162,7 +170,7 @@ class _WalletNetworkSettingsViewState } } - if (!Platform.isLinux) Wakelock.disable(); + if (!Platform.isLinux) await Wakelock.disable(); } String _percentString(double value) { @@ -262,9 +270,11 @@ class _WalletNetworkSettingsViewState @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; + final bool isDesktop = Util.isDesktop; - final progressLength = - screenWidth - (_padding * 2) - (_boxPadding * 3) - _iconSize; + final progressLength = isDesktop + ? 430.0 + : screenWidth - (_padding * 2) - (_boxPadding * 3) - _iconSize; final coin = ref .read(walletsChangeNotifierProvider) @@ -300,443 +310,601 @@ class _WalletNetworkSettingsViewState } } - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Network", - 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("walletNetworkSettingsAddNewNodeViewButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.verticalEllipsis, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( onPressed: () { - showDialog<dynamic>( - barrierColor: Colors.transparent, - barrierDismissible: true, - context: context, - builder: (_) { - return Stack( - children: [ - Positioned( - top: 9, - right: 10, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius), - // boxShadow: [CFColors.standardBoxShadow], - boxShadow: const [], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - Navigator.of(context).pop(); - showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return ConfirmFullRescanDialog( - onConfirm: _attemptRescan, - ); - }, - ); - }, - child: RoundedWhiteContainer( - child: Material( - color: Colors.transparent, - child: Text( - "Rescan blockchain", - style: STextStyles.baseXS(context), + Navigator.of(context).pop(); + }, + ), + title: Text( + "Network", + 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( + "walletNetworkSettingsAddNewNodeViewButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension<StackColors>()! + .background, + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + showDialog<dynamic>( + barrierColor: Colors.transparent, + barrierDismissible: true, + context: context, + builder: (_) { + return Stack( + children: [ + Positioned( + top: 9, + right: 10, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + // boxShadow: [CFColors.standardBoxShadow], + boxShadow: const [], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + showDialog<void>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return ConfirmFullRescanDialog( + onConfirm: _attemptRescan, + ); + }, + ); + }, + child: RoundedWhiteContainer( + child: Material( + color: Colors.transparent, + child: Text( + "Rescan blockchain", + style: + STextStyles.baseXS(context), + ), + ), + ), ), - ), + ], ), ), - ], - ), - ), - ), - ], - ); - }, - ); - }, + ), + ], + ); + }, + ); + }, + ), + ), + ), + ], + ), + body: Padding( + padding: EdgeInsets.only( + top: 12, + left: _padding, + right: _padding, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + child, + ], + ), ), ), ), - ], - ), - body: Padding( - padding: EdgeInsets.only( - top: 12, - left: _padding, - right: _padding, - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Blockchain status", - textAlign: TextAlign.left, - style: STextStyles.smallMed12(context), - ), - GestureDetector( - onTap: () { - ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .refresh(); - }, - child: Text( - "Resync", - style: STextStyles.link2(context), - ), - ), - ], - ), - const SizedBox( - height: 9, - ), - if (_currentSyncStatus == WalletSyncStatus.synced) - RoundedWhiteContainer( - child: Row( - children: [ - Container( - width: _iconSize, - height: _iconSize, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen - .withOpacity(0.2), - borderRadius: BorderRadius.circular(_iconSize), - ), - child: Center( - child: SvgPicture.asset( - Assets.svg.radio, - height: 14, - width: 14, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - ), - ), - ), - SizedBox( - width: _boxPadding, - ), - Column( - children: [ - SizedBox( - width: progressLength, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Synchronized", - style: STextStyles.w600_10(context), - ), - Text( - "100%", - style: STextStyles.syncPercent(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - ), - ), - ], - ), - ), - const SizedBox( - height: 4, - ), - ProgressBar( - width: progressLength, - height: 5, - fillColor: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - percent: 1, - ), - ], - ), - ], - ), - ), - if (_currentSyncStatus == WalletSyncStatus.syncing) - RoundedWhiteContainer( - child: Row( - children: [ - Container( - width: _iconSize, - height: _iconSize, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow - .withOpacity(0.2), - borderRadius: BorderRadius.circular(_iconSize), - ), - child: Center( - child: SvgPicture.asset( - Assets.svg.radioSyncing, - height: 14, - width: 14, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow, - ), - ), - ), - SizedBox( - width: _boxPadding, - ), - Column( - children: [ - SizedBox( - width: progressLength, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - AnimatedText( - style: STextStyles.w600_10(context), - stringsToLoopThrough: const [ - "Synchronizing", - "Synchronizing.", - "Synchronizing..", - "Synchronizing...", - ], - ), - Row( - children: [ - Text( - _percentString(_percent), - style: - STextStyles.syncPercent(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow, - ), - ), - if (coin == Coin.monero || - coin == Coin.wownero || - coin == Coin.epicCash) - Text( - " (Blocks to go: ${_blocksRemaining == -1 ? "?" : _blocksRemaining})", - style: - STextStyles.syncPercent(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow, - ), - ), - ], - ) - ], - ), - ), - const SizedBox( - height: 4, - ), - ProgressBar( - width: progressLength, - height: 5, - fillColor: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - percent: _percent, - ), - ], - ), - ], - ), - ), - if (_currentSyncStatus == WalletSyncStatus.unableToSync) - RoundedWhiteContainer( - child: Row( - children: [ - Container( - width: _iconSize, - height: _iconSize, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed - .withOpacity(0.2), - borderRadius: BorderRadius.circular(_iconSize), - ), - child: Center( - child: SvgPicture.asset( - Assets.svg.radioProblem, - height: 14, - width: 14, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed, - ), - ), - ), - SizedBox( - width: _boxPadding, - ), - Column( - children: [ - SizedBox( - width: progressLength, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Unable to synchronize", - style: - STextStyles.w600_10(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed, - ), - ), - Text( - "0%", - style: STextStyles.syncPercent(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed, - ), - ), - ], - ), - ), - const SizedBox( - height: 4, - ), - ProgressBar( - width: progressLength, - height: 5, - fillColor: Theme.of(context) - .extension<StackColors>()! - .accentColorRed, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - percent: 0, - ), - ], - ), - ], - ), - ), - if (_currentSyncStatus == WalletSyncStatus.unableToSync) - Padding( - padding: const EdgeInsets.only( - top: 12, - ), - child: RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .warningBackground, - child: Text( - "Please check your internet connection and make sure your current node is not having issues.", - style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, - ), - ), - ), - ), - const SizedBox( - height: 20, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${ref.watch(walletsChangeNotifierProvider.select((value) => value.getManager(widget.walletId).coin)).prettyName} nodes", - textAlign: TextAlign.left, - style: STextStyles.smallMed12(context), - ), - BlueTextButton( - text: "Add new node", - onTap: () { - Navigator.of(context).pushNamed( - AddEditNodeView.routeName, - arguments: Tuple4( - AddEditNodeViewType.add, - ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .coin, - null, - WalletNetworkSettingsView.routeName, - ), - ); - }, - ), - ], - ), - const SizedBox( - height: 8, - ), - NodesList( - coin: ref.watch(walletsChangeNotifierProvider.select( - (value) => value.getManager(widget.walletId).coin)), - popBackToRoute: WalletNetworkSettingsView.routeName, - ), - ], + Text( + "Blockchain status", + textAlign: TextAlign.left, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + GestureDetector( + onTap: () { + ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .refresh(); + }, + child: Text( + "Resync", + style: STextStyles.link2(context), + ), ), ], ), - ), + SizedBox( + height: isDesktop ? 12 : 9, + ), + if (_currentSyncStatus == WalletSyncStatus.synced) + RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: _iconSize, + height: _iconSize, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen + .withOpacity(0.2), + borderRadius: BorderRadius.circular(_iconSize), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.radio, + height: isDesktop ? 19 : 14, + width: isDesktop ? 19 : 14, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ), + ), + ), + SizedBox( + width: _boxPadding, + ), + Column( + children: [ + SizedBox( + width: progressLength, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Synchronized", + style: STextStyles.w600_10(context), + ), + Text( + "100%", + style: STextStyles.syncPercent(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ), + ), + ], + ), + ), + const SizedBox( + height: 4, + ), + ProgressBar( + width: progressLength, + height: 5, + fillColor: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + percent: 1, + ), + ], + ), + ], + ), + ), + if (_currentSyncStatus == WalletSyncStatus.syncing) + RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: _iconSize, + height: _iconSize, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + .withOpacity(0.2), + borderRadius: BorderRadius.circular(_iconSize), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.radioSyncing, + height: 14, + width: 14, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow, + ), + ), + ), + SizedBox( + width: _boxPadding, + ), + Column( + children: [ + SizedBox( + width: progressLength, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AnimatedText( + style: STextStyles.w600_10(context), + stringsToLoopThrough: const [ + "Synchronizing", + "Synchronizing.", + "Synchronizing..", + "Synchronizing...", + ], + ), + Row( + children: [ + Text( + _percentString(_percent), + style: + STextStyles.syncPercent(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow, + ), + ), + if (coin == Coin.monero || + coin == Coin.wownero || + coin == Coin.epicCash) + Text( + " (Blocks to go: ${_blocksRemaining == -1 ? "?" : _blocksRemaining})", + style: STextStyles.syncPercent(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow, + ), + ), + ], + ) + ], + ), + ), + const SizedBox( + height: 4, + ), + ProgressBar( + width: progressLength, + height: 5, + fillColor: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + percent: _percent, + ), + ], + ), + ], + ), + ), + if (_currentSyncStatus == WalletSyncStatus.unableToSync) + RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: _iconSize, + height: _iconSize, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed + .withOpacity(0.2), + borderRadius: BorderRadius.circular(_iconSize), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.radioProblem, + height: 14, + width: 14, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed, + ), + ), + ), + SizedBox( + width: _boxPadding, + ), + Column( + children: [ + SizedBox( + width: progressLength, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Unable to synchronize", + style: STextStyles.w600_10(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed, + ), + ), + Text( + "0%", + style: STextStyles.syncPercent(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed, + ), + ), + ], + ), + ), + const SizedBox( + height: 4, + ), + ProgressBar( + width: progressLength, + height: 5, + fillColor: Theme.of(context) + .extension<StackColors>()! + .accentColorRed, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + percent: 0, + ), + ], + ), + ], + ), + ), + if (_currentSyncStatus == WalletSyncStatus.unableToSync) + Padding( + padding: const EdgeInsets.only( + top: 12, + ), + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .warningBackground, + child: Text( + "Please check your internet connection and make sure your current node is not having issues.", + style: STextStyles.baseXS(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + ), + ), + ), + SizedBox( + height: isDesktop ? 32 : 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${ref.watch(walletsChangeNotifierProvider.select((value) => value.getManager(widget.walletId).coin)).prettyName} nodes", + textAlign: TextAlign.left, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + BlueTextButton( + text: "Add new node", + onTap: () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.add, + ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .coin, + null, + WalletNetworkSettingsView.routeName, + ), + ); + }, + ), + ], + ), + SizedBox( + height: isDesktop ? 12 : 8, + ), + NodesList( + coin: ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).coin)), + popBackToRoute: WalletNetworkSettingsView.routeName, + ), + if (isDesktop) + const SizedBox( + height: 32, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + bottom: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Advanced", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + if (isDesktop) + RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Expandable( + onExpandChanged: (state) { + setState(() { + _advancedIsExpanded = state == ExpandableState.expanded; + }); + }, + header: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: _iconSize, + height: _iconSize, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(_iconSize), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.networkWired, + width: 24, + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ), + const SizedBox( + width: 10, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Advanced", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + Text( + "Rescan blockchain", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ) + ], + ), + SvgPicture.asset( + _advancedIsExpanded + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ], + ), + body: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 50, + top: 16, + bottom: 6, + ), + child: BlueTextButton( + text: "Rescan", + onTap: () async { + await Navigator.of(context).push( + FadePageRoute<void>( + ConfirmFullRescanDialog( + onConfirm: _attemptRescan, + ), + const RouteSettings(), + ), + ); + // await showDialog<dynamic>( + // context: context, + // builder: (context) { + // return ConfirmFullRescanDialog( + // onConfirm: _attemptRescan, + // ); + // }, + // ); + }, + ), + ), + ], + ), + ), + ), + ], ), ); } diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 6e5cdd5ed..92b712111 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -23,9 +23,10 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/utilities/assets.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; @@ -132,196 +133,199 @@ class _WalletSettingsViewState extends State<WalletSettingsView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Settings", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Settings", - 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: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - SettingsListButton( - iconAssetName: Assets.svg.addressBook, - iconSize: 16, - title: "Address book", - onPressed: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: coin, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.node, - iconSize: 16, - title: "Network", - onPressed: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - widget.initialNodeStatus, - ), - ); - }, - ), - const SizedBox( - height: 8, - ), - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.lock, - iconSize: 16, - title: "Wallet backup", - onPressed: () async { - final mnemonic = await ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .mnemonic; + 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: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: Column( + children: [ + SettingsListButton( + iconAssetName: Assets.svg.addressBook, + iconSize: 16, + title: "Address book", + onPressed: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: coin, + ); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.node, + iconSize: 16, + title: "Network", + onPressed: () { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + widget.initialNodeStatus, + ), + ); + }, + ), + const SizedBox( + height: 8, + ), + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.lock, + iconSize: 16, + title: "Wallet backup", + onPressed: () async { + final mnemonic = await ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .mnemonic; - if (mounted) { - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: - Tuple2(walletId, mnemonic), - showBackButton: true, - routeOnSuccess: - WalletBackupView.routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery phrase", - biometricsAuthenticationTitle: - "View recovery phrase", + if (mounted) { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: + Tuple2(walletId, mnemonic), + showBackButton: true, + routeOnSuccess: + WalletBackupView.routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery phrase", + biometricsAuthenticationTitle: + "View recovery phrase", + ), + settings: const RouteSettings( + name: + "/viewRecoverPhraseLockscreen"), ), - settings: const RouteSettings( - name: - "/viewRecoverPhraseLockscreen"), - ), - ); - } - }, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.downloadFolder, - title: "Wallet settings", - iconSize: 16, - onPressed: () { - Navigator.of(context).pushNamed( - WalletSettingsWalletSettingsView.routeName, - arguments: walletId, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.arrowRotate3, - title: "Syncing preferences", - onPressed: () { - Navigator.of(context).pushNamed( - SyncingPreferencesView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.ellipsis, - title: "Debug Info", - onPressed: () { - Navigator.of(context) - .pushNamed(DebugView.routeName); - }, - ), - ], + ); + } + }, + ); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.downloadFolder, + title: "Wallet settings", + iconSize: 16, + onPressed: () { + Navigator.of(context).pushNamed( + WalletSettingsWalletSettingsView + .routeName, + arguments: walletId, + ); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.arrowRotate3, + title: "Syncing preferences", + onPressed: () { + Navigator.of(context).pushNamed( + SyncingPreferencesView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.ellipsis, + title: "Debug Info", + onPressed: () { + Navigator.of(context) + .pushNamed(DebugView.routeName); + }, + ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - const Spacer(), - Consumer( - builder: (_, ref, __) { - return TextButton( - onPressed: () { - ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .isActiveWallet = false; - ref - .read(transactionFilterProvider.state) - .state = null; + const SizedBox( + height: 12, + ), + const Spacer(), + Consumer( + builder: (_, ref, __) { + return TextButton( + onPressed: () { + ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .isActiveWallet = false; + ref + .read(transactionFilterProvider.state) + .state = null; - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName), - ); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Log out", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ); - }, - ), - ], + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName), + ); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Log out", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ); + }, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } @@ -374,6 +378,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 +387,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(), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart index 66d666c20..5543bf1c0 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/constants.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/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -54,164 +55,167 @@ class _DeleteWalletRecoveryPhraseViewState Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - actions: [ - Padding( - padding: const EdgeInsets.all(10), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - color: Theme.of(context).extension<StackColors>()!.background, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, - ), - onPressed: () async { - final words = await _manager.mnemonic; - await _clipboardInterface - .setData(ClipboardData(text: words.join(" "))); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - ), - ), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - ], - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 4, - ), - Text( - _manager.walletName, - textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - const SizedBox( - height: 4, - ), - Text( - "Recovery Phrase", - textAlign: TextAlign.center, - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", - style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ), - const SizedBox( - height: 8, - ), - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: _mnemonic, - isDesktop: false, - ), - ), - ), - const SizedBox( - height: 16, - ), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - showDialog<dynamic>( - barrierDismissible: true, - context: context, - builder: (_) => StackDialog( - title: "Thanks! Your wallet will be deleted.", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - final walletId = _manager.walletId; - final walletsInstance = - ref.read(walletsChangeNotifierProvider); - await ref - .read(walletsServiceChangeNotifierProvider) - .deleteWallet(_manager.walletName, true); - - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName)); - } - - // wait for widget tree to dispose of any widgets watching the manager - await Future<void>.delayed(const Duration(seconds: 1)); - walletsInstance.removeWallet(walletId: walletId); - }, - child: Text( - "Ok", - style: STextStyles.button(context), - ), - ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: Theme.of(context).extension<StackColors>()!.background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, ), - ); - }, - child: Text( - "Continue", - style: STextStyles.button(context), + onPressed: () async { + final words = await _manager.mnemonic; + await _clipboardInterface + .setData(ClipboardData(text: words.join(" "))); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + ), ), ), ], ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 4, + ), + Text( + _manager.walletName, + textAlign: TextAlign.center, + style: STextStyles.label(context).copyWith( + fontSize: 12, + ), + ), + const SizedBox( + height: 4, + ), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + style: STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: SingleChildScrollView( + child: MnemonicTable( + words: _mnemonic, + isDesktop: false, + ), + ), + ), + const SizedBox( + height: 16, + ), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + showDialog<dynamic>( + barrierDismissible: true, + context: context, + builder: (_) => StackDialog( + title: "Thanks! Your wallet will be deleted.", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () async { + final walletId = _manager.walletId; + final walletsInstance = + ref.read(walletsChangeNotifierProvider); + await ref + .read(walletsServiceChangeNotifierProvider) + .deleteWallet(_manager.walletName, true); + + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName)); + } + + // wait for widget tree to dispose of any widgets watching the manager + await Future<void>.delayed( + const Duration(seconds: 1)); + walletsInstance.removeWallet(walletId: walletId); + }, + child: Text( + "Ok", + style: STextStyles.button(context), + ), + ), + ), + ); + }, + child: Text( + "Continue", + style: STextStyles.button(context), + ), + ), + ], + ), + ), ), ); } diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart index ec8c5a128..f740eee12 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart @@ -4,6 +4,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set 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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:tuple/tuple.dart'; @@ -20,93 +21,96 @@ class DeleteWalletWarningView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 32, - ), - Center( - child: Text( - "Attention!", - style: STextStyles.pageTitleH1(context), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 32, ), - ), - const SizedBox( - height: 16, - ), - RoundedContainer( - color: - Theme.of(context).extension<StackColors>()!.warningBackground, - child: Text( - "You are going to permanently delete you wallet.\n\nIf you delete your wallet, the only way you can have access to your funds is by using your backup key.\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", - style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, + Center( + child: Text( + "Attention!", + style: STextStyles.pageTitleH1(context), ), ), - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .warningBackground, + child: Text( + "You are going to permanently delete you wallet.\n\nIf you delete your wallet, the only way you can have access to your funds is by using your backup key.\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.baseXS(context).copyWith( color: Theme.of(context) .extension<StackColors>()! - .accentColorDark), - ), - ), - const SizedBox( - height: 12, - ), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(walletId); - final mnemonic = await manager.mnemonic; - Navigator.of(context).pushNamed( - DeleteWalletRecoveryPhraseView.routeName, - arguments: Tuple2( - manager, - mnemonic, + .warningForeground, ), - ); - }, - child: Text( - "View Backup Key", - style: STextStyles.button(context), + ), ), - ), - const SizedBox( - height: 16, - ), - ], + const Spacer(), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + const SizedBox( + height: 12, + ), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () async { + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId); + final mnemonic = await manager.mnemonic; + Navigator.of(context).pushNamed( + DeleteWalletRecoveryPhraseView.routeName, + arguments: Tuple2( + manager, + mnemonic, + ), + ); + }, + child: Text( + "View Backup Key", + style: STextStyles.button(context), + ), + ), + const SizedBox( + height: 16, + ), + ], + ), ), ), ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart index b876216e0..f76422750 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart @@ -6,6 +6,8 @@ import 'package:stackwallet/utilities/constants.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -51,100 +53,104 @@ class _RenameWalletViewState extends ConsumerState<RenameWalletView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Rename wallet", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Rename wallet", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - controller: _controller, - focusNode: _focusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Wallet name", - _focusNode, - context, - ).copyWith( - suffixIcon: _controller.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _controller.text = ""; - }); - }, - ), - ], + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _controller, + focusNode: _focusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Wallet name", + _focusNode, + context, + ).copyWith( + suffixIcon: _controller.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _controller.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, + ), ), ), - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - final newName = _controller.text; - final success = await ref - .read(walletsServiceChangeNotifierProvider) - .renameWallet( - from: originalName, - to: newName, - shouldNotifyListeners: true, - ); + const Spacer(), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () async { + final newName = _controller.text; + final success = await ref + .read(walletsServiceChangeNotifierProvider) + .renameWallet( + from: originalName, + to: newName, + shouldNotifyListeners: true, + ); - if (success) { - ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .walletName = newName; - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.success, - message: "Wallet renamed", - context: context, - ); - } else { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Wallet named \"$newName\" already exists", - context: context, - ); - } - }, - child: Text( - "Save", - style: STextStyles.button(context), + if (success) { + ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .walletName = newName; + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.success, + message: "Wallet renamed", + context: context, + ); + } else { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Wallet named \"$newName\" already exists", + context: context, + ); + } + }, + child: Text( + "Save", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart index 52aa6027a..a9c0f92af 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/route_generator.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -24,149 +25,151 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Wallet settings", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Wallet settings", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - RenameWalletView.routeName, - arguments: walletId, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, - ), - child: Row( - children: [ - Text( - "Rename wallet", - style: STextStyles.titleBold12(context), - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( padding: const EdgeInsets.all(0), - onPressed: () { - showDialog( - barrierDismissible: true, - context: context, - builder: (_) => StackDialog( - title: - "Do you want to delete ${ref.read(walletsChangeNotifierProvider).getManager(walletId).walletName}?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: walletId, - showBackButton: true, - routeOnSuccess: - DeleteWalletWarningView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to delete wallet", - biometricsAuthenticationTitle: - "Delete wallet", - ), - settings: const RouteSettings( - name: "/deleteWalletLockscreen"), - ), - ); - }, - child: Text( - "Delete", - style: STextStyles.button(context), - ), - ), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, ), - child: Row( - children: [ - Text( - "Delete wallet", - style: STextStyles.titleBold12(context), - ), - ], + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + RenameWalletView.routeName, + arguments: walletId, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Rename wallet", + style: STextStyles.titleBold12(context), + ), + ], + ), ), ), ), - ), - ], + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: const EdgeInsets.all(0), + onPressed: () { + showDialog( + barrierDismissible: true, + context: context, + builder: (_) => StackDialog( + title: + "Do you want to delete ${ref.read(walletsChangeNotifierProvider).getManager(walletId).walletName}?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: walletId, + showBackButton: true, + routeOnSuccess: + DeleteWalletWarningView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to delete wallet", + biometricsAuthenticationTitle: + "Delete wallet", + ), + settings: const RouteSettings( + name: "/deleteWalletLockscreen"), + ), + ); + }, + child: Text( + "Delete", + style: STextStyles.button(context), + ), + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Delete wallet", + style: STextStyles.titleBold12(context), + ), + ], + ), + ), + ), + ), + ], + ), ), ), ), diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 2aa2a5c8a..3f819492e 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -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); -// } diff --git a/lib/pages/wallet_view/sub_widgets/transactions_list.dart b/lib/pages/wallet_view/sub_widgets/transactions_list.dart index 2246882ab..11353c7c6 100644 --- a/lib/pages/wallet_view/sub_widgets/transactions_list.dart +++ b/lib/pages/wallet_view/sub_widgets/transactions_list.dart @@ -7,9 +7,14 @@ import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/manager.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/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/trade_card.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; @@ -67,6 +72,126 @@ 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: () async { + if (Util.isDesktop) { + await showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: trade.tradeId, + transactionIfSentFromStack: tx, + walletName: + ref.read(managerProvider).walletName, + walletId: widget.walletId, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, + ), + ); + } else { + 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 +244,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); + }, + ), ); } }, diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index c9ff64393..74308f2e8 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -3,14 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; - class WalletBalanceToggleSheet extends ConsumerWidget { const WalletBalanceToggleSheet({ Key? key, @@ -153,7 +152,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) @@ -195,7 +194,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) @@ -287,7 +286,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) @@ -329,7 +328,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) diff --git a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart index 7faae106f..603b72338 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart @@ -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, ), ), ), diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 78f24ba6a..b9ea02e79 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -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( + buttonHeight: ButtonHeight.l, + 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,436 @@ 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 = _transaction.amount; + return Text( + "$prefix${Format.satoshiAmountToPrettyString(amount, locale, coin)} ${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: (_) { + int value = _transaction.amount; + + return Text( + "$prefix${Format.localizedStringAsFixed( + value: Format.satoshisToAmount(value, coin: coin) * + 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, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/transaction_views/edit_note_view.dart b/lib/pages/wallet_view/transaction_views/edit_note_view.dart index aa085429b..e8d7b05f9 100644 --- a/lib/pages/wallet_view/transaction_views/edit_note_view.dart +++ b/lib/pages/wallet_view/transaction_views/edit_note_view.dart @@ -4,7 +4,12 @@ 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/background.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_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 +36,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(); @@ -47,30 +55,186 @@ 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), + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: child, + ), + child: Scaffold( + 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 +243,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, ), ), ), ); - }), - )); + }, + ), + ); + } } } diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 8d7aac85f..738ef721b 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; 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:stackwallet/models/models.dart'; @@ -23,8 +24,17 @@ import 'package:stackwallet/utilities/format.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/background.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/blue_text_button.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/icon_widgets/copy_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/pencil_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; @@ -51,6 +61,7 @@ class TransactionDetailsView extends ConsumerStatefulWidget { class _TransactionDetailsViewState extends ConsumerState<TransactionDetailsView> { + late final bool isDesktop; late Transaction _transaction; late final String walletId; @@ -63,18 +74,19 @@ class _TransactionDetailsViewState @override void initState() { + isDesktop = Util.isDesktop; _transaction = widget.transaction; walletId = widget.walletId; coin = widget.coin; - amount = Format.satoshisToAmount(_transaction.amount); - fee = Format.satoshisToAmount(_transaction.fees); + amount = Format.satoshisToAmount(_transaction.amount, coin: coin); + fee = Format.satoshisToAmount(_transaction.fees, coin: coin); if ((coin == Coin.firo || coin == Coin.firoTestNet) && _transaction.subType == "mint") { amountPrefix = ""; } else { - amountPrefix = _transaction.txType.toLowerCase() == "sent" ? "- " : "+ "; + amountPrefix = _transaction.txType.toLowerCase() == "sent" ? "-" : "+"; } // if (coin == Coin.firo || coin == Coin.firoTestNet) { @@ -145,663 +157,1316 @@ class _TransactionDetailsViewState Future<bool> showExplorerWarning(String explorer) async { final bool? shouldContinue = await showDialog<bool>( - context: context, - barrierDismissible: false, - builder: (_) => StackDialog( - title: "Attention", - message: - "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", - icon: Row( - children: [ - Consumer(builder: (_, ref, __) { - return Checkbox( - value: ref.watch(prefsChangeNotifierProvider - .select((value) => value.hideBlockExplorerWarning)), - onChanged: (value) { - if (value is bool) { - ref - .read(prefsChangeNotifierProvider) - .hideBlockExplorerWarning = value; - setState(() {}); - } + context: context, + barrierDismissible: false, + builder: (_) { + if (!isDesktop) { + return StackDialog( + title: "Attention", + message: + "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", + icon: Row( + children: [ + Consumer(builder: (_, ref, __) { + return Checkbox( + value: ref.watch(prefsChangeNotifierProvider + .select((value) => value.hideBlockExplorerWarning)), + onChanged: (value) { + if (value is bool) { + ref + .read(prefsChangeNotifierProvider) + .hideBlockExplorerWarning = value; + setState(() {}); + } + }, + ); + }), + Text( + "Never show again", + style: STextStyles.smallMed14(context), + ) + ], + ), + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(false); }, - ); - }), - Text( - "Never show again", - style: STextStyles.smallMed14(context), - ) - ], - ), - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + rightButton: TextButton( + style: Theme.of(context) .extension<StackColors>()! - .accentColorDark), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text( - "Continue", - style: STextStyles.button(context), - ), - ), - ), - ); + .getPrimaryEnabledButtonColor(context), + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text( + "Continue", + style: STextStyles.button(context), + ), + ), + ); + } else { + return DesktopDialog( + maxWidth: 550, + maxHeight: 300, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Attention", + style: STextStyles.desktopH2(context), + ), + Row( + children: [ + Consumer(builder: (_, ref, __) { + return Checkbox( + value: ref.watch(prefsChangeNotifierProvider + .select((value) => + value.hideBlockExplorerWarning)), + onChanged: (value) { + if (value is bool) { + ref + .read(prefsChangeNotifierProvider) + .hideBlockExplorerWarning = value; + setState(() {}); + } + }, + ); + }), + Text( + "Never show again", + style: STextStyles.smallMed14(context), + ) + ], + ), + ], + ), + const SizedBox(height: 16), + Text( + "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 35), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(false); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(true); + }, + ), + ], + ), + ], + ), + ), + ); + } + }); return shouldContinue ?? false; } @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(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Transaction details", - style: STextStyles.navBarTitle(context), - ), + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: child, ), - body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - "$amountPrefix${Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (amount / 10000.toDecimal()).toDecimal() - : coin == Coin.wownero - ? (amount / 1000.toDecimal()).toDecimal() - : amount, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - decimalPlaces: Constants.decimalPlaces, - )} ${coin.ticker}", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - if (ref.watch(prefsChangeNotifierProvider - .select((value) => value.externalCalls))) - SelectableText( - "${Format.localizedStringAsFixed(value: (coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() : coin == Coin.wownero ? (amount / 1000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), decimalPlaces: 2)} ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, + child: Scaffold( + 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(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Transaction details", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(12), + child: Column( + children: [ + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction details", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: isDesktop + ? const EdgeInsets.only( + right: 32, + bottom: 32, + ) + : const EdgeInsets.all(0), + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context) + .extension<StackColors>()! + .background + : null, + padding: const EdgeInsets.all(0), + child: child, + ); + }, + child: SingleChildScrollView( + primary: isDesktop ? false : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Container( + decoration: isDesktop + ? BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ) + : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + if (isDesktop) + Row( + children: [ + TxIcon( + transaction: _transaction, + ), + const SizedBox( + width: 16, + ), + SelectableText( + _transaction.isCancelled + ? "Cancelled" + : whatIsIt( + _transaction.txType), + style: + STextStyles.desktopTextMedium( + context), + ), + ], + ), + Column( + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + SelectableText( + "$amountPrefix${Format.localizedStringAsFixed( + value: amount, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => + value.locale), + ), + decimalPlaces: Constants + .decimalPlacesForCoin(coin), + )} ${coin.ticker}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.titleBold12( + context), + ), + const SizedBox( + height: 2, + ), + if (ref.watch( + prefsChangeNotifierProvider + .select((value) => + value.externalCalls))) + SelectableText( + "$amountPrefix${Format.localizedStringAsFixed( + value: amount * + ref.watch( + priceAnd24hChangeNotifierProvider + .select((value) => + value + .getPrice( + coin) + .item1), + ), + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => + value.locale), + ), + decimalPlaces: 2, + )} ${ref.watch( + prefsChangeNotifierProvider + .select( + (value) => value.currency, + ), + )}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + ], + ), + if (!isDesktop) + TxIcon( + transaction: _transaction, + ), + ], + ), ), - )}", - style: STextStyles.itemSubtitle(context), - ), - ], - ), - TxIcon( - transaction: _transaction, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.isCancelled - ? "Cancelled" - : whatIsIt(_transaction.txType), - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - if (!((coin == Coin.monero || coin == Coin.wownero) && - _transaction.txType.toLowerCase() == "sent") && - !((coin == Coin.firo || coin == Coin.firoTestNet) && - _transaction.subType == "mint")) - const SizedBox( - height: 12, - ), - if (!((coin == Coin.monero || coin == Coin.wownero) && - _transaction.txType.toLowerCase() == "sent") && - !((coin == Coin.firo || coin == Coin.firoTestNet) && - _transaction.subType == "mint")) - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _transaction.txType.toLowerCase() == "sent" - ? "Sent to" - : "Received on", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), - _transaction.txType.toLowerCase() == "received" - ? FutureBuilder( - future: - fetchContactNameFor(_transaction.address), - builder: (builderContext, - AsyncSnapshot<String> snapshot) { - String addressOrContactName = - _transaction.address; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - addressOrContactName = snapshot.data!; - } - return SelectableText( - addressOrContactName, - style: STextStyles.itemSubtitle12(context), - ); - }, - ) - : SelectableText( - _transaction.address, - style: STextStyles.itemSubtitle12(context), ), - ], + ), + + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.isCancelled + ? "Cancelled" + : whatIsIt(_transaction.txType), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: _transaction.txType == "Sent" + ? Theme.of(context) + .extension<StackColors>()! + .accentColorOrange + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ) + : STextStyles.itemSubtitle12(context), + ), + // ), + // ), + ], + ), + ), + if (!((coin == Coin.monero || + coin == Coin.wownero) && + _transaction.txType.toLowerCase() == + "sent") && + !((coin == Coin.firo || + coin == Coin.firoTestNet) && + _transaction.subType == "mint")) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (!((coin == Coin.monero || + coin == Coin.wownero) && + _transaction.txType.toLowerCase() == + "sent") && + !((coin == Coin.firo || + coin == Coin.firoTestNet) && + _transaction.subType == "mint")) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + _transaction.txType.toLowerCase() == + "sent" + ? "Sent to" + : "Receiving address", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + const SizedBox( + height: 8, + ), + _transaction.txType.toLowerCase() == + "received" + ? FutureBuilder( + future: fetchContactNameFor( + _transaction.address), + builder: (builderContext, + AsyncSnapshot<String> + snapshot) { + String + addressOrContactName = + _transaction.address; + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + addressOrContactName = + snapshot.data!; + } + return SelectableText( + addressOrContactName, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ); + }, + ) + : SelectableText( + _transaction.address, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ), + ], + ), + ), + if (isDesktop) + IconCopyButton( + data: _transaction.address, + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Note", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconPencilButton( + onPressed: () { + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditNoteView( + txid: _transaction.txid, + walletId: walletId, + note: _note, + ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditNoteView.routeName, + arguments: Tuple3( + _transaction.txid, + walletId, + _note, + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: Theme.of(context) + .extension< + StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Edit", + style: STextStyles.link2( + context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 8, + ), + FutureBuilder( + future: ref.watch( + notesServiceChangeNotifierProvider( + walletId) + .select((value) => value.getNoteFor( + txid: _transaction.txid))), + builder: (builderContext, + AsyncSnapshot<String> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + _note = snapshot.data ?? ""; + } + return SelectableText( + _note, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ); + }, + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Date", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], + ), + if (!isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + IconCopyButton( + data: Format.extractDateFrom( + _transaction.timestamp, + ), + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Builder(builder: (context) { + final feeString = showFeePending + ? _transaction.confirmedStatus + ? Format.localizedStringAsFixed( + value: fee, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => + value.locale)), + decimalPlaces: + Constants.decimalPlacesForCoin( + coin)) + : "Pending" + : Format.localizedStringAsFixed( + value: fee, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale)), + decimalPlaces: + Constants.decimalPlacesForCoin(coin)); + + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Transaction fee", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + feeString, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], + ), + if (!isDesktop) + SelectableText( + feeString, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + if (isDesktop) + IconCopyButton(data: feeString) + ], + ); + }), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Builder(builder: (context) { + final height = widget.coin != Coin.epicCash && + _transaction.confirmedStatus + ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" + : _transaction.confirmations > 0 + ? "${_transaction.height}" + : "Pending"; + + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Block height", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], + ), + if (!isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + if (isDesktop) IconCopyButton(data: height), + ], + ); + }), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Transaction ID", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + const SizedBox( + height: 8, + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.txid, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + if (coin != Coin.epicCash) + const SizedBox( + height: 8, + ), + if (coin != Coin.epicCash) + BlueTextButton( + text: "Open in block explorer", + onTap: () async { + final uri = + getBlockExplorerTransactionUrlFor( + coin: coin, + txid: _transaction.txid, + ); + + if (ref + .read( + prefsChangeNotifierProvider) + .hideBlockExplorerWarning == + false) { + final shouldContinue = + await showExplorerWarning( + "${uri.scheme}://${uri.host}"); + + if (!shouldContinue) { + return; + } + } + + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + try { + await launchUrl( + uri, + mode: LaunchMode + .externalApplication, + ); + } catch (_) { + unawaited( + showDialog<void>( + context: context, + builder: (_) => + StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), + ), + ); + } finally { + // Future<void>.delayed( + // const Duration(seconds: 1), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + } + }, + ), + // ), + // ), + ], + ), + ), + if (isDesktop) + const SizedBox( + width: 12, + ), + if (isDesktop) + IconCopyButton( + data: _transaction.txid, + ), + ], + ), + ), + // if ((coin == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // const SizedBox( + // height: 12, + // ), + // if ((coin == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Mint Transaction ID", + // style: STextStyles.itemSubtitle(context), + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // // Flexible( + // // child: FittedBox( + // // fit: BoxFit.scaleDown, + // // child: + // SelectableText( + // _transaction.otherData ?? "Unknown", + // style: STextStyles.itemSubtitle12(context), + // ), + // // ), + // // ), + // const SizedBox( + // height: 8, + // ), + // BlueTextButton( + // text: "Open in block explorer", + // onTap: () async { + // final uri = getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction.otherData ?? "Unknown", + // ); + // // ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = false; + // try { + // await launchUrl( + // uri, + // mode: LaunchMode.externalApplication, + // ); + // } catch (_) { + // unawaited(showDialog<void>( + // context: context, + // builder: (_) => StackOkDialog( + // title: "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // )); + // } finally { + // // Future<void>.delayed( + // // const Duration(seconds: 1), + // // () => ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = true, + // // ); + // } + // }, + // ), + // ], + // ), + // ), + if (coin == Coin.epicCash) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (coin == Coin.epicCash) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Slate ID", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.slateId ?? "Unknown", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + // ), + // ), + ], + ), + if (isDesktop) + const SizedBox( + width: 12, + ), + if (isDesktop) + IconCopyButton( + data: _transaction.slateId ?? "Unknown", + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + ], + ), + ), ), ), - const SizedBox( - height: 12, ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Note", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple3( - _transaction.txid, - walletId, - _note, - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 8, - ), - FutureBuilder( - future: ref.watch( - notesServiceChangeNotifierProvider(walletId).select( - (value) => - value.getNoteFor(txid: _transaction.txid))), - builder: - (builderContext, AsyncSnapshot<String> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _note = snapshot.data ?? ""; - } - return SelectableText( - _note, - style: STextStyles.itemSubtitle12(context), - ); - }, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Date", - style: STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - Format.extractDateFrom(_transaction.timestamp), - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction fee", - style: STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - showFeePending - ? _transaction.confirmedStatus - ? Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (fee / 10000.toDecimal()).toDecimal() - : coin == Coin.wownero - ? (fee / 1000.toDecimal()) - .toDecimal() - : fee, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: Constants.decimalPlaces) - : "Pending" - : Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (fee / 10000.toDecimal()).toDecimal() - : coin == Coin.wownero - ? (fee / 1000.toDecimal()).toDecimal() - : fee, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: Constants.decimalPlaces), - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Block height", - style: STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - widget.coin != Coin.epicCash && - _transaction.confirmedStatus - ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" - : _transaction.confirmations > 0 - ? "${_transaction.height}" - : "Pending", - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction ID", - style: STextStyles.itemSubtitle(context), - ), - ], - ), - const SizedBox( - height: 8, - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.txid, - style: STextStyles.itemSubtitle12(context), - ), - if (coin != Coin.epicCash) - const SizedBox( - height: 8, - ), - if (coin != Coin.epicCash) - BlueTextButton( - text: "Open in block explorer", - onTap: () async { - final uri = getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); - - if (ref - .read(prefsChangeNotifierProvider) - .hideBlockExplorerWarning == - false) { - final shouldContinue = await showExplorerWarning( - "${uri.scheme}://${uri.host}"); - - if (!shouldContinue) { - return; - } - } - - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - try { - await launchUrl( - uri, - mode: LaunchMode.externalApplication, - ); - } catch (_) { - unawaited(showDialog<void>( - context: context, - builder: (_) => StackOkDialog( - title: "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", - ), - )); - } finally { - // Future<void>.delayed( - // const Duration(seconds: 1), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - } - }, - ), - // ), - // ), - ], - ), - ), - // if ((coin == Coin.firoTestNet || coin == Coin.firo) && - // _transaction.subType == "mint") - // const SizedBox( - // height: 12, - // ), - // if ((coin == Coin.firoTestNet || coin == Coin.firo) && - // _transaction.subType == "mint") - // RoundedWhiteContainer( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "Mint Transaction ID", - // style: STextStyles.itemSubtitle(context), - // ), - // ], - // ), - // const SizedBox( - // height: 8, - // ), - // // Flexible( - // // child: FittedBox( - // // fit: BoxFit.scaleDown, - // // child: - // SelectableText( - // _transaction.otherData ?? "Unknown", - // style: STextStyles.itemSubtitle12(context), - // ), - // // ), - // // ), - // const SizedBox( - // height: 8, - // ), - // BlueTextButton( - // text: "Open in block explorer", - // onTap: () async { - // final uri = getBlockExplorerTransactionUrlFor( - // coin: coin, - // txid: _transaction.otherData ?? "Unknown", - // ); - // // ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = false; - // try { - // await launchUrl( - // uri, - // mode: LaunchMode.externalApplication, - // ); - // } catch (_) { - // unawaited(showDialog<void>( - // context: context, - // builder: (_) => StackOkDialog( - // title: "Could not open in block explorer", - // message: - // "Failed to open \"${uri.toString()}\"", - // ), - // )); - // } finally { - // // Future<void>.delayed( - // // const Duration(seconds: 1), - // // () => ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = true, - // // ); - // } - // }, - // ), - // ], - // ), - // ), - if (coin == Coin.epicCash) - const SizedBox( - height: 12, - ), - if (coin == Coin.epicCash) - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Slate ID", - style: STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.slateId ?? "Unknown", - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - const SizedBox( - height: 12, - ), - ], - ), + ), + ], ), ), - ), - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: (coin == Coin.epicCash && - _transaction.confirmedStatus == false && - _transaction.isCancelled == false && - _transaction.txType == "Sent") - ? SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: TextButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all<Color>( - Theme.of(context).extension<StackColors>()!.textError, + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: (coin == Coin.epicCash && + _transaction.confirmedStatus == false && + _transaction.isCancelled == false && + _transaction.txType == "Sent") + ? SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all<Color>( + Theme.of(context).extension<StackColors>()!.textError, + ), ), - ), - onPressed: () async { - final Manager manager = ref - .read(walletsChangeNotifierProvider) - .getManager(walletId); + onPressed: () async { + final Manager manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId); - if (manager.wallet is EpicCashWallet) { - final String? id = _transaction.slateId; - if (id == null) { + if (manager.wallet is EpicCashWallet) { + final String? id = _transaction.slateId; + if (id == null) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not find Epic transaction ID", + context: context, + )); + return; + } + + unawaited(showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => + const CancellingTransactionProgressDialog(), + )); + + final result = await (manager.wallet as EpicCashWallet) + .cancelPendingTransactionAndPost(id); + if (mounted) { + // pop progress dialog + Navigator.of(context).pop(); + + if (result.isEmpty) { + await showDialog<dynamic>( + context: context, + builder: (_) => StackOkDialog( + title: "Transaction cancelled", + onOkPressed: (_) { + manager.refresh(); + Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName)); + }, + ), + ); + } else { + await showDialog<dynamic>( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to cancel transaction", + message: result, + ), + ); + } + } + } else { unawaited(showFloatingFlushBar( type: FlushBarType.warning, - message: "Could not find Epic transaction ID", + message: "ERROR: Wallet type is not Epic Cash", context: context, )); return; } - - unawaited(showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => - const CancellingTransactionProgressDialog(), - )); - - final result = await (manager.wallet as EpicCashWallet) - .cancelPendingTransactionAndPost(id); - if (mounted) { - // pop progress dialog - Navigator.of(context).pop(); - - if (result.isEmpty) { - await showDialog<dynamic>( - context: context, - builder: (_) => StackOkDialog( - title: "Transaction cancelled", - onOkPressed: (_) { - manager.refresh(); - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName)); - }, - ), - ); - } else { - await showDialog<dynamic>( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to cancel transaction", - message: result, - ), - ); - } - } - } else { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "ERROR: Wallet type is not Epic Cash", - context: context, - )); - return; - } - }, - child: Text( - "Cancel Transaction", - style: STextStyles.button(context), + }, + child: Text( + "Cancel Transaction", + style: STextStyles.button(context), + ), ), - ), - ) - : null, + ) + : null, + ), + ); + } +} + +class _Divider extends StatelessWidget { + const _Divider({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ); + } +} + +class IconCopyButton extends StatelessWidget { + const IconCopyButton({ + Key? key, + required this.data, + }) : super(key: key); + + final String data; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 26, + child: RawMaterialButton( + fillColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + elevation: 0, + hoverElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + onPressed: () async { + await Clipboard.setData(ClipboardData(text: data)); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.all(5), + child: CopyIcon( + width: 16, + height: 16, + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ), + ); + } +} + +class IconPencilButton extends StatelessWidget { + const IconPencilButton({ + Key? key, + this.onPressed, + }) : super(key: key); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 26, + child: RawMaterialButton( + fillColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + elevation: 0, + hoverElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + onPressed: () => onPressed?.call(), + child: Padding( + padding: const EdgeInsets.all(5), + child: PencilIcon( + width: 16, + height: 16, + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ), ); } } diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index 8175597f6..7e1b53cbb 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -15,7 +15,12 @@ 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/background.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/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_text_field.dart'; @@ -43,6 +48,7 @@ class _TransactionSearchViewState bool _isActiveReceivedCheckbox = false; bool _isActiveSentCheckbox = false; + bool _isActiveTradeCheckbox = false; String _fromDateString = ""; String _toDateString = ""; @@ -63,12 +69,18 @@ class _TransactionSearchViewState _selectedFromDate = filterState.from; _keywordTextEditingController.text = filterState.keyword; + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + _toDateString = + _selectedToDate == null ? "" : Format.formatDate(_selectedToDate!); + // TODO: Fix XMR (modify Format.funcs to take optional Coin parameter) // final amt = Format.satoshisToAmount(widget.coin == Coin.monero ? ) String amount = ""; if (filterState.amount != null) { amount = Format.satoshiAmountToPrettyString(filterState.amount!, - ref.read(localeServiceChangeNotifierProvider).locale); + ref.read(localeServiceChangeNotifierProvider).locale, widget.coin); } _amountTextEditingController.text = amount; } @@ -110,8 +122,8 @@ class _TransactionSearchViewState ); } - var _selectedFromDate = DateTime(2007); - var _selectedToDate = DateTime.now(); + DateTime? _selectedFromDate = DateTime(2007); + DateTime? _selectedToDate = DateTime.now(); MaterialRoundedDatePickerStyle _buildDatePickerStyle() { return MaterialRoundedDatePickerStyle( @@ -166,104 +178,118 @@ class _TransactionSearchViewState Widget _buildDateRangePicker() { const middleSeparatorPadding = 2.0; const middleSeparatorWidth = 12.0; - final width = (MediaQuery.of(context).size.width - - (middleSeparatorWidth + - (2 * middleSeparatorPadding) + - (2 * Constants.size.standardPadding))) / - 2; + final isDesktop = Util.isDesktop; + + final width = isDesktop + ? null + : (MediaQuery.of(context).size.width - + (middleSeparatorWidth + + (2 * middleSeparatorPadding) + + (2 * Constants.size.standardPadding))) / + 2; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - GestureDetector( - key: const Key("transactionSearchViewFromDatePickerKey"), - onTap: () async { - final color = - Theme.of(context).extension<StackColors>()!.accentColorDark; - final height = MediaQuery.of(context).size.height; - // check and hide keyboard - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 125)); - } - - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - initialDate: DateTime.now(), - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _selectedFromDate = date; - - // flag to adjust date so from date is always before to date - final flag = !_selectedFromDate.isBefore(_selectedToDate); - if (flag) { - _selectedToDate = DateTime.fromMillisecondsSinceEpoch( - _selectedFromDate.millisecondsSinceEpoch); + Expanded( + child: GestureDetector( + key: const Key("transactionSearchViewFromDatePickerKey"), + onTap: () async { + final color = + Theme.of(context).extension<StackColors>()!.accentColorDark; + final height = MediaQuery.of(context).size.height; + // check and hide keyboard + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 125)); } - setState(() { + final date = await showRoundedDatePicker( + // This doesn't change statusbar color... + // background: CFColors.starryNight.withOpacity(0.8), + context: context, + initialDate: DateTime.now(), + height: height * 0.5, + theme: ThemeData( + primarySwatch: Util.createMaterialColor( + color, + ), + ), + //TODO pick a better initial date + // 2007 chosen as that is just before bitcoin launched + firstDate: DateTime(2007), + lastDate: DateTime.now(), + borderRadius: Constants.size.circularBorderRadius * 2, + + textPositiveButton: "SELECT", + + styleDatePicker: _buildDatePickerStyle(), + styleYearPicker: _buildYearPickerStyle(), + ); + if (date != null) { + _selectedFromDate = date; + + // flag to adjust date so from date is always before to date + final flag = _selectedToDate != null && + !_selectedFromDate!.isBefore(_selectedToDate!); if (flag) { - _toDateString = Format.formatDate(_selectedToDate); + _selectedToDate = DateTime.fromMillisecondsSinceEpoch( + _selectedFromDate!.millisecondsSinceEpoch); } - _fromDateString = Format.formatDate(_selectedFromDate); - }); - } - }, - child: Container( - width: width, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), - border: Border.all( + + setState(() { + if (flag) { + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); + } + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + }); + } + }, + child: Container( + width: width, + decoration: BoxDecoration( color: Theme.of(context) .extension<StackColors>()! .textFieldDefaultBG, - width: 1, + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + border: Border.all( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + width: 1, + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.calendar, - height: 20, - width: 20, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - ), - const SizedBox( - width: 10, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: _dateFromText, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: isDesktop ? 17 : 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.calendar, + height: 20, + width: 20, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, ), - ) - ], + const SizedBox( + width: 10, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: _dateFromText, + ), + ) + ], + ), ), ), ), @@ -277,466 +303,662 @@ class _TransactionSearchViewState // color: CFColors.smoke, ), ), - GestureDetector( - key: const Key("transactionSearchViewToDatePickerKey"), - onTap: () async { - final color = - Theme.of(context).extension<StackColors>()!.accentColorDark; - final height = MediaQuery.of(context).size.height; - // check and hide keyboard - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 125)); - } - - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - initialDate: DateTime.now(), - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _selectedToDate = date; - - // flag to adjust date so from date is always before to date - final flag = !_selectedToDate.isAfter(_selectedFromDate); - if (flag) { - _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( - _selectedToDate.millisecondsSinceEpoch); + Expanded( + child: GestureDetector( + key: const Key("transactionSearchViewToDatePickerKey"), + onTap: () async { + final color = + Theme.of(context).extension<StackColors>()!.accentColorDark; + final height = MediaQuery.of(context).size.height; + // check and hide keyboard + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 125)); } - setState(() { + final date = await showRoundedDatePicker( + // This doesn't change statusbar color... + // background: CFColors.starryNight.withOpacity(0.8), + context: context, + height: height * 0.5, + theme: ThemeData( + primarySwatch: Util.createMaterialColor( + color, + ), + ), + //TODO pick a better initial date + // 2007 chosen as that is just before bitcoin launched + initialDate: DateTime.now(), + firstDate: DateTime(2007), + lastDate: DateTime.now(), + borderRadius: Constants.size.circularBorderRadius * 2, + + textPositiveButton: "SELECT", + + styleDatePicker: _buildDatePickerStyle(), + styleYearPicker: _buildYearPickerStyle(), + ); + if (date != null) { + _selectedToDate = date; + + // flag to adjust date so from date is always before to date + final flag = _selectedFromDate != null && + !_selectedToDate!.isAfter(_selectedFromDate!); if (flag) { - _fromDateString = Format.formatDate(_selectedFromDate); + _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( + _selectedToDate!.millisecondsSinceEpoch); } - _toDateString = Format.formatDate(_selectedToDate); - }); - } - }, - child: Container( - width: width, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), - border: Border.all( + + setState(() { + if (flag) { + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + } + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); + }); + } + }, + child: Container( + width: width, + decoration: BoxDecoration( color: Theme.of(context) .extension<StackColors>()! .textFieldDefaultBG, - width: 1, + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + border: Border.all( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + width: 1, + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.calendar, - height: 20, - width: 20, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - ), - const SizedBox( - width: 10, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: _dateToText, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: isDesktop ? 17 : 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.calendar, + height: 20, + width: 20, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, ), - ) - ], + const SizedBox( + width: 10, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: _dateToText, + ), + ) + ], + ), ), ), ), ), + if (isDesktop) + const SizedBox( + width: 24, + ), ], ); } @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(); - } - }, + if (Util.isDesktop) { + return DesktopDialog( + maxWidth: 576, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: _buildContent(context), ), - title: Text( - "Transactions filter", - style: STextStyles.navBarTitle(context), + ); + } else { + return Background( + 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(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Transactions filter", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: EdgeInsets.symmetric( + horizontal: Constants.size.standardPadding, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: _buildContent(context), + ), + ), + ); + }, + ), + ), ), - ), - body: Padding( - padding: EdgeInsets.symmetric( - horizontal: Constants.size.standardPadding, + ); + } + } + + Widget _buildContent(BuildContext context) { + final isDesktop = Util.isDesktop; + + return Column( + children: [ + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction filter", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + const DesktopDialogCloseButton(), + ], + ), + SizedBox( + height: isDesktop ? 14 : 10, ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Transactions", - style: STextStyles.smallMed12(context), - ), - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - GestureDetector( - onTap: () { - setState(() { - _isActiveSentCheckbox = - !_isActiveSentCheckbox; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - SizedBox( - height: 20, - width: 20, - child: Checkbox( - key: const Key( - "transactionSearchViewSentCheckboxKey"), - materialTapTargetSize: - MaterialTapTargetSize - .shrinkWrap, - value: _isActiveSentCheckbox, - onChanged: (newValue) { - setState(() { - _isActiveSentCheckbox = - newValue!; - }); - }, - ), - ), - const SizedBox( - width: 14, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Sent", - style: STextStyles.itemSubtitle12( - context), - ), - ), - ) - ], - ), - ), - ), - ], - ), - const SizedBox( - height: 10, - ), - Row( - children: [ - GestureDetector( - onTap: () { - setState(() { - _isActiveReceivedCheckbox = - !_isActiveReceivedCheckbox; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - SizedBox( - height: 20, - width: 20, - child: Checkbox( - key: const Key( - "transactionSearchViewReceivedCheckboxKey"), - materialTapTargetSize: - MaterialTapTargetSize - .shrinkWrap, - value: _isActiveReceivedCheckbox, - onChanged: (newValue) { - setState(() { - _isActiveReceivedCheckbox = - newValue!; - }); - }, - ), - ), - const SizedBox( - width: 14, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Received", - style: STextStyles.itemSubtitle12( - context), - ), - ), - ) - ], - ), - ), - ), - ], - ), - ], - ), - ), - const SizedBox( - height: 24, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Date", - style: STextStyles.smallMed12(context), - ), - ), - ), - const SizedBox( - height: 8, - ), - _buildDateRangePicker(), - const SizedBox( - height: 24, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Amount", - style: STextStyles.smallMed12(context), - ), - ), - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("transactionSearchViewAmountFieldKey"), - controller: _amountTextEditingController, - focusNode: amountTextFieldFocusNode, - onChanged: (_) => setState(() {}), - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, - newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${widget.coin.ticker} amount...", - keywordTextFieldFocusNode, - context, - ).copyWith( - suffixIcon: _amountTextEditingController - .text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _amountTextEditingController - .text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 24, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Keyword", - style: STextStyles.smallMed12(context), - ), - ), - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: - const Key("transactionSearchViewKeywordFieldKey"), - controller: _keywordTextEditingController, - focusNode: keywordTextFieldFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type keyword...", - keywordTextFieldFocusNode, - context, - ).copyWith( - suffixIcon: _keywordTextEditingController - .text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _keywordTextEditingController - .text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const Spacer(), - const SizedBox( - height: 20, - ), - Row( + if (!isDesktop) + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Transactions", + style: STextStyles.smallMed12(context), + ), + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: EdgeInsets.all(isDesktop ? 0 : 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _isActiveSentCheckbox = !_isActiveSentCheckbox; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( children: [ - Expanded( - child: SizedBox( - height: 48, - child: TextButton( - 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(); - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), + SizedBox( + height: 20, + width: 20, + child: Checkbox( + key: const Key( + "transactionSearchViewSentCheckboxKey"), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _isActiveSentCheckbox, + onChanged: (newValue) { + setState(() { + _isActiveSentCheckbox = newValue!; + }); + }, ), ), const SizedBox( - width: 16, + width: 14, ), - Expanded( - child: SizedBox( - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - _onApplyPressed(); - }, - child: Text( - "Save", - style: STextStyles.button(context), - ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Column( + children: [ + Text( + "Sent", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + height: 4, + ), + ], ), ), - ), + ) ], ), - const SizedBox( - height: 20, - ), - ], + ), ), - ), + ], ), - ); - }, + SizedBox( + height: isDesktop ? 4 : 10, + ), + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _isActiveReceivedCheckbox = !_isActiveReceivedCheckbox; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + height: 20, + width: 20, + child: Checkbox( + key: const Key( + "transactionSearchViewReceivedCheckboxKey"), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _isActiveReceivedCheckbox, + onChanged: (newValue) { + setState(() { + _isActiveReceivedCheckbox = newValue!; + }); + }, + ), + ), + const SizedBox( + width: 14, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Column( + children: [ + Text( + "Received", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + height: 4, + ), + ], + ), + ), + ) + ], + ), + ), + ), + ], + ), + SizedBox( + height: isDesktop ? 4 : 10, + ), + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _isActiveTradeCheckbox = !_isActiveTradeCheckbox; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + height: 20, + width: 20, + child: Checkbox( + key: const Key( + "transactionSearchViewSentCheckboxKey"), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _isActiveTradeCheckbox, + onChanged: (newValue) { + setState(() { + _isActiveTradeCheckbox = newValue!; + }); + }, + ), + ), + const SizedBox( + width: 14, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Column( + children: [ + Text( + "Trades", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + height: 4, + ), + ], + ), + ), + ) + ], + ), + ), + ), + ], + ), + ], + ), ), - ), + SizedBox( + height: isDesktop ? 32 : 24, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Date", + style: isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + _buildDateRangePicker(), + SizedBox( + height: isDesktop ? 32 : 24, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Amount", + style: isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + Padding( + padding: EdgeInsets.only(right: isDesktop ? 32 : 0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("transactionSearchViewAmountFieldKey"), + controller: _amountTextEditingController, + focusNode: amountTextFieldFocusNode, + onChanged: (_) => setState(() {}), + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${widget.coin.ticker} amount...", + keywordTextFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: _amountTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _amountTextEditingController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + SizedBox( + height: isDesktop ? 32 : 24, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Keyword", + style: isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + Padding( + padding: EdgeInsets.only(right: isDesktop ? 32 : 0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("transactionSearchViewKeywordFieldKey"), + controller: _keywordTextEditingController, + focusNode: keywordTextFieldFocusNode, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type keyword...", + keywordTextFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: _keywordTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _keywordTextEditingController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + if (!isDesktop) const Spacer(), + SizedBox( + height: isDesktop ? 32 : 20, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop) { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration( + milliseconds: 75, + ), + ); + } + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + // Expanded( + // child: SizedBox( + // height: 48, + // child: TextButton( + // 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(); + // } + // }, + // style: Theme.of(context) + // .extension<StackColors>()! + // .getSecondaryEnabledButtonColor(context), + // child: Text( + // "Cancel", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context) + // .extension<StackColors>()! + // .accentColorDark), + // ), + // ), + // ), + // ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + await _onApplyPressed(); + }, + label: "Save", + ), + ), + // Expanded( + // child: SizedBox( + // height: 48, + // child: TextButton( + // style: Theme.of(context) + // .extension<StackColors>()! + // .getPrimaryEnabledButtonColor(context), + // onPressed: () async { + // await _onApplyPressed(); + // }, + // child: Text( + // "Save", + // style: STextStyles.button(context), + // ), + // ), + // ), + // ), + if (isDesktop) + const SizedBox( + width: 32, + ), + ], + ), + if (!isDesktop) + const SizedBox( + height: 20, + ), + ], ); } @@ -750,27 +972,13 @@ class _TransactionSearchViewState } int? amount; if (amountDecimal != null) { - if (widget.coin == Coin.monero) { - amount = (amountDecimal * Decimal.fromInt(Constants.satsPerCoinMonero)) - .floor() - .toBigInt() - .toInt(); - } else if (widget.coin == Coin.wownero) { - amount = (amountDecimal * Decimal.fromInt(Constants.satsPerCoinWownero)) - .floor() - .toBigInt() - .toInt(); - } else { - amount = (amountDecimal * Decimal.fromInt(Constants.satsPerCoin)) - .floor() - .toBigInt() - .toInt(); - } + amount = Format.decimalAmountToSatoshis(amountDecimal, widget.coin); } final TransactionFilter filter = TransactionFilter( sent: _isActiveSentCheckbox, received: _isActiveReceivedCheckbox, + trade: _isActiveTradeCheckbox, from: _selectedFromDate, to: _selectedToDate, amount: amount, diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index c84ddf2b9..61a1da0ac 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -40,6 +40,7 @@ import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.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/background.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/custom_loading_overlay.dart'; @@ -378,401 +379,415 @@ class _WalletViewState extends ConsumerState<WalletView> { return WillPopScope( onWillPop: _onWillPop, - child: Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - _logout(); - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - width: 24, - height: 24, - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Text( - ref.watch( - managerProvider.select((value) => value.walletName)), - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, + child: Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + _logout(); + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + // color: Theme.of(context).extension<StackColors>()!.accentColorDark + width: 24, + height: 24, ), - ) + const SizedBox( + width: 16, + ), + Expanded( + child: Text( + ref.watch( + managerProvider.select((value) => value.walletName)), + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletViewRadioButton"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: _buildNetworkIcon(_currentSyncStatus), + onPressed: () { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + _currentNodeStatus, + ), + ); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletViewAlertsButton"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + ref.watch(notificationsProvider.select((value) => + value.hasUnreadNotificationsFor(walletId))) + ? Assets.svg.bellNew(context) + : Assets.svg.bell, + width: 20, + height: 20, + color: ref.watch(notificationsProvider.select((value) => + value.hasUnreadNotificationsFor(walletId))) + ? null + : Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + ), + onPressed: () { + // reset unread state + ref.refresh(unreadNotificationsStateProvider); + + Navigator.of(context) + .pushNamed( + NotificationsView.routeName, + arguments: walletId, + ) + .then((_) { + final Set<int> unreadNotificationIds = ref + .read(unreadNotificationsStateProvider.state) + .state; + if (unreadNotificationIds.isEmpty) return; + + List<Future<dynamic>> futures = []; + for (int i = 0; + i < unreadNotificationIds.length - 1; + i++) { + futures.add(ref + .read(notificationsProvider) + .markAsRead( + unreadNotificationIds.elementAt(i), false)); + } + + // wait for multiple to update if any + Future.wait(futures).then((_) { + // only notify listeners once + ref + .read(notificationsProvider) + .markAsRead(unreadNotificationIds.last, true); + }); + }); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletViewSettingsButton"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.bars, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + debugPrint("wallet view settings tapped"); + Navigator.of(context).pushNamed( + WalletSettingsView.routeName, + arguments: Tuple4( + walletId, + ref.read(managerProvider).coin, + _currentSyncStatus, + _currentNodeStatus, + ), + ); + }, + ), + ), + ), ], ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletViewRadioButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: _buildNetworkIcon(_currentSyncStatus), - onPressed: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - _currentNodeStatus, - ), - ); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletViewAlertsButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - ref.watch(notificationsProvider.select((value) => - value.hasUnreadNotificationsFor(walletId))) - ? Assets.svg.bellNew(context) - : Assets.svg.bell, - width: 20, - height: 20, - color: ref.watch(notificationsProvider.select((value) => - value.hasUnreadNotificationsFor(walletId))) - ? null - : Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, - ), - onPressed: () { - // reset unread state - ref.refresh(unreadNotificationsStateProvider); - - Navigator.of(context) - .pushNamed( - NotificationsView.routeName, - arguments: walletId, - ) - .then((_) { - final Set<int> unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - List<Future<dynamic>> futures = []; - for (int i = 0; - i < unreadNotificationIds.length - 1; - i++) { - futures.add(ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.elementAt(i), false)); - } - - // wait for multiple to update if any - Future.wait(futures).then((_) { - // only notify listeners once - ref - .read(notificationsProvider) - .markAsRead(unreadNotificationIds.last, true); - }); - }); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletViewSettingsButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.bars, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - debugPrint("wallet view settings tapped"); - Navigator.of(context).pushNamed( - WalletSettingsView.routeName, - arguments: Tuple4( - walletId, - ref.read(managerProvider).coin, - _currentSyncStatus, - _currentNodeStatus, - ), - ); - }, - ), - ), - ), - ], - ), - body: SafeArea( - child: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: WalletSummary( - walletId: walletId, - managerProvider: managerProvider, - initialSyncStatus: ref.watch(managerProvider - .select((value) => value.isRefreshing)) - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), - ), - ), - if (coin == Coin.firo) + body: SafeArea( + child: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Column( + children: [ const SizedBox( height: 10, ), - if (coin == Coin.firo) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - onPressed: () async { - await showDialog<void>( - context: context, - builder: (context) => StackDialog( - title: "Attention!", - message: - "You're about to anonymize all of your public funds.", - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - "Cancel", - style: - STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: WalletSummary( + walletId: walletId, + managerProvider: managerProvider, + initialSyncStatus: ref.watch(managerProvider + .select((value) => value.isRefreshing)) + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + ), + ), + if (coin == Coin.firo) + const SizedBox( + height: 10, + ), + if (coin == Coin.firo) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + onPressed: () async { + await showDialog<void>( + context: context, + builder: (context) => StackDialog( + title: "Attention!", + message: + "You're about to anonymize all of your public funds.", + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + "Cancel", + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(); + + unawaited(attemptAnonymize()); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor( + context), + child: Text( + "Continue", + style: STextStyles.button(context), ), ), ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(); - - unawaited(attemptAnonymize()); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Continue", - style: STextStyles.button(context), - ), - ), + ); + }, + child: Text( + "Anonymize funds", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, ), - ); - }, - child: Text( - "Anonymize funds", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, ), ), ), + ], + ), + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + BlueTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: walletId, + ); + }, ), ], ), ), - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transactions", - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - ), - BlueTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: walletId, - ); - }, - ), - ], + const SizedBox( + height: 12, ), - ), - const SizedBox( - height: 12, - ), - Expanded( - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Padding( - padding: const EdgeInsets.only(bottom: 14), - child: ClipRRect( - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - bottom: Radius.circular( - // WalletView.navBarHeight / 2.0, - Constants.size.circularBorderRadius, - ), - ), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( + Expanded( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Padding( + padding: const EdgeInsets.only(bottom: 14), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottom: Radius.circular( + // WalletView.navBarHeight / 2.0, Constants.size.circularBorderRadius, ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: TransactionsList( - managerProvider: managerProvider, - walletId: walletId, - ), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TransactionsList( + managerProvider: managerProvider, + walletId: walletId, + ), + ), + ], + ), ), ), ), ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 14, - left: 16, - right: 16, - ), - child: SizedBox( - height: WalletView.navBarHeight, - child: WalletNavigationBar( - enableExchange: Constants.enableExchange && - ref.watch(managerProvider.select( - (value) => value.coin)) != - Coin.epicCash, + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 14, + left: 16, + right: 16, + ), + child: SizedBox( height: WalletView.navBarHeight, - onExchangePressed: () => - _onExchangePressed(context), - onReceivePressed: () async { - final coin = - ref.read(managerProvider).coin; - if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - ReceiveView.routeName, + child: WalletNavigationBar( + enableExchange: + Constants.enableExchange && + ref.watch(managerProvider.select( + (value) => value.coin)) != + Coin.epicCash, + height: WalletView.navBarHeight, + onExchangePressed: () => + _onExchangePressed(context), + onReceivePressed: () async { + final coin = + ref.read(managerProvider).coin; + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + ReceiveView.routeName, + arguments: Tuple2( + walletId, + coin, + ), + )); + } + }, + onSendPressed: () { + final walletId = + ref.read(managerProvider).walletId; + final coin = + ref.read(managerProvider).coin; + switch (ref + .read( + walletBalanceToggleStateProvider + .state) + .state) { + case WalletBalanceToggleState.full: + ref + .read( + publicPrivateBalanceStateProvider + .state) + .state = "Public"; + break; + case WalletBalanceToggleState + .available: + ref + .read( + publicPrivateBalanceStateProvider + .state) + .state = "Private"; + break; + } + Navigator.of(context).pushNamed( + SendView.routeName, arguments: Tuple2( walletId, coin, ), - )); - } - }, - onSendPressed: () { - final walletId = - ref.read(managerProvider).walletId; - final coin = - ref.read(managerProvider).coin; - switch (ref - .read(walletBalanceToggleStateProvider - .state) - .state) { - case WalletBalanceToggleState.full: - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state = "Public"; - break; - case WalletBalanceToggleState.available: - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state = "Private"; - break; - } - Navigator.of(context).pushNamed( - SendView.routeName, - arguments: Tuple2( - walletId, - coin, - ), - ); - }, - onBuyPressed: () {}, + ); + }, + onBuyPressed: () {}, + ), ), ), - ), - ], - ), - ], - ) - ], + ], + ), + ], + ) + ], + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 924e904f3..24b1e021a 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -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,8 @@ 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:stackwallet/widgets/conditional_parent.dart'; import 'package:tuple/tuple.dart'; class FavoriteCard extends ConsumerStatefulWidget { @@ -46,189 +49,247 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> { super.initState(); } + bool _hovering = false; + @override Widget build(BuildContext context) { final coin = ref.watch(managerProvider.select((value) => value.coin)); final externalCalls = ref.watch( prefsChangeNotifierProvider.select((value) => value.externalCalls)); - return GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: Tuple2( - walletId, - managerProvider, - ), - ); - }, - child: SizedBox( - width: widget.width, - height: widget.height, - child: CardOverlayStack( - background: Stack( - children: [ - Container( - width: widget.width, - height: widget.height, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .colorForCoin(coin), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - Column( - children: [ - const Spacer(), - SizedBox( - height: widget.width * 0.3, - child: Row( - children: [ - const Spacer( - flex: 9, - ), - SvgPicture.asset( - Assets.svg.ellipse2, - height: widget.width * 0.3, - ), - // ), - const Spacer( - flex: 2, - ), - ], + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: _hovering ? 1.05 : 1, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: _hovering + ? BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ), - ], - ), - Row( - children: [ - const Spacer( - flex: 5, - ), - SizedBox( - width: widget.width * 0.45, - child: Column( - children: [ - SvgPicture.asset( - Assets.svg.ellipse1, - width: widget.width * 0.45, - ), - const Spacer(), - ], - ), - ), - const Spacer( - flex: 1, - ), - ], - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - ref.watch(managerProvider - .select((value) => value.walletName)), - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), - overflow: TextOverflow.fade, - ), - ), - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), + boxShadow: [ + Theme.of(context) + .extension<StackColors>()! + .standardBoxShadow, + Theme.of(context) + .extension<StackColors>()! + .standardBoxShadow, + Theme.of(context) + .extension<StackColors>()! + .standardBoxShadow, ], + ) + : BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: child, + ), + ), + ), + child: GestureDetector( + onTap: () { + 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, + height: widget.height, + child: CardOverlayStack( + background: Stack( + children: [ + Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .colorForCoin(coin), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), ), - FutureBuilder( - future: ref.watch( - managerProvider.select((value) => value.totalBalance)), - builder: (builderContext, AsyncSnapshot<Decimal> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - if (snapshot.data != null) { - _cachedBalance = snapshot.data!; - if (externalCalls) { - _cachedFiatValue = _cachedBalance * - ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ) - .item1; - } - } - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "${Format.localizedStringAsFixed( - decimalPlaces: 8, - value: _cachedBalance, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${coin.ticker}", - style: STextStyles.titleBold12(context).copyWith( - fontSize: 16, - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), + Column( + children: [ + const Spacer(), + SizedBox( + height: widget.width * 0.3, + child: Row( + children: [ + const Spacer( + flex: 9, ), - ), - if (externalCalls) - const SizedBox( - height: 4, + SvgPicture.asset( + Assets.svg.ellipse2, + height: widget.width * 0.3, ), - if (externalCalls) - Text( - "${Format.localizedStringAsFixed( - decimalPlaces: 2, - value: _cachedFiatValue, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - prefsChangeNotifierProvider - .select((value) => value.currency), - )}", - style: STextStyles.itemSubtitle12(context).copyWith( - fontSize: 10, - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), + // ), + const Spacer( + flex: 2, ), - ], - ); - }, + ], + ), + ), + ], + ), + Row( + children: [ + const Spacer( + flex: 5, + ), + SizedBox( + width: widget.width * 0.45, + child: Column( + children: [ + SvgPicture.asset( + Assets.svg.ellipse1, + width: widget.width * 0.45, + ), + const Spacer(), + ], + ), + ), + const Spacer( + flex: 1, + ), + ], ), ], ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + ref.watch(managerProvider + .select((value) => value.walletName)), + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + overflow: TextOverflow.fade, + ), + ), + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + ], + ), + ), + FutureBuilder( + future: ref.watch( + managerProvider.select((value) => value.totalBalance)), + builder: (builderContext, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + if (snapshot.data != null) { + _cachedBalance = snapshot.data!; + if (externalCalls) { + _cachedFiatValue = _cachedBalance * + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ) + .item1; + } + } + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "${Format.localizedStringAsFixed( + decimalPlaces: 8, + value: _cachedBalance, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${coin.ticker}", + style: STextStyles.titleBold12(context).copyWith( + fontSize: 16, + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ), + ), + if (externalCalls) + const SizedBox( + height: 4, + ), + if (externalCalls) + Text( + "${Format.localizedStringAsFixed( + decimalPlaces: 2, + value: _cachedFiatValue, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + )}", + style: + STextStyles.itemSubtitle12(context).copyWith( + fontSize: 10, + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ), + ], + ); + }, + ), + ], + ), + ), ), ), ), diff --git a/lib/pages_desktop_specific/create_password/create_password_view.dart b/lib/pages_desktop_specific/create_password/create_password_view.dart index 2391a22f6..a8c9e7758 100644 --- a/lib/pages_desktop_specific/create_password/create_password_view.dart +++ b/lib/pages_desktop_specific/create_password/create_password_view.dart @@ -1,13 +1,15 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.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_desktop_specific/home/desktop_home_view.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -18,23 +20,18 @@ 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( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/createPasswordDesktop"; - 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 +73,26 @@ class _CreatePasswordViewState extends State<CreatePasswordView> { return; } - await widget.secureStore - .write(key: "stackDesktopPassword", value: passphrase); + try { + if (await ref.read(storageCryptoHandlerProvider).hasPassword()) { + throw Exception( + "Tried creating a new password and attempted to overwrite an existing entry!"); + } + + await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase); + await (ref.read(secureStoreProvider).store as DesktopSecureStore).init(); + + // load default nodes now as node service requires storage handler to exist + + await ref.read(nodeServiceChangeNotifierProvider).updateDefaults(); + } catch (e) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Error: $e", + context: context, + )); + return; + } if (mounted) { unawaited(Navigator.of(context) @@ -192,15 +207,18 @@ class _CreatePasswordViewState extends State<CreatePasswordView> { 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, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 19, + ), ), ), ), @@ -343,22 +361,25 @@ class _CreatePasswordViewState extends State<CreatePasswordView> { height: 32, width: 32, child: Center( - child: SvgPicture.asset( - fieldsMatch && passwordStrength == 1 - ? Assets.svg.checkCircle - : hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: fieldsMatch && - passwordStrength == 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorGreen - : Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 24, - height: 19, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + fieldsMatch && passwordStrength == 1 + ? Assets.svg.checkCircle + : hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: fieldsMatch && + passwordStrength == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorGreen + : Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 19, + ), ), ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart new file mode 100644 index 000000000..105c485f0 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages/exchange_view/exchange_form.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopExchangeView extends StatefulWidget { + const DesktopExchangeView({Key? key}) : super(key: key); + + static const String routeName = "/desktopExchange"; + + @override + State<DesktopExchangeView> createState() => _DesktopExchangeViewState(); +} + +class _DesktopExchangeViewState extends State<DesktopExchangeView> { + @override + Widget build(BuildContext context) { + return DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Padding( + padding: const EdgeInsets.only( + left: 24, + ), + child: Text( + "Exchange", + style: STextStyles.desktopH3(context), + ), + ), + ), + body: Padding( + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Exchange details", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 16, + ), + const RoundedWhiteContainer( + padding: EdgeInsets.all(24), + child: ExchangeForm(), + ), + ], + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Row( + children: const [ + Expanded( + child: DesktopTradeHistory(), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart new file mode 100644 index 000000000..8dbaf9580 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; + +class StepScaffold extends StatefulWidget { + const StepScaffold({ + Key? key, + required this.body, + required this.step, + required this.model, + }) : super(key: key); + + final Widget body; + final int step; + final IncompleteExchangeModel model; + + @override + State<StepScaffold> createState() => _StepScaffoldState(); +} + +class _StepScaffoldState extends State<StepScaffold> { + int currentStep = 0; + late final IncompleteExchangeModel model; + + @override + void initState() { + currentStep = widget.step; + model = widget.model; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + children: [ + const AppBarBackButton( + isCompact: true, + iconSize: 23, + ), + Text( + "Exchange ${model.sendTicker.toUpperCase()} to ${model.receiveTicker.toUpperCase()}", + style: STextStyles.desktopH3(context), + ), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: DesktopExchangeStepsIndicator( + currentStep: currentStep, + ), + ), + const SizedBox( + height: 32, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: widget.body, + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart new file mode 100644 index 000000000..031dc0649 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.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/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopStep1 extends ConsumerWidget { + const DesktopStep1({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + children: [ + Text( + "Confirm amount", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: Column( + children: [ + DesktopStepItem( + label: "Exchange", + value: ref.watch(currentExchangeNameStateProvider.state).state, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + label: "You send", + value: + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + label: "You receive", + value: + "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + label: model.rateType == ExchangeRateType.estimated + ? "Estimated rate" + : "Fixed rate", + value: model.rateInfo, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + buttonHeight: ButtonHeight.l, + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 2, + model: model, + body: DesktopStep2( + model: model, + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart new file mode 100644 index 000000000..7b793210c --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -0,0 +1,620 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; +import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.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/logger.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'; +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/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class DesktopStep2 extends ConsumerStatefulWidget { + const DesktopStep2({ + Key? key, + required this.model, + this.clipboard = const ClipboardWrapper(), + }) : super(key: key); + + final IncompleteExchangeModel model; + final ClipboardInterface clipboard; + + @override + ConsumerState<DesktopStep2> createState() => _DesktopStep2State(); +} + +class _DesktopStep2State extends ConsumerState<DesktopStep2> { + late final IncompleteExchangeModel model; + late final ClipboardInterface clipboard; + + late final TextEditingController _toController; + late final TextEditingController _refundController; + + late final FocusNode _toFocusNode; + late final FocusNode _refundFocusNode; + + bool enableNext = false; + + bool isStackCoin(String ticker) { + try { + coinFromTickerCaseInsensitive(ticker); + return true; + } on ArgumentError catch (_) { + return false; + } + } + + void selectRecipientAddressFromStack() async { + try { + final coin = coinFromTickerCaseInsensitive( + model.receiveTicker, + ); + + final address = await showDialog<String?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack( + coin: coin, + ), + ), + ), + ); + + if (address is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(address); + + _toController.text = manager.walletName; + model.recipientAddress = await manager.currentReceivingAddress; + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Info); + } + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); + } + + void selectRefundAddressFromStack() async { + try { + final coin = coinFromTickerCaseInsensitive( + model.sendTicker, + ); + + final address = await showDialog<String?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack( + coin: coin, + ), + ), + ), + ); + if (address is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(address); + + _refundController.text = manager.walletName; + model.refundAddress = await manager.currentReceivingAddress; + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Info); + } + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); + } + + void selectRecipientFromAddressBook() async { + final coin = coinFromTickerCaseInsensitive( + model.receiveTicker, + ); + + final entry = await showDialog<ContactAddressEntry?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], + ), + ), + ); + + if (entry != null) { + _toController.text = entry.address; + model.recipientAddress = entry.address; + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); + } + } + + void selectRefundFromAddressBook() async { + final coin = coinFromTickerCaseInsensitive( + model.sendTicker, + ); + + final entry = await showDialog<ContactAddressEntry?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], + ), + ), + ); + + if (entry != null) { + _refundController.text = entry.address; + model.refundAddress = entry.address; + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); + } + } + + @override + void initState() { + model = widget.model; + clipboard = widget.clipboard; + + _toController = TextEditingController(); + _refundController = TextEditingController(); + + _toFocusNode = FocusNode(); + _refundFocusNode = FocusNode(); + + final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state; + if (tuple != null) { + if (model.receiveTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase()) { + ref + .read(walletsChangeNotifierProvider) + .getManager(tuple.item1) + .currentReceivingAddress + .then((value) { + _toController.text = value; + model.recipientAddress = _toController.text; + }); + } else { + if (model.sendTicker.toUpperCase() == + tuple.item2.ticker.toUpperCase()) { + ref + .read(walletsChangeNotifierProvider) + .getManager(tuple.item1) + .currentReceivingAddress + .then((value) { + _refundController.text = value; + model.refundAddress = _refundController.text; + }); + } + } + } + + super.initState(); + } + + @override + void dispose() { + _toController.dispose(); + _refundController.dispose(); + + _toFocusNode.dispose(); + _refundFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Enter exchange details", + style: STextStyles.desktopTextMedium(context), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 8, + ), + Text( + "Enter your recipient and refund addresses", + style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipient Wallet", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight), + ), + if (isStackCoin(model.receiveTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: selectRecipientAddressFromStack, + ), + ], + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + onTap: () {}, + key: const Key("recipientExchangeStep2ViewAddressFieldKey"), + controller: _toController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _toFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter the ${model.receiveTicker.toUpperCase()} payout address", + _toFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _toController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _toController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _toController.text = ""; + model.recipientAddress = _toController.text; + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + _toController.text = content; + model.recipientAddress = _toController.text; + setState(() { + enableNext = + _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + } + }, + child: _toController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_toController.text.isEmpty && + isStackCoin(model.receiveTicker)) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: selectRecipientFromAddressBook, + child: const AddressBookIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 10, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + child: Text( + "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Refund Wallet (required)", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight), + ), + if (isStackCoin(model.sendTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: selectRefundAddressFromStack, + ), + ], + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("refundExchangeStep2ViewAddressFieldKey"), + controller: _refundController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _refundFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter ${model.sendTicker.toUpperCase()} refund address", + _refundFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _refundController.text.isEmpty + ? const EdgeInsets.only(right: 16) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _refundController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _refundController.text = ""; + model.refundAddress = _refundController.text; + + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + + _refundController.text = content; + model.refundAddress = _refundController.text; + + setState(() { + enableNext = + _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + } + }, + child: _refundController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_refundController.text.isEmpty && + isStackCoin(model.sendTicker)) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: selectRefundFromAddressBook, + child: const AddressBookIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 10, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + child: Text( + "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + enabled: enableNext, + buttonHeight: ButtonHeight.l, + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 3, + model: model, + body: DesktopStep3( + model: model, + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart new file mode 100644 index 000000000..284416545 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -0,0 +1,254 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/models/exchange/response_objects/trade.dart'; +import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; +import 'package:stackwallet/providers/exchange/current_exchange_name_state_provider.dart'; +import 'package:stackwallet/providers/exchange/exchange_provider.dart'; +import 'package:stackwallet/providers/global/trades_service_provider.dart'; +import 'package:stackwallet/services/exchange/exchange_response.dart'; +import 'package:stackwallet/services/notifications_api.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/widgets/custom_loading_overlay.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/desktop/simple_desktop_dialog.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopStep3 extends ConsumerStatefulWidget { + const DesktopStep3({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + ConsumerState<DesktopStep3> createState() => _DesktopStep3State(); +} + +class _DesktopStep3State extends ConsumerState<DesktopStep3> { + late final IncompleteExchangeModel model; + + Future<void> createTrade() async { + unawaited( + showDialog<void>( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension<StackColors>()! + .overlay + .withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Creating a trade", + eventBus: null, + ), + ), + ), + ), + ); + + final ExchangeResponse<Trade> response = + await ref.read(exchangeProvider).createTrade( + from: model.sendTicker, + to: model.receiveTicker, + fixedRate: model.rateType != ExchangeRateType.estimated, + amount: model.reversed ? model.receiveAmount : model.sendAmount, + addressTo: model.recipientAddress!, + extraId: null, + addressRefund: model.refundAddress!, + refundExtraId: "", + rateId: model.rateId, + reversed: model.reversed, + ); + + if (response.value == null) { + if (mounted) { + Navigator.of(context).pop(); + } + + unawaited( + showDialog<void>( + context: context, + barrierDismissible: true, + builder: (_) => SimpleDesktopDialog( + title: "Failed to create trade", + message: response.exception?.toString() ?? ""), + ), + ); + return; + } + + // save trade to hive + await ref.read(tradesServiceProvider).add( + trade: response.value!, + shouldNotifyListeners: true, + ); + + String status = response.value!.status; + + model.trade = response.value!; + + // extra info if status is waiting + if (status == "Waiting") { + status += " for deposit"; + } + + if (mounted) { + Navigator.of(context).pop(); + } + + unawaited( + NotificationApi.showNotification( + changeNowId: model.trade!.tradeId, + title: status, + body: "Trade ID ${model.trade!.tradeId}", + walletId: "", + iconAssetName: Assets.svg.arrowRotate, + date: model.trade!.timestamp, + shouldWatchForUpdates: true, + coinName: "coinName", + ), + ); + + if (mounted) { + unawaited( + showDialog<void>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 4, + model: model, + body: DesktopStep4( + model: model, + ), + ), + ); + }, + ), + ); + } + } + + @override + void initState() { + model = widget.model; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Confirm exchange details", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: Column( + children: [ + DesktopStepItem( + label: "Exchange", + value: ref.watch(currentExchangeNameStateProvider.state).state, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + label: "You send", + value: + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + label: "You receive", + value: + "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + label: model.rateType == ExchangeRateType.estimated + ? "Estimated rate" + : "Fixed rate", + value: model.rateInfo, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + vertical: true, + label: "Recipient ${model.receiveTicker.toUpperCase()} address", + value: model.recipientAddress!, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + vertical: true, + label: "Refund ${model.sendTicker.toUpperCase()} address", + value: model.refundAddress!, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Confirm", + buttonHeight: ButtonHeight.l, + onPressed: createTrade, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart new file mode 100644 index 000000000..5b69f064d --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -0,0 +1,301 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages/exchange_view/send_from_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.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/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopStep4 extends ConsumerStatefulWidget { + const DesktopStep4({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + ConsumerState<DesktopStep4> createState() => _DesktopStep4State(); +} + +class _DesktopStep4State extends ConsumerState<DesktopStep4> { + late final IncompleteExchangeModel model; + + String _statusString = "New"; + + Timer? _statusTimer; + + bool _isWalletCoinAndHasWallet(String ticker) { + try { + final coin = coinFromTickerCaseInsensitive(ticker); + return ref + .read(walletsChangeNotifierProvider) + .managers + .where((element) => element.coin == coin) + .isNotEmpty; + } catch (_) { + return false; + } + } + + Future<void> _updateStatus() async { + final statusResponse = + await ref.read(exchangeProvider).updateTrade(model.trade!); + String status = "Waiting"; + if (statusResponse.value != null) { + status = statusResponse.value!.status; + } + + // extra info if status is waiting + if (status == "Waiting") { + status += " for deposit"; + } + + if (mounted) { + setState(() { + _statusString = status; + }); + } + } + + @override + void initState() { + model = widget.model; + + _statusTimer = Timer.periodic(const Duration(seconds: 60), (_) { + _updateStatus(); + }); + + super.initState(); + } + + @override + void dispose() { + _statusTimer?.cancel(); + _statusTimer = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Send ${model.sendTicker.toUpperCase()} to the address below", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + RoundedContainer( + color: Theme.of(context).extension<StackColors>()!.warningBackground, + child: RichText( + text: TextSpan( + text: + "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", + style: STextStyles.label700(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + fontSize: 14, + ), + children: [ + TextSpan( + text: + "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", + style: STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + fontSize: 14, + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: Column( + children: [ + DesktopStepItem( + vertical: true, + label: "Send ${model.sendTicker.toUpperCase()} to this address", + value: model.trade!.payInAddress, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + label: "Amount", + value: + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + label: "Trade ID", + value: model.trade!.tradeId, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + Text( + _statusString, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .colorForStatus(_statusString), + ), + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Send from Stack Wallet", + buttonHeight: ButtonHeight.l, + onPressed: () { + final trade = model.trade!; + final amount = Decimal.parse(trade.payInAmount); + final address = trade.payInAddress; + + final coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: SendFromView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + SendFromView( + coin: coin, + trade: trade, + amount: amount, + address: address, + shouldPopRoot: true, + fromDesktopStep4: true, + ), + const RouteSettings( + name: SendFromView.routeName, + ), + ), + ]; + }, + ), + ); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Show QR code", + buttonHeight: ButtonHeight.l, + onPressed: () { + showDialog<dynamic>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: true, + builder: (_) { + return DesktopDialog( + maxHeight: 720, + maxWidth: 720, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Send ${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker} to this address", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 48, + ), + Center( + child: QrImage( + // TODO: grab coin uri scheme from somewhere + // data: "${coin.uriScheme}:$receivingAddress", + data: model.trade!.payInAddress, + size: 290, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + const SizedBox( + height: 48, + ), + SecondaryButton( + label: "Cancel", + width: 310, + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ], + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart new file mode 100644 index 000000000..323517e13 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; + +class DesktopStepItem extends StatelessWidget { + const DesktopStepItem( + {Key? key, + required this.label, + required this.value, + this.padding = const EdgeInsets.all(16), + this.vertical = false}) + : super(key: key); + + final String label; + final String value; + final EdgeInsets padding; + final bool vertical; + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding, + child: ConditionalParent( + condition: vertical, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + const SizedBox( + height: 2, + ), + Text( + value, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + if (!vertical) + Text( + value, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart new file mode 100644 index 000000000..a3fb91f61 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart @@ -0,0 +1,329 @@ +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/providers/providers.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/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_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_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; + +class DesktopChooseFromStack extends ConsumerStatefulWidget { + const DesktopChooseFromStack({ + Key? key, + required this.coin, + }) : super(key: key); + + final Coin coin; + + @override + ConsumerState<DesktopChooseFromStack> createState() => + _DesktopChooseFromStackState(); +} + +class _DesktopChooseFromStackState + extends ConsumerState<DesktopChooseFromStack> { + late final TextEditingController _searchController; + late final FocusNode searchFieldFocusNode; + + String _searchTerm = ""; + + List<String> filter(List<String> walletIds, String searchTerm) { + if (searchTerm.isEmpty) { + return walletIds; + } + + final List<String> result = []; + for (final walletId in walletIds) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if (manager.walletName.toLowerCase().contains(searchTerm.toLowerCase())) { + result.add(walletId); + } + } + + return result; + } + + @override + void initState() { + searchFieldFocusNode = FocusNode(); + _searchController = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Choose from Stack", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 28, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Search", + searchFieldFocusNode, + context, + desktopMed: true, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 18, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + ), + ), + 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, + ), + Flexible( + child: Builder( + builder: (context) { + List<String> walletIds = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getWalletIdsFor(coin: widget.coin), + ), + ); + + if (walletIds.isEmpty) { + return Column( + children: [ + RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Center( + child: Text( + "No ${widget.coin.ticker.toUpperCase()} wallets", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ), + ], + ); + } + + walletIds = filter(walletIds, _searchTerm); + + return ListView.separated( + primary: false, + itemCount: walletIds.length, + separatorBuilder: (_, __) => const SizedBox( + height: 5, + ), + itemBuilder: (context, index) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletIds[index]))); + + return RoundedWhiteContainer( + borderColor: + Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + WalletInfoCoinIcon(coin: widget.coin), + const SizedBox( + width: 12, + ), + Text( + manager.walletName, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + const Spacer(), + BalanceDisplay( + walletId: walletIds[index], + ), + const SizedBox( + width: 80, + ), + BlueTextButton( + text: "Select wallet", + onTap: () { + Navigator.of(context).pop(manager.walletId); + }, + ), + ], + ), + ); + }, + ); + }, + ), + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + ], + ) + ], + ); + } +} + +class BalanceDisplay extends ConsumerStatefulWidget { + const BalanceDisplay({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<BalanceDisplay> createState() => _BalanceDisplayState(); +} + +class _BalanceDisplayState extends ConsumerState<BalanceDisplay> { + late final String walletId; + + Decimal? _cachedBalance; + + static const loopedText = [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ]; + + @override + void initState() { + walletId = widget.walletId; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + final locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + return FutureBuilder( + future: manager.availableBalance, + builder: (context, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + _cachedBalance = snapshot.data; + } + + if (_cachedBalance == null) { + return AnimatedText( + stringsToLoopThrough: loopedText, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textSubtitle1, + ), + ); + } else { + return Text( + "${Format.localizedStringAsFixed( + value: _cachedBalance!, + locale: locale, + decimalPlaces: 8, + )} ${manager.coin.ticker}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textSubtitle1, + ), + textAlign: TextAlign.right, + ); + } + }, + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart new file mode 100644 index 000000000..ddcd2e6c4 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class DesktopExchangeStepsIndicator extends StatelessWidget { + const DesktopExchangeStepsIndicator({Key? key, required this.currentStep}) + : super(key: key); + + final int currentStep; + + Color getColor(BuildContext context, int step) { + if (currentStep > step) { + return Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + .withOpacity(0.5); + } else if (currentStep < step) { + return Theme.of(context).extension<StackColors>()!.textSubtitle3; + } else { + return Theme.of(context).extension<StackColors>()!.accentColorBlue; + } + } + + static const double verticalSpacing = 6; + static const double horizontalSpacing = 16; + static const double barHeight = 4; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + children: [ + Text( + "Confirm amount", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 1), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 1), + height: barHeight, + width: double.infinity, + ), + ], + ), + ), + const SizedBox( + width: horizontalSpacing, + ), + Expanded( + child: Column( + children: [ + Text( + "Enter details", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 2), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 2), + height: barHeight, + width: double.infinity, + ), + ], + ), + ), + const SizedBox( + width: horizontalSpacing, + ), + Expanded( + child: Column( + children: [ + Text( + "Confirm details", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 3), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 3), + height: barHeight, + width: double.infinity, + ), + ], + ), + ), + const SizedBox( + width: horizontalSpacing, + ), + Expanded( + child: Column( + children: [ + Text( + "Complete exchange", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 4), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 4), + height: barHeight, + width: double.infinity, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart new file mode 100644 index 000000000..e31a87dd4 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -0,0 +1,271 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; +import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; +import 'package:stackwallet/providers/global/trades_service_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.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/rounded_white_container.dart'; +import 'package:stackwallet/widgets/trade_card.dart'; + +import '../../../route_generator.dart'; +import '../../../widgets/desktop/desktop_dialog.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; + +class DesktopTradeHistory extends ConsumerStatefulWidget { + const DesktopTradeHistory({Key? key}) : super(key: key); + + @override + ConsumerState<DesktopTradeHistory> createState() => + _DesktopTradeHistoryState(); +} + +class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { + BorderRadius get _borderRadiusFirst { + return BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + BorderRadius get _borderRadiusLast { + return BorderRadius.only( + bottomLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottomRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + @override + Widget build(BuildContext context) { + final trades = + ref.watch(tradesServiceProvider.select((value) => value.trades)); + + final tradeCount = trades.length; + final hasHistory = tradeCount > 0; + + if (hasHistory) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Exchange details", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: ListView.separated( + shrinkWrap: true, + primary: false, + itemBuilder: (context, index) { + BorderRadius? radius; + if (index == tradeCount - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: radius, + ), + child: TradeCard( + key: Key("tradeCard_${trades[index].uuid}"), + trade: trades[index], + onTap: () async { + final String tradeId = trades[index].tradeId; + + final lookup = + ref.read(tradeSentFromStackLookupProvider).all; + + debugPrint("ALL: $lookup"); + + final String? txid = ref + .read(tradeSentFromStackLookupProvider) + .getTxidForTradeId(tradeId); + final List<String>? walletIds = ref + .read(tradeSentFromStackLookupProvider) + .getWalletIdsForTradeId(tradeId); + + if (txid != null && + walletIds != null && + walletIds.isNotEmpty) { + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletIds.first); + + debugPrint("name: ${manager.walletName}"); + + // TODO store tx data completely locally in isar so we don't lock up ui here when querying txData + final txData = await manager.transactionData; + + final tx = txData.getAllTransactions()[txid]; + + if (mounted) { + await showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3( + context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: tx, + walletName: manager.walletName, + walletId: walletIds.first, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, + ), + ); + } + } else { + unawaited( + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3( + context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: null, + walletName: null, + walletId: walletIds?.first, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, + ), + ), + ); + } + }, + ), + ); + }, + separatorBuilder: (context, index) { + return Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ); + }, + itemCount: tradeCount, + ), + ), + ], + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "Trades will appear here", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ); + } + } +} diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart new file mode 100644 index 000000000..9bddda3da --- /dev/null +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -0,0 +1,250 @@ +import 'dart:async'; + +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_desktop_specific/forgot_password_desktop_view.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/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.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'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; + +class DesktopLoginView extends ConsumerStatefulWidget { + const DesktopLoginView({ + Key? key, + this.startupWalletId, + this.load, + }) : super(key: key); + + static const String routeName = "/desktopLogin"; + + final String? startupWalletId; + final Future<void> Function()? load; + + @override + ConsumerState<DesktopLoginView> createState() => _DesktopLoginViewState(); +} + +class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { + late final TextEditingController passwordController; + + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + bool _continueEnabled = false; + + Future<void> login() async { + try { + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed(const Duration(seconds: 1)); + + await ref + .read(storageCryptoHandlerProvider) + .initFromExisting(passwordController.text); + + await (ref.read(secureStoreProvider).store as DesktopSecureStore).init(); + + await widget.load?.call(); + + // if no errors passphrase is correct + if (mounted) { + // pop loading indicator + Navigator.of(context).pop(); + + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + DesktopHomeView.routeName, + (route) => false, + ), + ); + } + } catch (e) { + // pop loading indicator + Navigator.of(context).pop(); + + await Future<void>.delayed(const Duration(seconds: 1)); + + await showFloatingFlushBar( + type: FlushBarType.warning, + message: e.toString(), + context: context, + ); + } + } + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopScaffold( + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 480, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 100, + ), + const SizedBox( + height: 42, + ), + Text( + "Stack Wallet", + style: STextStyles.desktopH1(context), + ), + const SizedBox( + height: 24, + ), + SizedBox( + width: 350, + child: Text( + "Open source multicoin wallet for everyone", + textAlign: TextAlign.center, + style: STextStyles.desktopSubtitleH1(context), + ), + ), + const SizedBox( + height: 24, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopLoginPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_continueEnabled) { + login(); + } + }, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _continueEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox( + height: 24, + ), + PrimaryButton( + label: "Continue", + enabled: _continueEnabled, + onPressed: login, + ), + const SizedBox( + height: 60, + ), + BlueTextButton( + text: "Forgot password?", + textSize: 20, + onTap: () { + Navigator.of(context).pushNamed( + ForgotPasswordDesktopView.routeName, + ); + }, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/forgot_password_desktop_view.dart b/lib/pages_desktop_specific/forgot_password_desktop_view.dart new file mode 100644 index 000000000..d501cbd38 --- /dev/null +++ b/lib/pages_desktop_specific/forgot_password_desktop_view.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +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/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/desktop/secondary_button.dart'; + +class ForgotPasswordDesktopView extends StatefulWidget { + const ForgotPasswordDesktopView({ + Key? key, + }) : super(key: key); + + static const String routeName = "/forgotPasswordDesktop"; + + @override + State<ForgotPasswordDesktopView> createState() => + _ForgotPasswordDesktopViewState(); +} + +class _ForgotPasswordDesktopViewState extends State<ForgotPasswordDesktopView> { + @override + Widget build(BuildContext context) { + return DesktopScaffold( + appBar: DesktopAppBar( + leading: AppBarBackButton( + onPressed: () async { + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + isCompactHeight: false, + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 480, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 100, + ), + const SizedBox( + height: 42, + ), + Text( + "Stack Wallet", + style: STextStyles.desktopH1(context), + ), + const SizedBox( + height: 24, + ), + SizedBox( + width: 400, + child: Text( + "Stack Wallet does not store your password. Create new wallet or use a Stack backup file to restore your wallet.", + textAlign: TextAlign.center, + style: STextStyles.desktopTextSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ), + const SizedBox( + height: 48, + ), + PrimaryButton( + label: "Create new wallet", + onPressed: () { + // // todo delete everything and start fresh? + }, + ), + const SizedBox( + height: 24, + ), + SecondaryButton( + label: "Restore from backup", + onPressed: () { + // todo SWB restore + }, + ), + const SizedBox( + height: kDesktopAppBarHeight, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart new file mode 100644 index 000000000..e5432081c --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -0,0 +1,438 @@ +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/contact_address_entry.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_book_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/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/address_book_card.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/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/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class DesktopAddressBook extends ConsumerStatefulWidget { + const DesktopAddressBook({Key? key}) : super(key: key); + + static const String routeName = "/desktopAddressBook"; + + @override + ConsumerState<DesktopAddressBook> createState() => _DesktopAddressBook(); +} + +class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { + late final TextEditingController _searchController; + + late final FocusNode _searchFocusNode; + + String _searchTerm = ""; + + String? currentContactId; + + Future<void> selectCryptocurrency() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DesktopDialog( + maxHeight: 609, + maxWidth: 576, + child: AddressBookFilterView(), + ); + }, + ); + } + + Future<void> newContact() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DesktopDialog( + maxHeight: 609, + maxWidth: 576, + child: AddAddressBookEntryView(), + ); + }, + ); + } + + @override + void initState() { + _searchController = TextEditingController(); + _searchFocusNode = FocusNode(); + + ref.refresh(addressBookFilterProvider); + + // if (widget.coin == null) { + List<Coin> coins = Coin.values.toList(); + coins.remove(Coin.firoTestNet); + + bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; + + if (showTestNet) { + ref.read(addressBookFilterProvider).addAll(coins, false); + } else { + ref + .read(addressBookFilterProvider) + .addAll(coins.getRange(0, coins.length - kTestNetCoinCount), false); + } + // } else { + // ref.read(addressBookFilterProvider).add(widget.coin!, false); + // } + + WidgetsBinding.instance.addPostFrameCallback((_) async { + List<ContactAddressEntry> addresses = []; + final managers = ref.read(walletsChangeNotifierProvider).managers; + for (final manager in managers) { + addresses.add( + ContactAddressEntry( + coin: manager.coin, + address: await manager.currentReceivingAddress, + label: "Current Receiving", + other: manager.walletName, + ), + ); + } + final self = Contact( + name: "My Stack", + addresses: addresses, + isFavorite: true, + id: "default", + ); + await ref.read(addressBookServiceProvider).editContact(self); + }); + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final contacts = + ref.watch(addressBookServiceProvider.select((value) => value.contacts)); + + final allContacts = contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where( + (e) => ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + .toList(); + + final favorites = contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + .where((element) => element.isFavorite) + .toList(); + + return DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + ), + Text( + "Address Book", + style: STextStyles.desktopH3(context), + ) + ], + ), + ), + body: Padding( + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + ), + child: DesktopAddressBookScaffold( + controlsLeft: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 20, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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, + ), + ), + ), + controlsRight: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SecondaryButton( + width: 184, + label: "Filter", + buttonHeight: ButtonHeight.l, + icon: SvgPicture.asset( + Assets.svg.filter, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + onPressed: selectCryptocurrency, + ), + const SizedBox( + width: 20, + ), + PrimaryButton( + width: 184, + label: "Add new", + buttonHeight: ButtonHeight.l, + icon: SvgPicture.asset( + Assets.svg.circlePlus, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + onPressed: newContact, + ), + ], + ), + filterItems: Container(), + upperLabel: favorites.isEmpty && allContacts.isEmpty + ? null + : Text( + favorites.isEmpty ? "All contacts" : "Favorites", + style: STextStyles.smallMed12(context), + ), + lowerLabel: favorites.isEmpty + ? null + : Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 12, + ), + child: Text( + "All contacts", + style: STextStyles.smallMed12(context), + ), + ), + favorites: favorites.isEmpty + ? contacts.isNotEmpty + ? null + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ) + : RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + for (int i = 0; i < favorites.length; i++) + Column( + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(4), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity( + currentContactId == favorites[i].id + ? 0.08 + : 0, + ), + child: RawMaterialButton( + onPressed: () { + setState(() { + currentContactId = favorites[i].id; + }); + }, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: AddressBookCard( + key: Key( + "favContactCard_${favorites[i].id}_key"), + contactId: favorites[i].id, + desktopSendFrom: false, + ), + ), + ), + ), + ], + ), + ], + ), + ), + all: allContacts.isEmpty + ? contacts.isNotEmpty + ? null + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ) + : Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + for (int i = 0; i < allContacts.length; i++) + Column( + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(4), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity( + currentContactId == allContacts[i].id + ? 0.08 + : 0, + ), + child: RawMaterialButton( + onPressed: () { + setState(() { + currentContactId = allContacts[i].id; + }); + }, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: AddressBookCard( + key: Key( + "favContactCard_${allContacts[i].id}_key"), + contactId: allContacts[i].id, + desktopSendFrom: false, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + details: currentContactId == null + ? Container() + : DesktopContactDetails( + contactId: currentContactId!, + ), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart new file mode 100644 index 000000000..36e44e6d4 --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart @@ -0,0 +1,112 @@ +import 'package:flutter/widgets.dart'; + +class DesktopAddressBookScaffold extends StatelessWidget { + const DesktopAddressBookScaffold({ + Key? key, + required this.controlsLeft, + required this.controlsRight, + required this.filterItems, + required this.upperLabel, + required this.lowerLabel, + required this.favorites, + required this.all, + required this.details, + }) : super(key: key); + + final Widget? controlsLeft; + final Widget? controlsRight; + final Widget? filterItems; + final Widget? upperLabel; + final Widget? lowerLabel; + final Widget? favorites; + final Widget? all; + final Widget? details; + + static const double weirdRowHeight = 30; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded( + flex: 6, + child: controlsLeft ?? Container(), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 5, + child: controlsRight ?? Container(), + ), + ], + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + Expanded( + child: filterItems ?? Container(), + ), + ], + ), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 6, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + primary: false, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: weirdRowHeight, + child: upperLabel, + ), + favorites ?? Container(), + lowerLabel ?? Container(), + all ?? Container(), + ], + ), + ), + ), + ); + }, + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 5, + child: Column( + children: [ + const SizedBox( + height: weirdRowHeight, + ), + Flexible( + child: details ?? Container(), + ), + ], + ), + ), + ], + ), + ) + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart new file mode 100644 index 000000000..4d58dc474 --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -0,0 +1,153 @@ +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:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_address_view.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.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/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'; + +class DesktopAddressCard extends StatelessWidget { + const DesktopAddressCard({ + Key? key, + required this.entry, + required this.contactId, + this.clipboard = const ClipboardWrapper(), + }) : super(key: key); + + final ContactAddressEntry entry; + final String contactId; + final ClipboardInterface clipboard; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.iconFor( + coin: entry.coin, + ), + height: 32, + width: 32, + ), + const SizedBox( + width: 16, + ), + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + "${entry.label} (${entry.coin.ticker})", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + entry.address, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + BlueTextButton( + text: "Copy", + onTap: () { + clipboard.setData( + ClipboardData(text: entry.address), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + ), + if (contactId != "default") + const SizedBox( + width: 16, + ), + if (contactId != "default") + Consumer( + builder: (context, ref, child) { + return BlueTextButton( + text: "Edit", + onTap: () async { + ref.refresh( + addressEntryDataProviderFamilyRefresher); + ref.read(addressEntryDataProvider(0)).address = + entry.address; + ref.read(addressEntryDataProvider(0)).addressLabel = + entry.label; + ref.read(addressEntryDataProvider(0)).coin = + entry.coin; + + await showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: 566, + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 8, + ), + const AppBarBackButton( + isCompact: true, + ), + Text( + "Edit address", + style: STextStyles.desktopH3(context), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 20, + left: 32, + right: 32, + bottom: 32, + ), + child: EditContactAddressView( + contactId: contactId, + addressEntry: entry, + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ], + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart new file mode 100644 index 000000000..62cd993af --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -0,0 +1,352 @@ +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/models/paymint/transactions_model.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; +import 'package:stackwallet/services/coins/manager.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/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/secondary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/transaction_card.dart'; +import 'package:tuple/tuple.dart'; + +class DesktopContactDetails extends ConsumerStatefulWidget { + const DesktopContactDetails({ + Key? key, + required this.contactId, + }) : super(key: key); + + final String contactId; + + @override + ConsumerState<DesktopContactDetails> createState() => + _DesktopContactDetailsState(); +} + +class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { + List<Tuple2<String, Transaction>> _cachedTransactions = []; + + bool _contactHasAddress(String address, Contact contact) { + for (final entry in contact.addresses) { + if (entry.address == address) { + return true; + } + } + return false; + } + + Future<List<Tuple2<String, Transaction>>> _filteredTransactionsByContact( + List<Manager> managers, + ) async { + final contact = + ref.read(addressBookServiceProvider).getContactById(widget.contactId); + + // TODO: optimise + + List<Tuple2<String, Transaction>> result = []; + for (final manager in managers) { + final transactions = (await manager.transactionData) + .getAllTransactions() + .values + .toList() + .where((e) => _contactHasAddress(e.address, contact)); + + for (final tx in transactions) { + result.add(Tuple2(manager.walletId, tx)); + } + } + // sort by date + result.sort((a, b) => b.item2.timestamp - a.item2.timestamp); + + return result; + } + + @override + Widget build(BuildContext context) { + // provider hack to prevent trying to update widget with deleted contact + Contact? _contact; + try { + _contact = ref.watch(addressBookServiceProvider + .select((value) => value.getContactById(widget.contactId))); + } catch (_) { + return Container(); + } + + final contact = _contact!; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: contact.id == "default" + ? Colors.transparent + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), + ), + child: contact.id == "default" + ? Center( + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 32, + ), + ) + : contact.emojiChar != null + ? Center( + child: Text(contact.emojiChar!), + ) + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), + ), + const SizedBox( + width: 16, + ), + Text( + contact.name, + style: STextStyles.desktopTextSmall(context), + ), + ], + ), + if (widget.contactId != "default") + SecondaryButton( + label: "Options", + width: 96, + buttonHeight: ButtonHeight.xxs, + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + builder: (context) { + return DesktopContactOptionsMenuPopup( + contactId: contact.id, + ); + }, + ); + }, + ), + ], + ), + const SizedBox( + height: 24, + ), + Flexible( + child: ListView( + primary: false, + shrinkWrap: true, + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Addresses", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + BlueTextButton( + text: "Add new", + onTap: () async { + ref.refresh( + addressEntryDataProviderFamilyRefresher); + + await showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: 566, + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 8, + ), + const AppBarBackButton( + isCompact: true, + ), + Text( + "Add new address", + style: + STextStyles.desktopH3(context), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 20, + left: 32, + right: 32, + bottom: 32, + ), + child: AddNewContactAddressView( + contactId: widget.contactId, + ), + ), + ), + ], + ), + ), + ); + }, + ), + ], + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < contact.addresses.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(18), + child: DesktopAddressCard( + entry: contact.addresses[i], + contactId: contact.id, + ), + ), + ], + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 12, + ), + child: Text( + "Transaction history", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + FutureBuilder( + future: _filteredTransactionsByContact( + ref.watch(walletsChangeNotifierProvider).managers), + builder: (_, + AsyncSnapshot<List<Tuple2<String, Transaction>>> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + _cachedTransactions = snapshot.data!; + + if (_cachedTransactions.isNotEmpty) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, + ), + ), + ], + ), + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "No transactions found", + style: STextStyles.itemSubtitle(context), + ), + ), + ); + } + } else { + // TODO: proper loading animation + if (_cachedTransactions.isEmpty) { + return const LoadingIndicator(); + } else { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, + ), + ), + ], + ), + ); + } + } + }, + ), + ], + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart new file mode 100644 index 000000000..690d1be98 --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart @@ -0,0 +1,402 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.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/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'; + +class DesktopContactOptionsMenuPopup extends ConsumerStatefulWidget { + const DesktopContactOptionsMenuPopup({Key? key, required this.contactId}) + : super(key: key); + + final String contactId; + + @override + ConsumerState<DesktopContactOptionsMenuPopup> createState() => + _DesktopContactOptionsMenuPopupState(); +} + +class _DesktopContactOptionsMenuPopupState + extends ConsumerState<DesktopContactOptionsMenuPopup> { + bool hoveredOnStar = false; + bool hoveredOnPencil = false; + bool hoveredOnTrash = false; + + void editContact() { + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 580, + maxHeight: 400, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Edit contact", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 32, + ), + child: EditContactNameEmojiView( + contactId: widget.contactId, + ), + ), + ), + ], + ), + ), + ); + } + + void attemptDeleteContact() { + final contact = + ref.read(addressBookServiceProvider).getContactById(widget.contactId); + + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 500, + maxHeight: 400, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Delete ${contact.name}?", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer( + flex: 1, + ), + Text( + "Contact will be deleted permanently!", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + buttonHeight: ButtonHeight.l, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Consumer( + builder: (context, ref, __) => PrimaryButton( + label: "Delete", + buttonHeight: ButtonHeight.l, + onPressed: () { + ref + .read(addressBookServiceProvider) + .removeContact(contact.id); + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.success, + message: "${contact.name} deleted", + context: context, + ); + }, + ), + ), + ), + ], + ) + ], + ), + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 210, + left: MediaQuery.of(context).size.width - 280, + child: Container( + width: 270, + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + boxShadow: [ + Theme.of(context).extension<StackColors>()!.standardBoxShadow, + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnStar = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnStar = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + final contact = + ref.read(addressBookServiceProvider).getContactById( + widget.contactId, + ); + ref.read(addressBookServiceProvider).editContact( + contact.copyWith( + isFavorite: !contact.isFavorite, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.star, + width: 24, + height: 22, + color: hoveredOnStar + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + ref.watch(addressBookServiceProvider.select( + (value) => value + .getContactById(widget.contactId) + .isFavorite)) + ? "Remove from favorites" + : "Add to favorites", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnPencil = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnPencil = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: editContact, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 24, + height: 22, + color: hoveredOnPencil + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Edit contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnTrash = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnTrash = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: attemptDeleteContact, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.trash, + width: 24, + height: 22, + color: hoveredOnTrash + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Delete contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 4ca78894b..3e0b9311b 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -1,10 +1,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.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/pages_desktop_specific/home/notifications/desktop_notifications_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; +import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; +import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; +import 'package:stackwallet/providers/global/notifications_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; +import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; + +final currentWalletIdProvider = StateProvider<String?>((_) => null); class DesktopHomeView extends ConsumerStatefulWidget { const DesktopHomeView({Key? key}) : super(key: key); @@ -16,55 +32,149 @@ class DesktopHomeView extends ConsumerStatefulWidget { } class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { - int currentViewIndex = 0; - final List<Widget> contentViews = [ - const Navigator( + final GlobalKey key = GlobalKey<NavigatorState>(); + late final Navigator myStackViewNav; + + @override + void initState() { + myStackViewNav = Navigator( + key: key, onGenerateRoute: RouteGenerator.generateRoute, initialRoute: MyStackView.routeName, - ), - Container( - color: Colors.green, - ), - Container( - color: Colors.red, - ), - Container( - color: Colors.orange, - ), - const Navigator( - onGenerateRoute: RouteGenerator.generateRoute, - initialRoute: SettingsMenu.routeName, - ), - Container( - color: Colors.blue, - ), - Container( - color: Colors.pink, - ), - Container( - color: Colors.purple, - ), - ]; + ); + super.initState(); + } - void onMenuSelectionChanged(int newIndex) { - setState(() { - currentViewIndex = newIndex; - }); + final Map<DesktopMenuItemId, Widget> contentViews = { + DesktopMenuItemId.myStack: Container( + // key: Key("desktopStackHomeKey"), + // onGenerateRoute: RouteGenerator.generateRoute, + // initialRoute: MyStackView.routeName, + ), + DesktopMenuItemId.exchange: const Navigator( + key: Key("desktopExchangeHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopExchangeView.routeName, + ), + DesktopMenuItemId.notifications: const Navigator( + key: Key("desktopNotificationsHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopNotificationsView.routeName, + ), + DesktopMenuItemId.addressBook: const Navigator( + key: Key("desktopAddressBookHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopAddressBook.routeName, + ), + DesktopMenuItemId.settings: const Navigator( + key: Key("desktopSettingHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopSettingsView.routeName, + ), + DesktopMenuItemId.support: const Navigator( + key: Key("desktopSupportHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopSupportView.routeName, + ), + DesktopMenuItemId.about: const Navigator( + key: Key("desktopAboutHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopAboutView.routeName, + ), + }; + + DesktopMenuItemId prev = DesktopMenuItemId.myStack; + + void onMenuSelectionWillChange(DesktopMenuItemId newKey) { + if (prev == DesktopMenuItemId.myStack && prev == newKey) { + Navigator.of(key.currentContext!) + .popUntil(ModalRoute.withName(MyStackView.routeName)); + if (ref.read(currentWalletIdProvider.state).state != null) { + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(ref.read(currentWalletIdProvider.state).state!); + if (ref.read(managerProvider).shouldAutoSync) { + ref.read(managerProvider).shouldAutoSync = false; + } + ref.read(transactionFilterProvider.state).state = null; + if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && + ref.read(prefsChangeNotifierProvider).backupFrequencyType == + BackupFrequencyType.afterClosingAWallet) { + ref.read(autoSWBServiceProvider).doBackup(); + } + ref.read(managerProvider.notifier).isActiveWallet = false; + } + } + prev = newKey; + + // check for unread notifications and refresh provider before + // showing notifications view + if (newKey == DesktopMenuItemId.notifications) { + ref.refresh(unreadNotificationsStateProvider); + } + // mark notifications as read if leaving notifications view + if (ref.read(currentDesktopMenuItemProvider.state).state == + DesktopMenuItemId.notifications && + newKey != DesktopMenuItemId.notifications) { + final Set<int> unreadNotificationIds = + ref.read(unreadNotificationsStateProvider.state).state; + + if (unreadNotificationIds.isNotEmpty) { + List<Future<void>> futures = []; + for (int i = 0; i < unreadNotificationIds.length - 1; i++) { + futures.add(ref + .read(notificationsProvider) + .markAsRead(unreadNotificationIds.elementAt(i), false)); + } + + // wait for multiple to update if any + Future.wait(futures).then((_) { + // only notify listeners once + ref + .read(notificationsProvider) + .markAsRead(unreadNotificationIds.last, true); + }); + } + } } @override Widget build(BuildContext context) { return Material( color: Theme.of(context).extension<StackColors>()!.background, - child: Row( - children: [ - DesktopMenu( - onSelectionChanged: onMenuSelectionChanged, - ), - Expanded( - child: contentViews[currentViewIndex], - ), - ], + child: Background( + child: Row( + children: [ + DesktopMenu( + // onSelectionChanged: onMenuSelectionChanged, + onSelectionWillChange: onMenuSelectionWillChange, + ), + Container( + width: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + Expanded( + child: IndexedStack( + index: ref + .watch(currentDesktopMenuItemProvider.state) + .state + .index > + 0 + ? 1 + : 0, + children: [ + myStackViewNav, + contentViews[ + ref.watch(currentDesktopMenuItemProvider.state).state]!, + ], + ), + ), + // Expanded( + // child: contentViews[ + // ref.watch(currentDesktopMenuItemProvider.state).state]!, + // ), + ], + ), ), ); } diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index b71c20f6e..fd404db94 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -1,18 +1,35 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu_item.dart'; +import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; +import 'package:stackwallet/providers/providers.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/widgets/desktop/living_stack_icon.dart'; + +enum DesktopMenuItemId { + myStack, + exchange, + notifications, + addressBook, + settings, + support, + about, +} class DesktopMenu extends ConsumerStatefulWidget { const DesktopMenu({ Key? key, - required this.onSelectionChanged, + this.onSelectionChanged, + this.onSelectionWillChange, }) : super(key: key); - final void Function(int)? onSelectionChanged; + final void Function(DesktopMenuItemId)? onSelectionChanged; + final void Function(DesktopMenuItemId)? onSelectionWillChange; @override ConsumerState<DesktopMenu> createState() => _DesktopMenuState(); @@ -22,184 +39,316 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { static const expandedWidth = 225.0; static const minimizedWidth = 72.0; - double _width = expandedWidth; - int selectedMenuItem = 0; + final Duration duration = const Duration(milliseconds: 250); + late final List<DMIController> controllers; - void updateSelectedMenuItem(int index) { - setState(() { - selectedMenuItem = index; - }); - widget.onSelectionChanged?.call(index); + double _width = expandedWidth; + + void updateSelectedMenuItem(DesktopMenuItemId idKey) { + widget.onSelectionWillChange?.call(idKey); + + ref.read(currentDesktopMenuItemProvider.state).state = idKey; + + widget.onSelectionChanged?.call(idKey); } void toggleMinimize() { + final expanded = _width == expandedWidth; + + for (var e in controllers) { + e.toggle?.call(); + } + setState(() { - _width = _width == expandedWidth ? minimizedWidth : expandedWidth; + _width = expanded ? minimizedWidth : expandedWidth; }); } + @override + void initState() { + controllers = [ + DMIController(), + DMIController(), + DMIController(), + DMIController(), + DMIController(), + DMIController(), + DMIController(), + DMIController(), + ]; + + super.initState(); + } + + @override + void dispose() { + for (var e in controllers) { + e.dispose(); + } + super.dispose(); + } + @override Widget build(BuildContext context) { return Material( color: Theme.of(context).extension<StackColors>()!.popupBG, - child: SizedBox( + child: AnimatedContainer( width: _width, + duration: duration, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox( - height: _width == expandedWidth ? 22 : 25, + const SizedBox( + height: 25, ), - SizedBox( + AnimatedContainer( + duration: duration, width: _width == expandedWidth ? 70 : 32, - height: _width == expandedWidth ? 70 : 32, - child: SvgPicture.asset( - Assets.svg.stackIcon(context), + child: LivingStackIcon( + onPressed: toggleMinimize, ), ), const SizedBox( height: 10, ), - Text( - _width == expandedWidth ? "Stack Wallet" : "", - style: STextStyles.desktopH2(context).copyWith( - fontSize: 18, - height: 23.4 / 18, + AnimatedOpacity( + duration: duration, + opacity: _width == expandedWidth ? 1 : 0, + child: SizedBox( + height: 28, + child: Text( + "Stack Wallet", + style: STextStyles.desktopH2(context).copyWith( + fontSize: 18, + height: 23.4 / 18, + ), + ), ), ), 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: AnimatedContainer( + duration: duration, + width: _width == expandedWidth + ? _width - 32 // 16 padding on either side + : _width - 16, // 8 padding on either side + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DesktopMenuItem( + duration: duration, + icon: SvgPicture.asset( + Assets.svg.walletDesktop, + width: 20, + height: 20, + color: DesktopMenuItemId.myStack == + ref + .watch(currentDesktopMenuItemProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "My Stack", + value: DesktopMenuItemId.myStack, + onChanged: updateSelectedMenuItem, + controller: controllers[0], ), - 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( + duration: duration, + icon: SvgPicture.asset( + Assets.svg.exchangeDesktop, + width: 20, + height: 20, + color: DesktopMenuItemId.exchange == + ref + .watch(currentDesktopMenuItemProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "Exchange", + value: DesktopMenuItemId.exchange, + onChanged: updateSelectedMenuItem, + controller: controllers[1], ), - 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( + duration: duration, + icon: SvgPicture.asset( + ref.watch(notificationsProvider.select( + (value) => value.hasUnreadNotifications)) + ? Assets.svg.bellNew(context) + : Assets.svg.bell, + width: 20, + height: 20, + color: ref.watch(notificationsProvider.select( + (value) => value.hasUnreadNotifications)) + ? null + : DesktopMenuItemId.notifications == + ref + .watch(currentDesktopMenuItemProvider + .state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "Notifications", + value: DesktopMenuItemId.notifications, + onChanged: updateSelectedMenuItem, + controller: controllers[2], ), - 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( + duration: duration, + icon: SvgPicture.asset( + Assets.svg.addressBookDesktop, + width: 20, + height: 20, + color: DesktopMenuItemId.addressBook == + ref + .watch(currentDesktopMenuItemProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "Address Book", + value: DesktopMenuItemId.addressBook, + onChanged: updateSelectedMenuItem, + controller: controllers[3], ), - 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( + duration: duration, + icon: SvgPicture.asset( + Assets.svg.gear, + width: 20, + height: 20, + color: DesktopMenuItemId.settings == + ref + .watch(currentDesktopMenuItemProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "Settings", + value: DesktopMenuItemId.settings, + onChanged: updateSelectedMenuItem, + controller: controllers[4], + ), + const SizedBox( + height: 2, + ), + DesktopMenuItem( + duration: duration, + icon: SvgPicture.asset( + Assets.svg.messageQuestion, + width: 20, + height: 20, + color: DesktopMenuItemId.support == + ref + .watch(currentDesktopMenuItemProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "Support", + value: DesktopMenuItemId.support, + onChanged: updateSelectedMenuItem, + controller: controllers[5], + ), + const SizedBox( + height: 2, + ), + DesktopMenuItem( + duration: duration, + icon: SvgPicture.asset( + Assets.svg.aboutDesktop, + width: 20, + height: 20, + color: DesktopMenuItemId.about == + ref + .watch(currentDesktopMenuItemProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "About", + value: DesktopMenuItemId.about, + onChanged: updateSelectedMenuItem, + controller: controllers[6], + ), + const Spacer(), + DesktopMenuItem( + duration: duration, + labelLength: 123, + icon: SvgPicture.asset( + Assets.svg.exitDesktop, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "Exit", + value: 7, + onChanged: (_) { + // todo: save stuff/ notify before exit? + exit(0); + }, + controller: controllers[7], + ), + ], + ), ), ), - const Spacer(), Row( - mainAxisAlignment: MainAxisAlignment.end, children: [ const Spacer(), IconButton( @@ -212,7 +361,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), ), ], - ) + ), ], ), ), diff --git a/lib/pages_desktop_specific/home/desktop_menu_item.dart b/lib/pages_desktop_specific/home/desktop_menu_item.dart index 76d945e2d..78dcde79b 100644 --- a/lib/pages_desktop_specific/home/desktop_menu_item.dart +++ b/lib/pages_desktop_specific/home/desktop_menu_item.dart @@ -1,27 +1,96 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -class DesktopMenuItem<T> extends StatelessWidget { +class DMIController { + VoidCallback? toggle; + void dispose() { + toggle = null; + } +} + +class DesktopMenuItem<T> extends ConsumerStatefulWidget { const DesktopMenuItem({ Key? key, required this.icon, required this.label, required this.value, - required this.group, required this.onChanged, - required this.iconOnly, + required this.duration, + this.labelLength = 125, + this.controller, }) : super(key: key); final Widget icon; final String label; final T value; - final T group; final void Function(T) onChanged; - final bool iconOnly; + final Duration duration; + final double labelLength; + final DMIController? controller; + + @override + ConsumerState<DesktopMenuItem<T>> createState() => _DesktopMenuItemState<T>(); +} + +class _DesktopMenuItemState<T> extends ConsumerState<DesktopMenuItem<T>> + with SingleTickerProviderStateMixin { + late final Widget icon; + late final String label; + late final T value; + late final void Function(T) onChanged; + late final Duration duration; + late final double labelLength; + + late final DMIController? controller; + + late final AnimationController animationController; + + bool _iconOnly = false; + + void toggle() { + setState(() { + _iconOnly = !_iconOnly; + }); + if (_iconOnly) { + animationController.reverse(); + } else { + animationController.forward(); + } + } + + @override + void initState() { + icon = widget.icon; + label = widget.label; + value = widget.value; + onChanged = widget.onChanged; + duration = widget.duration; + labelLength = widget.labelLength; + controller = widget.controller; + + controller?.toggle = toggle; + animationController = AnimationController( + vsync: this, + duration: duration, + )..forward(); + + super.initState(); + } + + @override + void dispose() { + controller?.dispose(); + animationController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { + final group = ref.watch(currentDesktopMenuItemProvider.state).state; + debugPrint("============ value:$value ============ group:$group"); return TextButton( style: value == group ? Theme.of(context) @@ -34,26 +103,42 @@ class DesktopMenuItem<T> extends StatelessWidget { onChanged(value); }, child: Padding( - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( vertical: 16, - horizontal: iconOnly ? 0 : 16, ), child: Row( - mainAxisAlignment: - iconOnly ? MainAxisAlignment.center : MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ + AnimatedContainer( + duration: duration, + width: _iconOnly ? 0 : 16, + ), icon, - if (!iconOnly) - const SizedBox( - width: 12, - ), - if (!iconOnly) - Text( - label, - style: value == group - ? STextStyles.desktopMenuItemSelected(context) - : STextStyles.desktopMenuItem(context), + AnimatedOpacity( + duration: duration, + opacity: _iconOnly ? 0 : 1.0, + child: SizeTransition( + sizeFactor: animationController, + axis: Axis.horizontal, + axisAlignment: -1, + child: SizedBox( + width: labelLength, + child: Row( + children: [ + const SizedBox( + width: 12, + ), + Text( + label, + style: value == group + ? STextStyles.desktopMenuItemSelected(context) + : STextStyles.desktopMenuItem(context), + ), + ], + ), + ), ), + ) ], ), ), diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index bfe9272f2..63e1ee613 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -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, diff --git a/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart index 64e4a23d3..1edb93e06 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.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/rounded_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; class CoinWalletsTable extends ConsumerWidget { @@ -23,8 +26,10 @@ class CoinWalletsTable extends ConsumerWidget { ), child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, + // horizontal: 20, + // vertical: 16, + horizontal: 6, + vertical: 6, ), child: Column( children: [ @@ -35,8 +40,29 @@ class CoinWalletsTable extends ConsumerWidget { const SizedBox( height: 32, ), - WalletInfoRow( - walletId: walletIds[i], + Stack( + children: [ + WalletInfoRow( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, + ), + walletId: walletIds[i], + ), + Positioned.fill( + child: WalletRowHoverOverlay( + onPressed: () async { + ref.read(currentWalletIdProvider.state).state = + walletIds[i]; + + await Navigator.of(context).pushNamed( + DesktopWalletView.routeName, + arguments: walletIds[i], + ); + }, + ), + ), + ], ), ], ), @@ -46,3 +72,55 @@ class CoinWalletsTable extends ConsumerWidget { ); } } + +class WalletRowHoverOverlay extends StatefulWidget { + const WalletRowHoverOverlay({ + Key? key, + required this.onPressed, + }) : super(key: key); + + final VoidCallback onPressed; + + @override + State<WalletRowHoverOverlay> createState() => _WalletRowHoverOverlayState(); +} + +class _WalletRowHoverOverlayState extends State<WalletRowHoverOverlay> { + late final VoidCallback onPressed; + + bool _hovering = false; + + @override + void initState() { + onPressed = widget.onPressed; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: GestureDetector( + onTap: onPressed, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: _hovering ? 0.1 : 0, + child: RoundedContainer( + color: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + ), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart b/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart index 6b60902c4..6710c23a4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_wallets import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; class MyStackView extends ConsumerStatefulWidget { @@ -23,36 +24,38 @@ class _MyStackViewState extends ConsumerState<MyStackView> { debugPrint("BUILD: $runtimeType"); final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets; - return Column( - children: [ - DesktopAppBar( - isCompactHeight: true, - leading: Row( - children: [ - const SizedBox( - width: 24, - ), - SizedBox( - width: 32, - height: 32, - child: SvgPicture.asset( - Assets.svg.stackIcon(context), + return Background( + child: Column( + children: [ + DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, ), - ), - const SizedBox( - width: 12, - ), - Text( - "My Stack", - style: STextStyles.desktopH3(context), - ) - ], + SizedBox( + width: 32, + height: 32, + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + ), + ), + const SizedBox( + width: 12, + ), + Text( + "My Stack", + style: STextStyles.desktopH3(context), + ) + ], + ), ), - ), - Expanded( - child: hasWallets ? const MyWallets() : const EmptyWallets(), - ), - ], + Expanded( + child: hasWallets ? const MyWallets() : const EmptyWallets(), + ), + ], + ), ); } } diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart new file mode 100644 index 000000000..d870835f1 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -0,0 +1,579 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +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/delete_wallet_button.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/coins/firo/firo_wallet.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/custom_loading_overlay.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/primary_button.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(transactionFilterProvider.state).state = null; + if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && + ref.read(prefsChangeNotifierProvider).backupFrequencyType == + BackupFrequencyType.afterClosingAWallet) { + unawaited(ref.read(autoSWBServiceProvider).doBackup()); + } + ref.read(managerProvider.notifier).isActiveWallet = false; + } + + 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, + ), + ), + ); + } + } + } + + Future<void> attemptAnonymize() async { + final managerProvider = + ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); + + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Anonymizing balance", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), + ), + ); + final firoWallet = ref.read(managerProvider).wallet as FiroWallet; + + final publicBalance = await firoWallet.availablePublicBalance(); + if (publicBalance <= Decimal.zero) { + shouldPop = true; + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "No funds available to anonymize!", + context: context, + ), + ); + } + return; + } + + try { + await firoWallet.anonymizeAllPublicFunds(); + shouldPop = true; + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Anonymize transaction submitted", + context: context, + ), + ); + } + } catch (e) { + shouldPop = true; + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + await showDialog<dynamic>( + context: context, + builder: (_) => DesktopDialog( + maxWidth: 400, + maxHeight: 300, + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Anonymize all failed", + style: STextStyles.desktopH3(context), + ), + const Spacer( + flex: 1, + ), + Text( + "Reason: $e", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: + Navigator.of(context, rootNavigator: true).pop, + ), + ), + ], + ) + ], + ), + ), + ), + ); + } + } + } + + @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: 2, + ), + WalletKeysButton( + walletId: walletId, + ), + const SizedBox( + width: 2, + ), + DeleteWalletButton( + walletId: walletId, + ), + const SizedBox( + width: 12, + ), + ], + ), + ], + ), + ), + 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(), + if (coin == Coin.firo) const SizedBox(width: 10), + if (coin == Coin.firo) + SecondaryButton( + width: 180, + buttonHeight: ButtonHeight.l, + label: "Anonymize funds", + onPressed: () async { + await showDialog<void>( + context: context, + barrierDismissible: false, + builder: (context) => DesktopDialog( + maxWidth: 500, + maxHeight: 210, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, vertical: 20), + child: Column( + children: [ + Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 16), + Text( + "You're about to anonymize all of your public funds.", + style: + STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(); + + unawaited(attemptAnonymize()); + }, + ) + ], + ), + ], + ), + ), + ), + ); + }, + ), + // if (coin == Coin.firo) const SizedBox(width: 16), + // SecondaryButton( + // width: 180, + // buttonHeight: ButtonHeight.l, + // 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, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart new file mode 100644 index 000000000..92dd9f6fc --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart @@ -0,0 +1,267 @@ +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); + + final totalLength = favorites.length + + contacts.length + + 2; // +2 for "fav" and "all" headers + + return ListView.separated( + primary: false, + shrinkWrap: true, + itemCount: totalLength, + separatorBuilder: (context, index) { + return const SizedBox( + height: 10, + ); + }, + 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 + 1) { + 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 - 2].id; + return ContactListItem( + key: Key("contactContactListItem_${id}_key"), + contactId: id, + filterByCoin: widget.coin, + ); + } + }, + ); + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart new file mode 100644 index 000000000..d7bfefb1f --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart @@ -0,0 +1,149 @@ +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, + ), + ), + 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: [ + Flexible( + child: Row( + children: [ + WalletInfoCoinIcon(coin: e.coin), + const SizedBox( + width: 12, + ), + Flexible( + child: 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, + ), + ), + Row( + children: [ + Flexible( + child: Text( + e.address, + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + BlueTextButton( + text: "Select wallet", + onTap: () { + Navigator.of(context).pop(e); + }, + ), + ], + ), + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart new file mode 100644 index 000000000..a2071e7d6 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -0,0 +1,164 @@ +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/desktop_delete_wallet_dialog.dart'; +import 'package:stackwallet/route_generator.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'; + +class DeleteWalletButton extends ConsumerStatefulWidget { + const DeleteWalletButton({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<DeleteWalletButton> createState() => _DeleteWalletButton(); +} + +class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { + late final String walletId; + + @override + void initState() { + walletId = widget.walletId; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: () async { + final shouldOpenDeleteDialog = await showDialog<bool?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) { + return DeletePopupButton( + onTap: () async { + Navigator.of(context).pop(true); + }, + ); + }, + ); + + if (shouldOpenDeleteDialog == true) { + final result = await showDialog<bool?>( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: DesktopDeleteWalletDialog.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: DesktopDeleteWalletDialog.routeName, + arguments: walletId, + ), + ), + ]; + }, + ), + ); + + if (result == true) { + if (mounted) { + Navigator.of(context).pop(); + } + } + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 19, + horizontal: 32, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.ellipsis, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ], + ), + ), + ); + } +} + +class DeletePopupButton extends StatefulWidget { + const DeletePopupButton({ + Key? key, + this.onTap, + }) : super(key: key); + + final VoidCallback? onTap; + + @override + State<DeletePopupButton> createState() => _DeletePopupButtonState(); +} + +class _DeletePopupButtonState extends State<DeletePopupButton> { + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 24, + left: MediaQuery.of(context).size.width - 234, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: widget.onTap, + child: Container( + width: 210, + height: 70, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + color: Theme.of(context).extension<StackColors>()!.popupBG, + boxShadow: [ + Theme.of(context) + .extension<StackColors>()! + .standardBoxShadow, + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 24), + SvgPicture.asset( + Assets.svg.trash, + ), + const SizedBox(width: 14), + Text( + "Delete wallet", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + ), + ], + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart new file mode 100644 index 000000000..a2d58465b --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -0,0 +1,257 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/global/wallets_service_provider.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.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'; + +class DeleteWalletKeysPopup extends ConsumerStatefulWidget { + const DeleteWalletKeysPopup({ + Key? key, + required this.walletId, + required this.words, + this.clipboardInterface = const ClipboardWrapper(), + }) : super(key: key); + + final String walletId; + final List<String> words; + final ClipboardInterface clipboardInterface; + + static const String routeName = "/desktopDeleteWalletKeysPopup"; + + @override + ConsumerState<DeleteWalletKeysPopup> createState() => + _DeleteWalletKeysPopup(); +} + +class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { + late final String _walletId; + late final List<String> _words; + late final ClipboardInterface _clipboardInterface; + + @override + void initState() { + _walletId = widget.walletId; + _words = widget.words; + _clipboardInterface = widget.clipboardInterface; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Wallet keys", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ], + ), + const SizedBox( + height: 28, + ), + Text( + "Recovery phrase", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Text( + "Please write down your recovery phrase in the correct order and " + "save it to keep your funds secure. You will be shown your recovery phrase on the next screen.", + style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: RawMaterialButton( + hoverColor: Colors.transparent, + onPressed: () async { + await _clipboardInterface.setData( + ClipboardData(text: _words.join(" ")), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + }, + child: MnemonicTable( + words: widget.words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: PrimaryButton( + label: "Continue", + onPressed: () async { + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (context) { + return ConfirmDelete( + walletId: _walletId, + ); + }, + settings: const RouteSettings( + name: "/desktopConfirmDelete", + ), + ), + ); + }, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} + +class ConfirmDelete extends ConsumerStatefulWidget { + const ConfirmDelete({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<ConfirmDelete> createState() => _ConfirmDeleteState(); +} + +class _ConfirmDeleteState extends ConsumerState<ConfirmDelete> { + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: 350, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Thanks! " + "\n\nYour wallet will be deleted.", + style: STextStyles.desktopH2(context), + textAlign: TextAlign.center, + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Continue", + onPressed: () async { + final walletsInstance = + ref.read(walletsChangeNotifierProvider); + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId); + + final _managerWalletId = manager.walletId; + // + await ref + .read(walletsServiceChangeNotifierProvider) + .deleteWallet(manager.walletName, true); + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(true); + } + + // wait for widget tree to dispose of any widgets watching the manager + await Future<void>.delayed(const Duration(seconds: 1)); + walletsInstance.removeWallet(walletId: _managerWalletId); + }, + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart new file mode 100644 index 000000000..a614be3a6 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; +import 'package:stackwallet/providers/global/wallets_provider.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/rounded_container.dart'; +import 'package:tuple/tuple.dart'; + +class DesktopAttentionDeleteWallet extends ConsumerStatefulWidget { + const DesktopAttentionDeleteWallet({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + static const String routeName = "/desktopAttentionDeleteWallet"; + + @override + ConsumerState<DesktopAttentionDeleteWallet> createState() => + _DesktopAttentionDeleteWallet(); +} + +class _DesktopAttentionDeleteWallet + extends ConsumerState<DesktopAttentionDeleteWallet> { + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 610, + maxHeight: 530, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackError, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " + "the only way you can have access to your funds is by using your backup key." + "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." + "\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "View Backup Key", + onPressed: () async { + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + await Navigator.of(context).pushNamed( + DeleteWalletKeysPopup.routeName, + arguments: Tuple2( + widget.walletId, + words, + ), + ); + } + }, + ), + ], + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart new file mode 100644 index 000000000..20bb3f95a --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart @@ -0,0 +1,205 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.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/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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'; + +class DesktopAuthSend extends ConsumerStatefulWidget { + const DesktopAuthSend({ + Key? key, + required this.coin, + }) : super(key: key); + + final Coin coin; + + @override + ConsumerState<DesktopAuthSend> createState() => _DesktopAuthSendState(); +} + +class _DesktopAuthSendState extends ConsumerState<DesktopAuthSend> { + late final TextEditingController passwordController; + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + + bool _confirmEnabled = false; + + Future<bool> verifyPassphrase() async { + return await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + } + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.keys, + width: 100, + ), + const SizedBox( + height: 56, + ), + Text( + "Confirm transaction", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 16, + ), + Text( + "Enter your wallet password to send ${widget.coin.ticker.toUpperCase()}", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + ), + const SizedBox( + height: 24, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopLoginPasswordFieldKey"), + 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: [ + const SizedBox( + width: 24, + ), + 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: 24, + height: 24, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _confirmEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox( + height: 48, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + enabled: _confirmEnabled, + label: "Confirm", + buttonHeight: ButtonHeight.l, + onPressed: () async { + unawaited( + showDialog<void>( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed(const Duration(seconds: 1)); + + final passwordIsValid = await verifyPassphrase(); + + if (mounted) { + Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: true, + ).pop(passwordIsValid); + await Future<void>.delayed(const Duration( + milliseconds: 100, + )); + } + }, + ), + ), + ], + ) + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart new file mode 100644 index 000000000..9c890b223 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class DesktopBalanceToggleButton extends ConsumerWidget { + const DesktopBalanceToggleButton({ + Key? key, + this.onPressed, + }) : super(key: key); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + height: 22, + width: 22, + child: MaterialButton( + color: Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + splashColor: Theme.of(context).extension<StackColors>()!.highlight, + onPressed: () { + if (ref.read(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + } else { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + } + onPressed?.call(); + }, + elevation: 0, + highlightElevation: 0, + hoverElevation: 0, + padding: EdgeInsets.zero, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Center( + child: Image( + image: AssetImage( + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available + ? Assets.png.glassesHidden + : Assets.png.glasses, + ), + width: 16, + ), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart new file mode 100644 index 000000000..e8d9c2dc5 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart @@ -0,0 +1,256 @@ +import 'dart:async'; + +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_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.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/loading_indicator.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; + +class DesktopDeleteWalletDialog extends ConsumerStatefulWidget { + const DesktopDeleteWalletDialog({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + static const String routeName = "/desktopDeleteWalletDialog"; + + @override + ConsumerState<DesktopDeleteWalletDialog> createState() => + _DesktopDeleteWalletDialog(); +} + +class _DesktopDeleteWalletDialog + extends ConsumerState<DesktopDeleteWalletDialog> { + late final TextEditingController passwordController; + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + bool _continueEnabled = false; + + Future<void> enterPassphrase() async { + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed(const Duration(seconds: 1)); + + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (verified) { + Navigator.of(context, rootNavigator: true).pop(); + + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + Navigator.of(context).pop(); + + unawaited( + Navigator.of(context).pushNamed( + DesktopAttentionDeleteWallet.routeName, + arguments: widget.walletId, + ), + ); + } + } else { + Navigator.of(context, rootNavigator: true).pop(); + + await Future<void>.delayed(const Duration(milliseconds: 300)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase!", + context: context, + ), + ); + } + } + + @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: 580, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + const SizedBox(height: 16), + Text( + "Delete wallet", + 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), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopDeleteWalletPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_continueEnabled) { + enterPassphrase(); + } + }, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + GestureDetector( + key: const Key( + "desktopDeleteWalletShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _continueEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + enabled: _continueEnabled, + label: "Continue", + onPressed: _continueEnabled + ? () async { + // add loading indicator + enterPassphrase(); + } + : null, + ), + ], + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart new file mode 100644 index 000000000..25e1f47ab --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart @@ -0,0 +1,379 @@ +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:decimal/decimal.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/models.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.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/fee_rate_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/widgets/animated_text.dart'; + +class DesktopFeeDropDown extends ConsumerStatefulWidget { + const DesktopFeeDropDown({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<DesktopFeeDropDown> createState() => _DesktopFeeDropDownState(); +} + +class _DesktopFeeDropDownState extends ConsumerState<DesktopFeeDropDown> { + late final String walletId; + + FeeObject? feeObject; + FeeRateType feeRateType = FeeRateType.average; + + final stringsToLoopThrough = [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ]; + + Future<Decimal> feeFor({ + required int amount, + required FeeRateType feeRateType, + required int feeRate, + required Coin coin, + }) async { + switch (feeRateType) { + case FeeRateType.fast: + if (ref.read(feeSheetSessionCacheProvider).fast[amount] == null) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.fast.raw!); + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); + } else { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate), + coin: coin); + } + } + return ref.read(feeSheetSessionCacheProvider).fast[amount]!; + + case FeeRateType.average: + if (ref.read(feeSheetSessionCacheProvider).average[amount] == null) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.regular.raw!); + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); + } else { + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate), + coin: coin); + } + } + return ref.read(feeSheetSessionCacheProvider).average[amount]!; + + case FeeRateType.slow: + if (ref.read(feeSheetSessionCacheProvider).slow[amount] == null) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.slow.raw!); + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); + } else { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate), + coin: coin); + } + } + return ref.read(feeSheetSessionCacheProvider).slow[amount]!; + } + } + + @override + void initState() { + walletId = widget.walletId; + super.initState(); + } + + String? labelSlow; + String? labelAverage; + String? labelFast; + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + + return FutureBuilder( + future: manager.fees, + builder: (context, AsyncSnapshot<FeeObject> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + feeObject = snapshot.data!; + } + return DropdownButtonHideUnderline( + child: DropdownButton2( + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: ref.watch(feeRateTypeStateProvider.state).state, + // selectedItemBuilder: (s) { + // return [ + // ...FeeRateType.values.map( + // (e) => DropdownMenuItem( + // value: e, + // child: FeeDropDownChild( + // feeObject: feeObject, + // feeRateType: e, + // walletId: walletId, + // amount: amount, + // feeFor: feeFor, + // isSelected: true, + // ), + // ), + // ), + // ]; + // }, + items: [ + ...FeeRateType.values.map( + (e) => DropdownMenuItem( + value: e, + child: FeeDropDownChild( + feeObject: feeObject, + feeRateType: e, + walletId: walletId, + feeFor: feeFor, + isSelected: false, + ), + ), + ), + ], + onChanged: (newRateType) { + if (newRateType is FeeRateType) { + ref.read(feeRateTypeStateProvider.state).state = newRateType; + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ); + }); + } +} + +final sendAmountProvider = + StateProvider.autoDispose<Decimal>((_) => Decimal.zero); + +class FeeDropDownChild extends ConsumerWidget { + const FeeDropDownChild({ + Key? key, + required this.feeObject, + required this.feeRateType, + required this.walletId, + required this.feeFor, + required this.isSelected, + }) : super(key: key); + + final FeeObject? feeObject; + final FeeRateType feeRateType; + final String walletId; + final Future<Decimal> Function({ + required int amount, + required FeeRateType feeRateType, + required int feeRate, + required Coin coin, + }) feeFor; + final bool isSelected; + + static const stringsToLoopThrough = [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ]; + + String estimatedTimeToBeIncludedInNextBlock( + int targetBlockTime, int estimatedNumberOfBlocks) { + int time = targetBlockTime * estimatedNumberOfBlocks; + + int hours = (time / 3600).floor(); + if (hours > 1) { + return "~$hours hours"; + } else if (hours == 1) { + return "~$hours hour"; + } + + // less than an hour + + final string = (time / 60).toStringAsFixed(1); + + if (string == "1.0") { + return "~1 minute"; + } else { + if (string.endsWith(".0")) { + return "~${(time / 60).floor()} minutes"; + } + return "~$string minutes"; + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + debugPrint("BUILD: $runtimeType : $feeRateType"); + + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + + if (feeObject == null) { + return AnimatedText( + stringsToLoopThrough: stringsToLoopThrough, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textFieldActiveText, + ), + ); + } else { + return FutureBuilder( + future: feeFor( + coin: manager.coin, + feeRateType: feeRateType, + feeRate: feeRateType == FeeRateType.fast + ? feeObject!.fast + : feeRateType == FeeRateType.slow + ? feeObject!.slow + : feeObject!.medium, + amount: Format.decimalAmountToSatoshis( + ref.watch(sendAmountProvider.state).state, + manager.coin, + ), + ), + builder: (_, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${feeRateType.prettyName} (~${snapshot.data!} ${manager.coin.ticker})", + style: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + ), + textAlign: TextAlign.left, + ), + if (feeObject != null) + Text( + estimatedTimeToBeIncludedInNextBlock( + Constants.targetBlockTimeInSeconds(manager.coin), + feeRateType == FeeRateType.fast + ? feeObject!.numberOfBlocksFast + : feeRateType == FeeRateType.slow + ? feeObject!.numberOfBlocksSlow + : feeObject!.numberOfBlocksAverage, + ), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + ), + ], + ); + } else { + return AnimatedText( + stringsToLoopThrough: stringsToLoopThrough, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + ), + ); + } + }, + ); + } + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart new file mode 100644 index 000000000..1dd2e607e --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -0,0 +1,316 @@ +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: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: 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( + buttonHeight: ButtonHeight.l, + 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, + ), + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart new file mode 100644 index 000000000..72690c7e5 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -0,0 +1,1400 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +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:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/models/send_view_auto_fill_data.dart'; +import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; +import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/barcode_scanner_interface.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/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.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/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:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.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 DesktopSend extends ConsumerStatefulWidget { + const DesktopSend({ + Key? key, + required this.walletId, + this.autoFillData, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + }) : super(key: key); + + final String walletId; + final SendViewAutoFillData? autoFillData; + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + + @override + ConsumerState<DesktopSend> createState() => _DesktopSendState(); +} + +class _DesktopSendState extends ConsumerState<DesktopSend> { + late final String walletId; + late final Coin coin; + late final ClipboardInterface clipboard; + late final BarcodeScannerInterface scanner; + + late TextEditingController sendToController; + late TextEditingController cryptoAmountController; + late TextEditingController baseAmountController; + late TextEditingController noteController; + // late TextEditingController feeController; + + late final SendViewAutoFillData? _data; + + final _addressFocusNode = FocusNode(); + final _noteFocusNode = FocusNode(); + final _cryptoFocus = FocusNode(); + final _baseFocus = FocusNode(); + + Decimal? _amountToSend; + Decimal? _cachedAmountToSend; + String? _address; + + String? _privateBalanceString; + String? _publicBalanceString; + + bool _addressToggleFlag = false; + + bool _cryptoAmountChangeLock = false; + late VoidCallback onCryptoAmountChanged; + + Future<void> previewSend() async { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + // TODO: remove the need for this!! + final bool isOwnAddress = await manager.isOwnAddress(_address!); + if (isOwnAddress) { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction failed", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Text( + "Sending to self is currently disabled", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ), + ); + }, + ); + return; + } + + final amount = Format.decimalAmountToSatoshis(_amountToSend!, coin); + int availableBalance; + if ((coin == Coin.firo || coin == Coin.firoTestNet)) { + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + availableBalance = Format.decimalAmountToSatoshis( + await (manager.wallet as FiroWallet).availablePrivateBalance(), + coin); + } else { + availableBalance = Format.decimalAmountToSatoshis( + await (manager.wallet as FiroWallet).availablePublicBalance(), + coin); + } + } else { + availableBalance = + Format.decimalAmountToSatoshis(await manager.availableBalance, coin); + } + + // confirm send all + if (amount == availableBalance) { + final bool? shouldSendAll = await showDialog<bool>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Confirm send all", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Text( + "You are about to send your entire balance. Would you like to continue?", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Yes", + onPressed: () { + Navigator.of(context).pop(true); + + setState(() { + sendToController.text = ""; + cryptoAmountController.text = ""; + baseAmountController.text = ""; + noteController.text = ""; + }); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + + if (shouldSendAll == null || shouldSendAll == false) { + // cancel preview + return; + } + } + + try { + bool wasCancelled = false; + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ), + ), + ); + }, + )); + + Map<String, dynamic> txData; + + if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + txData = await (manager.wallet as FiroWallet).prepareSendPublic( + address: _address!, + satoshiAmount: amount, + args: {"feeRate": ref.read(feeRateTypeStateProvider)}, + ); + } else { + txData = await manager.prepareSend( + address: _address!, + satoshiAmount: amount, + args: {"feeRate": ref.read(feeRateTypeStateProvider)}, + ); + } + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of( + context, + rootNavigator: true, + ).pop(); + txData["note"] = noteController.text; + txData["address"] = _address; + + unawaited( + showDialog( + context: context, + builder: (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + routeOnSuccessName: DesktopHomeView.routeName, + ), + ), + ), + ); + } + } catch (e) { + if (mounted) { + // pop building dialog + Navigator.of( + context, + rootNavigator: true, + ).pop(); + + unawaited( + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction failed", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Text( + e.toString(), + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Yes", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + } + } + + void _cryptoAmountChanged() async { + if (!_cryptoAmountChangeLock) { + final String cryptoAmount = cryptoAmountController.text; + if (cryptoAmount.isNotEmpty && + cryptoAmount != "." && + cryptoAmount != ",") { + _amountToSend = cryptoAmount.contains(",") + ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) + : Decimal.parse(cryptoAmount); + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + + if (price > Decimal.zero) { + final String fiatAmountString = Format.localizedStringAsFixed( + value: _amountToSend! * price, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: 2, + ); + + baseAmountController.text = fiatAmountString; + } + } else { + _amountToSend = null; + baseAmountController.text = ""; + } + + _updatePreviewButtonState(_address, _amountToSend); + } + } + + String? _updateInvalidAddressText(String address, Manager manager) { + if (_data != null && _data!.contactLabel == address) { + return null; + } + if (address.isNotEmpty && !manager.validateAddress(address)) { + return "Invalid address"; + } + return null; + } + + void _updatePreviewButtonState(String? address, Decimal? amount) { + final isValidAddress = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(address ?? ""); + ref.read(previewTxButtonStateProvider.state).state = + (isValidAddress && amount != null && amount > Decimal.zero); + } + + // late Future<String> _calculateFeesFuture; + + // Map<int, String> cachedFees = {}; + // Map<int, String> cachedFiroPrivateFees = {}; + // Map<int, String> cachedFiroPublicFees = {}; + + // Future<String> calculateFees(int amount) async { + // if (amount <= 0) { + // return "0"; + // } + // + // if (coin == Coin.firo || coin == Coin.firoTestNet) { + // if (ref.read(publicPrivateBalanceStateProvider.state).state == + // "Private") { + // if (cachedFiroPrivateFees[amount] != null) { + // return cachedFiroPrivateFees[amount]!; + // } + // } else { + // if (cachedFiroPublicFees[amount] != null) { + // return cachedFiroPublicFees[amount]!; + // } + // } + // } else if (cachedFees[amount] != null) { + // return cachedFees[amount]!; + // } + // + // final manager = + // ref.read(walletsChangeNotifierProvider).getManager(walletId); + // final feeObject = await manager.fees; + // + // late final int feeRate; + // + // switch (ref.read(feeRateTypeStateProvider.state).state) { + // case FeeRateType.fast: + // feeRate = feeObject.fast; + // break; + // case FeeRateType.average: + // feeRate = feeObject.medium; + // break; + // case FeeRateType.slow: + // feeRate = feeObject.slow; + // break; + // } + // + // int fee; + // + // if (coin == Coin.firo || coin == Coin.firoTestNet) { + // if (ref.read(publicPrivateBalanceStateProvider.state).state == + // "Private") { + // fee = await manager.estimateFeeFor(amount, feeRate); + // + // cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee) + // .toStringAsFixed(Constants.decimalPlaces); + // + // return cachedFiroPrivateFees[amount]!; + // } else { + // fee = await (manager.wallet as FiroWallet) + // .estimateFeeForPublic(amount, feeRate); + // + // cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee) + // .toStringAsFixed(Constants.decimalPlaces); + // + // return cachedFiroPublicFees[amount]!; + // } + // } else { + // fee = await manager.estimateFeeFor(amount, feeRate); + // cachedFees[amount] = + // Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces); + // + // return cachedFees[amount]!; + // } + // } + + Future<String?> _firoBalanceFuture( + ChangeNotifierProvider<Manager> provider, + String locale, + bool private, + ) async { + final wallet = ref.read(provider).wallet as FiroWallet?; + + if (wallet != null) { + Decimal? balance; + if (private) { + balance = await wallet.availablePrivateBalance(); + } else { + balance = await wallet.availablePublicBalance(); + } + + return Format.localizedStringAsFixed( + value: balance, locale: locale, decimalPlaces: 8); + } + + return null; + } + + Widget firoBalanceFutureBuilder( + BuildContext context, + AsyncSnapshot<String?> snapshot, + bool private, + ) { + if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { + if (private) { + _privateBalanceString = snapshot.data!; + } else { + _publicBalanceString = snapshot.data!; + } + } + if (private && _privateBalanceString != null) { + return Text( + "$_privateBalanceString ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else if (!private && _publicBalanceString != null) { + return Text( + "$_publicBalanceString ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance...", + ], + style: STextStyles.itemSubtitle(context), + ); + } + } + + Future<void> scanQr() async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + Logging.instance.log("qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); + + if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { + // auto fill address + _address = results["address"] ?? ""; + sendToController.text = _address!; + + // autofill notes field + if (results["message"] != null) { + noteController.text = results["message"]!; + } else if (results["label"] != null) { + noteController.text = results["label"]!; + } + + // autofill amount field + if (results["amount"] != null) { + final amount = Decimal.parse(results["amount"]!); + cryptoAmountController.text = Format.localizedStringAsFixed( + value: amount, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlacesForCoin(coin), + ); + amount.toString(); + _amountToSend = amount; + } + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(qrResult.rawContent)) { + _address = qrResult.rawContent; + sendToController.text = _address ?? ""; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); + } + } + + Future<void> pasteAddress() async { + final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring(0, content.indexOf("\n")); + } + + sendToController.text = content; + _address = content; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } + + void fiatTextFieldOnChanged(String baseAmountString) { + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = baseAmountString.contains(",") + ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) + : Decimal.parse(baseAmountString); + + var _price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + + if (_price == Decimal.zero) { + _amountToSend = Decimal.zero; + } else { + _amountToSend = baseAmount <= Decimal.zero + ? Decimal.zero + : (baseAmount / _price).toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); + } + if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final amountString = Format.localizedStringAsFixed( + value: _amountToSend!, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlacesForCoin(coin), + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Decimal.zero; + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis( + // _amountToSend!)); + // }); + _updatePreviewButtonState(_address, _amountToSend); + } + + Future<void> sendAllTapped() async { + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .wallet as FiroWallet; + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + cryptoAmountController.text = + (await firoWallet.availablePrivateBalance()) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + } else { + cryptoAmountController.text = + (await firoWallet.availablePublicBalance()) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + } + } else { + cryptoAmountController.text = (await ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .availableBalance) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + } + } + + @override + void initState() { + ref.refresh(feeSheetSessionCacheProvider); + + // _calculateFeesFuture = calculateFees(0); + _data = widget.autoFillData; + walletId = widget.walletId; + coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; + clipboard = widget.clipboard; + scanner = widget.barcodeScanner; + + sendToController = TextEditingController(); + cryptoAmountController = TextEditingController(); + baseAmountController = TextEditingController(); + noteController = TextEditingController(); + // feeController = TextEditingController(); + + onCryptoAmountChanged = _cryptoAmountChanged; + cryptoAmountController.addListener(onCryptoAmountChanged); + + if (_data != null) { + if (_data!.amount != null) { + cryptoAmountController.text = _data!.amount!.toString(); + } + sendToController.text = _data!.contactLabel; + _address = _data!.address; + _addressToggleFlag = true; + } + + _cryptoFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + ref.refresh(sendAmountProvider); + } else { + ref.read(sendAmountProvider.state).state = _amountToSend!; + } + } + }); + + _baseFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + ref.refresh(sendAmountProvider); + } else { + ref.read(sendAmountProvider.state).state = _amountToSend!; + } + } + }); + + super.initState(); + } + + @override + void dispose() { + cryptoAmountController.removeListener(onCryptoAmountChanged); + + sendToController.dispose(); + cryptoAmountController.dispose(); + baseAmountController.dispose(); + noteController.dispose(); + // feeController.dispose(); + + _noteFocusNode.dispose(); + _addressFocusNode.dispose(); + _cryptoFocus.dispose(); + _baseFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final provider = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManagerProvider(walletId))); + final String locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + // if (coin == Coin.firo || coin == Coin.firoTestNet) { + // ref.listen(publicPrivateBalanceStateProvider, (previous, next) { + // if (_amountToSend == null) { + // setState(() { + // _calculateFeesFuture = calculateFees(0); + // }); + // } else { + // setState(() { + // _calculateFeesFuture = + // calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + // }); + // } + // }); + // } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + if (coin == Coin.firo) + Text( + "Send from", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + if (coin == Coin.firo) + const SizedBox( + height: 10, + ), + if (coin == Coin.firo) + DropdownButtonHideUnderline( + child: DropdownButton2( + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: ref.watch(publicPrivateBalanceStateProvider.state).state, + items: [ + DropdownMenuItem( + value: "Private", + child: Row( + children: [ + Text( + "Private balance", + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _firoBalanceFuture(provider, locale, true), + builder: (context, AsyncSnapshot<String?> snapshot) => + firoBalanceFutureBuilder( + context, + snapshot, + true, + ), + ), + ], + ), + ), + DropdownMenuItem( + value: "Public", + child: Row( + children: [ + Text( + "Public balance", + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _firoBalanceFuture(provider, locale, false), + builder: (context, AsyncSnapshot<String?> snapshot) => + firoBalanceFutureBuilder( + context, + snapshot, + false, + ), + ), + ], + ), + ), + ], + onChanged: (value) { + if (value is String) { + setState(() { + ref.watch(publicPrivateBalanceStateProvider.state).state = + value; + }); + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + if (coin == Coin.firo) + const SizedBox( + height: 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + BlueTextButton( + text: "Send all ${coin.ticker}", + onTap: sendAllTapped, + ), + ], + ), + const SizedBox( + height: 10, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + key: const Key("amountInputFieldCryptoTextFieldKey"), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 22, + right: 12, + bottom: 22, + ), + hintText: "0", + hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + coin.ticker, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ), + if (Prefs.instance.externalCalls) + const SizedBox( + height: 10, + ), + 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>()!.textDark, + ), + key: const Key("amountInputFieldFiatTextFieldKey"), + controller: baseAmountController, + focusNode: _baseFocus, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a fiat amount with 2 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: fiatTextFieldOnChanged, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 22, + right: 12, + bottom: 22, + ), + hintText: "0", + hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)), + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + Text( + "Send to", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + key: const Key("sendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow( + // RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _address = newValue; + _updatePreviewButtonState(_address, _amountToSend); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Enter ${coin.ticker} address", + _addressFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: pasteAddress, + child: sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: () async { + final entry = + await showDialog<ContactAddressEntry?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: + STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], + ), + ), + ); + + if (entry != null) { + sendToController.text = + entry.other ?? entry.label; + + _address = entry.address; + + _updatePreviewButtonState( + _address, + _amountToSend, + ); + + setState(() { + _addressToggleFlag = true; + }); + } + }, + child: const AddressBookIcon(), + ), + // if (sendToController.text.isEmpty) + // TextFieldIconButton( + // key: const Key("sendViewScanQrButtonKey"), + // onTap: scanQr, + // child: const QrCodeIcon(), + // ) + ], + ), + ), + ), + ), + ), + ), + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ref.read(walletsChangeNotifierProvider).getManager(walletId), + ); + + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: STextStyles.label(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textError, + ), + ), + ), + ); + } + }, + ), + const SizedBox( + height: 20, + ), + Text( + "Note (optional)", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + 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 SizedBox( + height: 20, + ), + Text( + "Transaction fee (estimated)", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + DesktopFeeDropDown( + walletId: walletId, + ), + const SizedBox( + height: 36, + ), + PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Preview send", + enabled: ref.watch(previewTxButtonStateProvider.state).state, + onPressed: ref.watch(previewTxButtonStateProvider.state).state + ? previewSend + : null, + ) + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart new file mode 100644 index 000000000..d6e99ce70 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -0,0 +1,208 @@ +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_refresh_button.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.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/enums/wallet_balance_toggle_state.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; + + Decimal? _balanceTotalCached; + Decimal? _balanceCached; + + @override + void initState() { + walletId = widget.walletId; + managerProvider = widget.managerProvider; + super.initState(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Consumer( + builder: (context, ref, __) { + final Coin coin = + ref.watch(managerProvider.select((value) => value.coin)); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Consumer( + builder: (_, ref, __) { + 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 = 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: [ + 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: [ + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ], + style: STextStyles.desktopH3(context).copyWith( + fontSize: 24, + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + 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, + ), + ), + ], + ); + } + }, + ); + }, + ), + ], + ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + const SizedBox( + width: 8, + ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + const DesktopBalanceToggleButton(), + const SizedBox( + width: 8, + ), + WalletRefreshButton( + walletId: walletId, + initialSyncStatus: widget.initialSyncStatus, + ) + ], + ); + }, + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart new file mode 100644 index 000000000..82b1f6ee7 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -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, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart new file mode 100644 index 000000000..5195a4b9b --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -0,0 +1,297 @@ +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; + final color = _getColor(status, context); + switch (status) { + case WalletSyncStatus.unableToSync: + return SvgPicture.asset( + Assets.svg.radioProblem, + color: color, + width: size, + height: size, + ); + case WalletSyncStatus.synced: + return SvgPicture.asset( + Assets.svg.radio, + color: color, + width: size, + height: size, + ); + case WalletSyncStatus.syncing: + return SvgPicture.asset( + Assets.svg.radioSyncing, + color: color, + width: size, + height: size, + ); + } + } + + Widget _buildText(WalletSyncStatus status, BuildContext context) { + String label; + + switch (status) { + case WalletSyncStatus.unableToSync: + label = "Unable to sync"; + break; + case WalletSyncStatus.synced: + label = "Synchronized"; + break; + case WalletSyncStatus.syncing: + label = "Synchronizing"; + break; + } + + return Text( + label, + style: STextStyles.desktopMenuItemSelected(context).copyWith( + color: _getColor(status, context), + ), + ); + } + + Color _getColor(WalletSyncStatus status, BuildContext context) { + switch (status) { + case WalletSyncStatus.unableToSync: + return Theme.of(context).extension<StackColors>()!.accentColorRed; + case WalletSyncStatus.synced: + return Theme.of(context).extension<StackColors>()!.accentColorGreen; + case WalletSyncStatus.syncing: + return Theme.of(context).extension<StackColors>()!.accentColorYellow; + } + } + + @override + Widget build(BuildContext context) { + return RawMaterialButton( + hoverColor: _getColor(_currentSyncStatus, context).withOpacity(0.1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: () { + 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: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 32, + ), + child: Row( + children: [ + _buildNetworkIcon(_currentSyncStatus, context), + const SizedBox( + width: 6, + ), + _buildText(_currentSyncStatus, context), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart new file mode 100644 index 000000000..3a7a94885 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart @@ -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, + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart new file mode 100644 index 000000000..59e01f0b7 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart @@ -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, + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart new file mode 100644 index 000000000..b2f6156c2 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart @@ -0,0 +1,136 @@ +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: MouseRegion( + cursor: SystemMouseCursors.click, + 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: MouseRegion( + cursor: SystemMouseCursors.click, + 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, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart new file mode 100644 index 000000000..e6d0ff390 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -0,0 +1,329 @@ +import 'dart:async'; + +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_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.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/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/loading_indicator.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; + + Future<void> enterPassphrase() async { + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed(const Duration(seconds: 1)); + + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (verified) { + Navigator.of(context, rootNavigator: true).pop(); + + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + await Navigator.of(context).pushReplacementNamed( + WalletKeysDesktopPopup.routeName, + arguments: words, + ); + } + } else { + Navigator.of(context, rootNavigator: true).pop(); + + await Future<void>.delayed(const Duration(milliseconds: 300)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase!", + context: context, + ), + ); + } + } + + @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, + autofocus: true, + onSubmitted: (_) { + if (continueEnabled) { + enterPassphrase(); + } + }, + 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 { + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed( + const Duration(seconds: 1)); + + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (verified) { + Navigator.of(context, rootNavigator: true).pop(); + + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + await Navigator.of(context) + .pushReplacementNamed( + WalletKeysDesktopPopup.routeName, + arguments: words, + ); + } + } else { + Navigator.of(context, rootNavigator: true).pop(); + + await Future<void>.delayed( + const Duration(milliseconds: 300)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase!", + context: context, + ), + ); + } + } + : null, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart new file mode 100644 index 000000000..32994dcb9 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart @@ -0,0 +1,70 @@ +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 RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: () { + 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, + ), + ) + ]; + }, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 19, + horizontal: 32, + ), + 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), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart new file mode 100644 index 000000000..1ed646b30 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.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/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'; + +class WalletKeysDesktopPopup extends StatelessWidget { + const WalletKeysDesktopPopup({ + Key? key, + required this.words, + this.clipboardInterface = const ClipboardWrapper(), + }) : super(key: key); + + final List<String> words; + final ClipboardInterface clipboardInterface; + + static const String routeName = "walletKeysDesktopPopup"; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Wallet keys", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ], + ), + const SizedBox( + height: 28, + ), + Text( + "Recovery phrase", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: MnemonicTable( + words: words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show QR code", + onPressed: () { + final String value = AddressUtils.encodeQRSeedData(words); + Navigator.of(context).pushNamed( + QRCodeDesktopPopupContent.routeName, + arguments: value, + ); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Copy", + onPressed: () async { + await clipboardInterface.setData( + ClipboardData(text: words.join(" ")), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + }, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart b/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart new file mode 100644 index 000000000..b9c03ff19 --- /dev/null +++ b/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/notifications/notification_card.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/unread_notifications_provider.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'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopNotificationsView extends ConsumerStatefulWidget { + const DesktopNotificationsView({Key? key}) : super(key: key); + + static const String routeName = "/desktopNotifications"; + + @override + ConsumerState<DesktopNotificationsView> createState() => + _DesktopNotificationsViewState(); +} + +class _DesktopNotificationsViewState + extends ConsumerState<DesktopNotificationsView> { + @override + Widget build(BuildContext context) { + final notifications = + ref.watch(notificationsProvider.select((value) => value.notifications)); + + return DesktopScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Padding( + padding: const EdgeInsets.only(left: 24), + child: Text( + "Notifications", + style: STextStyles.desktopH3(context), + ), + ), + ), + body: notifications.isEmpty + ? Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: RoundedWhiteContainer( + child: Center( + child: Text( + "Notifications will appear here", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ), + ), + ], + ) + : ListView.builder( + primary: false, + itemCount: notifications.length, + itemBuilder: (context, index) { + final notification = notifications[index]; + if (notification.read == false) { + ref + .read(unreadNotificationsStateProvider.state) + .state + .add(notification.id); + } + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 5, + ), + child: NotificationCard( + notification: notification, + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart new file mode 100644 index 000000000..621683e65 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings/stack_privacy_dialog.dart'; +import 'package:stackwallet/providers/global/prefs_provider.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/widgets/custom_buttons/draggable_switch_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +import 'debug_info_dialog.dart'; + +class AdvancedSettings extends ConsumerStatefulWidget { + const AdvancedSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuAdvanced"; + + @override + ConsumerState<AdvancedSettings> createState() => _AdvancedSettings(); +} + +class _AdvancedSettings extends ConsumerState<AdvancedSettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleSliders, + width: 48, + height: 48, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Advanced", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nConfigurate these settings only if you know what you are doing!", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Toggle testnet coins", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.showTestNetCoins), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .showTestNetCoins = newValue; + }, + ), + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + + /// TODO: Make a dialog popup + Consumer(builder: (_, ref, __) { + final externalCalls = ref.watch( + prefsChangeNotifierProvider + .select((value) => value.externalCalls), + ); + return Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Stack Experience", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + Text( + externalCalls ? "Easy crypto" : "Incognito", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + PrimaryButton( + label: "Change", + buttonHeight: ButtonHeight.xs, + width: 86, + onPressed: () async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const StackPrivacyDialog(); + }, + ); + }, + ) + ], + ), + ); + }), + ], + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + + /// TODO: Make a dialog popup + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Debug info", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + PrimaryButton( + buttonHeight: ButtonHeight.xs, + label: "Show logs", + width: 101, + onPressed: () async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DebugInfoDialog(); + }, + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart new file mode 100644 index 000000000..3235a5549 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart @@ -0,0 +1,351 @@ +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/isar/models/log.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/providers/global/debug_service_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; +import 'package:stackwallet/utilities/enums/log_level_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/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/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'; + +class DebugInfoDialog extends ConsumerStatefulWidget { + const DebugInfoDialog({Key? key}) : super(key: key); + + @override + ConsumerState<DebugInfoDialog> createState() => _DebugInfoDialog(); +} + +class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> { + late final TextEditingController searchDebugController; + late final FocusNode searchDebugFocusNode; + + final scrollController = ScrollController(); + + String _searchTerm = ""; + + List<Log> filtered(List<Log> unfiltered, String filter) { + if (filter == "") { + return unfiltered; + } + return unfiltered + .where( + (e) => (e.toString().toLowerCase().contains(filter.toLowerCase()))) + .toList(); + } + + BorderRadius? _borderRadius(int index, int listLength) { + if (index == 0 && listLength == 1) { + return BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == 0) { + return BorderRadius.vertical( + bottom: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } else if (index == listLength - 1) { + return BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + return null; + } + + @override + void initState() { + searchDebugController = TextEditingController(); + searchDebugFocusNode = FocusNode(); + + ref.read(debugServiceProvider).updateRecentLogs(); + super.initState(); + } + + @override + void dispose() { + searchDebugFocusNode.dispose(); + searchDebugController.dispose(); + + scrollController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: 850, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Debug info", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: searchDebugController, + focusNode: searchDebugFocusNode, + onChanged: (newString) { + setState(() => _searchTerm = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchDebugFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: searchDebugController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchDebugController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + Expanded( + // flex: 24, + child: NestedScrollView( + // floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context), + sliver: SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 32), + child: Column( + children: const [], + ), + ), + ), + ), + ]; + }, + body: Builder( + builder: (context) { + final logs = filtered( + ref.watch(debugServiceProvider + .select((value) => value.recentLogs)), + _searchTerm) + .reversed + .toList(growable: false); + return CustomScrollView( + reverse: true, + // shrinkWrap: true, + controller: scrollController, + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final log = logs[index]; + + return Container( + key: Key( + "log_${log.id}_${log.timestampInMillisUTC}"), + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: _borderRadius(index, logs.length), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + " [${log.logLevel.name}]", + style: STextStyles.baseXS(context) + .copyWith( + fontSize: 8, + color: (log.logLevel == + LogLevel.Info + ? Theme.of(context) + .extension< + StackColors>()! + .topNavIconGreen + : (log.logLevel == + LogLevel.Warning + ? Theme.of(context) + .extension< + StackColors>()! + .topNavIconYellow + : (log.logLevel == + LogLevel.Error + ? Colors.orange + : Theme.of(context) + .extension< + StackColors>()! + .topNavIconRed))), + ), + ), + Text( + "[${DateTime.fromMillisecondsSinceEpoch(log.timestampInMillisUTC, isUtc: true)}]: ", + style: STextStyles.baseXS(context) + .copyWith( + fontSize: 12, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ], + ), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 20, + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SelectableText( + log.message, + style: STextStyles.baseXS( + context) + .copyWith( + fontSize: 11.5), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + childCount: logs.length, + ), + ), + ], + ); + }, + ), + ), + ), + // const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Clear logs", + onPressed: () async { + await ref.read(debugServiceProvider).deleteAllMessages(); + await ref.read(debugServiceProvider).updateRecentLogs(); + + if (mounted) { + Navigator.pop(context); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + context: context, + message: 'Logs cleared!')); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save logs to file", + onPressed: () {}, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/stack_privacy_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/stack_privacy_dialog.dart new file mode 100644 index 000000000..32c20c010 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/stack_privacy_dialog.dart @@ -0,0 +1,436 @@ +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/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/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/rounded_white_container.dart'; + +class StackPrivacyDialog extends ConsumerStatefulWidget { + const StackPrivacyDialog({Key? key}) : super(key: key); + + @override + ConsumerState<StackPrivacyDialog> createState() => _StackPrivacyDialog(); +} + +class _StackPrivacyDialog extends ConsumerState<StackPrivacyDialog> { + late final bool isDesktop; + late bool isEasy; + late bool infoToggle; + + @override + void initState() { + isDesktop = Util.isDesktop; + isEasy = ref.read(prefsChangeNotifierProvider).externalCalls; + infoToggle = isEasy; + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: 650, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Choose Your Stack Experience", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 35, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: PrivacyToggle( + externalCallsEnabled: isEasy, + onChanged: (externalCalls) { + isEasy = externalCalls; + setState(() { + infoToggle = isEasy; + }); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(32.0), + child: RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + child: Center( + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : 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: 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(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () {}, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + 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 (isDesktop) { + Navigator.pop(context); + } + }, + ), + ) + ], + ), + ), + ], + ), + ); + } +} + +class PrivacyToggle extends StatefulWidget { + const PrivacyToggle({ + Key? key, + required this.externalCallsEnabled, + this.onChanged, + }) : super(key: key); + + final bool externalCallsEnabled; + final void Function(bool)? onChanged; + + @override + State<PrivacyToggle> createState() => _PrivacyToggleState(); +} + +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(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: RawMaterialButton( + elevation: 0, + hoverElevation: 0, + fillColor: Theme.of(context).extension<StackColors>()!.popupBG, + shape: RoundedRectangleBorder( + side: !externalCallsEnabled + ? BorderSide.none + : BorderSide( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 2, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + ), + onPressed: () { + setState(() { + // update toggle state + externalCallsEnabled = true; + }); + // call callback with newly set value + widget.onChanged?.call(externalCallsEnabled); + }, + child: Padding( + padding: const EdgeInsets.all( + 12, + ), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + const SizedBox( + height: 10, + ), + SvgPicture.asset( + Assets.svg.personaEasy, + width: 120, + height: 120, + ), + if (isDesktop) + const SizedBox( + height: 12, + ), + Center( + child: Text( + "Easy Crypto", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.label700(context), + ), + ), + Center( + child: Text( + "Recommended", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context), + ), + ), + if (isDesktop) + const SizedBox( + height: 12, + ), + ], + ), + if (externalCallsEnabled) + Positioned( + top: 4, + right: 4, + child: SvgPicture.asset( + Assets.svg.checkCircle, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + if (!externalCallsEnabled) + Positioned( + top: 4, + right: 4, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1000), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: RawMaterialButton( + elevation: 0, + hoverElevation: 0, + fillColor: Theme.of(context).extension<StackColors>()!.popupBG, + shape: RoundedRectangleBorder( + side: externalCallsEnabled + ? BorderSide.none + : BorderSide( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 2, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + ), + onPressed: () { + setState(() { + // update toggle state + externalCallsEnabled = false; + }); + // call callback with newly set value + widget.onChanged?.call(externalCallsEnabled); + }, + child: Padding( + padding: const EdgeInsets.all( + 12, + ), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + const SizedBox( + height: 10, + ), + SvgPicture.asset( + Assets.svg.personaIncognito, + width: 120, + height: 120, + ), + if (isDesktop) + const SizedBox( + height: 12, + ), + Center( + child: Text( + "Incognito", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.label700(context), + ), + ), + Center( + child: Text( + "Privacy conscious", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context), + ), + ), + if (isDesktop) + const SizedBox( + height: 12, + ), + ], + ), + if (!externalCallsEnabled) + Positioned( + top: 4, + right: 4, + child: SvgPicture.asset( + Assets.svg.checkCircle, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + if (externalCallsEnabled) + Positioned( + top: 4, + right: 4, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1000), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart new file mode 100644 index 000000000..bfd5f3b61 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -0,0 +1,501 @@ +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/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/ui/color_theme_provider.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/color_theme.dart'; +import 'package:stackwallet/utilities/theme/dark_colors.dart'; +import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class AppearanceOptionSettings extends ConsumerStatefulWidget { + const AppearanceOptionSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuAppearance"; + + @override + ConsumerState<AppearanceOptionSettings> createState() => + _AppearanceOptionSettings(); +} + +class _AppearanceOptionSettings + extends ConsumerState<AppearanceOptionSettings> { + // late bool isLight; + + // @override + // void initState() { + // + // super.initState(); + // } + // + // @override + // void dispose() { + // super.dispose(); + // } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleSun, + width: 48, + height: 48, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: "Appearances", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nCustomize how your Stack Wallet looks according to your preferences.", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ], + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Display favorite wallets", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.showFavoriteWallets), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .showFavoriteWallets = newValue; + }, + ), + ) + ], + ), + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Choose theme", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.all(10), + child: ThemeToggle(), + ), + ], + ), + ), + ), + ], + ); + } +} + +class ThemeToggle extends ConsumerStatefulWidget { + const ThemeToggle({ + Key? key, + }) : super(key: key); + + // final bool externalCallsEnabled; + // final void Function(bool)? onChanged; + + @override + ConsumerState<ThemeToggle> createState() => _ThemeToggle(); +} + +class _ThemeToggle extends ConsumerState<ThemeToggle> { + // late bool externalCallsEnabled; + + late String _selectedTheme; + + @override + void initState() { + _selectedTheme = + DB.instance.get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme") + as String? ?? + "light"; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "light" + ? Theme.of(context) + .extension<StackColors>()! + .infoItemIcons + : Theme.of(context).extension<StackColors>()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: SvgPicture.asset( + Assets.svg.themeLight, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "light", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "light") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Light", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + width: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "oceanBreeze" + ? Theme.of(context) + .extension<StackColors>()! + .infoItemIcons + : Theme.of(context).extension<StackColors>()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: SvgPicture.asset( + Assets.svg.themeOcean, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "oceanBreeze", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "oceanBreeze") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Ocean Breeze", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + width: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "dark" + ? Theme.of(context) + .extension<StackColors>()! + .infoItemIcons + : Theme.of(context).extension<StackColors>()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: SvgPicture.asset( + Assets.svg.themeDark, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "dark", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "dark") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Dark", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart new file mode 100644 index 000000000..c82b5f923 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -0,0 +1,667 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:intl/intl.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; +import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_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/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.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/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class BackupRestoreSettings extends ConsumerStatefulWidget { + const BackupRestoreSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuBackupRestore"; + + @override + ConsumerState<BackupRestoreSettings> createState() => + _BackupRestoreSettings(); +} + +class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { + late bool createBackup = false; + late bool restoreBackup = false; + + final toggleController = DSBController(); + + late final TextEditingController fileLocationController; + late final TextEditingController passwordController; + late final TextEditingController frequencyController; + + late final FocusNode fileLocationFocusNode; + late final FocusNode passwordFocusNode; + + String prettySinceLastBackupString(DateTime? time) { + if (time == null) { + return "-"; + } + final difference = DateTime.now().difference(time); + int value; + String postfix; + if (difference < const Duration(seconds: 60)) { + value = difference.inSeconds; + postfix = "seconds"; + } else if (difference < const Duration(minutes: 60)) { + value = difference.inMinutes; + postfix = "minutes"; + } else if (difference < const Duration(hours: 24)) { + value = difference.inHours; + postfix = "hours"; + } else if (difference.inDays < 8) { + value = difference.inDays; + postfix = "days"; + } else { + // if greater than a week return the actual date + return DateFormat.yMMMMd( + ref.read(localeServiceChangeNotifierProvider).locale) + .format(time); + } + + if (value == 1) { + postfix = postfix.substring(0, postfix.length - 1); + } + + return "$value $postfix ago"; + } + + Future<void> enableAutoBackup(BuildContext context) async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const EnableBackupDialog(); + }, + ); + } + + Future<void> createAutoBackup() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const CreateAutoBackup(); + }, + ); + } + + Future<void> editAutoBackup() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Edit auto backup", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: EditAutoBackupView(), + ), + ], + ), + ), + ); + } + + Future<void> attemptDisable() async { + final result = await showDialog<bool?>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return !Util.isDesktop + ? StackDialog( + title: "Disable Auto Backup", + message: + "You are turning off Auto Backup. You can turn it back on at any time. Your previous Auto Backup file will not be deleted. Remember to backup your wallets manually so you don't lose important information.", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Disable", + style: STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(); + setState(() { + ref + .watch(prefsChangeNotifierProvider) + .isAutoBackupEnabled = false; + }); + }, + ), + ) + : DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Disable Auto Backup", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 600, + child: Text( + "You are turning off Auto Backup. You can turn it back on at any time. Your previous Auto Backup file will not be deleted. Remember to backup your wallets manually so you don't lose important information.", + style: STextStyles.desktopTextSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + const SizedBox( + height: 48, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Disable", + onPressed: () { + ref + .read(prefsChangeNotifierProvider) + .isAutoBackupEnabled = false; + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ); + if (mounted) { + if (result is bool && result) { + ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = false; + Navigator.of(context).pop(); + } else { + toggleController.activate?.call(); + } + } + } + + @override + void initState() { + fileLocationController = TextEditingController(); + passwordController = TextEditingController(); + frequencyController = TextEditingController(); + + passwordController.text = "---------------"; + fileLocationController.text = + ref.read(prefsChangeNotifierProvider).autoBackupLocation ?? " "; + frequencyController.text = Format.prettyFrequencyType( + ref.read(prefsChangeNotifierProvider).backupFrequencyType); + + fileLocationFocusNode = FocusNode(); + passwordFocusNode = FocusNode(); + + // _toggle = ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled; + super.initState(); + } + + @override + void dispose() { + fileLocationController.dispose(); + passwordController.dispose(); + frequencyController.dispose(); + + fileLocationFocusNode.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider + .select((value) => value.isAutoBackupEnabled)); + + ref.listen( + prefsChangeNotifierProvider + .select((value) => value.backupFrequencyType), + (previous, BackupFrequencyType next) { + frequencyController.text = Format.prettyFrequencyType(next); + }); + + return LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset( + Assets.svg.backupAuto, + width: 48, + height: 48, + ), + isEnabledAutoBackup + ? SvgPicture.asset( + Assets.svg.enableButton, + ) + : SvgPicture.asset( + Assets.svg.disableButton, + ), + ], + ), + ), + Center( + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Auto Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." + "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " + "else on the internet before. Your password is not stored.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + TextSpan( + text: + "\n\nFor more information, please see our website ", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + TextSpan( + text: "stackwallet.com", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/"), + mode: LaunchMode + .externalApplication, + ); + }, + ), + ], + ), + ), + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: !isEnabledAutoBackup + ? PrimaryButton( + buttonHeight: ButtonHeight.m, + width: 200, + label: "Enable auto backup", + onPressed: () { + enableAutoBackup(context); + }, + ) + : Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + RoundedContainer( + width: 403, + color: Theme.of(context) + .extension<StackColors>()! + .background, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", + style: + STextStyles.itemSubtitle( + context), + ), + BlueTextButton( + text: "Back up now", + onTap: () { + ref + .read( + autoSWBServiceProvider) + .doBackup(); + }, + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + PrimaryButton( + buttonHeight: ButtonHeight.m, + width: 190, + label: "Disable auto backup", + onPressed: () { + attemptDisable(); + }, + ), + const SizedBox(width: 16), + SecondaryButton( + buttonHeight: ButtonHeight.m, + width: 190, + label: "Edit auto backup", + onPressed: () { + editAutoBackup(); + }, + ), + ], + ) + ], + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.backupAdd, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), + ), + Center( + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Manual Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nCreate manual backup to easily transfer your data between devices. " + "You will create a backup file that can be later used in the Restore option. " + "Use a strong password to encrypt your data.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all( + 10, + ), + child: createBackup + ? const SizedBox( + width: 512, + child: CreateBackupView(), + ) + : PrimaryButton( + buttonHeight: ButtonHeight.m, + width: 200, + label: "Create manual backup", + onPressed: () { + setState(() { + createBackup = true; + }); + }, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, + bottom: 40, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.backupRestore, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), + ), + Center( + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Restore Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nUse your Stack Wallet backup file to restore your wallets, address book " + "and wallet preferences.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all( + 10, + ), + child: restoreBackup + ? const SizedBox( + width: 512, + child: RestoreFromFileView(), + ) + : PrimaryButton( + buttonHeight: ButtonHeight.m, + width: 200, + label: "Restore backup", + onPressed: () { + setState(() { + restoreBackup = true; + }); + }, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + )); + }); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart new file mode 100644 index 000000000..df80da732 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -0,0 +1,841 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stack_wallet_backup/stack_wallet_backup.dart'; +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/swb_file_system.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.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/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/progress_bar.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:zxcvbn/zxcvbn.dart'; + +class CreateAutoBackup extends ConsumerStatefulWidget { + const CreateAutoBackup({ + Key? key, + }) : super(key: key); + + @override + ConsumerState<CreateAutoBackup> createState() => _CreateAutoBackup(); +} + +class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { + late final TextEditingController fileLocationController; + late final TextEditingController passphraseController; + late final TextEditingController passphraseRepeatController; + + late final SecureStorageInterface secureStore; + + late final SWBFileSystem stackFileSystem; + late final FocusNode passphraseFocusNode; + late final FocusNode passphraseRepeatFocusNode; + final zxcvbn = Zxcvbn(); + + bool shouldShowPasswordHint = true; + bool hidePassword = true; + + String passwordFeedback = + "Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters."; + double passwordStrength = 0.0; + + bool get shouldEnableCreate { + return fileLocationController.text.isNotEmpty && + passphraseController.text.isNotEmpty && + passphraseRepeatController.text.isNotEmpty; + } + + bool get fieldsMatch => + passphraseController.text == passphraseRepeatController.text; + + BackupFrequencyType _currentDropDownValue = + BackupFrequencyType.everyTenMinutes; + + final List<BackupFrequencyType> _dropDownItems = [ + BackupFrequencyType.everyTenMinutes, + BackupFrequencyType.everyAppStart, + BackupFrequencyType.afterClosingAWallet, + ]; + + @override + void initState() { + secureStore = ref.read(secureStoreProvider); + stackFileSystem = SWBFileSystem(); + + fileLocationController = TextEditingController(); + passphraseController = TextEditingController(); + passphraseRepeatController = TextEditingController(); + + passphraseFocusNode = FocusNode(); + passphraseRepeatFocusNode = FocusNode(); + + if (Platform.isAndroid) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final dir = await stackFileSystem.prepareStorage(); + if (mounted) { + setState(() { + fileLocationController.text = dir.path; + }); + } + }); + } + + super.initState(); + } + + @override + void dispose() { + fileLocationController.dispose(); + passphraseController.dispose(); + passphraseRepeatController.dispose(); + + passphraseFocusNode.dispose(); + passphraseRepeatFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType "); + + // bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider + // .select((value) => value.isAutoBackupEnabled)); + + final isDesktop = Util.isDesktop; + return DesktopDialog( + maxHeight: 680, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Create auto backup", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 30, + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose file location", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.left, + ), + ), + const SizedBox( + height: 10, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Platform.isAndroid) + Consumer(builder: (context, ref, __) { + return Container( + color: Colors.transparent, + child: TextField( + autocorrect: false, + enableSuggestions: false, + 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) {}, + ), + ); + }), + if (!Platform.isAndroid) + const SizedBox( + height: 24, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Text( + "Create a passphrase", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey1"), + focusNode: passphraseFocusNode, + controller: passphraseController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Create passphrase", + passphraseFocusNode, + context, + ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, + 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 (passphraseFocusNode.hasFocus || + passphraseRepeatFocusNode.hasFocus || + passphraseController.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 (passphraseFocusNode.hasFocus || + passphraseRepeatFocusNode.hasFocus || + passphraseController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key("createStackBackUpProgressBar"), + width: 512, + 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: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey2"), + focusNode: passphraseRepeatFocusNode, + controller: passphraseRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm passphrase", + passphraseRepeatFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context), + 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: 24, + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 32), + child: Text( + "Auto Backup frequency", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.left, + ), + ), + const SizedBox( + height: 10, + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + ), + child: isDesktop + ? DropdownButtonHideUnderline( + child: DropdownButton2( + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: _currentDropDownValue, + items: [ + ..._dropDownItems.map( + (e) { + String message = ""; + switch (e) { + case BackupFrequencyType.everyTenMinutes: + message = "Every 10 minutes"; + break; + case BackupFrequencyType.everyAppStart: + message = "Every app startup"; + break; + case BackupFrequencyType.afterClosingAWallet: + message = + "After closing a cryptocurrency wallet"; + break; + } + + return DropdownMenuItem( + value: e, + child: Text( + message, + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ); + }, + ), + ], + onChanged: (value) { + if (value is BackupFrequencyType) { + if (ref + .read(prefsChangeNotifierProvider) + .backupFrequencyType != + value) { + ref + .read(prefsChangeNotifierProvider) + .backupFrequencyType = value; + } + setState(() { + _currentDropDownValue = value; + }); + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ) + : null, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Enable Auto Backup", + enabled: shouldEnableCreate, + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = passphraseController.text; + final String repeatPassphrase = + passphraseRepeatController.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: (_) { + if (Util.isDesktop) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all( + 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Encrypting initial backup", + style: STextStyles.desktopH3( + context), + ), + const SizedBox( + height: 40, + ), + Text( + "This shouldn't take long", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ); + } else { + return const StackDialog( + title: "Encrypting initial backup", + message: "This shouldn't take long", + ); + } + }, + ), + ); + + // make sure the dialog is able to be displayed for at least some time + final fut = Future<void>.delayed( + const Duration(milliseconds: 300)); + + String adkString; + int adkVersion; + try { + final adk = + await compute(generateAdk, passphrase); + adkString = Format.uint8listToString(adk.item2); + adkVersion = adk.item1; + } on Exception catch (e, s) { + String err = getErrorMessageFromSWBException(e); + Logging.instance + .log("$err\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: err, + context: context, + ), + ); + return; + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$e", + context: context, + ), + ); + return; + } + + await secureStore.write( + key: "auto_adk_string", value: adkString); + await secureStore.write( + key: "auto_adk_version_string", + value: adkVersion.toString()); + + final DateTime now = DateTime.now(); + final String fileToSave = + createAutoBackupFilename(pathToSave, now); + + final backup = await SWB.createStackWalletJSON( + secureStorage: secureStore, + ); + + bool result = await SWB.encryptStackWalletWithADK( + fileToSave, + adkString, + jsonEncode(backup), + adkVersion: adkVersion, + ); + + // this future should already be complete unless there was an error encrypting + await Future.wait([fut]); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + ref + .read(prefsChangeNotifierProvider) + .autoBackupLocation = pathToSave; + ref + .read(prefsChangeNotifierProvider) + .lastAutoBackup = now; + + ref + .read(prefsChangeNotifierProvider) + .isAutoBackupEnabled = true; + + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (context) { + if (Platform.isAndroid) { + return StackOkDialog( + title: + "Stack Auto Backup enabled and saved to:", + message: fileToSave, + ); + } else if (Util.isDesktop) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 500, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Stack Auto Backup enabled!", + style: + STextStyles.desktopH3( + context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 40, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: + ButtonHeight.l, + onPressed: () { + Navigator.of(context) + .pop(); + }, + ), + ), + ], + ) + ], + ), + ), + ); + } else { + return const StackOkDialog( + title: "Stack Auto Backup enabled!", + ); + } + }, + ); + if (mounted) { + passphraseController.text = ""; + passphraseRepeatController.text = ""; + + Navigator.of(context).pop(); + } + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Failed to enable Auto Backup"), + ); + } + } + }, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart new file mode 100644 index 000000000..df9f18b52 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.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'; + +class EnableBackupDialog extends StatelessWidget { + const EnableBackupDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Future<void> createAutoBackup() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const CreateAutoBackup(); + }, + ); + } + + return DesktopDialog( + maxHeight: 300, + maxWidth: 570, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Enable Auto Backup", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 30, + ), + Text( + "To enable Auto Backup, you need to create a backup file.", + style: STextStyles.desktopTextSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.center, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(); + createAutoBackup(); + }, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart new file mode 100644 index 000000000..9f5f42b7c --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/currency_view.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.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/rounded_white_container.dart'; + +class CurrencySettings extends ConsumerStatefulWidget { + const CurrencySettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuCurrency"; + + @override + ConsumerState<CurrencySettings> createState() => _CurrencySettings(); +} + +Future<void> chooseCurrency(BuildContext context) async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: 800, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Select currency", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const Expanded( + child: BaseCurrencySettingsView(), + ), + ], + ), + ); + }, + ); +} + +class _CurrencySettings extends ConsumerState<CurrencySettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleDollarSign, + width: 48, + height: 48, + ), + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Currency", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nSelect a fiat currency to evaluate your crypto assets. We use CoinGecko conversion rates " + "when displaying your balance and transaction amounts.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all( + 10, + ), + child: PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Change currency", + onPressed: () { + chooseCurrency(context); + }, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart new file mode 100644 index 000000000..d07c9729f --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart @@ -0,0 +1,349 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/languages_enum.dart'; +import 'package:stackwallet/utilities/text_styles.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'; + +import '../../../../utilities/assets.dart'; +import '../../../../utilities/theme/stack_colors.dart'; +import '../../../../widgets/icon_widgets/x_icon.dart'; +import '../../../../widgets/rounded_container.dart'; +import '../../../../widgets/textfield_icon_button.dart'; + +class LanguageDialog extends ConsumerStatefulWidget { + const LanguageDialog({Key? key}) : super(key: key); + + @override + ConsumerState<LanguageDialog> createState() => _LanguageDialog(); +} + +class _LanguageDialog extends ConsumerState<LanguageDialog> { + late final TextEditingController searchLanguageController; + + late final FocusNode searchLanguageFocusNode; + + final languages = Language.values.map((e) => e.description).toList(); + + late String current; + late List<String> listWithoutSelected; + + void onTap(int index) { + if (index == 0 || current.isEmpty) { + // ignore if already selected language + return; + } + current = listWithoutSelected[index]; + listWithoutSelected.remove(current); + listWithoutSelected.insert(0, current); + ref.read(prefsChangeNotifierProvider).language = current; + } + + BorderRadius? _borderRadius(int index) { + if (index == 0 && listWithoutSelected.length == 1) { + return BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == 0) { + return BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } else if (index == listWithoutSelected.length - 1) { + return BorderRadius.vertical( + bottom: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + return null; + } + + String filter = ""; + + List<String> _filtered() { + return listWithoutSelected + .where( + (element) => element.toLowerCase().contains(filter.toLowerCase())) + .toList(); + } + + @override + void initState() { + searchLanguageController = TextEditingController(); + + searchLanguageFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + searchLanguageController.dispose(); + + searchLanguageFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + current = ref + .watch(prefsChangeNotifierProvider.select((value) => value.language)); + + listWithoutSelected = languages; + if (current.isNotEmpty) { + listWithoutSelected.remove(current); + listWithoutSelected.insert(0, current); + } + listWithoutSelected = _filtered(); + + return DesktopDialog( + maxHeight: 700, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Select language", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + flex: 24, + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context), + sliver: SliverToBoxAdapter( + child: Padding( + padding: + EdgeInsets.symmetric(vertical: 16, horizontal: 32), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: searchLanguageController, + focusNode: searchLanguageFocusNode, + style: STextStyles.desktopTextMedium(context) + .copyWith( + height: 2, + ), + textAlign: TextAlign.left, + decoration: standardInputDecoration("Search", + searchLanguageFocusNode, context) + .copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: searchLanguageController + .text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchLanguageController + .text = ""; + filter = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ]; + }, + body: Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: _borderRadius(index), + ), + child: Padding( + padding: const EdgeInsets.all(4), + key: Key( + "desktopSelectLanguage_${listWithoutSelected[index]}"), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: index == 0 + ? Theme.of(context) + .extension<StackColors>()! + .currencyListItemBG + : Theme.of(context) + .extension<StackColors>()! + .popupBG, + child: RawMaterialButton( + onPressed: () async { + onTap(index); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: true, + groupValue: index == 0, + onChanged: (_) { + onTap(index); + }, + ), + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + listWithoutSelected[index], + key: (index == 0) + ? const Key( + "desktopSettingsSelectedLanguageText") + : null, + style: + STextStyles.largeMedium14( + context), + ), + const SizedBox( + height: 2, + ), + Text( + listWithoutSelected[index], + key: (index == 0) + ? const Key( + "desktopSettingsSelectedLanguageTextDescription") + : null, + style: + STextStyles.itemSubtitle( + context), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ); + }, + childCount: listWithoutSelected.length, + ), + ), + ], + ); + }, + ), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save Changes", + onPressed: () {}, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart new file mode 100644 index 000000000..3c511236c --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -0,0 +1,105 @@ +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/settings_menu/language_settings/language_dialog.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class LanguageOptionSettings extends ConsumerStatefulWidget { + const LanguageOptionSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuLanguage"; + + @override + ConsumerState<LanguageOptionSettings> createState() => + _LanguageOptionSettings(); +} + +Future<void> chooseLanguage(BuildContext context) async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const LanguageDialog(); + }, + ); +} + +class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleLanguage, + width: 48, + height: 48, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Language", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nSelect the language of your wallet. We use your system language by default.", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all( + 10, + ), + child: PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Change language", + onPressed: () { + chooseLanguage(context); + }, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart new file mode 100644 index 000000000..a7e95d33a --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -0,0 +1,301 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.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/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class NodesSettings extends ConsumerStatefulWidget { + const NodesSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuNodes"; + + @override + ConsumerState<NodesSettings> createState() => _NodesSettings(); +} + +class _NodesSettings extends ConsumerState<NodesSettings> { + List<Coin> _coins = [...Coin.values]; + + late final TextEditingController searchNodeController; + late final FocusNode searchNodeFocusNode; + + late final ScrollController nodeScrollController; + + String filter = ""; + + List<Coin> _search(String filter, List<Coin> coins) { + if (filter.isEmpty) { + return coins; + } + return coins + .where((coin) => + coin.prettyName.contains(filter) || + coin.name.contains(filter) || + coin.ticker.toLowerCase().contains(filter.toLowerCase())) + .toList(); + } + + @override + void initState() { + _coins = _coins.toList(); + _coins.remove(Coin.firoTestNet); + + searchNodeController = TextEditingController(); + searchNodeFocusNode = FocusNode(); + + nodeScrollController = ScrollController(); + + super.initState(); + } + + @override + void dispose() { + searchNodeController.dispose(); + searchNodeFocusNode.dispose(); + nodeScrollController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + bool showTestNet = ref.watch( + prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), + ); + + List<Coin> coins = showTestNet + ? _coins + : _coins.sublist(0, _coins.length - kTestNetCoinCount); + + coins = _search(filter, coins); + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only( + right: 30, + // bottom: 32, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.circleNode, + width: 48, + height: 48, + ), + const SizedBox( + height: 16, + ), + Text( + "Nodes", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox( + height: 16, + ), + Text( + "Select a coin to see nodes", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: searchNodeController, + focusNode: searchNodeFocusNode, + onChanged: (newString) { + setState(() => filter = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchNodeFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: searchNodeController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchNodeController.text = ""; + filter = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ], + ), + const SizedBox( + height: 20, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: ListView.separated( + controller: nodeScrollController, + physics: const AlwaysScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + primary: false, + shrinkWrap: true, + itemBuilder: (context, index) { + final coin = coins[index]; + final count = ref + .watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))) + .length; + + return Padding( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onPressed: () { + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: CoinNodesView.routeName, + onGenerateRoute: + RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + CoinNodesView( + coin: coin, + rootNavigator: true, + ), + const RouteSettings( + name: CoinNodesView.routeName, + ), + ), + ]; + }, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.all( + 12.0, + ), + child: Row( + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${coin.prettyName} nodes", + style: STextStyles.titleBold12( + context), + ), + Text( + count > 1 + ? "$count nodes" + : "Default", + style: STextStyles.label(context), + ), + ], + ), + ], + ), + Expanded( + child: SvgPicture.asset( + Assets.svg.chevronRight, + alignment: Alignment.centerRight, + ), + ), + ], + ), + ), + ), + ); + }, + separatorBuilder: (context, index) => Container( + height: 1, + color: Theme.of(context) + .extension<StackColors>()! + .background, + ), + itemCount: coins.length, + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart new file mode 100644 index 000000000..ff7537126 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -0,0 +1,533 @@ +import 'dart:async'; + +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/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'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/progress_bar.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:zxcvbn/zxcvbn.dart'; + +class SecuritySettings extends ConsumerStatefulWidget { + const SecuritySettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuSecurity"; + + @override + ConsumerState<SecuritySettings> createState() => _SecuritySettings(); +} + +class _SecuritySettings extends ConsumerState<SecuritySettings> { + late bool changePassword = false; + + late final TextEditingController passwordCurrentController; + late final TextEditingController passwordController; + late final TextEditingController passwordRepeatController; + + late final FocusNode passwordCurrentFocusNode; + late final FocusNode passwordFocusNode; + late final FocusNode passwordRepeatFocusNode; + final zxcvbn = Zxcvbn(); + + bool hidePassword = true; + bool shouldShowPasswordHint = true; + + double passwordStrength = 0.0; + + bool get shouldEnableSave { + return passwordCurrentController.text.isNotEmpty && + passwordController.text.isNotEmpty && + passwordRepeatController.text.isNotEmpty; + } + + String passwordFeedback = + "Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters."; + + Future<bool> attemptChangePW() async { + final String pw = passwordCurrentController.text; + final String pwNew = passwordController.text; + final String pwNewRepeat = passwordRepeatController.text; + + final verified = + await ref.read(storageCryptoHandlerProvider).verifyPassphrase(pw); + + if (verified) { + if (pwNew != pwNewRepeat) { + await Future<void>.delayed(const Duration(seconds: 1)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "New passphrase does not match!", + context: context, + ), + ); + return false; + } else { + final success = + await ref.read(storageCryptoHandlerProvider).changePassphrase( + pw, + pwNew, + ); + + if (success) { + await Future<void>.delayed(const Duration(seconds: 1)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Passphrase successfully changed", + context: context, + ), + ); + return true; + } else { + await Future<void>.delayed(const Duration(seconds: 1)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase change failed", + context: context, + ), + ); + return false; + } + } + } else { + await Future<void>.delayed(const Duration(seconds: 1)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Current passphrase is not valid!", + context: context, + ), + ); + return false; + } + } + + @override + void initState() { + passwordCurrentController = TextEditingController(); + passwordController = TextEditingController(); + passwordRepeatController = TextEditingController(); + + passwordCurrentFocusNode = FocusNode(); + passwordFocusNode = FocusNode(); + passwordRepeatFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordCurrentController.dispose(); + passwordController.dispose(); + passwordRepeatController.dispose(); + + passwordCurrentFocusNode.dispose(); + passwordFocusNode.dispose(); + passwordRepeatFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleLock, + width: 48, + height: 48, + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Change Password", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox( + height: 16, + ), + Text( + "Protect your Stack Wallet with a strong password. Stack Wallet does not store " + "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + changePassword + ? SizedBox( + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Current password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldKey"), + focusNode: passwordCurrentFocusNode, + controller: passwordCurrentController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter current password", + passwordCurrentFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey"), + 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(() {}); + }, + ), + ), + const SizedBox(height: 16), + Text( + "New password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey1"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter new password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey1"), + 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( + "desktopSecurityCreateStackBackUpProgressBar"), + width: 450, + 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: 16), + Text( + "Confirm new password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey2"), + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm new password", + passwordRepeatFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey2"), + 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(() {}); + }, + ), + ), + const SizedBox(height: 20), + PrimaryButton( + width: 160, + buttonHeight: ButtonHeight.l, + enabled: shouldEnableSave, + label: "Save changes", + onPressed: () async { + final didChangePW = + await attemptChangePW(); + if (didChangePW) { + setState(() { + changePassword = false; + }); + } + }, + ) + ], + ), + ) + : PrimaryButton( + width: 210, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Set up new password", + onPressed: () { + setState(() { + changePassword = true; + }); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart index de800b51f..b743fdb39 100644 --- a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart +++ b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu_item.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class SettingsMenu extends ConsumerStatefulWidget { @@ -35,155 +34,167 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Material( - color: Theme.of(context).extension<StackColors>()!.background, - child: SizedBox( - width: 300, - child: Padding( - padding: const EdgeInsets.fromLTRB(24.0, 10.0, 0, 0), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - height: 20, - // width: 300, - ), - Text( - "Settings", - style: STextStyles.desktopH3(context).copyWith( - fontSize: 24, + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 0 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, ), + label: "Backup and restore", + value: 0, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, ), - Row( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB( - 3.0, - 30.0, - 55.0, - 0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Backup and restore", - value: 0, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Security", - value: 1, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Currency", - value: 2, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Language", - value: 3, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Nodes", - value: 4, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Syncing preferences", - value: 5, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Appearance", - value: 6, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Advanced", - value: 7, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - ], - ), - ), - ], + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Security", + value: 1, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 2 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Currency", + value: 2, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 3 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Language", + value: 3, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 4 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Nodes", + value: 4, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 5 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Syncing preferences", + value: 5, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 6 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Appearance", + value: 6, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 7 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Advanced", + value: 7, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, ), ], ), ), - ), + ], ); } } diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart new file mode 100644 index 000000000..720d77b8b --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/utilities/assets.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/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class SyncingPreferencesSettings extends ConsumerStatefulWidget { + const SyncingPreferencesSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuSyncingPref"; + + @override + ConsumerState<SyncingPreferencesSettings> createState() => + _SyncingPreferencesSettings(); +} + +class _SyncingPreferencesSettings + extends ConsumerState<SyncingPreferencesSettings> { + String _currentTypeDescription(SyncingType type) { + switch (type) { + case SyncingType.currentWalletOnly: + return "Sync only currently open wallet"; + case SyncingType.selectedWalletsAtStartup: + return "Sync only selected wallets at startup"; + case SyncingType.allWalletsOnStartup: + return "Sync all wallets at startup"; + } + } + + late bool changePrefs = false; + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleArrowRotate, + width: 48, + height: 48, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondaryDisabled, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + _currentTypeDescription(ref.watch( + prefsChangeNotifierProvider + .select((value) => value.syncType))), + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2), + textAlign: TextAlign.left, + ), + ), + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Syncing Preferences", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nSet up your syncing preferences for all wallets in your Stack.", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all( + 10, + ), + child: changePrefs + ? SizedBox( + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SyncingOptionsView(), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Save", + onPressed: () { + setState(() { + changePrefs = false; + }); + }, + ), + ], + ), + ) + : Column( + children: [ + const SizedBox(height: 10), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Change preferences", + onPressed: () { + setState(() { + changePrefs = true; + }); + }, + ), + ], + )), + ], + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart new file mode 100644 index 000000000..18988cb68 --- /dev/null +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart @@ -0,0 +1,716 @@ +import 'dart:convert'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS; +import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS; +import 'package:package_info_plus/package_info_plus.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/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:url_launcher/url_launcher.dart'; + +const kGithubAPI = "https://api.github.com"; +const kGithubSearch = "/search/commits"; +const kGithubHead = "/repos"; + +enum CommitStatus { isHead, isOldCommit, notACommit, notLoaded } + +Future<bool> doesCommitExist( + String organization, + String project, + String commit, +) async { + Logging.instance.log("doesCommitExist", level: LogLevel.Info); + final Client client = Client(); + try { + final uri = Uri.parse( + "$kGithubAPI$kGithubHead/$organization/$project/commits/$commit"); + + final commitQuery = await client.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + final response = jsonDecode(commitQuery.body.toString()); + Logging.instance.log("doesCommitExist $project $commit $response", + level: LogLevel.Info); + bool isThereCommit; + try { + isThereCommit = response['sha'] == commit; + Logging.instance + .log("isThereCommit $isThereCommit", level: LogLevel.Info); + return isThereCommit; + } catch (e, s) { + return false; + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Error); + return false; + } +} + +Future<bool> isHeadCommit( + String organization, + String project, + String branch, + String commit, +) async { + Logging.instance.log("doesCommitExist", level: LogLevel.Info); + final Client client = Client(); + try { + final uri = Uri.parse( + "$kGithubAPI$kGithubHead/$organization/$project/commits/$branch"); + + final commitQuery = await client.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + final response = jsonDecode(commitQuery.body.toString()); + Logging.instance.log("isHeadCommit $project $commit $branch $response", + level: LogLevel.Info); + bool isHead; + try { + isHead = response['sha'] == commit; + Logging.instance.log("isHead $isHead", level: LogLevel.Info); + return isHead; + } catch (e, s) { + return false; + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Error); + return false; + } +} + +class DesktopAboutView extends ConsumerWidget { + const DesktopAboutView({Key? key}) : super(key: key); + + static const String routeName = "/desktopAboutView"; + + @override + Widget build(BuildContext context, WidgetRef ref) { + String firoCommit = FIRO_VERSIONS.getPluginVersion(); + String epicCashCommit = EPIC_VERSIONS.getPluginVersion(); + String moneroCommit = MONERO_VERSIONS.getPluginVersion(); + List<Future> futureFiroList = [ + doesCommitExist("cypherstack", "flutter_liblelantus", firoCommit), + isHeadCommit("cypherstack", "flutter_liblelantus", "main", firoCommit), + ]; + Future commitFiroFuture = Future.wait(futureFiroList); + List<Future> futureEpicList = [ + doesCommitExist("cypherstack", "flutter_libepiccash", epicCashCommit), + isHeadCommit( + "cypherstack", "flutter_libepiccash", "main", epicCashCommit), + ]; + Future commitEpicFuture = Future.wait(futureEpicList); + List<Future> futureMoneroList = [ + doesCommitExist("cypherstack", "flutter_libmonero", moneroCommit), + isHeadCommit("cypherstack", "flutter_libmonero", "main", moneroCommit), + ]; + Future commitMoneroFuture = Future.wait(futureMoneroList); + + debugPrint("BUILD: $runtimeType"); + return DesktopScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + height: 24, + ), + Text( + "About", + style: STextStyles.desktopH3(context), + ) + ], + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 10, 24, 35), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + width: 929, + height: 411, + child: Padding( + padding: const EdgeInsets.only(left: 10, top: 10), + child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Stack Wallet", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.start, + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + RichText( + textAlign: TextAlign.start, + text: TextSpan( + style: STextStyles.label(context), + children: [ + TextSpan( + text: + "By using Stack Wallet, you agree to the ", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Terms of service", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html"), + mode: + LaunchMode.externalApplication, + ); + }, + ), + TextSpan( + text: " and ", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Privacy policy", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/privacy-policy.html"), + mode: + LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + Padding( + padding: + const EdgeInsets.only(right: 10, bottom: 10), + child: Column( + children: [ + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, + AsyncSnapshot<PackageInfo> snapshot) { + String version = ""; + String signature = ""; + String build = ""; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + version = snapshot.data!.version; + build = snapshot.data!.buildNumber; + signature = snapshot.data!.buildSignature; + } + + return Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Version", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + version, + style: + STextStyles.itemSubtitle( + context), + ), + ], + ), + const SizedBox( + width: 400, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Build number", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + build, + style: + STextStyles.itemSubtitle( + context), + ), + ], + ), + ], + ), + const SizedBox(height: 32), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Build signature", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + signature, + style: + STextStyles.itemSubtitle( + context), + ), + ], + ), + const SizedBox( + width: 350, + ), + FutureBuilder( + future: commitFiroFuture, + builder: (context, + AsyncSnapshot<dynamic> + snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + commitExists = snapshot + .data![0] as bool; + isHead = snapshot.data![1] + as bool; + if (commitExists && + isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus + .isOldCommit; + } else { + stateOfCommit = + CommitStatus + .notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus + .isOldCommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus + .notACommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Firo Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + firoCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + FutureBuilder( + future: commitEpicFuture, + builder: (context, + AsyncSnapshot<dynamic> + snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + commitExists = snapshot + .data![0] as bool; + isHead = snapshot.data![1] + as bool; + if (commitExists && + isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus + .isOldCommit; + } else { + stateOfCommit = + CommitStatus + .notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus + .isOldCommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus + .notACommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Epic Cash Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + epicCashCommit, + style: indicationStyle, + ), + ], + ); + }), + const SizedBox( + width: 105, + ), + FutureBuilder( + future: commitMoneroFuture, + builder: (context, + AsyncSnapshot<dynamic> + snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + commitExists = snapshot + .data![0] as bool; + isHead = snapshot.data![1] + as bool; + if (commitExists && + isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus + .isOldCommit; + } else { + stateOfCommit = + CommitStatus + .notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus + .isOldCommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus + .notACommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Monero Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + moneroCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Website", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + BlueTextButton( + text: + "https://stackwallet.com", + onTap: () { + launchUrl( + Uri.parse( + "https://stackwallet.com"), + mode: LaunchMode + .externalApplication, + ); + }, + ), + ], + ) + ], + ) + ], + ); + }, + ) + ], + ), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart new file mode 100644 index 000000000..ce3e3f3cc --- /dev/null +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/support_view.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 DesktopSupportView extends ConsumerStatefulWidget { + const DesktopSupportView({Key? key}) : super(key: key); + + static const String routeName = "/desktopSupportView"; + + @override + ConsumerState<DesktopSupportView> createState() => _DesktopSupportView(); +} + +class _DesktopSupportView extends ConsumerState<DesktopSupportView> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return DesktopScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + height: 24, + ), + Text( + "Support", + style: STextStyles.desktopH3(context), + ) + ], + ), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 10, 0, 0), + child: Row( + children: const [ + SizedBox( + width: 576, + child: SupportView(), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/providers/desktop/current_desktop_menu_item.dart b/lib/providers/desktop/current_desktop_menu_item.dart new file mode 100644 index 000000000..6a58db6a0 --- /dev/null +++ b/lib/providers/desktop/current_desktop_menu_item.dart @@ -0,0 +1,5 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; + +final currentDesktopMenuItemProvider = + StateProvider<DesktopMenuItemId>((ref) => DesktopMenuItemId.myStack); diff --git a/lib/providers/desktop/storage_crypto_handler_provider.dart b/lib/providers/desktop/storage_crypto_handler_provider.dart new file mode 100644 index 000000000..5b15ccaf3 --- /dev/null +++ b/lib/providers/desktop/storage_crypto_handler_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/utilities/desktop_password_service.dart'; + +final storageCryptoHandlerProvider = Provider<DPS>((ref) => DPS()); diff --git a/lib/providers/global/auto_swb_service_provider.dart b/lib/providers/global/auto_swb_service_provider.dart index 51a7c4e59..10cf2592f 100644 --- a/lib/providers/global/auto_swb_service_provider.dart +++ b/lib/providers/global/auto_swb_service_provider.dart @@ -1,5 +1,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/services/auto_swb_service.dart'; -final autoSWBServiceProvider = - ChangeNotifierProvider<AutoSWBService>((_) => AutoSWBService()); +final autoSWBServiceProvider = ChangeNotifierProvider<AutoSWBService>( + (ref) => AutoSWBService( + secureStorageInterface: ref.read(secureStoreProvider), + ), +); diff --git a/lib/providers/global/node_service_provider.dart b/lib/providers/global/node_service_provider.dart index 97cea48f9..81ab22426 100644 --- a/lib/providers/global/node_service_provider.dart +++ b/lib/providers/global/node_service_provider.dart @@ -1,15 +1,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/services/node_service.dart'; int _count = 0; final nodeServiceChangeNotifierProvider = - ChangeNotifierProvider<NodeService>((_) { + ChangeNotifierProvider<NodeService>((ref) { if (kDebugMode) { _count++; debugPrint( "nodeServiceChangeNotifierProvider instantiation count: $_count"); } - return NodeService(); + return NodeService(secureStorageInterface: ref.read(secureStoreProvider)); }); diff --git a/lib/providers/global/secure_store_provider.dart b/lib/providers/global/secure_store_provider.dart new file mode 100644 index 000000000..32304d0f2 --- /dev/null +++ b/lib/providers/global/secure_store_provider.dart @@ -0,0 +1,18 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/util.dart'; + +final secureStoreProvider = Provider<SecureStorageInterface>((ref) { + if (Util.isDesktop) { + final handler = ref.read(storageCryptoHandlerProvider).handler; + return SecureStorageWrapper( + store: DesktopSecureStore(handler), isDesktop: true); + } else { + return const SecureStorageWrapper( + store: FlutterSecureStorage(), + isDesktop: false, + ); + } +}); diff --git a/lib/providers/global/wallets_service_provider.dart b/lib/providers/global/wallets_service_provider.dart index 7e46d076b..363d57613 100644 --- a/lib/providers/global/wallets_service_provider.dart +++ b/lib/providers/global/wallets_service_provider.dart @@ -1,16 +1,19 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/services/wallets_service.dart'; int _count = 0; final walletsServiceChangeNotifierProvider = - ChangeNotifierProvider<WalletsService>((_) { + ChangeNotifierProvider<WalletsService>((ref) { if (kDebugMode) { _count++; debugPrint( "walletsServiceChangeNotifierProvider instantiation count: $_count"); } - return WalletsService(); + return WalletsService( + secureStorageInterface: ref.read(secureStoreProvider), + ); }); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index d318b85a6..cbc4cb343 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -37,7 +37,9 @@ import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; +import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; +import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart'; @@ -83,10 +85,29 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; +import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.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/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; +import 'package:stackwallet/pages_desktop_specific/home/notifications/desktop_notifications_view.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/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/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; import 'package:stackwallet/services/coins/manager.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'; @@ -94,6 +115,9 @@ import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:tuple/tuple.dart'; +import 'pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; +import 'pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart'; + class RouteGenerator { static const bool useMaterialPageRoute = true; @@ -769,6 +793,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ConfirmTransactionView.routeName: + if (args is Tuple2<Map<String, dynamic>, String>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ConfirmTransactionView( + transactionInfo: args.item1, + walletId: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case WalletInitiatedExchangeView.routeName: if (args is Tuple3<String, Coin, VoidCallback>) { return getRoute( @@ -944,6 +983,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case GenerateUriQrCodeView.routeName: + if (args is Tuple2<Coin, String>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => GenerateUriQrCodeView( + coin: args.item1, + receivingAddress: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == Desktop specific routes ============================================ case CreatePasswordView.routeName: return getRoute( @@ -951,17 +1005,35 @@ class RouteGenerator { builder: (_) => const CreatePasswordView(), settings: RouteSettings(name: settings.name)); + case ForgotPasswordDesktopView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const ForgotPasswordDesktopView(), + settings: RouteSettings(name: settings.name)); + case DesktopHomeView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const DesktopHomeView(), settings: RouteSettings(name: settings.name)); - // case DesktopSettingsView.routeName: - // return getRoute( - // shouldUseMaterialRoute: useMaterialPageRoute, - // builder: (_) => const DesktopSettingsView(), - // settings: RouteSettings(name: settings.name)); + case DesktopNotificationsView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopNotificationsView(), + settings: RouteSettings(name: settings.name)); + + case DesktopExchangeView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopExchangeView(), + settings: RouteSettings(name: settings.name)); + + case DesktopSettingsView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopSettingsView(), + settings: RouteSettings(name: settings.name)); case MyStackView.routeName: return getRoute( @@ -969,6 +1041,20 @@ class RouteGenerator { builder: (_) => const MyStackView(), settings: RouteSettings(name: settings.name)); + case DesktopWalletView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => DesktopWalletView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SettingsMenu.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, @@ -977,6 +1063,205 @@ class RouteGenerator { ), settings: RouteSettings(name: settings.name)); + case BackupRestoreSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const BackupRestoreSettings(), + settings: RouteSettings(name: settings.name)); + + case SecuritySettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const SecuritySettings(), + settings: RouteSettings(name: settings.name)); + + case CurrencySettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const CurrencySettings(), + settings: RouteSettings(name: settings.name)); + + case LanguageOptionSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const LanguageOptionSettings(), + settings: RouteSettings(name: settings.name)); + + case NodesSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const NodesSettings(), + settings: RouteSettings(name: settings.name)); + + case SyncingPreferencesSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const SyncingPreferencesSettings(), + settings: RouteSettings(name: settings.name)); + + case AppearanceOptionSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const AppearanceOptionSettings(), + settings: RouteSettings(name: settings.name)); + + case AdvancedSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const AdvancedSettings(), + settings: RouteSettings(name: settings.name)); + + case DesktopSupportView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopSupportView(), + settings: RouteSettings(name: settings.name)); + + case DesktopAboutView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopAboutView(), + settings: RouteSettings(name: settings.name)); + + case DesktopAddressBook.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopAddressBook(), + settings: RouteSettings(name: settings.name)); + + case WalletKeysDesktopPopup.routeName: + if (args is List<String>) { + return FadePageRoute( + WalletKeysDesktopPopup( + words: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case UnlockWalletKeysDesktop.routeName: + if (args is String) { + return FadePageRoute( + UnlockWalletKeysDesktop( + walletId: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case DesktopDeleteWalletDialog.routeName: + if (args is String) { + return FadePageRoute( + DesktopDeleteWalletDialog( + walletId: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case DesktopAttentionDeleteWallet.routeName: + if (args is String) { + return FadePageRoute( + DesktopAttentionDeleteWallet( + walletId: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case DeleteWalletKeysPopup.routeName: + if (args is Tuple2<String, List<String>>) { + return FadePageRoute( + DeleteWalletKeysPopup( + walletId: args.item1, + words: args.item2, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case QRCodeDesktopPopupContent.routeName: + if (args is String) { + return FadePageRoute( + QRCodeDesktopPopupContent( + value: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => QRCodeDesktopPopupContent( + // value: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == End of desktop specific routes ===================================== default: @@ -1047,3 +1332,37 @@ class RouteGenerator { builder: (_) => errorView); } } + +class FadePageRoute<T> extends PageRoute<T> { + FadePageRoute(this.child, RouteSettings settings) : _settings = settings; + + final Widget child; + final RouteSettings _settings; + + @override + Color? get barrierColor => null; + + @override + String? get barrierLabel => null; + + @override + Widget buildPage( + BuildContext context, + Animation<double> animation, + Animation<double> secondaryAnimation, + ) { + return FadeTransition( + opacity: animation, + child: child, + ); + } + + @override + bool get maintainState => true; + + @override + Duration get transitionDuration => const Duration(milliseconds: 100); + + @override + RouteSettings get settings => _settings; +} diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index 6f7d2b9bd..f51eefbba 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -20,10 +20,13 @@ class AddressBookService extends ChangeNotifier { List<Contact> get contacts { final keys = List<String>.from( DB.instance.keys<dynamic>(boxName: DB.boxNameAddressBook)); - return keys + final _contacts = keys .map((id) => Contact.fromJson(Map<String, dynamic>.from(DB.instance .get<dynamic>(boxName: DB.boxNameAddressBook, key: id) as Map))) .toList(growable: false); + _contacts + .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + return _contacts; } Future<List<Contact>>? _addressBookEntries; diff --git a/lib/services/auto_swb_service.dart b/lib/services/auto_swb_service.dart index e3ce02f15..f7efc994e 100644 --- a/lib/services/auto_swb_service.dart +++ b/lib/services/auto_swb_service.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -25,11 +24,9 @@ class AutoSWBService extends ChangeNotifier { bool _isActiveTimer = false; bool get isActivePeriodicTimer => _isActiveTimer; - final FlutterSecureStorageInterface secureStorageInterface; + final SecureStorageInterface secureStorageInterface; - AutoSWBService( - {this.secureStorageInterface = - const SecureStorageWrapper(FlutterSecureStorage())}); + AutoSWBService({required this.secureStorageInterface}); /// Attempt a backup. Future<void> doBackup() async { diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index da3bdfed0..25ec6b519 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -10,8 +10,8 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -174,9 +174,10 @@ class BitcoinWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -199,19 +200,21 @@ class BitcoinWallet extends CoinServiceAPI { Future<Decimal> get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future<Decimal> get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -221,13 +224,13 @@ class BitcoinWallet extends CoinServiceAPI { .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -265,7 +268,8 @@ class BitcoinWallet extends CoinServiceAPI { @override Future<int> get maxFee async { final fee = (await fees).fast as String; - final satsFee = Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = + Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -1092,7 +1096,8 @@ class BitcoinWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1282,6 +1287,54 @@ class BitcoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network); @@ -1307,7 +1360,7 @@ class BitcoinWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1319,7 +1372,7 @@ class BitcoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1329,13 +1382,12 @@ class BitcoinWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1373,7 +1425,8 @@ class BitcoinWallet extends CoinServiceAPI { } Future<ElectrumXNode> getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( @@ -1448,9 +1501,9 @@ class BitcoinWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1919,7 +1972,7 @@ class BitcoinWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1929,15 +1982,16 @@ class BitcoinWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2483,7 +2537,7 @@ class BitcoinWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"]!.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2496,7 +2550,7 @@ class BitcoinWallet extends CoinServiceAPI { final String address = output["scriptPubKey"]!["address"] as String; final value = output["value"]!; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2521,7 +2575,7 @@ class BitcoinWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["address"]; if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2544,7 +2598,7 @@ class BitcoinWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2566,7 +2620,7 @@ class BitcoinWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2576,7 +2630,7 @@ class BitcoinWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2660,6 +2714,7 @@ class BitcoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } @@ -3703,7 +3758,8 @@ class BitcoinWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 3a5cebdec..59b2454b4 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -6,13 +6,13 @@ import 'dart:typed_data'; import 'package:bech32/bech32.dart'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; -import 'package:bitbox/bitbox.dart' as Bitbox; +import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -170,19 +170,21 @@ class BitcoinCashWallet extends CoinServiceAPI { Future<Decimal> get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future<Decimal> get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -192,13 +194,13 @@ class BitcoinCashWallet extends CoinServiceAPI { .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -207,9 +209,9 @@ class BitcoinCashWallet extends CoinServiceAPI { _getCurrentAddressForChain(0, DerivePathType.bip44); Future<String>? _currentReceivingAddressP2PKH; - Future<String> get currentReceivingAddressP2SH => - _currentReceivingAddressP2SH ??= - _getCurrentAddressForChain(0, DerivePathType.bip49); + // Future<String> get currentReceivingAddressP2SH => + // _currentReceivingAddressP2SH ??= + // _getCurrentAddressForChain(0, DerivePathType.bip49); Future<String>? _currentReceivingAddressP2SH; @override @@ -232,8 +234,8 @@ class BitcoinCashWallet extends CoinServiceAPI { @override Future<int> get maxFee async { final fee = (await fees).fast; - final satsFee = - Format.satoshisToAmount(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = Format.satoshisToAmount(fee, coin: coin) * + Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -258,7 +260,7 @@ class BitcoinCashWallet extends CoinServiceAPI { } Future<void> updateStoredChainHeight({required int newHeight}) async { - DB.instance.put<dynamic>( + await DB.instance.put<dynamic>( boxName: walletId, key: "storedChainHeight", value: newHeight); } @@ -266,8 +268,13 @@ class BitcoinCashWallet extends CoinServiceAPI { Uint8List? decodeBase58; Segwit? decodeBech32; try { - if (Bitbox.Address.detectFormat(address) == 0) { - address = Bitbox.Address.toLegacyAddress(address); + if (bitbox.Address.detectFormat(address) == + bitbox.Address.formatCashAddr) { + if (validateCashAddr(address)) { + address = bitbox.Address.toLegacyAddress(address); + } else { + throw ArgumentError('$address is not currently supported'); + } } } catch (e, s) {} try { @@ -292,11 +299,14 @@ class BitcoinCashWallet extends CoinServiceAPI { } catch (err) { // Bech32 decode fail } - if (_network.bech32 != decodeBech32!.hrp) { - throw ArgumentError('Invalid prefix or Network mismatch'); - } - if (decodeBech32.version != 0) { - throw ArgumentError('Invalid address version'); + + if (decodeBech32 != null) { + if (_network.bech32 != decodeBech32.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + if (decodeBech32.version != 0) { + throw ArgumentError('Invalid address version'); + } } } throw ArgumentError('$address has no matching Script'); @@ -609,7 +619,9 @@ class BitcoinCashWallet extends CoinServiceAPI { // get address tx counts final counts = await _getBatchTxCount(addresses: txCountCallArgs); - print("Counts $counts"); + if (kDebugMode) { + print("Counts $counts"); + } // check and add appropriate addresses for (int k = 0; k < txCountBatchSize; k++) { int count = counts["${_id}_$k"]!; @@ -745,31 +757,35 @@ class BitcoinCashWallet extends CoinServiceAPI { // notify on new incoming transaction for (final tx in unconfirmedTxnsToNotifyPending) { if (tx.txType == "Received") { - NotificationApi.showNotification( - title: "Incoming transaction", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.now(), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, + unawaited( + NotificationApi.showNotification( + title: "Incoming transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + ), ); await txTracker.addNotifiedPending(tx.txid); } else if (tx.txType == "Sent") { - NotificationApi.showNotification( - title: "Sending transaction", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, + unawaited( + NotificationApi.showNotification( + title: "Sending transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + ), ); await txTracker.addNotifiedPending(tx.txid); } @@ -778,26 +794,30 @@ class BitcoinCashWallet extends CoinServiceAPI { // notify on confirmed for (final tx in unconfirmedTxnsToNotifyConfirmed) { if (tx.txType == "Received") { - NotificationApi.showNotification( - title: "Incoming transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.now(), - shouldWatchForUpdates: false, - coinName: coin.name, + unawaited( + NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: false, + coinName: coin.name, + ), ); await txTracker.addNotifiedConfirmed(tx.txid); } else if (tx.txType == "Sent") { - NotificationApi.showNotification( - title: "Outgoing transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.now(), - shouldWatchForUpdates: false, - coinName: coin.name, + unawaited( + NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: false, + coinName: coin.name, + ), ); await txTracker.addNotifiedConfirmed(tx.txid); } @@ -862,7 +882,7 @@ class BitcoinCashWallet extends CoinServiceAPI { if (currentHeight != storedHeight) { if (currentHeight != -1) { // -1 failed to fetch current height - updateStoredChainHeight(newHeight: currentHeight); + await updateStoredChainHeight(newHeight: currentHeight); } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); @@ -970,7 +990,8 @@ class BitcoinCashWallet extends CoinServiceAPI { } // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1143,14 +1164,82 @@ class BitcoinCashWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + + bool validateCashAddr(String cashAddr) { + String addr = cashAddr; + if (cashAddr.contains(":")) { + addr = cashAddr.split(":").last; + } + + return addr.startsWith("q"); + } + @override bool validateAddress(String address) { try { // 0 for bitcoincash: address scheme, 1 for legacy address - final format = Bitbox.Address.detectFormat(address); - print("format $format"); - return true; - } catch (e, s) { + final format = bitbox.Address.detectFormat(address); + if (kDebugMode) { + print("format $format"); + } + + if (_coin == Coin.bitcoincashTestnet) { + return true; + } + + if (format == bitbox.Address.formatCashAddr) { + return validateCashAddr(address); + } else { + return address.startsWith("1"); + } + } catch (e) { return false; } } @@ -1175,7 +1264,7 @@ class BitcoinCashWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1187,7 +1276,7 @@ class BitcoinCashWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1197,13 +1286,12 @@ class BitcoinCashWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1226,7 +1314,7 @@ class BitcoinCashWallet extends CoinServiceAPI { ); if (shouldRefresh) { - refresh(); + unawaited(refresh()); } } @@ -1241,7 +1329,8 @@ class BitcoinCashWallet extends CoinServiceAPI { } Future<ElectrumXNode> getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( @@ -1298,9 +1387,9 @@ class BitcoinCashWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1522,12 +1611,15 @@ class BitcoinCashWallet extends CoinServiceAPI { break; } - print("Array key is ${jsonEncode(arrayKey)}"); + if (kDebugMode) { + print("Array key is ${jsonEncode(arrayKey)}"); + } final internalChainArray = DB.instance.get<dynamic>(boxName: walletId, key: arrayKey); if (derivePathType == DerivePathType.bip44) { - if (Bitbox.Address.detectFormat(internalChainArray.last as String) == 1) { - return Bitbox.Address.toCashAddress(internalChainArray.last as String); + if (bitbox.Address.detectFormat(internalChainArray.last as String) == + bitbox.Address.formatLegacy) { + return bitbox.Address.toCashAddress(internalChainArray.last as String); } } return internalChainArray.last as String; @@ -1642,7 +1734,9 @@ class BitcoinCashWallet extends CoinServiceAPI { batches[batchNumber] = {}; } final scripthash = _convertToScriptHash(allAddresses[i], _network); - print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); + if (kDebugMode) { + print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); + } batches[batchNumber]!.addAll({ scripthash: [scripthash] }); @@ -1700,7 +1794,7 @@ class BitcoinCashWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1710,15 +1804,16 @@ class BitcoinCashWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -1818,20 +1913,28 @@ class BitcoinCashWallet extends CoinServiceAPI { }) async { try { final Map<String, List<dynamic>> args = {}; - print("Address $addresses"); + if (kDebugMode) { + print("Address $addresses"); + } for (final entry in addresses.entries) { args[entry.key] = [_convertToScriptHash(entry.value, _network)]; } - print("Args ${jsonEncode(args)}"); + if (kDebugMode) { + print("Args ${jsonEncode(args)}"); + } final response = await electrumXClient.getBatchHistory(args: args); - print("Response ${jsonEncode(response)}"); + if (kDebugMode) { + print("Response ${jsonEncode(response)}"); + } final Map<String, int> result = {}; for (final entry in response.entries) { result[entry.key] = entry.value.length; } - print("result ${jsonEncode(result)}"); + if (kDebugMode) { + print("result ${jsonEncode(result)}"); + } return result; } catch (e, s) { Logging.instance.log( @@ -1995,8 +2098,10 @@ class BitcoinCashWallet extends CoinServiceAPI { /// Returns the scripthash or throws an exception on invalid bch address String _convertToScriptHash(String bchAddress, NetworkType network) { try { - if (Bitbox.Address.detectFormat(bchAddress) == 0) { - bchAddress = Bitbox.Address.toLegacyAddress(bchAddress); + if (bitbox.Address.detectFormat(bchAddress) == + bitbox.Address.formatCashAddr && + validateCashAddr(bchAddress)) { + bchAddress = bitbox.Address.toLegacyAddress(bchAddress); } final output = Address.addressToOutputScript(bchAddress, network); final hash = sha256.convert(output.toList(growable: false)).toString(); @@ -2073,8 +2178,9 @@ class BitcoinCashWallet extends CoinServiceAPI { List<String> allAddressesOld = await _fetchAllOwnAddresses(); List<String> allAddresses = []; for (String address in allAddressesOld) { - if (Bitbox.Address.detectFormat(address) == 1) { - allAddresses.add(Bitbox.Address.toCashAddress(address)); + if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy && + addressType(address: address) == DerivePathType.bip44) { + allAddresses.add(bitbox.Address.toCashAddress(address)); } else { allAddresses.add(address); } @@ -2085,8 +2191,9 @@ class BitcoinCashWallet extends CoinServiceAPI { as List<dynamic>; List<dynamic> changeAddressesP2PKH = []; for (var address in changeAddressesP2PKHOld) { - if (Bitbox.Address.detectFormat(address as String) == 1) { - changeAddressesP2PKH.add(Bitbox.Address.toCashAddress(address)); + if (bitbox.Address.detectFormat(address as String) == + bitbox.Address.formatLegacy) { + changeAddressesP2PKH.add(bitbox.Address.toCashAddress(address)); } else { changeAddressesP2PKH.add(address); } @@ -2108,21 +2215,27 @@ class BitcoinCashWallet extends CoinServiceAPI { unconfirmedCachedTransactions .removeWhere((key, value) => value.confirmedStatus); - print("CACHED_TRANSACTIONS_IS $cachedTransactions"); + if (kDebugMode) { + print("CACHED_TRANSACTIONS_IS $cachedTransactions"); + } if (cachedTransactions != null) { for (final tx in allTxHashes.toList(growable: false)) { final txHeight = tx["height"] as int; if (txHeight > 0 && txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { - print(cachedTransactions.findTransaction(tx["tx_hash"] as String)); - print(unconfirmedCachedTransactions[tx["tx_hash"] as String]); + if (kDebugMode) { + print( + cachedTransactions.findTransaction(tx["tx_hash"] as String)); + print(unconfirmedCachedTransactions[tx["tx_hash"] as String]); + } final cachedTx = cachedTransactions.findTransaction(tx["tx_hash"] as String); if (!(cachedTx != null && addressType(address: cachedTx.address) == DerivePathType.bip44 && - Bitbox.Address.detectFormat(cachedTx.address) == 1)) { + bitbox.Address.detectFormat(cachedTx.address) == + bitbox.Address.formatLegacy)) { allTxHashes.remove(tx); } } @@ -2223,7 +2336,7 @@ class BitcoinCashWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2236,7 +2349,7 @@ class BitcoinCashWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["addresses"][0]; final value = output["value"]; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2261,7 +2374,7 @@ class BitcoinCashWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["addresses"][0]; if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2285,7 +2398,7 @@ class BitcoinCashWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2307,7 +2420,7 @@ class BitcoinCashWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2317,7 +2430,7 @@ class BitcoinCashWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2401,6 +2514,7 @@ class BitcoinCashWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } @@ -2782,8 +2896,14 @@ class BitcoinCashWallet extends CoinServiceAPI { final n = output["n"]; if (n != null && n == utxosToUse[i].vout) { String address = output["scriptPubKey"]["addresses"][0] as String; - if (Bitbox.Address.detectFormat(address) == 0) { - address = Bitbox.Address.toLegacyAddress(address); + if (bitbox.Address.detectFormat(address) == + bitbox.Address.formatCashAddr) { + if (validateCashAddr(address)) { + address = bitbox.Address.toLegacyAddress(address); + } else { + throw Exception( + "Unsupported address found during fetchBuildTxData(): $address"); + } } if (!addressTxid.containsKey(address)) { addressTxid[address] = <String>[]; @@ -2814,9 +2934,6 @@ class BitcoinCashWallet extends CoinServiceAPI { ); for (int i = 0; i < p2pkhLength; i++) { String address = addressesP2PKH[i]; - if (Bitbox.Address.detectFormat(address) == 0) { - address = Bitbox.Address.toLegacyAddress(address); - } // receives final receiveDerivation = receiveDerivations[address]; @@ -2950,36 +3067,36 @@ class BitcoinCashWallet extends CoinServiceAPI { required List<String> recipients, required List<int> satoshiAmounts, }) async { - final builder = Bitbox.Bitbox.transactionBuilder(); + final builder = bitbox.Bitbox.transactionBuilder(); // retrieve address' utxos from the rest api - List<Bitbox.Utxo> _utxos = + List<bitbox.Utxo> _utxos = []; // await Bitbox.Address.utxo(address) as List<Bitbox.Utxo>; - utxosToUse.forEach((element) { - _utxos.add(Bitbox.Utxo( + for (var element in utxosToUse) { + _utxos.add(bitbox.Utxo( element.txid, element.vout, - Bitbox.BitcoinCash.fromSatoshi(element.value), + bitbox.BitcoinCash.fromSatoshi(element.value), element.value, 0, MINIMUM_CONFIRMATIONS + 1)); - }); - Logger.print("bch utxos: ${_utxos}"); + } + Logger.print("bch utxos: $_utxos"); // placeholder for input signatures - final signatures = <Map>[]; + final List<Map<dynamic, dynamic>> signatures = []; // placeholder for total input balance - int totalBalance = 0; + // int totalBalance = 0; // iterate through the list of address _utxos and use them as inputs for the // withdrawal transaction - _utxos.forEach((Bitbox.Utxo utxo) { + for (var utxo in _utxos) { // add the utxo as an input for the transaction builder.addInput(utxo.txid, utxo.vout); final ec = utxoSigningData[utxo.txid]["keyPair"] as ECPair; - final bitboxEC = Bitbox.ECPair.fromWIF(ec.toWIF()); + final bitboxEC = bitbox.ECPair.fromWIF(ec.toWIF()); // add a signature to the list to be used later signatures.add({ @@ -2988,15 +3105,15 @@ class BitcoinCashWallet extends CoinServiceAPI { "original_amount": utxo.satoshis }); - totalBalance += utxo.satoshis; - }); + // totalBalance += utxo.satoshis; + } // calculate the fee based on number of inputs and one expected output - final fee = - Bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length); + // final fee = + // bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length); // calculate how much balance will be left over to spend after the fee - final sendAmount = totalBalance - fee; + // final sendAmount = totalBalance - fee; // add the output based on the address provided in the testing data for (int i = 0; i < recipients.length; i++) { @@ -3006,12 +3123,12 @@ class BitcoinCashWallet extends CoinServiceAPI { } // sign all inputs - signatures.forEach((signature) { + for (var signature in signatures) { builder.sign( signature["vin"] as int, - signature["key_pair"] as Bitbox.ECPair, + signature["key_pair"] as bitbox.ECPair, signature["original_amount"] as int); - }); + } // build the transaction final tx = builder.build(); @@ -3038,7 +3155,7 @@ class BitcoinCashWallet extends CoinServiceAPI { ); // clear cache - _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); + await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); // back up data await _rescanBackup(); @@ -3326,9 +3443,10 @@ class BitcoinCashWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -3343,7 +3461,8 @@ class BitcoinCashWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index d2ac174f5..697c59281 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -11,11 +11,13 @@ import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; -import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'litecoin/litecoin_wallet.dart'; + abstract class CoinServiceAPI { CoinServiceAPI(); @@ -23,6 +25,7 @@ abstract class CoinServiceAPI { Coin coin, String walletId, String walletName, + SecureStorageInterface secureStorageInterface, NodeModel node, TransactionNotificationTracker tracker, Prefs prefs, @@ -67,6 +70,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -76,6 +80,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -86,6 +91,29 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, + client: client, + cachedClient: cachedClient, + tracker: tracker, + ); + + case Coin.litecoin: + return LitecoinWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + secureStore: secureStorageInterface, + client: client, + cachedClient: cachedClient, + tracker: tracker, + ); + + case Coin.litecoinTestNet: + return LitecoinWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -96,6 +124,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -106,6 +135,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -116,6 +146,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -126,6 +157,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -136,6 +168,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, // tracker: tracker, ); @@ -144,6 +177,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, // tracker: tracker, ); @@ -161,6 +195,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, // tracker: tracker, ); @@ -169,6 +204,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, tracker: tracker, cachedClient: cachedClient, client: client, @@ -179,6 +215,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -265,4 +302,7 @@ abstract class CoinServiceAPI { Future<int> estimateFeeFor(int satoshiAmount, int feeRate); Future<bool> generateNewAddress(); + + // used for electrumx coins + Future<void> updateSentCachedTxData(Map<String, dynamic> txData); } diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 0235a0c02..f7372752b 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -10,8 +10,8 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -167,19 +167,21 @@ class DogecoinWallet extends CoinServiceAPI { Future<Decimal> get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future<Decimal> get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -189,13 +191,13 @@ class DogecoinWallet extends CoinServiceAPI { .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -225,8 +227,8 @@ class DogecoinWallet extends CoinServiceAPI { @override Future<int> get maxFee async { final fee = (await fees).fast; - final satsFee = - Format.satoshisToAmount(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = Format.satoshisToAmount(fee, coin: coin) * + Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -878,7 +880,8 @@ class DogecoinWallet extends CoinServiceAPI { } // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1051,6 +1054,54 @@ class DogecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network); @@ -1076,7 +1127,7 @@ class DogecoinWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1088,7 +1139,7 @@ class DogecoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1098,13 +1149,12 @@ class DogecoinWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1142,7 +1192,8 @@ class DogecoinWallet extends CoinServiceAPI { } Future<ElectrumXNode> getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( @@ -1199,9 +1250,9 @@ class DogecoinWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1602,7 +1653,7 @@ class DogecoinWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1612,15 +1663,16 @@ class DogecoinWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2096,7 +2148,7 @@ class DogecoinWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2109,7 +2161,7 @@ class DogecoinWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["addresses"][0]; final value = output["value"]; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2134,7 +2186,7 @@ class DogecoinWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["addresses"][0]; if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2157,7 +2209,7 @@ class DogecoinWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2179,7 +2231,7 @@ class DogecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2189,7 +2241,7 @@ class DogecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2273,6 +2325,7 @@ class DogecoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } @@ -2983,9 +3036,10 @@ class DogecoinWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -3000,7 +3054,8 @@ class DogecoinWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 7ccb7feaf..fcf728fb8 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -6,11 +6,9 @@ import 'dart:isolate'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_libepiccash/epic_cash.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:stack_wallet_backup/generate_password.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; @@ -32,6 +30,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; import 'package:tuple/tuple.dart'; @@ -251,26 +250,29 @@ Future<String> _deleteWalletWrapper(String wallet) async { Future<String> deleteEpicWallet({ required String walletId, - required FlutterSecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) async { - String? config = await secureStore.read(key: '${walletId}_config'); - if (Platform.isIOS) { - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - if (Platform.isLinux) { - appDir = Directory("${appDir.path}/.stackwallet"); - } - final path = "${appDir.path}/epiccash"; - final String name = walletId; - - final walletDir = '$path/$name'; - var editConfig = jsonDecode(config as String); - - editConfig["wallet_dir"] = walletDir; - config = jsonEncode(editConfig); - } + // is this even needed for anything? + // String? config = await secureStore.read(key: '${walletId}_config'); + // // TODO: why double check for iOS? + // if (Platform.isIOS) { + // Directory appDir = await StackFileSystem.applicationRootDirectory(); + // // todo why double check for ios? + // // if (Platform.isIOS) { + // // appDir = (await getLibraryDirectory()); + // // } + // // if (Platform.isLinux) { + // // appDir = Directory("${appDir.path}/.stackwallet"); + // // } + // final path = "${appDir.path}/epiccash"; + // final String name = walletId; + // + // final walletDir = '$path/$name'; + // var editConfig = jsonDecode(config as String); + // + // editConfig["wallet_dir"] = walletDir; + // config = jsonEncode(editConfig); + // } final wallet = await secureStore.read(key: '${walletId}_wallet'); @@ -518,14 +520,13 @@ class EpicCashWallet extends CoinServiceAPI { required String walletName, required Coin coin, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore}) { + required SecureStorageInterface secureStore}) { _walletId = walletId; _walletName = walletName; _coin = coin; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; Logging.instance.log("$walletName isolate length: ${isolates.length}", level: LogLevel.Info); @@ -537,7 +538,8 @@ class EpicCashWallet extends CoinServiceAPI { @override Future<void> updateNode(bool shouldRefresh) async { - _epicNode = NodeService().getPrimaryNodeFor(coin: coin) ?? + _epicNode = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); // TODO notify ui/ fire event for node changed? @@ -558,9 +560,10 @@ class EpicCashWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -658,7 +661,7 @@ class EpicCashWallet extends CoinServiceAPI { @override Coin get coin => _coin; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -832,10 +835,16 @@ class EpicCashWallet extends CoinServiceAPI { final txLogEntryFirst = txLogEntry[0]; Logger.print("TX_LOG_ENTRY_IS $txLogEntryFirst"); final wallet = await Hive.openBox<dynamic>(_walletId); - final slateToAddresses = (await wallet.get("slate_to_address")) as Map?; - slateToAddresses?[txLogEntryFirst['tx_slate_id']] = txData['addresss']; + final slateToAddresses = + (await wallet.get("slate_to_address")) as Map? ?? {}; + final slateId = txLogEntryFirst['tx_slate_id'] as String; + slateToAddresses[slateId] = txData['addresss']; await wallet.put('slate_to_address', slateToAddresses); - return txLogEntryFirst['tx_slate_id'] as String; + final slatesToCommits = await getSlatesToCommits(); + String? commitId = slatesToCommits[slateId]?['commitId'] as String?; + Logging.instance.log("sent commitId: $commitId", level: LogLevel.Info); + return commitId!; + // return txLogEntryFirst['tx_slate_id'] as String; } } catch (e, s) { Logging.instance.log("Error sending $e - $s", level: LogLevel.Error); @@ -1231,13 +1240,8 @@ class EpicCashWallet extends CoinServiceAPI { } Future<String> currentWalletDirPath() async { - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - if (Platform.isLinux) { - appDir = Directory("${appDir.path}/.stackwallet"); - } + Directory appDir = await StackFileSystem.applicationRootDirectory(); + final path = "${appDir.path}/epiccash"; final String name = _walletId.trim(); return '$path/$name'; @@ -2154,8 +2158,9 @@ class EpicCashWallet extends CoinServiceAPI { as String? ?? ""; String? commitId = slatesToCommits[slateId]?['commitId'] as String?; - Logging.instance - .log("commitId: $commitId $slateId", level: LogLevel.Info); + Logging.instance.log( + "commitId: $commitId, slateId: $slateId, id: ${tx["id"]}", + level: LogLevel.Info); bool isCancelled = tx["tx_type"] == "TxSentCancelled" || tx["tx_type"] == "TxReceivedCancelled"; @@ -2258,6 +2263,14 @@ class EpicCashWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + // not used in epic + TransactionData? cachedTxData; + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + // not used in epic + } + @override Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError(); diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 61ef2e9de..3e0cba75d 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -11,7 +11,6 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:lelantus/lelantus.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; @@ -621,7 +620,7 @@ Future<dynamic> isolateCreateJoinSplitTransaction( "txid": txId, "txHex": txHex, "value": amount, - "fees": Format.satoshisToAmount(fee).toDouble(), + "fees": Format.satoshisToAmount(fee, coin: coin).toDouble(), "fee": fee, "vSize": extTx.virtualSize(), "jmintValue": changeToMint, @@ -630,11 +629,11 @@ Future<dynamic> isolateCreateJoinSplitTransaction( "height": locktime, "txType": "Sent", "confirmed_status": false, - "amount": Format.satoshisToAmount(amount).toDouble(), + "amount": Format.satoshisToAmount(amount, coin: coin).toDouble(), "recipientAmt": amount, "worthNow": Format.localizedStringAsFixed( value: ((Decimal.fromInt(amount) * price) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale), @@ -821,9 +820,10 @@ class FiroWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -883,7 +883,7 @@ class FiroWallet extends CoinServiceAPI { Future<Decimal> get balanceMinusMaxFee async { final balances = await this.balances; final maxFee = await this.maxFee; - return balances[0] - Format.satoshisToAmount(maxFee); + return balances[0] - Format.satoshisToAmount(maxFee, coin: coin); } @override @@ -907,6 +907,52 @@ class FiroWallet extends CoinServiceAPI { Future<models.TransactionData> get _txnData => _transactionData ??= _fetchTransactionData(); + models.TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final currentPrice = await firoPrice; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + /// Holds wallet lelantus transaction data Future<models.TransactionData>? _lelantusTransactionData; Future<models.TransactionData> get lelantusTransactionData => @@ -1043,8 +1089,8 @@ class FiroWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; - final balance = - Format.decimalAmountToSatoshis(await availablePublicBalance()); + final balance = Format.decimalAmountToSatoshis( + await availablePublicBalance(), coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1109,6 +1155,9 @@ class FiroWallet extends CoinServiceAPI { final txHash = await _electrumXClient.broadcastTransaction( rawTx: txData["hex"] as String); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + txData["txid"] = txHash; + // dirty ui update hack + await updateSentCachedTxData(txData as Map<String, dynamic>); return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -1127,7 +1176,7 @@ class FiroWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; final balance = - Format.decimalAmountToSatoshis(await availablePrivateBalance()); + Format.decimalAmountToSatoshis(await availablePrivateBalance(), coin); if (satoshiAmount == balance) { // print("is send all"); isSendAll = true; @@ -1173,7 +1222,8 @@ class FiroWallet extends CoinServiceAPI { // temporarily update apdate available balance until a full refresh is done // TODO: something here causes an exception to be thrown giving user false info that the tx failed - Decimal sendTotal = Format.satoshisToAmount(txData["value"] as int); + Decimal sendTotal = + Format.satoshisToAmount(txData["value"] as int, coin: coin); sendTotal += Decimal.parse(txData["fees"].toString()); final bals = await balances; bals[0] -= sendTotal; @@ -1221,7 +1271,7 @@ class FiroWallet extends CoinServiceAPI { // temporarily update apdate available balance until a full refresh is done Decimal sendTotal = - Format.satoshisToAmount(txHexOrError["value"] as int); + Format.satoshisToAmount(txHexOrError["value"] as int, coin: coin); sendTotal += Decimal.parse(txHexOrError["fees"].toString()); final bals = await balances; bals[0] -= sendTotal; @@ -1256,7 +1306,7 @@ class FiroWallet extends CoinServiceAPI { late CachedElectrumX _cachedElectrumXClient; CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1271,7 +1321,7 @@ class FiroWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1281,8 +1331,7 @@ class FiroWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; Logging.instance.log("$walletName isolates length: ${isolates.length}", level: LogLevel.Info); @@ -1820,7 +1869,7 @@ class FiroWallet extends CoinServiceAPI { @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map( (e) => ElectrumXNode( @@ -2285,8 +2334,9 @@ class FiroWallet extends CoinServiceAPI { Future<int> _fetchMaxFee() async { final balance = await availableBalance; - int spendAmount = - (balance * Decimal.fromInt(Constants.satsPerCoin)).toBigInt().toInt(); + int spendAmount = (balance * Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); int fee = await estimateJoinSplitFee(spendAmount); return fee; } @@ -2432,18 +2482,20 @@ class FiroWallet extends CoinServiceAPI { } final int utxosIntValue = utxos.satoshiBalance; - final Decimal utxosValue = Format.satoshisToAmount(utxosIntValue); + final Decimal utxosValue = + Format.satoshisToAmount(utxosIntValue, coin: coin); List<Decimal> balances = List.empty(growable: true); - Decimal lelantusBalance = Format.satoshisToAmount(intLelantusBalance); + Decimal lelantusBalance = + Format.satoshisToAmount(intLelantusBalance, coin: coin); balances.add(lelantusBalance); balances.add(lelantusBalance * price); Decimal _unconfirmedLelantusBalance = - Format.satoshisToAmount(unconfirmedLelantusBalance); + Format.satoshisToAmount(unconfirmedLelantusBalance, coin: coin); balances.add(lelantusBalance + utxosValue + _unconfirmedLelantusBalance); @@ -2455,7 +2507,7 @@ class FiroWallet extends CoinServiceAPI { if (availableSats < 0) { availableSats = 0; } - balances.add(Format.satoshisToAmount(availableSats)); + balances.add(Format.satoshisToAmount(availableSats, coin: coin)); Logging.instance.log("balances $balances", level: LogLevel.Info); await DB.instance.put<dynamic>( @@ -2553,7 +2605,8 @@ class FiroWallet extends CoinServiceAPI { final feesObject = await fees; - final Decimal fastFee = Format.satoshisToAmount(feesObject.fast); + final Decimal fastFee = + Format.satoshisToAmount(feesObject.fast, coin: coin); int firoFee = (dvsize * fastFee * Decimal.fromInt(100000)).toDouble().ceil(); // int firoFee = (vsize * feesObject.fast * (1 / 1000.0) * 100000000).ceil(); @@ -2741,15 +2794,15 @@ class FiroWallet extends CoinServiceAPI { "txid": txId, "txHex": txHex, "value": amount - fee, - "fees": Format.satoshisToAmount(fee).toDouble(), + "fees": Format.satoshisToAmount(fee, coin: coin).toDouble(), "publicCoin": "", "height": height, "txType": "Sent", "confirmed_status": false, - "amount": Format.satoshisToAmount(amount).toDouble(), + "amount": Format.satoshisToAmount(amount, coin: coin).toDouble(), "worthNow": Format.localizedStringAsFixed( value: ((Decimal.fromInt(amount) * price) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!), @@ -2992,9 +3045,9 @@ class FiroWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -3021,7 +3074,8 @@ class FiroWallet extends CoinServiceAPI { } Future<ElectrumXNode> _getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( @@ -3279,7 +3333,7 @@ class FiroWallet extends CoinServiceAPI { if (nFees != null) { nFeesUsed = true; fees = (Decimal.parse(nFees.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -3304,7 +3358,7 @@ class FiroWallet extends CoinServiceAPI { if (value != null) { if (changeAddresses.contains(address)) { inputAmtSentFromWallet -= (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } else { @@ -3314,7 +3368,7 @@ class FiroWallet extends CoinServiceAPI { } if (value != null) { outAmount += (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -3327,7 +3381,7 @@ class FiroWallet extends CoinServiceAPI { final nFees = input["nFees"]; if (nFees != null) { fees += (Decimal.parse(nFees.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -3342,7 +3396,7 @@ class FiroWallet extends CoinServiceAPI { if (allAddresses.contains(address)) { outputAmtAddressedToWallet += (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); outAddress = address; @@ -3364,7 +3418,7 @@ class FiroWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -3379,7 +3433,7 @@ class FiroWallet extends CoinServiceAPI { final worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -3464,6 +3518,7 @@ class FiroWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } @@ -3539,7 +3594,7 @@ class FiroWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -3550,15 +3605,16 @@ class FiroWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -4521,8 +4577,9 @@ class FiroWallet extends CoinServiceAPI { ) async { var lelantusEntry = await _getLelantusEntry(); final balance = await availableBalance; - int spendAmount = - (balance * Decimal.fromInt(Constants.satsPerCoin)).toBigInt().toInt(); + int spendAmount = (balance * Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); if (spendAmount == 0 || lelantusEntry.isEmpty) { return LelantusFeeData(0, 0, []).fee; } @@ -4583,7 +4640,7 @@ class FiroWallet extends CoinServiceAPI { Future<int> estimateFeeForPublic(int satoshiAmount, int feeRate) async { final available = - Format.decimalAmountToSatoshis(await availablePublicBalance()); + Format.decimalAmountToSatoshis(await availablePublicBalance(), coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart new file mode 100644 index 000000000..9c4bb2305 --- /dev/null +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -0,0 +1,3884 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:bech32/bech32.dart'; +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bitcoindart/bitcoindart.dart'; +import 'package:bs58check/bs58check.dart' as bs58check; +import 'package:crypto/crypto.dart'; +import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; +import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/models/models.dart' as models; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/models/paymint/utxo_model.dart'; +import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_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/services/node_service.dart'; +import 'package:stackwallet/services/notifications_api.dart'; +import 'package:stackwallet/services/price.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:tuple/tuple.dart'; +import 'package:uuid/uuid.dart'; + +const int MINIMUM_CONFIRMATIONS = 1; +const int DUST_LIMIT = 294; + +const String GENESIS_HASH_MAINNET = + "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2"; +const String GENESIS_HASH_TESTNET = + "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0"; + +enum DerivePathType { bip44, bip49, bip84 } + +bip32.BIP32 getBip32Node( + int chain, + int index, + String mnemonic, + NetworkType network, + DerivePathType derivePathType, +) { + final root = getBip32Root(mnemonic, network); + + final node = getBip32NodeFromRoot(chain, index, root, derivePathType); + return node; +} + +/// wrapper for compute() +bip32.BIP32 getBip32NodeWrapper( + Tuple5<int, int, String, NetworkType, DerivePathType> args, +) { + return getBip32Node( + args.item1, + args.item2, + args.item3, + args.item4, + args.item5, + ); +} + +bip32.BIP32 getBip32NodeFromRoot( + int chain, + int index, + bip32.BIP32 root, + DerivePathType derivePathType, +) { + String coinType; + switch (root.network.wif) { + case 0xb0: // ltc mainnet wif + coinType = "2"; // ltc mainnet + break; + case 0xef: // ltc testnet wif + coinType = "1"; // ltc testnet + break; + default: + throw Exception("Invalid Litecoin network type used!"); + } + switch (derivePathType) { + case DerivePathType.bip44: + return root.derivePath("m/44'/$coinType'/0'/$chain/$index"); + case DerivePathType.bip49: + return root.derivePath("m/49'/$coinType'/0'/$chain/$index"); + case DerivePathType.bip84: + return root.derivePath("m/84'/$coinType'/0'/$chain/$index"); + default: + throw Exception("DerivePathType must not be null."); + } +} + +/// wrapper for compute() +bip32.BIP32 getBip32NodeFromRootWrapper( + Tuple4<int, int, bip32.BIP32, DerivePathType> args, +) { + return getBip32NodeFromRoot( + args.item1, + args.item2, + args.item3, + args.item4, + ); +} + +bip32.BIP32 getBip32Root(String mnemonic, NetworkType network) { + final seed = bip39.mnemonicToSeed(mnemonic); + final networkType = bip32.NetworkType( + wif: network.wif, + bip32: bip32.Bip32Type( + public: network.bip32.public, + private: network.bip32.private, + ), + ); + + final root = bip32.BIP32.fromSeed(seed, networkType); + return root; +} + +/// wrapper for compute() +bip32.BIP32 getBip32RootWrapper(Tuple2<String, NetworkType> args) { + return getBip32Root(args.item1, args.item2); +} + +class LitecoinWallet extends CoinServiceAPI { + static const integrationTestFlag = + bool.fromEnvironment("IS_INTEGRATION_TEST"); + + final _prefs = Prefs.instance; + + Timer? timer; + late Coin _coin; + + late final TransactionNotificationTracker txTracker; + + NetworkType get _network { + switch (coin) { + case Coin.litecoin: + return litecoin; + case Coin.litecoinTestNet: + return litecointestnet; + default: + throw Exception("Invalid network type!"); + } + } + + List<UtxoObject> outputsList = []; + + @override + set isFavorite(bool markFavorite) { + DB.instance.put<dynamic>( + boxName: walletId, key: "isFavorite", value: markFavorite); + } + + @override + bool get isFavorite { + try { + return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") + as bool; + } catch (e, s) { + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; + } + } + + @override + Coin get coin => _coin; + + @override + Future<List<String>> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future<List<String>>? _allOwnAddresses; + + Future<UtxoData>? _utxoData; + Future<UtxoData> get utxoData => _utxoData ??= _fetchUtxoData(); + + @override + Future<List<UtxoObject>> get unspentOutputs async => + (await utxoData).unspentOutputArray; + + @override + Future<Decimal> get availableBalance async { + final data = await utxoData; + return Format.satoshisToAmount( + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); + } + + @override + Future<Decimal> get pendingBalance async { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); + } + + @override + Future<Decimal> get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(); + + @override + Future<Decimal> get totalBalance async { + if (!isActive) { + final totalBalance = DB.instance + .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; + if (totalBalance == null) { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); + } else { + return Format.satoshisToAmount(totalBalance, coin: coin); + } + } + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); + } + + @override + Future<String> get currentReceivingAddress => _currentReceivingAddress ??= + _getCurrentAddressForChain(0, DerivePathType.bip84); + Future<String>? _currentReceivingAddress; + + Future<String> get currentLegacyReceivingAddress => + _currentReceivingAddressP2PKH ??= + _getCurrentAddressForChain(0, DerivePathType.bip44); + Future<String>? _currentReceivingAddressP2PKH; + + Future<String> get currentReceivingAddressP2SH => + _currentReceivingAddressP2SH ??= + _getCurrentAddressForChain(0, DerivePathType.bip49); + Future<String>? _currentReceivingAddressP2SH; + + @override + Future<void> exit() async { + _hasCalledExit = true; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } + + bool _hasCalledExit = false; + + @override + bool get hasCalledExit => _hasCalledExit; + + @override + Future<FeeObject> get fees => _feeObject ??= _getFees(); + Future<FeeObject>? _feeObject; + + @override + Future<int> get maxFee async { + final fee = (await fees).fast as String; + final satsFee = + Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); + return satsFee.floor().toBigInt().toInt(); + } + + @override + Future<List<String>> get mnemonic => _getMnemonicList(); + + Future<int> get chainHeight async { + try { + final result = await _electrumXClient.getBlockHeadTip(); + return result["height"] as int; + } catch (e, s) { + Logging.instance.log("Exception caught in chainHeight: $e\n$s", + level: LogLevel.Error); + return -1; + } + } + + int get storedChainHeight { + final storedHeight = DB.instance + .get<dynamic>(boxName: walletId, key: "storedChainHeight") as int?; + return storedHeight ?? 0; + } + + Future<void> updateStoredChainHeight({required int newHeight}) async { + await DB.instance.put<dynamic>( + boxName: walletId, key: "storedChainHeight", value: newHeight); + } + + DerivePathType addressType({required String address}) { + Uint8List? decodeBase58; + Segwit? decodeBech32; + try { + decodeBase58 = bs58check.decode(address); + } catch (err) { + // Base58check decode fail + } + if (decodeBase58 != null) { + if (decodeBase58[0] == _network.pubKeyHash) { + // P2PKH + return DerivePathType.bip44; + } + if (decodeBase58[0] == _network.scriptHash) { + // P2SH + return DerivePathType.bip49; + } + throw ArgumentError('Invalid version or Network mismatch'); + } else { + try { + decodeBech32 = segwit.decode(address, _network.bech32!); + } catch (err) { + // Bech32 decode fail + } + if (_network.bech32 != decodeBech32!.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + if (decodeBech32.version != 0) { + throw ArgumentError('Invalid address version'); + } + // P2WPKH + return DerivePathType.bip84; + } + } + + bool longMutex = false; + + @override + Future<void> recoverFromMnemonic({ + required String mnemonic, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { + longMutex = true; + final start = DateTime.now(); + try { + Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag", + level: LogLevel.Info); + if (!integrationTestFlag) { + final features = await electrumXClient.getServerFeatures(); + Logging.instance.log("features: $features", level: LogLevel.Info); + switch (coin) { + case Coin.litecoin: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.litecoinTestNet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a LitecoinWallet using a non litecoin coin type: ${coin.name}"); + } + // if (_networkType == BasicNetworkType.main) { + // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + // throw Exception("genesis hash does not match main net!"); + // } + // } else if (_networkType == BasicNetworkType.test) { + // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + // throw Exception("genesis hash does not match test net!"); + // } + // } + } + // check to make sure we aren't overwriting a mnemonic + // this should never fail + if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + longMutex = false; + throw Exception("Attempted to overwrite mnemonic on restore!"); + } + await _secureStore.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); + await _recoverWalletFromBIP32SeedPhrase( + mnemonic: mnemonic.trim(), + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, + ); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + longMutex = false; + rethrow; + } + longMutex = false; + + final end = DateTime.now(); + Logging.instance.log( + "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", + level: LogLevel.Info); + } + + Future<Map<String, dynamic>> _checkGaps( + int maxNumberOfIndexesToCheck, + int maxUnusedAddressGap, + int txCountBatchSize, + bip32.BIP32 root, + DerivePathType type, + int account) async { + List<String> addressArray = []; + int returningIndex = -1; + Map<String, Map<String, String>> derivations = {}; + int gapCounter = 0; + for (int index = 0; + index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap; + index += txCountBatchSize) { + List<String> iterationsAddressArray = []; + Logging.instance.log( + "index: $index, \t GapCounter $account ${type.name}: $gapCounter", + level: LogLevel.Info); + + final _id = "k_$index"; + Map<String, String> txCountCallArgs = {}; + final Map<String, dynamic> receivingNodes = {}; + + for (int j = 0; j < txCountBatchSize; j++) { + final node = await compute( + getBip32NodeFromRootWrapper, + Tuple4( + account, + index + j, + root, + type, + ), + ); + String? address; + switch (type) { + case DerivePathType.bip44: + address = P2PKH( + data: PaymentData(pubkey: node.publicKey), + network: _network) + .data + .address!; + break; + case DerivePathType.bip49: + address = P2SH( + data: PaymentData( + redeem: P2WPKH( + data: PaymentData(pubkey: node.publicKey), + network: _network, + overridePrefix: _network.bech32!) + .data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH( + network: _network, + data: PaymentData(pubkey: node.publicKey), + overridePrefix: _network.bech32!) + .data + .address!; + break; + default: + throw Exception("No Path type $type exists"); + } + receivingNodes.addAll({ + "${_id}_$j": { + "node": node, + "address": address, + } + }); + txCountCallArgs.addAll({ + "${_id}_$j": address, + }); + } + + // get address tx counts + final counts = await _getBatchTxCount(addresses: txCountCallArgs); + + // check and add appropriate addresses + for (int k = 0; k < txCountBatchSize; k++) { + int count = counts["${_id}_$k"]!; + if (count > 0) { + final node = receivingNodes["${_id}_$k"]; + // add address to array + addressArray.add(node["address"] as String); + iterationsAddressArray.add(node["address"] as String); + // set current index + returningIndex = index + k; + // reset counter + gapCounter = 0; + // add info to derivations + derivations[node["address"] as String] = { + "pubKey": Format.uint8listToString( + (node["node"] as bip32.BIP32).publicKey), + "wif": (node["node"] as bip32.BIP32).toWIF(), + }; + } + + // increase counter when no tx history found + if (count == 0) { + gapCounter++; + } + } + // cache all the transactions while waiting for the current function to finish. + unawaited(getTransactionCacheEarly(iterationsAddressArray)); + } + return { + "addressArray": addressArray, + "index": returningIndex, + "derivations": derivations + }; + } + + Future<void> getTransactionCacheEarly(List<String> allAddresses) async { + try { + final List<Map<String, dynamic>> allTxHashes = + await _fetchHistory(allAddresses); + for (final txHash in allTxHashes) { + try { + unawaited(cachedElectrumXClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + )); + } catch (e) { + continue; + } + } + } catch (e) { + // + } + } + + Future<void> _recoverWalletFromBIP32SeedPhrase({ + required String mnemonic, + int maxUnusedAddressGap = 20, + int maxNumberOfIndexesToCheck = 1000, + }) async { + longMutex = true; + + Map<String, Map<String, String>> p2pkhReceiveDerivations = {}; + Map<String, Map<String, String>> p2shReceiveDerivations = {}; + Map<String, Map<String, String>> p2wpkhReceiveDerivations = {}; + Map<String, Map<String, String>> p2pkhChangeDerivations = {}; + Map<String, Map<String, String>> p2shChangeDerivations = {}; + Map<String, Map<String, String>> p2wpkhChangeDerivations = {}; + + final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); + + List<String> p2pkhReceiveAddressArray = []; + List<String> p2shReceiveAddressArray = []; + List<String> p2wpkhReceiveAddressArray = []; + int p2pkhReceiveIndex = -1; + int p2shReceiveIndex = -1; + int p2wpkhReceiveIndex = -1; + + List<String> p2pkhChangeAddressArray = []; + List<String> p2shChangeAddressArray = []; + List<String> p2wpkhChangeAddressArray = []; + int p2pkhChangeIndex = -1; + int p2shChangeIndex = -1; + int p2wpkhChangeIndex = -1; + + // actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3 + const txCountBatchSize = 12; + + try { + // receiving addresses + Logging.instance + .log("checking receiving addresses...", level: LogLevel.Info); + final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); + + final resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 0); + + final resultReceive84 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 0); + + Logging.instance + .log("checking change addresses...", level: LogLevel.Info); + // change addresses + final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); + + final resultChange49 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 1); + + final resultChange84 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 1); + + await Future.wait([ + resultReceive44, + resultReceive49, + resultReceive84, + resultChange44, + resultChange49, + resultChange84 + ]); + + p2pkhReceiveAddressArray = + (await resultReceive44)['addressArray'] as List<String>; + p2pkhReceiveIndex = (await resultReceive44)['index'] as int; + p2pkhReceiveDerivations = (await resultReceive44)['derivations'] + as Map<String, Map<String, String>>; + + p2shReceiveAddressArray = + (await resultReceive49)['addressArray'] as List<String>; + p2shReceiveIndex = (await resultReceive49)['index'] as int; + p2shReceiveDerivations = (await resultReceive49)['derivations'] + as Map<String, Map<String, String>>; + + p2wpkhReceiveAddressArray = + (await resultReceive84)['addressArray'] as List<String>; + p2wpkhReceiveIndex = (await resultReceive84)['index'] as int; + p2wpkhReceiveDerivations = (await resultReceive84)['derivations'] + as Map<String, Map<String, String>>; + + p2pkhChangeAddressArray = + (await resultChange44)['addressArray'] as List<String>; + p2pkhChangeIndex = (await resultChange44)['index'] as int; + p2pkhChangeDerivations = (await resultChange44)['derivations'] + as Map<String, Map<String, String>>; + + p2shChangeAddressArray = + (await resultChange49)['addressArray'] as List<String>; + p2shChangeIndex = (await resultChange49)['index'] as int; + p2shChangeDerivations = (await resultChange49)['derivations'] + as Map<String, Map<String, String>>; + + p2wpkhChangeAddressArray = + (await resultChange84)['addressArray'] as List<String>; + p2wpkhChangeIndex = (await resultChange84)['index'] as int; + p2wpkhChangeDerivations = (await resultChange84)['derivations'] + as Map<String, Map<String, String>>; + + // save the derivations (if any) + if (p2pkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhReceiveDerivations); + } + if (p2shReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip49, + derivationsToAdd: p2shReceiveDerivations); + } + if (p2wpkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip84, + derivationsToAdd: p2wpkhReceiveDerivations); + } + if (p2pkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhChangeDerivations); + } + if (p2shChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip49, + derivationsToAdd: p2shChangeDerivations); + } + if (p2wpkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip84, + derivationsToAdd: p2wpkhChangeDerivations); + } + + // If restoring a wallet that never received any funds, then set receivingArray manually + // If we didn't do this, it'd store an empty array + if (p2pkhReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip44); + p2pkhReceiveAddressArray.add(address); + p2pkhReceiveIndex = 0; + } + if (p2shReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip49); + p2shReceiveAddressArray.add(address); + p2shReceiveIndex = 0; + } + if (p2wpkhReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip84); + p2wpkhReceiveAddressArray.add(address); + p2wpkhReceiveIndex = 0; + } + + // If restoring a wallet that never sent any funds with change, then set changeArray + // manually. If we didn't do this, it'd store an empty array. + if (p2pkhChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip44); + p2pkhChangeAddressArray.add(address); + p2pkhChangeIndex = 0; + } + if (p2shChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip49); + p2shChangeAddressArray.add(address); + p2shChangeIndex = 0; + } + if (p2wpkhChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip84); + p2wpkhChangeAddressArray.add(address); + p2wpkhChangeIndex = 0; + } + + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2WPKH', + value: p2wpkhReceiveAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2WPKH', + value: p2wpkhChangeAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: p2pkhReceiveAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: p2pkhChangeAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2SH', + value: p2shReceiveAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2SH', + value: p2shChangeAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH', + value: p2wpkhReceiveIndex); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2WPKH', + value: p2wpkhChangeIndex); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: p2pkhReceiveIndex); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2SH', + value: p2shReceiveIndex); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'changeIndexP2SH', value: p2shChangeIndex); + await DB.instance + .put<dynamic>(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put<dynamic>(boxName: walletId, key: "isFavorite", value: false); + + longMutex = false; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s", + level: LogLevel.Error); + + longMutex = false; + rethrow; + } + } + + Future<bool> refreshIfThereIsNewData() async { + if (longMutex) return false; + if (_hasCalledExit) return false; + Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); + + try { + bool needsRefresh = false; + Set<String> txnsToCheck = {}; + + for (final String txid in txTracker.pendings) { + if (!txTracker.wasNotifiedConfirmed(txid)) { + txnsToCheck.add(txid); + } + } + + for (String txid in txnsToCheck) { + final txn = await electrumXClient.getTransaction(txHash: txid); + int confirmations = txn["confirmations"] as int? ?? 0; + bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; + if (!isUnconfirmed) { + // unconfirmedTxs = {}; + needsRefresh = true; + break; + } + } + if (!needsRefresh) { + var allOwnAddresses = await _fetchAllOwnAddresses(); + List<Map<String, dynamic>> allTxs = + await _fetchHistory(allOwnAddresses); + final txData = await transactionData; + for (Map<String, dynamic> transaction in allTxs) { + if (txData.findTransaction(transaction['tx_hash'] as String) == + null) { + Logging.instance.log( + " txid not found in address history already ${transaction['tx_hash']}", + level: LogLevel.Info); + needsRefresh = true; + break; + } + } + } + return needsRefresh; + } catch (e, s) { + Logging.instance.log( + "Exception caught in refreshIfThereIsNewData: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> getAllTxsToWatch( + TransactionData txData, + ) async { + if (_hasCalledExit) return; + List<models.Transaction> unconfirmedTxnsToNotifyPending = []; + List<models.Transaction> unconfirmedTxnsToNotifyConfirmed = []; + + for (final chunk in txData.txChunks) { + for (final tx in chunk.transactions) { + if (tx.confirmedStatus) { + // get all transactions that were notified as pending but not as confirmed + if (txTracker.wasNotifiedPending(tx.txid) && + !txTracker.wasNotifiedConfirmed(tx.txid)) { + unconfirmedTxnsToNotifyConfirmed.add(tx); + } + } else { + // get all transactions that were not notified as pending yet + if (!txTracker.wasNotifiedPending(tx.txid)) { + unconfirmedTxnsToNotifyPending.add(tx); + } + } + } + } + + // notify on unconfirmed transactions + for (final tx in unconfirmedTxnsToNotifyPending) { + if (tx.txType == "Received") { + unawaited(NotificationApi.showNotification( + title: "Incoming transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } else if (tx.txType == "Sent") { + unawaited(NotificationApi.showNotification( + title: "Sending transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } + } + + // notify on confirmed + for (final tx in unconfirmedTxnsToNotifyConfirmed) { + if (tx.txType == "Received") { + unawaited(NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } else if (tx.txType == "Sent") { + unawaited(NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } + } + } + + bool _shouldAutoSync = false; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } else { + startNetworkAlivePinging(); + refresh(); + } + } + } + + @override + bool get isRefreshing => refreshMutex; + + bool refreshMutex = false; + + //TODO Show percentages properly/more consistently + /// Refreshes display data for the wallet + @override + Future<void> refresh() async { + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + try { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); + + final currentHeight = await chainHeight; + const storedHeight = 1; //await storedChainHeight; + + Logging.instance + .log("chain height: $currentHeight", level: LogLevel.Info); + Logging.instance + .log("cached height: $storedHeight", level: LogLevel.Info); + + if (currentHeight != storedHeight) { + if (currentHeight != -1) { + // -1 failed to fetch current height + unawaited(updateStoredChainHeight(newHeight: currentHeight)); + } + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); + final changeAddressForTransactions = + _checkChangeAddressForTransactions(DerivePathType.bip84); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); + final currentReceivingAddressesForTransactions = + _checkCurrentReceivingAddressesForTransactions(); + + final newTxData = _fetchTransactionData(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.50, walletId)); + + final newUtxoData = _fetchUtxoData(); + final feeObj = _getFees(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.60, walletId)); + + _transactionData = Future(() => newTxData); + + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.70, walletId)); + _feeObject = Future(() => feeObj); + _utxoData = Future(() => newUtxoData); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.80, walletId)); + + final allTxsToWatch = getAllTxsToWatch(await newTxData); + await Future.wait([ + newTxData, + changeAddressForTransactions, + currentReceivingAddressesForTransactions, + newUtxoData, + feeObj, + allTxsToWatch, + ]); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.90, walletId)); + } + + refreshMutex = false; + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + + if (shouldAutoSync) { + timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { + Logging.instance.log( + "Periodic refresh check for $walletId $walletName in object instance: $hashCode", + level: LogLevel.Info); + // chain height check currently broken + // if ((await chainHeight) != (await storedChainHeight)) { + if (await refreshIfThereIsNewData()) { + await refresh(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + } + // } + }); + } + } catch (error, strace) { + refreshMutex = false; + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + coin, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + Logging.instance.log( + "Caught exception in refreshWalletData(): $error\n$strace", + level: LogLevel.Error); + } + } + + @override + Future<Map<String, dynamic>> prepareSend({ + required String address, + required int satoshiAmount, + Map<String, dynamic>? args, + }) async { + try { + final feeRateType = args?["feeRate"]; + final feeRateAmount = args?["feeRateAmount"]; + if (feeRateType is FeeRateType || feeRateAmount is int) { + late final int rate; + if (feeRateType is FeeRateType) { + int fee = 0; + final feeObject = await fees; + switch (feeRateType) { + case FeeRateType.fast: + fee = feeObject.fast; + break; + case FeeRateType.average: + fee = feeObject.medium; + break; + case FeeRateType.slow: + fee = feeObject.slow; + break; + } + rate = fee; + } else { + rate = feeRateAmount as int; + } + + // check for send all + bool isSendAll = false; + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); + if (satoshiAmount == balance) { + isSendAll = true; + } + + final txData = + await coinSelection(satoshiAmount, rate, address, isSendAll); + + Logging.instance.log("prepare send: $txData", level: LogLevel.Info); + try { + if (txData is int) { + switch (txData) { + case 1: + throw Exception("Insufficient balance!"); + case 2: + throw Exception( + "Insufficient funds to pay for transaction fee!"); + default: + throw Exception("Transaction failed with error code $txData"); + } + } else { + final hex = txData["hex"]; + + if (hex is String) { + final fee = txData["fee"] as int; + final vSize = txData["vSize"] as int; + + Logging.instance + .log("prepared txHex: $hex", level: LogLevel.Info); + Logging.instance.log("prepared fee: $fee", level: LogLevel.Info); + Logging.instance + .log("prepared vSize: $vSize", level: LogLevel.Info); + + // fee should never be less than vSize sanity check + if (fee < vSize) { + throw Exception( + "Error in fee calculation: Transaction fee cannot be less than vSize"); + } + + return txData as Map<String, dynamic>; + } else { + throw Exception("prepared hex is not a String!!!"); + } + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future<String> confirmSend({required Map<String, dynamic> txData}) async { + try { + Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); + + final hex = txData["hex"] as String; + + final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex); + Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + + return txHash; + } catch (e, s) { + Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future<String> send({ + required String toAddress, + required int amount, + Map<String, String> args = const {}, + }) async { + try { + final txData = await prepareSend( + address: toAddress, satoshiAmount: amount, args: args); + final txHash = await confirmSend(txData: txData); + return txHash; + } catch (e, s) { + Logging.instance + .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + Future<bool> testNetworkConnection() async { + try { + final result = await _electrumXClient.ping(); + return result; + } catch (_) { + return false; + } + } + + Timer? _networkAliveTimer; + + void startNetworkAlivePinging() { + // call once on start right away + _periodicPingCheck(); + + // then periodically check + _networkAliveTimer = Timer.periodic( + Constants.networkAliveTimerDuration, + (_) async { + _periodicPingCheck(); + }, + ); + } + + void _periodicPingCheck() async { + bool hasNetwork = await testNetworkConnection(); + _isConnected = hasNetwork; + if (_isConnected != hasNetwork) { + NodeConnectionStatus status = hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + GlobalEventBus.instance + .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); + } + } + + void stopNetworkAlivePinging() { + _networkAliveTimer?.cancel(); + _networkAliveTimer = null; + } + + bool _isConnected = false; + + @override + bool get isConnected => _isConnected; + + @override + Future<void> initializeNew() async { + Logging.instance + .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); + + if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) != null) { + throw Exception( + "Attempted to initialize a new wallet using an existing wallet ID!"); + } + + await _prefs.init(); + try { + await _generateNewWallet(); + } catch (e, s) { + Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", + level: LogLevel.Fatal); + rethrow; + } + await Future.wait([ + DB.instance.put<dynamic>(boxName: walletId, key: "id", value: walletId), + DB.instance + .put<dynamic>(boxName: walletId, key: "isFavorite", value: false), + ]); + } + + @override + Future<void> initializeExisting() async { + Logging.instance.log("Opening existing ${coin.prettyName} wallet.", + level: LogLevel.Info); + + if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) == null) { + throw Exception( + "Attempted to initialize an existing wallet using an unknown wallet ID!"); + } + await _prefs.init(); + final data = + DB.instance.get<dynamic>(boxName: walletId, key: "latest_tx_model") + as TransactionData?; + if (data != null) { + _transactionData = Future(() => data); + } + } + + @override + Future<TransactionData> get transactionData => + _transactionData ??= _fetchTransactionData(); + Future<TransactionData>? _transactionData; + + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + + @override + bool validateAddress(String address) { + return Address.validateAddress(address, _network, _network.bech32!); + } + + @override + String get walletId => _walletId; + late String _walletId; + + @override + String get walletName => _walletName; + late String _walletName; + + // setter for updating on rename + @override + set walletName(String newName) => _walletName = newName; + + late ElectrumX _electrumXClient; + + ElectrumX get electrumXClient => _electrumXClient; + + late CachedElectrumX _cachedElectrumXClient; + + CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; + + late SecureStorageInterface _secureStore; + + late PriceAPI _priceAPI; + + LitecoinWallet({ + required String walletId, + required String walletName, + required Coin coin, + required ElectrumX client, + required CachedElectrumX cachedClient, + required TransactionNotificationTracker tracker, + PriceAPI? priceAPI, + required SecureStorageInterface secureStore, + }) { + txTracker = tracker; + _walletId = walletId; + _walletName = walletName; + _coin = coin; + _electrumXClient = client; + _cachedElectrumXClient = cachedClient; + + _priceAPI = priceAPI ?? PriceAPI(Client()); + _secureStore = secureStore; + } + + @override + Future<void> updateNode(bool shouldRefresh) async { + final failovers = NodeService(secureStorageInterface: _secureStore) + .failoverNodesFor(coin: coin) + .map((e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + )) + .toList(); + final newNode = await getCurrentNode(); + _cachedElectrumXClient = CachedElectrumX.from( + node: newNode, + prefs: _prefs, + failovers: failovers, + ); + _electrumXClient = ElectrumX.from( + node: newNode, + prefs: _prefs, + failovers: failovers, + ); + + if (shouldRefresh) { + unawaited(refresh()); + } + } + + Future<List<String>> _getMnemonicList() async { + final mnemonicString = + await _secureStore.read(key: '${_walletId}_mnemonic'); + if (mnemonicString == null) { + return []; + } + final List<String> data = mnemonicString.split(' '); + return data; + } + + Future<ElectrumXNode> getCurrentNode() async { + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + + return ElectrumXNode( + address: node.host, + port: node.port, + name: node.name, + useSSL: node.useSSL, + id: node.id, + ); + } + + Future<List<String>> _fetchAllOwnAddresses() async { + final List<String> allAddresses = []; + final receivingAddresses = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2WPKH') as List<dynamic>; + final changeAddresses = DB.instance.get<dynamic>( + boxName: walletId, key: 'changeAddressesP2WPKH') as List<dynamic>; + final receivingAddressesP2PKH = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2PKH') as List<dynamic>; + final changeAddressesP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH') + as List<dynamic>; + final receivingAddressesP2SH = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2SH') as List<dynamic>; + final changeAddressesP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2SH') + as List<dynamic>; + + for (var i = 0; i < receivingAddresses.length; i++) { + if (!allAddresses.contains(receivingAddresses[i])) { + allAddresses.add(receivingAddresses[i] as String); + } + } + for (var i = 0; i < changeAddresses.length; i++) { + if (!allAddresses.contains(changeAddresses[i])) { + allAddresses.add(changeAddresses[i] as String); + } + } + for (var i = 0; i < receivingAddressesP2PKH.length; i++) { + if (!allAddresses.contains(receivingAddressesP2PKH[i])) { + allAddresses.add(receivingAddressesP2PKH[i] as String); + } + } + for (var i = 0; i < changeAddressesP2PKH.length; i++) { + if (!allAddresses.contains(changeAddressesP2PKH[i])) { + allAddresses.add(changeAddressesP2PKH[i] as String); + } + } + for (var i = 0; i < receivingAddressesP2SH.length; i++) { + if (!allAddresses.contains(receivingAddressesP2SH[i])) { + allAddresses.add(receivingAddressesP2SH[i] as String); + } + } + for (var i = 0; i < changeAddressesP2SH.length; i++) { + if (!allAddresses.contains(changeAddressesP2SH[i])) { + allAddresses.add(changeAddressesP2SH[i] as String); + } + } + return allAddresses; + } + + Future<FeeObject> _getFees() async { + try { + //TODO adjust numbers for different speeds? + const int f = 1, m = 5, s = 20; + + final fast = await electrumXClient.estimateFee(blocks: f); + final medium = await electrumXClient.estimateFee(blocks: m); + final slow = await electrumXClient.estimateFee(blocks: s); + + final feeObject = FeeObject( + numberOfBlocksFast: f, + numberOfBlocksAverage: m, + numberOfBlocksSlow: s, + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), + ); + + Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); + return feeObject; + } catch (e) { + Logging.instance + .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); + rethrow; + } + } + + Future<void> _generateNewWallet() async { + Logging.instance + .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); + if (!integrationTestFlag) { + final features = await electrumXClient.getServerFeatures(); + Logging.instance.log("features: $features", level: LogLevel.Info); + switch (coin) { + case Coin.litecoin: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + print(features['genesis_hash']); + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.litecoinTestNet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a LitecoinWallet using a non litecoin coin type: ${coin.name}"); + } + } + + // this should never fail + if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + throw Exception( + "Attempted to overwrite mnemonic on generate new wallet!"); + } + await _secureStore.write( + key: '${_walletId}_mnemonic', + value: bip39.generateMnemonic(strength: 256)); + + // Set relevant indexes + await DB.instance + .put<dynamic>(boxName: walletId, key: "receivingIndexP2WPKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "changeIndexP2WPKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "receivingIndexP2PKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "changeIndexP2PKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "receivingIndexP2SH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "changeIndexP2SH", value: 0); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'blocked_tx_hashes', + value: ["0xdefault"], + ); // A list of transaction hashes to represent frozen utxos in wallet + // initialize address book entries + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'addressBookEntries', + value: <String, String>{}); + + // Generate and add addresses to relevant arrays + await Future.wait([ + // P2WPKH + _generateAddressForChain(0, 0, DerivePathType.bip84).then( + (initialReceivingAddressP2WPKH) { + _addToAddressesArrayForChain( + initialReceivingAddressP2WPKH, 0, DerivePathType.bip84); + _currentReceivingAddress = + Future(() => initialReceivingAddressP2WPKH); + }, + ), + _generateAddressForChain(1, 0, DerivePathType.bip84).then( + (initialChangeAddressP2WPKH) => _addToAddressesArrayForChain( + initialChangeAddressP2WPKH, + 1, + DerivePathType.bip84, + ), + ), + + // P2PKH + _generateAddressForChain(0, 0, DerivePathType.bip44).then( + (initialReceivingAddressP2PKH) { + _addToAddressesArrayForChain( + initialReceivingAddressP2PKH, 0, DerivePathType.bip44); + _currentReceivingAddressP2PKH = + Future(() => initialReceivingAddressP2PKH); + }, + ), + _generateAddressForChain(1, 0, DerivePathType.bip44).then( + (initialChangeAddressP2PKH) => _addToAddressesArrayForChain( + initialChangeAddressP2PKH, + 1, + DerivePathType.bip44, + ), + ), + + // P2SH + _generateAddressForChain(0, 0, DerivePathType.bip49).then( + (initialReceivingAddressP2SH) { + _addToAddressesArrayForChain( + initialReceivingAddressP2SH, 0, DerivePathType.bip49); + _currentReceivingAddressP2SH = + Future(() => initialReceivingAddressP2SH); + }, + ), + _generateAddressForChain(1, 0, DerivePathType.bip49).then( + (initialChangeAddressP2SH) => _addToAddressesArrayForChain( + initialChangeAddressP2SH, + 1, + DerivePathType.bip49, + ), + ), + ]); + + // // P2PKH + // _generateAddressForChain(0, 0, DerivePathType.bip44).then( + // (initialReceivingAddressP2PKH) { + // _addToAddressesArrayForChain( + // initialReceivingAddressP2PKH, 0, DerivePathType.bip44); + // this._currentReceivingAddressP2PKH = + // Future(() => initialReceivingAddressP2PKH); + // }, + // ); + // _generateAddressForChain(1, 0, DerivePathType.bip44) + // .then((initialChangeAddressP2PKH) => _addToAddressesArrayForChain( + // initialChangeAddressP2PKH, + // 1, + // DerivePathType.bip44, + // )); + // + // // P2SH + // _generateAddressForChain(0, 0, DerivePathType.bip49).then( + // (initialReceivingAddressP2SH) { + // _addToAddressesArrayForChain( + // initialReceivingAddressP2SH, 0, DerivePathType.bip49); + // this._currentReceivingAddressP2SH = + // Future(() => initialReceivingAddressP2SH); + // }, + // ); + // _generateAddressForChain(1, 0, DerivePathType.bip49) + // .then((initialChangeAddressP2SH) => _addToAddressesArrayForChain( + // initialChangeAddressP2SH, + // 1, + // DerivePathType.bip49, + // )); + + Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); + } + + /// Generates a new internal or external chain address for the wallet using a BIP84, BIP44, or BIP49 derivation path. + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + /// [index] - This can be any integer >= 0 + Future<String> _generateAddressForChain( + int chain, + int index, + DerivePathType derivePathType, + ) async { + final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); + final node = await compute( + getBip32NodeWrapper, + Tuple5( + chain, + index, + mnemonic!, + _network, + derivePathType, + ), + ); + final data = PaymentData(pubkey: node.publicKey); + String address; + + switch (derivePathType) { + case DerivePathType.bip44: + address = P2PKH(data: data, network: _network).data.address!; + break; + case DerivePathType.bip49: + address = P2SH( + data: PaymentData( + redeem: P2WPKH( + data: data, + network: _network, + overridePrefix: _network.bech32!) + .data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH( + network: _network, data: data, overridePrefix: _network.bech32!) + .data + .address!; + break; + } + + // add generated address & info to derivations + await addDerivation( + chain: chain, + address: address, + pubKey: Format.uint8listToString(node.publicKey), + wif: node.toWIF(), + derivePathType: derivePathType, + ); + + return address; + } + + /// Increases the index for either the internal or external chain, depending on [chain]. + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future<void> _incrementAddressIndexForChain( + int chain, DerivePathType derivePathType) async { + // Here we assume chain == 1 if it isn't 0 + String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + + final newIndex = + (DB.instance.get<dynamic>(boxName: walletId, key: indexKey)) + 1; + await DB.instance + .put<dynamic>(boxName: walletId, key: indexKey, value: newIndex); + } + + /// Adds [address] to the relevant chain's address array, which is determined by [chain]. + /// [address] - Expects a standard native segwit address + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future<void> _addToAddressesArrayForChain( + String address, int chain, DerivePathType derivePathType) async { + String chainArray = ''; + if (chain == 0) { + chainArray = 'receivingAddresses'; + } else { + chainArray = 'changeAddresses'; + } + switch (derivePathType) { + case DerivePathType.bip44: + chainArray += "P2PKH"; + break; + case DerivePathType.bip49: + chainArray += "P2SH"; + break; + case DerivePathType.bip84: + chainArray += "P2WPKH"; + break; + } + + final addressArray = + DB.instance.get<dynamic>(boxName: walletId, key: chainArray); + if (addressArray == null) { + Logging.instance.log( + 'Attempting to add the following to $chainArray array for chain $chain:${[ + address + ]}', + level: LogLevel.Info); + await DB.instance + .put<dynamic>(boxName: walletId, key: chainArray, value: [address]); + } else { + // Make a deep copy of the existing list + final List<String> newArray = []; + addressArray + .forEach((dynamic _address) => newArray.add(_address as String)); + newArray.add(address); // Add the address passed into the method + await DB.instance + .put<dynamic>(boxName: walletId, key: chainArray, value: newArray); + } + } + + /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] + /// and + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future<String> _getCurrentAddressForChain( + int chain, DerivePathType derivePathType) async { + // Here, we assume that chain == 1 if it isn't 0 + String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; + switch (derivePathType) { + case DerivePathType.bip44: + arrayKey += "P2PKH"; + break; + case DerivePathType.bip49: + arrayKey += "P2SH"; + break; + case DerivePathType.bip84: + arrayKey += "P2WPKH"; + break; + } + final internalChainArray = + DB.instance.get<dynamic>(boxName: walletId, key: arrayKey); + return internalChainArray.last as String; + } + + String _buildDerivationStorageKey({ + required int chain, + required DerivePathType derivePathType, + }) { + String key; + String chainId = chain == 0 ? "receive" : "change"; + switch (derivePathType) { + case DerivePathType.bip44: + key = "${walletId}_${chainId}DerivationsP2PKH"; + break; + case DerivePathType.bip49: + key = "${walletId}_${chainId}DerivationsP2SH"; + break; + case DerivePathType.bip84: + key = "${walletId}_${chainId}DerivationsP2WPKH"; + break; + } + return key; + } + + Future<Map<String, dynamic>> _fetchDerivations({ + required int chain, + required DerivePathType derivePathType, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + return Map<String, dynamic>.from( + jsonDecode(derivationsString ?? "{}") as Map); + } + + /// Add a single derivation to the local secure storage for [chain] and + /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. + /// This will overwrite a previous entry where the address of the new derivation + /// matches a derivation currently stored. + Future<void> addDerivation({ + required int chain, + required String address, + required String pubKey, + required String wif, + required DerivePathType derivePathType, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + final derivations = + Map<String, dynamic>.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations[address] = { + "pubKey": pubKey, + "wif": wif, + }; + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + /// Add multiple derivations to the local secure storage for [chain] and + /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. + /// This will overwrite any previous entries where the address of the new derivation + /// matches a derivation currently stored. + /// The [derivationsToAdd] must be in the format of: + /// { + /// addressA : { + /// "pubKey": <the pubKey string>, + /// "wif": <the wif string>, + /// }, + /// addressB : { + /// "pubKey": <the pubKey string>, + /// "wif": <the wif string>, + /// }, + /// } + Future<void> addDerivations({ + required int chain, + required DerivePathType derivePathType, + required Map<String, dynamic> derivationsToAdd, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + final derivations = + Map<String, dynamic>.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations.addAll(derivationsToAdd); + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + Future<UtxoData> _fetchUtxoData() async { + final List<String> allAddresses = await _fetchAllOwnAddresses(); + + try { + final fetchedUtxoList = <List<Map<String, dynamic>>>[]; + + final Map<int, Map<String, List<dynamic>>> batches = {}; + const batchSizeMax = 100; + int batchNumber = 0; + for (int i = 0; i < allAddresses.length; i++) { + if (batches[batchNumber] == null) { + batches[batchNumber] = {}; + } + final scripthash = _convertToScriptHash(allAddresses[i], _network); + batches[batchNumber]!.addAll({ + scripthash: [scripthash] + }); + if (i % batchSizeMax == batchSizeMax - 1) { + batchNumber++; + } + } + + for (int i = 0; i < batches.length; i++) { + final response = + await _electrumXClient.getBatchUTXOs(args: batches[i]!); + for (final entry in response.entries) { + if (entry.value.isNotEmpty) { + fetchedUtxoList.add(entry.value); + } + } + } + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List<Map<String, dynamic>> outputArray = []; + int satoshiBalance = 0; + int satoshiBalancePending = 0; + + for (int i = 0; i < fetchedUtxoList.length; i++) { + for (int j = 0; j < fetchedUtxoList[i].length; j++) { + int value = fetchedUtxoList[i][j]["value"] as int; + satoshiBalance += value; + + final txn = await cachedElectrumXClient.getTransaction( + txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + final Map<String, dynamic> utxo = {}; + final int confirmations = txn["confirmations"] as int? ?? 0; + final bool confirmed = confirmations >= MINIMUM_CONFIRMATIONS; + if (!confirmed) { + satoshiBalancePending += value; + } + + utxo["txid"] = txn["txid"]; + utxo["vout"] = fetchedUtxoList[i][j]["tx_pos"]; + utxo["value"] = value; + + utxo["status"] = <String, dynamic>{}; + utxo["status"]["confirmed"] = confirmed; + utxo["status"]["confirmations"] = confirmations; + utxo["status"]["block_height"] = fetchedUtxoList[i][j]["height"]; + utxo["status"]["block_hash"] = txn["blockhash"]; + utxo["status"]["block_time"] = txn["blocktime"]; + + final fiatValue = ((Decimal.fromInt(value) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2); + utxo["rawWorth"] = fiatValue; + utxo["fiatWorth"] = fiatValue.toString(); + outputArray.add(utxo); + } + } + + Decimal currencyBalanceRaw = + ((Decimal.fromInt(satoshiBalance) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2); + + final Map<String, dynamic> result = { + "total_user_currency": currencyBalanceRaw.toString(), + "total_sats": satoshiBalance, + "total_btc": (Decimal.fromInt(satoshiBalance) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) + .toString(), + "outputArray": outputArray, + "unconfirmed": satoshiBalancePending, + }; + + final dataModel = UtxoData.fromJson(result); + + final List<UtxoObject> allOutputs = dataModel.unspentOutputArray; + Logging.instance + .log('Outputs fetched: $allOutputs', level: LogLevel.Info); + await _sortOutputs(allOutputs); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_utxo_model', value: dataModel); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'totalBalance', + value: dataModel.satoshiBalance); + return dataModel; + } catch (e, s) { + Logging.instance + .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); + final latestTxModel = + DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model') + as models.UtxoData?; + + if (latestTxModel == null) { + final emptyModel = { + "total_user_currency": "0.00", + "total_sats": 0, + "total_btc": "0", + "outputArray": <dynamic>[] + }; + return UtxoData.fromJson(emptyModel); + } else { + Logging.instance + .log("Old output model located", level: LogLevel.Warning); + return latestTxModel; + } + } + } + + /// Takes in a list of UtxoObjects and adds a name (dependent on object index within list) + /// and checks for the txid associated with the utxo being blocked and marks it accordingly. + /// Now also checks for output labeling. + Future<void> _sortOutputs(List<UtxoObject> utxos) async { + final blockedHashArray = + DB.instance.get<dynamic>(boxName: walletId, key: 'blocked_tx_hashes') + as List<dynamic>?; + final List<String> lst = []; + if (blockedHashArray != null) { + for (var hash in blockedHashArray) { + lst.add(hash as String); + } + } + final labels = + DB.instance.get<dynamic>(boxName: walletId, key: 'labels') as Map? ?? + {}; + + outputsList = []; + + for (var i = 0; i < utxos.length; i++) { + if (labels[utxos[i].txid] != null) { + utxos[i].txName = labels[utxos[i].txid] as String? ?? ""; + } else { + utxos[i].txName = 'Output #$i'; + } + + if (utxos[i].status.confirmed == false) { + outputsList.add(utxos[i]); + } else { + if (lst.contains(utxos[i].txid)) { + utxos[i].blocked = true; + outputsList.add(utxos[i]); + } else if (!lst.contains(utxos[i].txid)) { + outputsList.add(utxos[i]); + } + } + } + } + + Future<int> getTxCount({required String address}) async { + String? scripthash; + try { + scripthash = _convertToScriptHash(address, _network); + final transactions = + await electrumXClient.getHistory(scripthash: scripthash); + return transactions.length; + } catch (e) { + Logging.instance.log( + "Exception rethrown in _getTxCount(address: $address, scripthash: $scripthash): $e", + level: LogLevel.Error); + rethrow; + } + } + + Future<Map<String, int>> _getBatchTxCount({ + required Map<String, String> addresses, + }) async { + try { + final Map<String, List<dynamic>> args = {}; + for (final entry in addresses.entries) { + args[entry.key] = [_convertToScriptHash(entry.value, _network)]; + } + final response = await electrumXClient.getBatchHistory(args: args); + + final Map<String, int> result = {}; + for (final entry in response.entries) { + result[entry.key] = entry.value.length; + } + return result; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown in _getBatchTxCount(address: $addresses: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _checkReceivingAddressForTransactions( + DerivePathType derivePathType) async { + try { + final String currentExternalAddr = + await _getCurrentAddressForChain(0, derivePathType); + final int txCount = await getTxCount(address: currentExternalAddr); + Logging.instance.log( + 'Number of txs for current receiving address $currentExternalAddr: $txCount', + level: LogLevel.Info); + + if (txCount >= 1) { + // First increment the receiving index + await _incrementAddressIndexForChain(0, derivePathType); + + // Check the new receiving index + String indexKey = "receivingIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + final newReceivingIndex = + DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new receiving address + final newReceivingAddress = await _generateAddressForChain( + 0, newReceivingIndex, derivePathType); + + // Add that new receiving address to the array of receiving addresses + await _addToAddressesArrayForChain( + newReceivingAddress, 0, derivePathType); + + // Set the new receiving address that the service + + switch (derivePathType) { + case DerivePathType.bip44: + _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); + break; + case DerivePathType.bip49: + _currentReceivingAddressP2SH = Future(() => newReceivingAddress); + break; + case DerivePathType.bip84: + _currentReceivingAddress = Future(() => newReceivingAddress); + break; + } + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _checkChangeAddressForTransactions( + DerivePathType derivePathType) async { + try { + final String currentExternalAddr = + await _getCurrentAddressForChain(1, derivePathType); + final int txCount = await getTxCount(address: currentExternalAddr); + Logging.instance.log( + 'Number of txs for current change address $currentExternalAddr: $txCount', + level: LogLevel.Info); + + if (txCount >= 1) { + // First increment the change index + await _incrementAddressIndexForChain(1, derivePathType); + + // Check the new change index + String indexKey = "changeIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + final newChangeIndex = + DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new change address + final newChangeAddress = + await _generateAddressForChain(1, newChangeIndex, derivePathType); + + // Add that new receiving address to the array of change addresses + await _addToAddressesArrayForChain(newChangeAddress, 1, derivePathType); + } + } on SocketException catch (se, s) { + Logging.instance.log( + "SocketException caught in _checkReceivingAddressForTransactions($derivePathType): $se\n$s", + level: LogLevel.Error); + return; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _checkCurrentReceivingAddressesForTransactions() async { + try { + for (final type in DerivePathType.values) { + await _checkReceivingAddressForTransactions(type); + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + /// public wrapper because dart can't test private... + Future<void> checkCurrentReceivingAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentReceivingAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + Future<void> _checkCurrentChangeAddressesForTransactions() async { + try { + for (final type in DerivePathType.values) { + await _checkChangeAddressForTransactions(type); + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + /// public wrapper because dart can't test private... + Future<void> checkCurrentChangeAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentChangeAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + /// attempts to convert a string to a valid scripthash + /// + /// Returns the scripthash or throws an exception on invalid litecoin address + String _convertToScriptHash(String litecoinAddress, NetworkType network) { + try { + final output = Address.addressToOutputScript( + litecoinAddress, network, _network.bech32!); + final hash = sha256.convert(output.toList(growable: false)).toString(); + + final chars = hash.split(""); + final reversedPairs = <String>[]; + var i = chars.length - 1; + while (i > 0) { + reversedPairs.add(chars[i - 1]); + reversedPairs.add(chars[i]); + i -= 2; + } + return reversedPairs.join(""); + } catch (e) { + rethrow; + } + } + + Future<List<Map<String, dynamic>>> _fetchHistory( + List<String> allAddresses) async { + try { + List<Map<String, dynamic>> allTxHashes = []; + + final Map<int, Map<String, List<dynamic>>> batches = {}; + final Map<String, String> requestIdToAddressMap = {}; + const batchSizeMax = 100; + int batchNumber = 0; + for (int i = 0; i < allAddresses.length; i++) { + if (batches[batchNumber] == null) { + batches[batchNumber] = {}; + } + final scripthash = _convertToScriptHash(allAddresses[i], _network); + final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); + requestIdToAddressMap[id] = allAddresses[i]; + batches[batchNumber]!.addAll({ + id: [scripthash] + }); + if (i % batchSizeMax == batchSizeMax - 1) { + batchNumber++; + } + } + + for (int i = 0; i < batches.length; i++) { + final response = + await _electrumXClient.getBatchHistory(args: batches[i]!); + for (final entry in response.entries) { + for (int j = 0; j < entry.value.length; j++) { + entry.value[j]["address"] = requestIdToAddressMap[entry.key]; + if (!allTxHashes.contains(entry.value[j])) { + allTxHashes.add(entry.value[j]); + } + } + } + } + + return allTxHashes; + } catch (e, s) { + Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + bool _duplicateTxCheck( + List<Map<String, dynamic>> allTransactions, String txid) { + for (int i = 0; i < allTransactions.length; i++) { + if (allTransactions[i]["txid"] == txid) { + return true; + } + } + return false; + } + + Future<List<Map<String, dynamic>>> fastFetch(List<String> allTxHashes) async { + List<Map<String, dynamic>> allTransactions = []; + + const futureLimit = 30; + List<Future<Map<String, dynamic>>> transactionFutures = []; + int currentFutureCount = 0; + for (final txHash in allTxHashes) { + Future<Map<String, dynamic>> transactionFuture = + cachedElectrumXClient.getTransaction( + txHash: txHash, + verbose: true, + coin: coin, + ); + transactionFutures.add(transactionFuture); + currentFutureCount++; + if (currentFutureCount > futureLimit) { + currentFutureCount = 0; + await Future.wait(transactionFutures); + for (final fTx in transactionFutures) { + final tx = await fTx; + + allTransactions.add(tx); + } + } + } + if (currentFutureCount != 0) { + currentFutureCount = 0; + await Future.wait(transactionFutures); + for (final fTx in transactionFutures) { + final tx = await fTx; + + allTransactions.add(tx); + } + } + return allTransactions; + } + + Future<TransactionData> _fetchTransactionData() async { + final List<String> allAddresses = await _fetchAllOwnAddresses(); + + final changeAddresses = DB.instance.get<dynamic>( + boxName: walletId, key: 'changeAddressesP2WPKH') as List<dynamic>; + final changeAddressesP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH') + as List<dynamic>; + final changeAddressesP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2SH') + as List<dynamic>; + + for (var i = 0; i < changeAddressesP2PKH.length; i++) { + changeAddresses.add(changeAddressesP2PKH[i] as String); + } + for (var i = 0; i < changeAddressesP2SH.length; i++) { + changeAddresses.add(changeAddressesP2SH[i] as String); + } + + final List<Map<String, dynamic>> allTxHashes = + await _fetchHistory(allAddresses); + + final cachedTransactions = + DB.instance.get<dynamic>(boxName: walletId, key: 'latest_tx_model') + as TransactionData?; + int latestTxnBlockHeight = + DB.instance.get<dynamic>(boxName: walletId, key: "storedTxnDataHeight") + as int? ?? + 0; + + final unconfirmedCachedTransactions = + cachedTransactions?.getAllTransactions() ?? {}; + unconfirmedCachedTransactions + .removeWhere((key, value) => value.confirmedStatus); + + if (cachedTransactions != null) { + for (final tx in allTxHashes.toList(growable: false)) { + final txHeight = tx["height"] as int; + if (txHeight > 0 && + txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { + if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { + allTxHashes.remove(tx); + } + } + } + } + + Set<String> hashes = {}; + for (var element in allTxHashes) { + hashes.add(element['tx_hash'] as String); + } + await fastFetch(hashes.toList()); + List<Map<String, dynamic>> allTransactions = []; + + for (final txHash in allTxHashes) { + final tx = await cachedElectrumXClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}"); + // TODO fix this for sent to self transactions? + if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { + tx["address"] = txHash["address"]; + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + + Logging.instance.log("addAddresses: $allAddresses", level: LogLevel.Info); + Logging.instance.log("allTxHashes: $allTxHashes", level: LogLevel.Info); + + Logging.instance.log("allTransactions length: ${allTransactions.length}", + level: LogLevel.Info); + + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List<Map<String, dynamic>> midSortedArray = []; + + Set<String> vHashes = {}; + for (final txObject in allTransactions) { + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"]![i] as Map; + final prevTxid = input["txid"] as String; + vHashes.add(prevTxid); + } + } + await fastFetch(vHashes.toList()); + + for (final txObject in allTransactions) { + List<String> sendersArray = []; + List<String> recipientsArray = []; + + // Usually only has value when txType = 'Send' + int inputAmtSentFromWallet = 0; + // Usually has value regardless of txType due to change addresses + int outputAmtAddressedToWallet = 0; + int fee = 0; + + Map<String, dynamic> midSortedTx = {}; + + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"]![i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, + coin: coin, + ); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + final address = out["scriptPubKey"]["addresses"][0] as String?; + if (address != null) { + sendersArray.add(address); + } + } + } + } + + Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info); + + for (final output in txObject["vout"] as List) { + final address = output["scriptPubKey"]["addresses"][0] as String?; + if (address != null) { + recipientsArray.add(address); + } + } + + Logging.instance + .log("recipientsArray: $recipientsArray", level: LogLevel.Info); + + final foundInSenders = + allAddresses.any((element) => sendersArray.contains(element)); + Logging.instance + .log("foundInSenders: $foundInSenders", level: LogLevel.Info); + + // If txType = Sent, then calculate inputAmtSentFromWallet + if (foundInSenders) { + int totalInput = 0; + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"]![i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, + coin: coin, + ); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + inputAmtSentFromWallet += + (Decimal.parse(out["value"]!.toString()) * + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); + } + } + } + totalInput = inputAmtSentFromWallet; + int totalOutput = 0; + + for (final output in txObject["vout"] as List) { + final String address = + output["scriptPubKey"]!["addresses"][0] as String; + final value = output["value"]!; + final _value = (Decimal.parse(value.toString()) * + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); + totalOutput += _value; + if (changeAddresses.contains(address)) { + inputAmtSentFromWallet -= _value; + } else { + // change address from 'sent from' to the 'sent to' address + txObject["address"] = address; + } + } + // calculate transaction fee + fee = totalInput - totalOutput; + // subtract fee from sent to calculate correct value of sent tx + inputAmtSentFromWallet -= fee; + } else { + // counters for fee calculation + int totalOut = 0; + int totalIn = 0; + + // add up received tx value + for (final output in txObject["vout"] as List) { + final address = output["scriptPubKey"]["addresses"][0]; + if (address != null) { + final value = (Decimal.parse(output["value"].toString()) * + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); + totalOut += value; + if (allAddresses.contains(address)) { + outputAmtAddressedToWallet += value; + } + } + } + + // calculate fee for received tx + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"][i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, + coin: coin, + ); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + totalIn += (Decimal.parse(out["value"].toString()) * + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); + } + } + } + fee = totalIn - totalOut; + } + + // create final tx map + midSortedTx["txid"] = txObject["txid"]; + midSortedTx["confirmed_status"] = (txObject["confirmations"] != null) && + (txObject["confirmations"] as int >= MINIMUM_CONFIRMATIONS); + midSortedTx["confirmations"] = txObject["confirmations"] ?? 0; + midSortedTx["timestamp"] = txObject["blocktime"] ?? + (DateTime.now().millisecondsSinceEpoch ~/ 1000); + + if (foundInSenders) { + midSortedTx["txType"] = "Sent"; + midSortedTx["amount"] = inputAmtSentFromWallet; + final String worthNow = + ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + } else { + midSortedTx["txType"] = "Received"; + midSortedTx["amount"] = outputAmtAddressedToWallet; + final worthNow = + ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + midSortedTx["worthNow"] = worthNow; + } + midSortedTx["aliens"] = <dynamic>[]; + midSortedTx["fees"] = fee; + midSortedTx["address"] = txObject["address"]; + midSortedTx["inputSize"] = txObject["vin"].length; + midSortedTx["outputSize"] = txObject["vout"].length; + midSortedTx["inputs"] = txObject["vin"]; + midSortedTx["outputs"] = txObject["vout"]; + + final int height = txObject["height"] as int; + midSortedTx["height"] = height; + + if (height >= latestTxnBlockHeight) { + latestTxnBlockHeight = height; + } + + midSortedArray.add(midSortedTx); + } + + // sort by date ---- //TODO not sure if needed + // shouldn't be any issues with a null timestamp but I got one at some point? + midSortedArray + .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + // { + // final aT = a["timestamp"]; + // final bT = b["timestamp"]; + // + // if (aT == null && bT == null) { + // return 0; + // } else if (aT == null) { + // return -1; + // } else if (bT == null) { + // return 1; + // } else { + // return bT - aT; + // } + // }); + + // buildDateTimeChunks + final Map<String, dynamic> result = {"dateTimeChunks": <dynamic>[]}; + final dateArray = <dynamic>[]; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = extractDateFromTimestamp(txObject["timestamp"] as int); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp(chunk["timestamp"] as int) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = <Map<String, dynamic>>[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + transactionsMap + .addAll(TransactionData.fromJson(result).getAllTransactions()); + + final txModel = TransactionData.fromMap(transactionsMap); + + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'storedTxnDataHeight', + value: latestTxnBlockHeight); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_tx_model', value: txModel); + + cachedTxData = txModel; + return txModel; + } + + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } + + /// The coinselection algorithm decides whether or not the user is eligible to make the transaction + /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return + /// a map containing the tx hex along with other important information. If not, then it will return + /// an integer (1 or 2) + dynamic coinSelection( + int satoshiAmountToSend, + int selectedTxFeeRate, + String _recipientAddress, + bool isSendAll, { + int additionalOutputs = 0, + List<UtxoObject>? utxos, + }) async { + Logging.instance + .log("Starting coinSelection ----------", level: LogLevel.Info); + final List<UtxoObject> availableOutputs = utxos ?? outputsList; + final List<UtxoObject> spendableOutputs = []; + int spendableSatoshiValue = 0; + + // Build list of spendable outputs and totaling their satoshi amount + for (var i = 0; i < availableOutputs.length; i++) { + if (availableOutputs[i].blocked == false && + availableOutputs[i].status.confirmed == true) { + spendableOutputs.add(availableOutputs[i]); + spendableSatoshiValue += availableOutputs[i].value; + } + } + + // sort spendable by age (oldest first) + spendableOutputs.sort( + (a, b) => b.status.confirmations.compareTo(a.status.confirmations)); + + Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", + level: LogLevel.Info); + Logging.instance + .log("spendableOutputs: $spendableOutputs", level: LogLevel.Info); + Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue", + level: LogLevel.Info); + Logging.instance + .log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info); + // If the amount the user is trying to send is smaller than the amount that they have spendable, + // then return 1, which indicates that they have an insufficient balance. + if (spendableSatoshiValue < satoshiAmountToSend) { + return 1; + // If the amount the user wants to send is exactly equal to the amount they can spend, then return + // 2, which indicates that they are not leaving enough over to pay the transaction fee + } else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) { + return 2; + } + // If neither of these statements pass, we assume that the user has a spendable balance greater + // than the amount they're attempting to send. Note that this value still does not account for + // the added transaction fee, which may require an extra input and will need to be checked for + // later on. + + // Possible situation right here + int satoshisBeingUsed = 0; + int inputsBeingConsumed = 0; + List<UtxoObject> utxoObjectsToUse = []; + + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + + Logging.instance + .log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info); + Logging.instance + .log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info); + Logging.instance + .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); + + // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray + List<String> recipientsArray = [_recipientAddress]; + List<int> recipientsAmtArray = [satoshiAmountToSend]; + + // gather required signing data + final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + + if (isSendAll) { + Logging.instance + .log("Attempting to send all $coin", level: LogLevel.Info); + + final int vSizeForOneOutput = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [_recipientAddress], + satoshiAmounts: [satoshisBeingUsed - 1], + ))["vSize"] as int; + int feeForOneOutput = estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ); + + final int roughEstimate = + roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + if (feeForOneOutput < roughEstimate) { + feeForOneOutput = roughEstimate; + } + + final int amount = satoshiAmountToSend - feeForOneOutput; + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: [amount], + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": amount, + "fee": feeForOneOutput, + "vSize": txn["vSize"], + }; + return transactionObject; + } + + final int vSizeForOneOutput = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [_recipientAddress], + satoshiAmounts: [satoshisBeingUsed - 1], + ))["vSize"] as int; + final int vSizeForTwoOutPuts = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [ + _recipientAddress, + await _getCurrentAddressForChain(1, DerivePathType.bip84), + ], + satoshiAmounts: [ + satoshiAmountToSend, + satoshisBeingUsed - satoshiAmountToSend - 1 + ], // dust limit is the minimum amount a change output should be + ))["vSize"] as int; + + // Assume 1 output, only for recipient and no change + final feeForOneOutput = estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ); + // Assume 2 outputs, one for recipient and one for change + final feeForTwoOutputs = estimateTxFee( + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ); + + Logging.instance + .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); + Logging.instance + .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); + + if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { + if (satoshisBeingUsed - satoshiAmountToSend > + feeForOneOutput + DUST_LIMIT) { + // Here, we know that theoretically, we may be able to include another output(change) but we first need to + // factor in the value of this output in satoshis. + int changeOutputSize = + satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; + // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and + // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new + // change address. + if (changeOutputSize > DUST_LIMIT && + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == + feeForTwoOutputs) { + // generate new change address if current change address has been used + await _checkChangeAddressForTransactions(DerivePathType.bip84); + final String newChangeAddress = + await _getCurrentAddressForChain(1, DerivePathType.bip84); + + int feeBeingPaid = + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; + + recipientsArray.add(newChangeAddress); + recipientsAmtArray.add(changeOutputSize); + // At this point, we have the outputs we're going to use, the amounts to send along with which addresses + // we intend to send these amounts to. We have enough to send instructions to build the transaction. + Logging.instance.log('2 outputs in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log('Change Output Size: $changeOutputSize', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): $feeBeingPaid sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + + // make sure minimum fee is accurate if that is being used + if (txn["vSize"] - feeBeingPaid == 1) { + int changeOutputSize = + satoshisBeingUsed - satoshiAmountToSend - (txn["vSize"] as int); + feeBeingPaid = + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; + recipientsAmtArray.removeLast(); + recipientsAmtArray.add(changeOutputSize); + Logging.instance.log('Adjusted Input size: $satoshisBeingUsed', + level: LogLevel.Info); + Logging.instance.log( + 'Adjusted Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Adjusted Change Output Size: $changeOutputSize', + level: LogLevel.Info); + Logging.instance.log( + 'Adjusted Difference (fee being paid): $feeBeingPaid sats', + level: LogLevel.Info); + Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', + level: LogLevel.Info); + txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + } + + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": feeBeingPaid, + "vSize": txn["vSize"], + }; + return transactionObject; + } else { + // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize + // is smaller than or equal to DUST_LIMIT. Revert to single output transaction. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": satoshisBeingUsed - satoshiAmountToSend, + "vSize": txn["vSize"], + }; + return transactionObject; + } + } else { + // No additional outputs needed since adding one would mean that it'd be smaller than DUST_LIMIT sats + // which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct + // the wallet to begin crafting the transaction that the user requested. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": satoshisBeingUsed - satoshiAmountToSend, + "vSize": txn["vSize"], + }; + return transactionObject; + } + } else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) { + // In this scenario, no additional change output is needed since inputs - outputs equal exactly + // what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin + // crafting the transaction that the user requested. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": feeForOneOutput, + "vSize": txn["vSize"], + }; + return transactionObject; + } else { + // Remember that returning 2 indicates that the user does not have a sufficient balance to + // pay for the transaction fee. Ideally, at this stage, we should check if the user has any + // additional outputs they're able to spend and then recalculate fees. + Logging.instance.log( + 'Cannot pay tx fee - checking for more outputs and trying again', + level: LogLevel.Warning); + // try adding more outputs + if (spendableOutputs.length > inputsBeingConsumed) { + return coinSelection(satoshiAmountToSend, selectedTxFeeRate, + _recipientAddress, isSendAll, + additionalOutputs: additionalOutputs + 1, utxos: utxos); + } + return 2; + } + } + + Future<Map<String, dynamic>> fetchBuildTxData( + List<UtxoObject> utxosToUse, + ) async { + // return data + Map<String, dynamic> results = {}; + Map<String, List<String>> addressTxid = {}; + + // addresses to check + List<String> addressesP2PKH = []; + List<String> addressesP2SH = []; + List<String> addressesP2WPKH = []; + + try { + // Populating the addresses to check + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + final address = output["scriptPubKey"]["addresses"][0] as String; + if (!addressTxid.containsKey(address)) { + addressTxid[address] = <String>[]; + } + (addressTxid[address] as List).add(txid); + switch (addressType(address: address)) { + case DerivePathType.bip44: + addressesP2PKH.add(address); + break; + case DerivePathType.bip49: + addressesP2SH.add(address); + break; + case DerivePathType.bip84: + addressesP2WPKH.add(address); + break; + } + } + } + } + + // p2pkh / bip44 + final p2pkhLength = addressesP2PKH.length; + if (p2pkhLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip44, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + ); + for (int i = 0; i < p2pkhLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final data = P2PKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network, + ).data; + + for (String tx in addressTxid[addressesP2PKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + receiveDerivation["wif"] as String, + network: _network, + ), + }; + } + } else { + // if its not a receive, check change + final changeDerivation = changeDerivations[addressesP2PKH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final data = P2PKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network, + ).data; + + for (String tx in addressTxid[addressesP2PKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + }; + } + } + } + } + } + + // p2sh / bip49 + final p2shLength = addressesP2SH.length; + if (p2shLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip49, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip49, + ); + for (int i = 0; i < p2shLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2SH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final p2wpkh = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network, + overridePrefix: _network.bech32!) + .data; + + final redeemScript = p2wpkh.output; + + final data = + P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; + + for (String tx in addressTxid[addressesP2SH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + receiveDerivation["wif"] as String, + network: _network, + ), + "redeemScript": redeemScript, + }; + } + } else { + // if its not a receive, check change + final changeDerivation = changeDerivations[addressesP2SH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final p2wpkh = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network, + overridePrefix: _network.bech32!) + .data; + + final redeemScript = p2wpkh.output; + + final data = + P2SH(data: PaymentData(redeem: p2wpkh), network: _network) + .data; + + for (String tx in addressTxid[addressesP2SH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + "redeemScript": redeemScript, + }; + } + } + } + } + } + + // p2wpkh / bip84 + final p2wpkhLength = addressesP2WPKH.length; + if (p2wpkhLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip84, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip84, + ); + + for (int i = 0; i < p2wpkhLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network, + overridePrefix: _network.bech32!) + .data; + + for (String tx in addressTxid[addressesP2WPKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + receiveDerivation["wif"] as String, + network: _network, + ), + }; + } + } else { + // if its not a receive, check change + final changeDerivation = changeDerivations[addressesP2WPKH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network, + overridePrefix: _network.bech32!) + .data; + + for (String tx in addressTxid[addressesP2WPKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + }; + } + } + } + } + } + + return results; + } catch (e, s) { + Logging.instance + .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); + rethrow; + } + } + + /// Builds and signs a transaction + Future<Map<String, dynamic>> buildTransaction({ + required List<UtxoObject> utxosToUse, + required Map<String, dynamic> utxoSigningData, + required List<String> recipients, + required List<int> satoshiAmounts, + }) async { + Logging.instance + .log("Starting buildTransaction ----------", level: LogLevel.Info); + + final txb = TransactionBuilder(network: _network); + txb.setVersion(1); + + // Add transaction inputs + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + txb.addInput(txid, utxosToUse[i].vout, null, + utxoSigningData[txid]["output"] as Uint8List, _network.bech32!); + } + + // Add transaction output + for (var i = 0; i < recipients.length; i++) { + txb.addOutput(recipients[i], satoshiAmounts[i], _network.bech32!); + } + + try { + // Sign the transaction accordingly + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + txb.sign( + vin: i, + keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + witnessValue: utxosToUse[i].value, + redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + overridePrefix: _network.bech32!); + } + } catch (e, s) { + Logging.instance.log("Caught exception while signing transaction: $e\n$s", + level: LogLevel.Error); + rethrow; + } + + final builtTx = txb.build(_network.bech32!); + final vSize = builtTx.virtualSize(); + + return {"hex": builtTx.toHex(), "vSize": vSize}; + } + + @override + Future<void> fullRescan( + int maxUnusedAddressGap, + int maxNumberOfIndexesToCheck, + ) async { + Logging.instance.log("Starting full rescan!", level: LogLevel.Info); + longMutex = true; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + // clear cache + await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); + + // back up data + await _rescanBackup(); + + try { + final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); + await _recoverWalletFromBIP32SeedPhrase( + mnemonic: mnemonic!, + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, + ); + + longMutex = false; + Logging.instance.log("Full rescan complete!", level: LogLevel.Info); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + } catch (e, s) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + + // restore from backup + await _rescanRestore(); + + longMutex = false; + Logging.instance.log("Exception rethrown from fullRescan(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _rescanRestore() async { + Logging.instance.log("starting rescan restore", level: LogLevel.Info); + + // restore from backup + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP'); + final tempChangeAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP'); + final tempReceivingIndexP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP'); + final tempChangeIndexP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeIndexP2PKH_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: tempReceivingAddressesP2PKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: tempChangeAddressesP2PKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: tempReceivingIndexP2PKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2PKH', + value: tempChangeIndexP2PKH); + await DB.instance.delete<dynamic>( + key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2PKH_BACKUP', boxName: walletId); + + // p2Sh + final tempReceivingAddressesP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2SH_BACKUP'); + final tempChangeAddressesP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2SH_BACKUP'); + final tempReceivingIndexP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2SH_BACKUP'); + final tempChangeIndexP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeIndexP2SH_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2SH', + value: tempReceivingAddressesP2SH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2SH', + value: tempChangeAddressesP2SH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2SH', + value: tempReceivingIndexP2SH); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'changeIndexP2SH', value: tempChangeIndexP2SH); + await DB.instance.delete<dynamic>( + key: 'receivingAddressesP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2SH_BACKUP', boxName: walletId); + + // p2wpkh + final tempReceivingAddressesP2WPKH = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2WPKH_BACKUP'); + final tempChangeAddressesP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2WPKH_BACKUP'); + final tempReceivingIndexP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2WPKH_BACKUP'); + final tempChangeIndexP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeIndexP2WPKH_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2WPKH', + value: tempReceivingAddressesP2WPKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2WPKH', + value: tempChangeAddressesP2WPKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH', + value: tempReceivingIndexP2WPKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2WPKH', + value: tempChangeIndexP2WPKH); + await DB.instance.delete<dynamic>( + key: 'receivingAddressesP2WPKH_BACKUP', boxName: walletId); + await DB.instance.delete<dynamic>( + key: 'changeAddressesP2WPKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2WPKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2WPKH_BACKUP', boxName: walletId); + + // P2PKH derivations + final p2pkhReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); + final p2pkhChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2PKH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2PKH", + value: p2pkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2PKH", + value: p2pkhChangeDerivationsString); + + await _secureStore.delete( + key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH_BACKUP"); + + // P2SH derivations + final p2shReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2SH_BACKUP"); + final p2shChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2SH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2SH", + value: p2shReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2SH", + value: p2shChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH_BACKUP"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH_BACKUP"); + + // P2WPKH derivations + final p2wpkhReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); + final p2wpkhChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2WPKH", + value: p2wpkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2WPKH", + value: p2wpkhChangeDerivationsString); + + await _secureStore.delete( + key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); + await _secureStore.delete( + key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); + + // UTXOs + final utxoData = DB.instance + .get<dynamic>(boxName: walletId, key: 'latest_utxo_model_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_utxo_model', value: utxoData); + await DB.instance + .delete<dynamic>(key: 'latest_utxo_model_BACKUP', boxName: walletId); + + Logging.instance.log("rescan restore complete", level: LogLevel.Info); + } + + Future<void> _rescanBackup() async { + Logging.instance.log("starting rescan backup", level: LogLevel.Info); + + // backup current and clear data + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2PKH_BACKUP', + value: tempReceivingAddressesP2PKH); + await DB.instance + .delete<dynamic>(key: 'receivingAddressesP2PKH', boxName: walletId); + + final tempChangeAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2PKH_BACKUP', + value: tempChangeAddressesP2PKH); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2PKH', boxName: walletId); + + final tempReceivingIndexP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'receivingIndexP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2PKH_BACKUP', + value: tempReceivingIndexP2PKH); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2PKH', boxName: walletId); + + final tempChangeIndexP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndexP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2PKH_BACKUP', + value: tempChangeIndexP2PKH); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2PKH', boxName: walletId); + + // p2sh + final tempReceivingAddressesP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2SH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2SH_BACKUP', + value: tempReceivingAddressesP2SH); + await DB.instance + .delete<dynamic>(key: 'receivingAddressesP2SH', boxName: walletId); + + final tempChangeAddressesP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2SH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2SH_BACKUP', + value: tempChangeAddressesP2SH); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2SH', boxName: walletId); + + final tempReceivingIndexP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'receivingIndexP2SH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2SH_BACKUP', + value: tempReceivingIndexP2SH); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2SH', boxName: walletId); + + final tempChangeIndexP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndexP2SH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2SH_BACKUP', + value: tempChangeIndexP2SH); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2SH', boxName: walletId); + + // p2wpkh + final tempReceivingAddressesP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2WPKH_BACKUP', + value: tempReceivingAddressesP2WPKH); + await DB.instance + .delete<dynamic>(key: 'receivingAddressesP2WPKH', boxName: walletId); + + final tempChangeAddressesP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2WPKH_BACKUP', + value: tempChangeAddressesP2WPKH); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2WPKH', boxName: walletId); + + final tempReceivingIndexP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH_BACKUP', + value: tempReceivingIndexP2WPKH); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2WPKH', boxName: walletId); + + final tempChangeIndexP2WPKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndexP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2WPKH_BACKUP', + value: tempChangeIndexP2WPKH); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2WPKH', boxName: walletId); + + // P2PKH derivations + final p2pkhReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2PKH"); + final p2pkhChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2PKH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2PKH_BACKUP", + value: p2pkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2PKH_BACKUP", + value: p2pkhChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH"); + + // P2SH derivations + final p2shReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2SH"); + final p2shChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2SH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2SH_BACKUP", + value: p2shReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2SH_BACKUP", + value: p2shChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH"); + + // P2WPKH derivations + final p2wpkhReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2WPKH"); + final p2wpkhChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2WPKH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2WPKH_BACKUP", + value: p2wpkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2WPKH_BACKUP", + value: p2wpkhChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2WPKH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2WPKH"); + + // UTXOs + final utxoData = + DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model'); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData); + await DB.instance + .delete<dynamic>(key: 'latest_utxo_model', boxName: walletId); + + Logging.instance.log("rescan backup complete", level: LogLevel.Info); + } + + bool isActive = false; + + @override + void Function(bool)? get onIsActiveWalletChanged => + (isActive) => this.isActive = isActive; + + @override + Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); + + if (available == satoshiAmount) { + return satoshiAmount - sweepAllEstimate(feeRate); + } else if (satoshiAmount <= 0 || satoshiAmount > available) { + return roughFeeEstimate(1, 2, feeRate); + } + + int runningBalance = 0; + int inputCount = 0; + for (final output in outputsList) { + runningBalance += output.value; + inputCount++; + if (runningBalance > satoshiAmount) { + break; + } + } + + final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); + final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); + + if (runningBalance - satoshiAmount > oneOutPutFee) { + if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - satoshiAmount - twoOutPutFee; + if (change > DUST_LIMIT && + runningBalance - satoshiAmount - change == twoOutPutFee) { + return runningBalance - satoshiAmount - change; + } else { + return runningBalance - satoshiAmount; + } + } else { + return runningBalance - satoshiAmount; + } + } else if (runningBalance - satoshiAmount == oneOutPutFee) { + return oneOutPutFee; + } else { + return twoOutPutFee; + } + } + + int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil(); + } + + int sweepAllEstimate(int feeRate) { + int available = 0; + int inputCount = 0; + for (final output in outputsList) { + if (output.status.confirmed) { + available += output.value; + inputCount++; + } + } + + // transaction will only have 1 output minus the fee + final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); + + return available - estimatedFee; + } + + @override + Future<bool> generateNewAddress() async { + try { + await _incrementAddressIndexForChain( + 0, DerivePathType.bip84); // First increment the receiving index + final newReceivingIndex = DB.instance.get<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH') as int; // Check the new receiving index + final newReceivingAddress = await _generateAddressForChain( + 0, + newReceivingIndex, + DerivePathType + .bip84); // Use new index to derive a new receiving address + await _addToAddressesArrayForChain( + newReceivingAddress, + 0, + DerivePathType + .bip84); // Add that new receiving address to the array of receiving addresses + _currentReceivingAddress = Future(() => + newReceivingAddress); // Set the new receiving address that the service + + return true; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from generateNewAddress(): $e\n$s", + level: LogLevel.Error); + return false; + } + } +} + +final litecoin = NetworkType( + messagePrefix: '\x19Litecoin Signed Message:\n', + bech32: 'ltc', + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x30, + scriptHash: 0x32, + wif: 0xb0); + +final litecointestnet = NetworkType( + messagePrefix: '\x19Litecoin Signed Message:\n', + bech32: 'tltc', + bip32: Bip32Type(public: 0x043587cf, private: 0x04358394), + pubKeyHash: 0x6f, + scriptHash: 0x3a, + wif: 0xef); diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index c8329ec28..8054fe168 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -108,6 +108,9 @@ class Manager with ChangeNotifier { try { final txid = await _currentWallet.confirmSend(txData: txData); + txData["txid"] = txid; + await _currentWallet.updateSentCachedTxData(txData); + notifyListeners(); return txid; } catch (e) { diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index b0ebac4e6..6f1e49ee5 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -23,11 +23,8 @@ import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; @@ -47,8 +44,10 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; const int MINIMUM_CONFIRMATIONS = 10; @@ -67,12 +66,13 @@ class MoneroWallet extends CoinServiceAPI { Timer? moneroAutosaveTimer; late Coin _coin; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; Future<NodeModel> getCurrentNode() async { - return NodeService().getPrimaryNodeFor(coin: coin) ?? + return NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); } @@ -81,14 +81,13 @@ class MoneroWallet extends CoinServiceAPI { required String walletName, required Coin coin, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore}) { + required SecureStorageInterface secureStore}) { _walletId = walletId; _walletName = walletName; _coin = coin; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } bool _shouldAutoSync = false; @@ -154,7 +153,7 @@ class MoneroWallet extends CoinServiceAPI { try { _height = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int blocksRemaining = -1; @@ -163,7 +162,7 @@ class MoneroWallet extends CoinServiceAPI { blocksRemaining = (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int currentHeight = _height + blocksRemaining; if (_height == -1 || blocksRemaining == -1) { @@ -187,8 +186,8 @@ class MoneroWallet extends CoinServiceAPI { try { if (walletBase!.syncStatus! is SyncedSyncStatus && walletBase!.syncStatus!.progress() == 1.0) { - Logging.instance - .log("currentSyncingHeight lol", level: LogLevel.Warning); + // Logging.instance + // .log("currentSyncingHeight lol", level: LogLevel.Warning); return getSyncingHeight(); } } catch (e, s) {} @@ -196,7 +195,7 @@ class MoneroWallet extends CoinServiceAPI { try { syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } final cachedHeight = DB.instance.get<dynamic>(boxName: walletId, key: "storedSyncingHeight") @@ -419,7 +418,7 @@ class MoneroWallet extends CoinServiceAPI { try { progress = (walletBase!.syncStatus!).progress(); } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } await _fetchTransactionData(); @@ -535,7 +534,8 @@ class MoneroWallet extends CoinServiceAPI { @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -544,16 +544,16 @@ class MoneroWallet extends CoinServiceAPI { @override Future<void> exit() async { - await stopSyncPercentTimer(); _hasCalledExit = true; - isActive = false; - await walletBase?.save(prioritySave: true); - walletBase?.close(); + stopNetworkAlivePinging(); moneroAutosaveTimer?.cancel(); moneroAutosaveTimer = null; timer?.cancel(); timer = null; - stopNetworkAlivePinging(); + await stopSyncPercentTimer(); + await walletBase?.save(prioritySave: true); + walletBase?.close(); + isActive = false; } bool _hasCalledExit = false; @@ -564,13 +564,15 @@ class MoneroWallet extends CoinServiceAPI { Future<String>? _currentReceivingAddress; Future<FeeObject> _getFees() async { + // TODO: not use random hard coded values here return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 10, - numberOfBlocksSlow: 10, - fast: 4, - medium: 2, - slow: 0); + numberOfBlocksFast: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, + fast: MoneroTransactionPriority.fast.raw!, + medium: MoneroTransactionPriority.regular.raw!, + slow: MoneroTransactionPriority.slow.raw!, + ); } @override @@ -670,11 +672,9 @@ class MoneroWallet extends CoinServiceAPI { "Attempted to overwrite mnemonic on generate new wallet!"); } - storage = const FlutterSecureStorage(); walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; try { @@ -699,7 +699,7 @@ class MoneroWallet extends CoinServiceAPI { name: name, type: WalletType.monero, isRecovery: false, - restoreHeight: credentials.height ?? 0, + restoreHeight: bufferedCreateHeight, date: DateTime.now(), path: path, dirPath: dirPath, @@ -708,8 +708,7 @@ class MoneroWallet extends CoinServiceAPI { credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( - secureStorage: storage, - sharedPreferences: prefs, + secureStorage: _secureStore, walletService: walletService, keyService: keysStorage, ); @@ -787,11 +786,10 @@ class MoneroWallet extends CoinServiceAPI { // Logging.instance.log("Caught in initializeWallet(): $e\n$s"); // return false; // } - storage = const FlutterSecureStorage(); + walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); await _generateNewWallet(); // var password; @@ -833,11 +831,9 @@ class MoneroWallet extends CoinServiceAPI { "Attempted to initialize an existing wallet using an unknown wallet ID!"); } - storage = const FlutterSecureStorage(); walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); await _prefs.init(); final data = @@ -876,8 +872,9 @@ class MoneroWallet extends CoinServiceAPI { Future<int> get maxFee async { var bal = await availableBalance; var fee = walletBase!.calculateEstimatedFee( - monero.getDefaultTransactionPriority(), bal.toBigInt().toInt()) ~/ - 10000; + monero.getDefaultTransactionPriority(), + Format.decimalAmountToSatoshis(bal, coin), + ); return fee; } @@ -889,9 +886,8 @@ class MoneroWallet extends CoinServiceAPI { bool longMutex = false; // TODO: are these needed? - FlutterSecureStorage? storage; + WalletService? walletService; - SharedPreferences? prefs; KeyService? keysStorage; MoneroWalletBase? walletBase; WalletCreationService? _walletCreationService; @@ -906,14 +902,8 @@ class MoneroWallet extends CoinServiceAPI { required String name, required WalletType type, }) async { - Directory root = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - root = (await getLibraryDirectory()); - } - // - if (Platform.isLinux) { - root = Directory("${root.path}/.stackwallet"); - } + Directory root = await StackFileSystem.applicationRootDirectory(); + final prefix = walletTypeToString(type).toLowerCase(); final walletsDir = Directory('${root.path}/wallets'); final walletDire = Directory('${walletsDir.path}/$prefix/$name'); @@ -970,11 +960,9 @@ class MoneroWallet extends CoinServiceAPI { await DB.instance .put<dynamic>(boxName: walletId, key: "restoreHeight", value: height); - storage = const FlutterSecureStorage(); walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; String name = _walletId; @@ -1001,8 +989,7 @@ class MoneroWallet extends CoinServiceAPI { credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( - secureStorage: storage, - sharedPreferences: prefs, + secureStorage: _secureStore, walletService: walletService, keyService: keysStorage, ); @@ -1190,6 +1177,14 @@ class MoneroWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + // not used in monero + TransactionData? cachedTxData; + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + // not used in monero + } + Future<TransactionData> _fetchTransactionData() async { final transactions = walletBase?.transactionHistory!.transactions; @@ -1345,10 +1340,8 @@ class MoneroWallet extends CoinServiceAPI { Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError(); @override - // TODO: implement validateAddress bool validateAddress(String address) { - bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || - RegExp("[a-zA-Z0-9]{106}").hasMatch(address); + bool valid = walletBase!.validateAddress(address); return valid; } @@ -1376,14 +1369,14 @@ class MoneroWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @override - // TODO: implement availableBalance Future<Decimal> get availableBalance async { var bal = 0; for (var element in walletBase!.balance!.entries) { @@ -1432,13 +1425,13 @@ class MoneroWallet extends CoinServiceAPI { try { final feeRate = args?["feeRate"]; if (feeRate is FeeRateType) { - MoneroTransactionPriority feePriority = MoneroTransactionPriority.slow; + MoneroTransactionPriority feePriority; switch (feeRate) { case FeeRateType.fast: - feePriority = MoneroTransactionPriority.fastest; + feePriority = MoneroTransactionPriority.fast; break; case FeeRateType.average: - feePriority = MoneroTransactionPriority.medium; + feePriority = MoneroTransactionPriority.regular; break; case FeeRateType.slow: feePriority = MoneroTransactionPriority.slow; @@ -1451,15 +1444,14 @@ class MoneroWallet extends CoinServiceAPI { bool isSendAll = false; final balance = await availableBalance; final satInDecimal = ((Decimal.fromInt(satoshiAmount) / - Decimal.fromInt(Constants.satsPerCoinMonero)) - .toDecimal() * - Decimal.fromInt(10000)); + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal()); if (satInDecimal == balance) { isSendAll = true; } Logging.instance .log("$toAddress $amount $args", level: LogLevel.Info); - String amountToSend = moneroAmountToString(amount: amount * 10000); + String amountToSend = moneroAmountToString(amount: amount); Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); monero_output.Output output = monero_output.Output(walletBase!); @@ -1481,10 +1473,9 @@ class MoneroWallet extends CoinServiceAPI { PendingMoneroTransaction pendingMoneroTransaction = await (awaitPendingTransaction!) as PendingMoneroTransaction; - int realfee = (Decimal.parse(pendingMoneroTransaction.feeFormatted) * - 100000000.toDecimal()) - .toBigInt() - .toInt(); + + int realfee = Format.decimalAmountToSatoshis( + Decimal.parse(pendingMoneroTransaction.feeFormatted), coin); debugPrint("fee? $realfee"); Map<String, dynamic> txData = { "pendingMoneroTransaction": pendingMoneroTransaction, @@ -1517,12 +1508,13 @@ class MoneroWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - MoneroTransactionPriority? priority; - FeeRateType feeRateType = FeeRateType.slow; + MoneroTransactionPriority priority; + FeeRateType feeRateType; + switch (feeRate) { case 1: priority = MoneroTransactionPriority.regular; - feeRateType = FeeRateType.slow; + feeRateType = FeeRateType.average; break; case 2: priority = MoneroTransactionPriority.medium; @@ -1530,7 +1522,7 @@ class MoneroWallet extends CoinServiceAPI { break; case 3: priority = MoneroTransactionPriority.fast; - feeRateType = FeeRateType.average; + feeRateType = FeeRateType.fast; break; case 4: priority = MoneroTransactionPriority.fastest; @@ -1542,27 +1534,29 @@ class MoneroWallet extends CoinServiceAPI { feeRateType = FeeRateType.slow; break; } - var aprox; + // int? aprox; - await estimateFeeMutex.protect(() async { - { - try { - aprox = (await prepareSend( - // This address is only used for getting an approximate fee, never for sending - address: - "8347huhmj6Ggzr1BpZPJAD5oa96ob5Fe8GtQdGZDYVVYVsCgtUNH3pEEzExDuaAVZdC16D4FkAb24J6wUfsKkcZtC8EPXB7", - satoshiAmount: satoshiAmount, - args: {"feeRate": feeRateType}))['fee']; - await Future.delayed(const Duration(milliseconds: 1000)); - } catch (e, s) { - Logging.instance.log("$feeRateType $e $s", level: LogLevel.Error); - aprox = -9999999999999999; - } - } - }); + // corrupted size vs. prev_size occurs but not sure if related to fees or just generating monero transactions in general + + // await estimateFeeMutex.protect(() async { + // { + // try { + // aprox = (await prepareSend( + // // This address is only used for getting an approximate fee, never for sending + // address: + // "8347huhmj6Ggzr1BpZPJAD5oa96ob5Fe8GtQdGZDYVVYVsCgtUNH3pEEzExDuaAVZdC16D4FkAb24J6wUfsKkcZtC8EPXB7", + // satoshiAmount: satoshiAmount, + // args: {"feeRate": feeRateType}))['fee'] as int?; + // await Future<void>.delayed(const Duration(milliseconds: 1000)); + // } catch (e, s) { + // Logging.instance.log("$feeRateType $e $s", level: LogLevel.Error); + final aprox = walletBase!.calculateEstimatedFee(priority, satoshiAmount); + // } + // } + // }); print("this is the aprox fee $aprox for $satoshiAmount"); - final fee = (aprox as int); + final fee = aprox; return fee; } diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index e9cae1ab2..142bfb379 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -10,8 +10,8 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -170,9 +170,10 @@ class NamecoinWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -195,19 +196,21 @@ class NamecoinWallet extends CoinServiceAPI { Future<Decimal> get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future<Decimal> get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -217,13 +220,13 @@ class NamecoinWallet extends CoinServiceAPI { .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -261,7 +264,8 @@ class NamecoinWallet extends CoinServiceAPI { @override Future<int> get maxFee async { final fee = (await fees).fast as String; - final satsFee = Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = + Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -1085,7 +1089,8 @@ class NamecoinWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1275,6 +1280,54 @@ class NamecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network, namecoin.bech32!); @@ -1300,7 +1353,7 @@ class NamecoinWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1312,7 +1365,7 @@ class NamecoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1322,13 +1375,12 @@ class NamecoinWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1366,7 +1418,8 @@ class NamecoinWallet extends CoinServiceAPI { } Future<ElectrumXNode> getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( @@ -1441,9 +1494,9 @@ class NamecoinWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1916,7 +1969,7 @@ class NamecoinWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1926,15 +1979,16 @@ class NamecoinWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2491,7 +2545,7 @@ class NamecoinWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"]!.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2505,7 +2559,7 @@ class NamecoinWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["address"]; final value = output["value"]; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2533,7 +2587,7 @@ class NamecoinWallet extends CoinServiceAPI { } if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2556,7 +2610,7 @@ class NamecoinWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2578,7 +2632,7 @@ class NamecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2588,7 +2642,7 @@ class NamecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2672,6 +2726,7 @@ class NamecoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } @@ -3722,7 +3777,8 @@ class NamecoinWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index d3aba5bbb..72580ea4a 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -24,11 +24,8 @@ import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' as wownero_output; import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; @@ -48,8 +45,10 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; const int MINIMUM_CONFIRMATIONS = 10; @@ -68,12 +67,13 @@ class WowneroWallet extends CoinServiceAPI { Timer? wowneroAutosaveTimer; late Coin _coin; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; Future<NodeModel> getCurrentNode() async { - return NodeService().getPrimaryNodeFor(coin: coin) ?? + return NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); } @@ -82,14 +82,13 @@ class WowneroWallet extends CoinServiceAPI { required String walletName, required Coin coin, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore}) { + required SecureStorageInterface secureStore}) { _walletId = walletId; _walletName = walletName; _coin = coin; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } bool _shouldAutoSync = false; @@ -155,7 +154,7 @@ class WowneroWallet extends CoinServiceAPI { try { _height = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int blocksRemaining = -1; @@ -164,7 +163,7 @@ class WowneroWallet extends CoinServiceAPI { blocksRemaining = (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int currentHeight = _height + blocksRemaining; if (_height == -1 || blocksRemaining == -1) { @@ -188,8 +187,8 @@ class WowneroWallet extends CoinServiceAPI { try { if (walletBase!.syncStatus! is SyncedSyncStatus && walletBase!.syncStatus!.progress() == 1.0) { - Logging.instance - .log("currentSyncingHeight lol", level: LogLevel.Warning); + // Logging.instance + // .log("currentSyncingHeight lol", level: LogLevel.Warning); return getSyncingHeight(); } } catch (e, s) {} @@ -197,7 +196,7 @@ class WowneroWallet extends CoinServiceAPI { try { syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } final cachedHeight = DB.instance.get<dynamic>(boxName: walletId, key: "storedSyncingHeight") @@ -420,7 +419,7 @@ class WowneroWallet extends CoinServiceAPI { try { progress = (walletBase!.syncStatus!).progress(); } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } await _fetchTransactionData(); @@ -536,8 +535,7 @@ class WowneroWallet extends CoinServiceAPI { @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(); + Format.satoshisToAmount(await maxFee, coin: Coin.wownero); @override Future<String> get currentReceivingAddress => @@ -565,13 +563,15 @@ class WowneroWallet extends CoinServiceAPI { Future<String>? _currentReceivingAddress; Future<FeeObject> _getFees() async { + // TODO: not use random hard coded values here return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 10, - numberOfBlocksSlow: 10, - fast: 4, - medium: 2, - slow: 0); + numberOfBlocksFast: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, + fast: MoneroTransactionPriority.fast.raw!, + medium: MoneroTransactionPriority.regular.raw!, + slow: MoneroTransactionPriority.slow.raw!, + ); } @override @@ -647,7 +647,7 @@ class WowneroWallet extends CoinServiceAPI { } //TODO: take in the default language when creating wallet. - Future<void> _generateNewWallet() async { + Future<void> _generateNewWallet({int seedWordsLength = 14}) async { Logging.instance .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); // TODO: ping wownero server and make sure the genesis hash matches @@ -671,12 +671,10 @@ class WowneroWallet extends CoinServiceAPI { "Attempted to overwrite mnemonic on generate new wallet!"); } - storage = const FlutterSecureStorage(); // TODO: Wallet Service may need to be switched to Wownero walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; try { @@ -685,9 +683,7 @@ class WowneroWallet extends CoinServiceAPI { await pathForWalletDir(name: name, type: WalletType.wownero); final path = await pathForWallet(name: name, type: WalletType.wownero); credentials = wownero.createWowneroNewWalletCredentials( - name: name, - language: "English", - ); + name: name, language: "English", seedWordsLength: seedWordsLength); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, WalletType.wownero), @@ -703,8 +699,7 @@ class WowneroWallet extends CoinServiceAPI { credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( - secureStorage: storage, - sharedPreferences: prefs, + secureStorage: _secureStore, walletService: walletService, keyService: keysStorage, ); @@ -712,9 +707,12 @@ class WowneroWallet extends CoinServiceAPI { // To restore from a seed final wallet = await _walletCreationService?.create(credentials); - // subtract a couple days to ensure we have a buffer for SWB - final bufferedCreateHeight = - getSeedHeightSync(wallet?.seed.trim() as String); + final bufferedCreateHeight = (seedWordsLength == 14) + ? getSeedHeightSync(wallet?.seed.trim() as String) + : wownero.getHeightByDate( + date: DateTime.now().subtract(const Duration( + days: + 2))); // subtract a couple days to ensure we have a buffer for SWB await DB.instance.put<dynamic>( boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); @@ -722,6 +720,7 @@ class WowneroWallet extends CoinServiceAPI { await _secureStore.write( key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); + walletInfo.address = wallet?.walletAddresses.address; await DB.instance .add<WalletInfo>(boxName: WalletInfo.boxName, value: walletInfo); @@ -778,7 +777,7 @@ class WowneroWallet extends CoinServiceAPI { @override // TODO: implement initializeWallet - Future<bool> initializeNew() async { + Future<bool> initializeNew({int seedWordsLength = 14}) async { await _prefs.init(); // TODO: ping actual wownero network // try { @@ -790,13 +789,11 @@ class WowneroWallet extends CoinServiceAPI { // Logging.instance.log("Caught in initializeWallet(): $e\n$s"); // return false; // } - storage = const FlutterSecureStorage(); walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); - await _generateNewWallet(); + await _generateNewWallet(seedWordsLength: seedWordsLength); // var password; // try { // password = @@ -836,11 +833,9 @@ class WowneroWallet extends CoinServiceAPI { "Attempted to initialize an existing wallet using an unknown wallet ID!"); } - storage = const FlutterSecureStorage(); walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); await _prefs.init(); final data = @@ -870,7 +865,8 @@ class WowneroWallet extends CoinServiceAPI { await DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int; // Use new index to derive a new receiving address final newReceivingAddress = await _generateAddressForChain(0, curIndex); - Logging.instance.log("xmr address in init existing: $newReceivingAddress", + Logging.instance.log( + "wownero address in init existing: $newReceivingAddress", level: LogLevel.Info); _currentReceivingAddress = Future(() => newReceivingAddress); } @@ -879,8 +875,9 @@ class WowneroWallet extends CoinServiceAPI { Future<int> get maxFee async { var bal = await availableBalance; var fee = walletBase!.calculateEstimatedFee( - wownero.getDefaultTransactionPriority(), bal.toBigInt().toInt()) ~/ - 10000; + wownero.getDefaultTransactionPriority(), + Format.decimalAmountToSatoshis(bal, coin), + ); return fee; } @@ -892,9 +889,8 @@ class WowneroWallet extends CoinServiceAPI { bool longMutex = false; // TODO: are these needed? - FlutterSecureStorage? storage; + WalletService? walletService; - SharedPreferences? prefs; KeyService? keysStorage; WowneroWalletBase? walletBase; WalletCreationService? _walletCreationService; @@ -909,13 +905,8 @@ class WowneroWallet extends CoinServiceAPI { required String name, required WalletType type, }) async { - Directory root = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - root = (await getLibraryDirectory()); - } - if (Platform.isLinux) { - root = Directory("${root.path}/.stackwallet"); - } + Directory root = await StackFileSystem.applicationRootDirectory(); + final prefix = walletTypeToString(type).toLowerCase(); final walletsDir = Directory('${root.path}/wallets'); final walletDire = Directory('${walletsDir.path}/$prefix/$name'); @@ -942,6 +933,11 @@ class WowneroWallet extends CoinServiceAPI { required int maxNumberOfIndexesToCheck, required int height, }) async { + final int seedLength = mnemonic.trim().split(" ").length; + if (!(seedLength == 14 || seedLength == 25)) { + throw Exception("Invalid wownero mnemonic length found: $seedLength"); + } + await _prefs.init(); longMutex = true; final start = DateTime.now(); @@ -969,16 +965,25 @@ class WowneroWallet extends CoinServiceAPI { await _secureStore.write( key: '${_walletId}_mnemonic', value: mnemonic.trim()); - height = getSeedHeightSync(mnemonic.trim()); + // extract seed height from 14 word seed + if (seedLength == 14) { + height = getSeedHeightSync(mnemonic.trim()); + } else { + // 25 word seed. TODO validate + if (height == 0) { + height = wownero.getHeightByDate( + date: DateTime.now().subtract(const Duration( + days: + 2))); // subtract a couple days to ensure we have a buffer for SWB\ + } + } await DB.instance .put<dynamic>(boxName: walletId, key: "restoreHeight", value: height); - storage = const FlutterSecureStorage(); walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; String name = _walletId; @@ -1005,8 +1010,7 @@ class WowneroWallet extends CoinServiceAPI { credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( - secureStorage: storage, - sharedPreferences: prefs, + secureStorage: _secureStore, walletService: walletService, keyService: keysStorage, ); @@ -1195,6 +1199,14 @@ class WowneroWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + // not used in wownero + TransactionData? cachedTxData; + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + // not used in wownero + } + Future<TransactionData> _fetchTransactionData() async { final transactions = walletBase?.transactionHistory!.transactions; @@ -1351,10 +1363,8 @@ class WowneroWallet extends CoinServiceAPI { Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError(); @override - // TODO: implement validateAddress bool validateAddress(String address) { - bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || - RegExp("[a-zA-Z0-9]{106}").hasMatch(address); + bool valid = walletBase!.validateAddress(address); return valid; } @@ -1382,9 +1392,10 @@ class WowneroWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -1438,13 +1449,13 @@ class WowneroWallet extends CoinServiceAPI { try { final feeRate = args?["feeRate"]; if (feeRate is FeeRateType) { - MoneroTransactionPriority feePriority = MoneroTransactionPriority.slow; + MoneroTransactionPriority feePriority; switch (feeRate) { case FeeRateType.fast: - feePriority = MoneroTransactionPriority.fastest; + feePriority = MoneroTransactionPriority.fast; break; case FeeRateType.average: - feePriority = MoneroTransactionPriority.medium; + feePriority = MoneroTransactionPriority.regular; break; case FeeRateType.slow: feePriority = MoneroTransactionPriority.slow; @@ -1457,15 +1468,14 @@ class WowneroWallet extends CoinServiceAPI { bool isSendAll = false; final balance = await availableBalance; final satInDecimal = ((Decimal.fromInt(satoshiAmount) / - Decimal.fromInt(Constants.satsPerCoinWownero)) - .toDecimal() * - Decimal.fromInt(1000)); + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal()); if (satInDecimal == balance) { isSendAll = true; } Logging.instance .log("$toAddress $amount $args", level: LogLevel.Info); - String amountToSend = wowneroAmountToString(amount: amount * 1000); + String amountToSend = wowneroAmountToString(amount: amount); Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); wownero_output.Output output = wownero_output.Output(walletBase!); @@ -1487,10 +1497,8 @@ class WowneroWallet extends CoinServiceAPI { PendingWowneroTransaction pendingWowneroTransaction = await (awaitPendingTransaction!) as PendingWowneroTransaction; - int realfee = (Decimal.parse(pendingWowneroTransaction.feeFormatted) * - 100000000.toDecimal()) - .toBigInt() - .toInt(); + int realfee = Format.decimalAmountToSatoshis( + Decimal.parse(pendingWowneroTransaction.feeFormatted), coin); debugPrint("fee? $realfee"); Map<String, dynamic> txData = { "pendingWowneroTransaction": pendingWowneroTransaction, @@ -1523,12 +1531,12 @@ class WowneroWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - MoneroTransactionPriority? priority; + MoneroTransactionPriority priority; FeeRateType feeRateType = FeeRateType.slow; switch (feeRate) { case 1: priority = MoneroTransactionPriority.regular; - feeRateType = FeeRateType.slow; + feeRateType = FeeRateType.average; break; case 2: priority = MoneroTransactionPriority.medium; @@ -1536,7 +1544,7 @@ class WowneroWallet extends CoinServiceAPI { break; case 3: priority = MoneroTransactionPriority.fast; - feeRateType = FeeRateType.average; + feeRateType = FeeRateType.fast; break; case 4: priority = MoneroTransactionPriority.fastest; @@ -1560,7 +1568,7 @@ class WowneroWallet extends CoinServiceAPI { args: {"feeRate": feeRateType}))['fee']; await Future.delayed(const Duration(milliseconds: 500)); } catch (e, s) { - aprox = -9999999999999999; + aprox = walletBase!.calculateEstimatedFee(priority, satoshiAmount); } } }); diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index 5b9fe5063..ca1aae082 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; @@ -13,13 +12,11 @@ import 'package:stackwallet/utilities/logger.dart'; const kStackCommunityNodesEndpoint = "https://extras.stackwallet.com"; class NodeService extends ChangeNotifier { - final FlutterSecureStorageInterface secureStorageInterface; + final SecureStorageInterface secureStorageInterface; /// Exposed [secureStorageInterface] in order to inject mock for tests NodeService({ - this.secureStorageInterface = const SecureStorageWrapper( - FlutterSecureStorage(), - ), + required this.secureStorageInterface, }); Future<void> updateDefaults() async { @@ -27,11 +24,14 @@ class NodeService extends ChangeNotifier { final savedNode = DB.instance .get<NodeModel>(boxName: DB.boxNameNodeModels, key: defaultNode.id); if (savedNode == null) { - // save the default node to hive - await DB.instance.put<NodeModel>( + // save the default node to hive only if no other nodes for the specific coin exist + if (getNodesFor(coinFromPrettyName(defaultNode.coinName)).isEmpty) { + await DB.instance.put<NodeModel>( boxName: DB.boxNameNodeModels, key: defaultNode.id, - value: defaultNode); + value: defaultNode, + ); + } } else { // update all fields but copy over previously set enabled state await DB.instance.put<NodeModel>( @@ -84,14 +84,16 @@ class NodeService extends ChangeNotifier { final list = DB.instance .values<NodeModel>(boxName: DB.boxNameNodeModels) .where((e) => - e.coinName == coin.name && e.name != DefaultNodes.defaultName) + e.coinName == coin.name && + !e.id.startsWith(DefaultNodes.defaultNodeIdPrefix)) .toList(); // add default to end of list list.addAll(DB.instance .values<NodeModel>(boxName: DB.boxNameNodeModels) .where((e) => - e.coinName == coin.name && e.name == DefaultNodes.defaultName) + e.coinName == coin.name && + e.id.startsWith(DefaultNodes.defaultNodeIdPrefix)) .toList()); // return reversed list so default node appears at beginning diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index db3011d4e..cebba2ce5 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -205,13 +205,14 @@ class Wallets extends ChangeNotifier { final txTracker = TransactionNotificationTracker(walletId: walletId); - final failovers = NodeService().failoverNodesFor(coin: coin); + final failovers = nodeService.failoverNodesFor(coin: coin); // load wallet final wallet = CoinServiceAPI.from( coin, walletId, entry.value.name, + nodeService.secureStorageInterface, node, txTracker, prefs, diff --git a/lib/services/wallets_service.dart b/lib/services/wallets_service.dart index b30f9e9e5..1371b17b6 100644 --- a/lib/services/wallets_service.dart +++ b/lib/services/wallets_service.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/notifications_service.dart'; @@ -48,17 +47,14 @@ class WalletInfo { } class WalletsService extends ChangeNotifier { - late final FlutterSecureStorageInterface _secureStore; + late final SecureStorageInterface _secureStore; Future<Map<String, WalletInfo>>? _walletNames; Future<Map<String, WalletInfo>> get walletNames => _walletNames ??= _fetchWalletNames(); WalletsService({ - FlutterSecureStorageInterface secureStorageInterface = - const SecureStorageWrapper( - FlutterSecureStorage(), - ), + required SecureStorageInterface secureStorageInterface, }) { _secureStore = secureStorageInterface; } diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 3e0664b3d..a6cbb8b58 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -6,6 +6,7 @@ import 'package:flutter_libepiccash/epic_cash.dart'; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -43,6 +44,8 @@ class AddressUtils { switch (coin) { case Coin.bitcoin: return Address.validateAddress(address, bitcoin); + case Coin.litecoin: + return Address.validateAddress(address, litecoin); case Coin.bitcoincash: return Address.validateAddress(address, bitcoincash); case Coin.dogecoin: @@ -63,6 +66,8 @@ class AddressUtils { return Address.validateAddress(address, particl); case Coin.bitcoinTestNet: return Address.validateAddress(address, testnet); + case Coin.litecoinTestNet: + return Address.validateAddress(address, litecointestnet); case Coin.bitcoincashTestnet: return Address.validateAddress(address, bitcoincashtestnet); case Coin.firoTestNet: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index a7fb6c3e2..5e7e15f0b 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; abstract class Assets { @@ -28,6 +29,16 @@ class _EXCHANGE { class _SVG { const _SVG(); + String? background(BuildContext context) { + switch (Theme.of(context).extension<StackColors>()!.themeType) { + case ThemeType.light: + case ThemeType.dark: + return null; + + case ThemeType.oceanBreeze: + return "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/bg.svg"; + } + } String bellNew(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/bell-new.svg"; @@ -59,6 +70,22 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; + String get themeOcean => "assets/svg/ocean-breeze-theme.svg"; + String get themeLight => "assets/svg/light-mode.svg"; + String get themeDark => "assets/svg/dark-theme.svg"; + + String get circleSliders => "assets/svg/configuration.svg"; + String get circlePlus => "assets/svg/plus-circle.svg"; + String get framedGear => "assets/svg/framed-gear.svg"; + String get framedAddressBook => "assets/svg/framed-address-book.svg"; + String get circleNode => "assets/svg/node-circle.svg"; + String get circleSun => "assets/svg/sun-circle.svg"; + String get circleArrowRotate => "assets/svg/rotate-circle.svg"; + String get circleLanguage => "assets/svg/language-circle.svg"; + String get circleDollarSign => "assets/svg/dollar-sign-circle.svg"; + String get circleLock => "assets/svg/lock-circle.svg"; + String get enableButton => "assets/svg/enabled-button.svg"; + String get disableButton => "assets/svg/Button.svg"; String get polygon => "assets/svg/Polygon.svg"; String get personaIncognito => "assets/svg/persona-incognito-1.svg"; String get personaEasy => "assets/svg/persona-easy-1.svg"; @@ -87,10 +114,13 @@ class _SVG { String get qrcode => "assets/svg/qrcode1.svg"; String get ellipsis => "assets/svg/gear-3.svg"; String get chevronDown => "assets/svg/chevron-down.svg"; + String get chevronUp => "assets/svg/chevron-up.svg"; String get swap => "assets/svg/swap.svg"; String get downloadFolder => "assets/svg/folder-down.svg"; String get lock => "assets/svg/lock-keyhole.svg"; + String get lockOpen => "assets/svg/lock-open.svg"; String get network => "assets/svg/network-wired.svg"; + String get networkWired => "assets/svg/network-wired-2.svg"; String get addressBook => "assets/svg/address-book.svg"; String get addressBook2 => "assets/svg/address-book2.svg"; String get arrowRotate3 => "assets/svg/rotate-exclamation.svg"; @@ -130,11 +160,19 @@ class _SVG { String get anonymize => "assets/svg/tx-icon-anonymize.svg"; String get anonymizePending => "assets/svg/tx-icon-anonymize-pending.svg"; String get anonymizeFailed => "assets/svg/tx-icon-anonymize-failed.svg"; + String get addressBookDesktop => "assets/svg/address-book-desktop.svg"; + String get exchangeDesktop => "assets/svg/exchange-desktop.svg"; + String get aboutDesktop => "assets/svg/about-desktop.svg"; + String get walletDesktop => "assets/svg/wallet-desktop.svg"; + String get exitDesktop => "assets/svg/exit-desktop.svg"; + String get keys => "assets/svg/keys.svg"; + String get arrowDown => "assets/svg/arrow-down.svg"; String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; String get bitcoin => "assets/svg/coin_icons/Bitcoin.svg"; + String get litecoin => "assets/svg/coin_icons/Litecoin.svg"; String get bitcoincash => "assets/svg/coin_icons/Bitcoincash.svg"; String get dogecoin => "assets/svg/coin_icons/Dogecoin.svg"; String get epicCash => "assets/svg/coin_icons/EpicCash.svg"; @@ -163,6 +201,9 @@ class _SVG { switch (coin) { case Coin.bitcoin: return bitcoin; + case Coin.litecoin: + case Coin.litecoinTestNet: + return litecoin; case Coin.bitcoincash: return bitcoincash; case Coin.dogecoin: @@ -202,16 +243,23 @@ class _PNG { String get firo => "assets/images/firo.png"; String get dogecoin => "assets/images/doge.png"; String get bitcoin => "assets/images/bitcoin.png"; + String get litecoin => "assets/images/litecoin.png"; String get epicCash => "assets/images/epic-cash.png"; String get bitcoincash => "assets/images/bitcoincash.png"; String get namecoin => "assets/images/namecoin.png"; String get particl => "assets/images/namecoin.png"; //TODO - use particl png + String get glasses => "assets/images/glasses.png"; + String get glassesHidden => "assets/images/glasses-hidden.png"; + String imageFor({required Coin coin}) { switch (coin) { case Coin.bitcoin: case Coin.bitcoinTestNet: return bitcoin; + case Coin.litecoin: + case Coin.litecoinTestNet: + return litecoin; case Coin.bitcoincash: case Coin.bitcoincashTestnet: return bitcoincash; diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index bc76b8173..4b406b704 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -7,6 +7,10 @@ Uri getBlockExplorerTransactionUrlFor({ switch (coin) { case Coin.bitcoin: return Uri.parse("https://chain.so/tx/BTC/$txid"); + case Coin.litecoin: + return Uri.parse("https://chain.so/tx/LTC/$txid"); + case Coin.litecoinTestNet: + return Uri.parse("https://chain.so/tx/LTCTEST/$txid"); case Coin.bitcoinTestNet: return Uri.parse("https://chain.so/tx/BTCTEST/$txid"); case Coin.dogecoin: diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index ee00bb596..25105d011 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -23,31 +23,77 @@ abstract class Constants { static bool enableExchange = Util.isDesktop || !Platform.isIOS; //TODO: correct for monero? - static const int satsPerCoinMonero = 1000000000000; - static const int satsPerCoinWownero = 100000000000; - static const int satsPerCoin = 100000000; - static const int decimalPlaces = 8; - static const int decimalPlacesWownero = 11; - static const int decimalPlacesMonero = 12; + static const int _satsPerCoinMonero = 1000000000000; + static const int _satsPerCoinWownero = 100000000000; + static const int _satsPerCoin = 100000000; + static const int _decimalPlaces = 8; + static const int _decimalPlacesWownero = 11; + static const int _decimalPlacesMonero = 12; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); static const int pinLength = 4; - // enable testnet - // TODO: currently unused - static const bool allowTestnets = true; - // Enable Logger.print statements static const bool disableLogger = false; static const int currentHiveDbVersion = 3; + static int satsPerCoin(Coin coin) { + switch (coin) { + case Coin.bitcoin: + case Coin.litecoin: + case Coin.litecoinTestNet: + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + case Coin.dogecoin: + case Coin.firo: + case Coin.bitcoinTestNet: + case Coin.dogecoinTestNet: + case Coin.firoTestNet: + case Coin.epicCash: + case Coin.namecoin: + return _satsPerCoin; + + case Coin.wownero: + return _satsPerCoinWownero; + + case Coin.monero: + return _satsPerCoinMonero; + } + } + + static int decimalPlacesForCoin(Coin coin) { + switch (coin) { + case Coin.bitcoin: + case Coin.litecoin: + case Coin.litecoinTestNet: + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + case Coin.dogecoin: + case Coin.firo: + case Coin.bitcoinTestNet: + case Coin.dogecoinTestNet: + case Coin.firoTestNet: + case Coin.epicCash: + case Coin.namecoin: + return _decimalPlaces; + + case Coin.wownero: + return _decimalPlacesWownero; + + case Coin.monero: + return _decimalPlacesMonero; + } + } + static List<int> possibleLengthsForCoin(Coin coin) { final List<int> values = []; switch (coin) { case Coin.bitcoin: + case Coin.litecoin: + case Coin.litecoinTestNet: case Coin.bitcoincash: case Coin.bitcoincashTestnet: case Coin.dogecoin: @@ -65,7 +111,7 @@ abstract class Constants { values.addAll([25]); break; case Coin.wownero: - values.addAll([14]); + values.addAll([14, 25]); break; } return values; @@ -86,6 +132,10 @@ abstract class Constants { case Coin.dogecoinTestNet: return 60; + case Coin.litecoin: + case Coin.litecoinTestNet: + return 150; + case Coin.firo: case Coin.firoTestNet: return 150; diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index d1763e266..ae5190fc4 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -1,4 +1,3 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/hive/db.dart'; @@ -17,9 +16,7 @@ import 'package:stackwallet/utilities/prefs.dart'; class DbVersionMigrator { Future<void> migrate( int fromVersion, { - FlutterSecureStorageInterface secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), + required SecureStorageInterface secureStore, }) async { Logging.instance.log( "Running migrate fromVersion $fromVersion", @@ -29,8 +26,9 @@ class DbVersionMigrator { case 0: await Hive.openBox<dynamic>(DB.boxNameAllWalletsData); await Hive.openBox<dynamic>(DB.boxNamePrefs); - final walletsService = WalletsService(); - final nodeService = NodeService(); + final walletsService = + WalletsService(secureStorageInterface: secureStore); + final nodeService = NodeService(secureStorageInterface: secureStore); final prefs = Prefs.instance; final walletInfoList = await walletsService.walletNames; await prefs.init(); @@ -118,7 +116,7 @@ class DbVersionMigrator { boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 1); // try to continue migrating - return await migrate(1); + return await migrate(1, secureStore: secureStore); case 1: await Hive.openBox<ExchangeTransaction>(DB.boxNameTrades); @@ -142,7 +140,7 @@ class DbVersionMigrator { boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 2); // try to continue migrating - return await migrate(2); + return await migrate(2, secureStore: secureStore); case 2: await Hive.openBox<dynamic>(DB.boxNamePrefs); final prefs = Prefs.instance; @@ -154,7 +152,7 @@ class DbVersionMigrator { // update version await DB.instance.put<dynamic>( boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 3); - return await migrate(3); + return await migrate(3, secureStore: secureStore); default: // finally return diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index e6cf4fc3e..dc0861714 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -4,11 +4,13 @@ import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; abstract class DefaultNodes { - static String _nodeId(Coin coin) => "default_${coin.name}"; + static const String defaultNodeIdPrefix = "default_"; + static String _nodeId(Coin coin) => "$defaultNodeIdPrefix${coin.name}"; static const String defaultName = "Stack Default"; static List<NodeModel> get all => [ bitcoin, + litecoin, dogecoin, firo, monero, @@ -17,6 +19,7 @@ abstract class DefaultNodes { namecoin, wownero, bitcoinTestnet, + litecoinTestNet, bitcoincashTestnet, dogecoinTestnet, firoTestnet, @@ -34,6 +37,30 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get litecoin => NodeModel( + host: "litecoin.stackwallet.com", + port: 20063, + name: defaultName, + id: _nodeId(Coin.litecoin), + useSSL: true, + enabled: true, + coinName: Coin.litecoin.name, + isFailover: true, + isDown: false, + ); + + static NodeModel get litecoinTestNet => NodeModel( + host: "litecoin.stackwallet.com", + port: 51002, + name: defaultName, + id: _nodeId(Coin.litecoinTestNet), + useSSL: true, + enabled: true, + coinName: Coin.litecoinTestNet.name, + isFailover: true, + isDown: false, + ); + static NodeModel get bitcoincash => NodeModel( host: "bitcoincash.stackwallet.com", port: 50002, @@ -182,6 +209,9 @@ abstract class DefaultNodes { case Coin.bitcoin: return bitcoin; + case Coin.litecoin: + return litecoin; + case Coin.bitcoincash: return bitcoincash; @@ -209,6 +239,9 @@ abstract class DefaultNodes { case Coin.bitcoinTestNet: return bitcoinTestnet; + case Coin.litecoinTestNet: + return litecoinTestNet; + case Coin.bitcoincashTestnet: return bitcoincashTestnet; diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart new file mode 100644 index 000000000..9ef83932b --- /dev/null +++ b/lib/utilities/desktop_password_service.dart @@ -0,0 +1,167 @@ +import 'package:hive/hive.dart'; +import 'package:stack_wallet_backup/secure_storage.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/utilities/logger.dart'; + +const String _kKeyBlobKey = "swbKeyBlobKeyStringID"; + +String _getMessageFromException(Object exception) { + if (exception is IncorrectPassphrase) { + return exception.errMsg(); + } + if (exception is BadDecryption) { + return exception.errMsg(); + } + if (exception is InvalidLength) { + return exception.errMsg(); + } + if (exception is EncodingError) { + return exception.errMsg(); + } + + return exception.toString(); +} + +class DPS { + StorageCryptoHandler? _handler; + + StorageCryptoHandler get handler { + if (_handler == null) { + throw Exception( + "DPS: attempted to access handler without proper authentication"); + } + return _handler!; + } + + DPS(); + + Future<void> initFromNew(String passphrase) async { + if (_handler != null) { + throw Exception("DPS: attempted to re initialize with new passphrase"); + } + + try { + _handler = await StorageCryptoHandler.fromNewPassphrase(passphrase); + + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + await DB.instance.put<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + value: await _handler!.getKeyBlob(), + ); + await box.close(); + } catch (e, s) { + Logging.instance.log( + "${_getMessageFromException(e)}\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + Future<void> initFromExisting(String passphrase) async { + if (_handler != null) { + throw Exception( + "DPS: attempted to re initialize with existing passphrase"); + } + + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + final keyBlob = DB.instance.get<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + ); + await box.close(); + + if (keyBlob == null) { + throw Exception( + "DPS: failed to find keyBlob while attempting to initialize with existing passphrase"); + } + + try { + _handler = await StorageCryptoHandler.fromExisting(passphrase, keyBlob); + } catch (e, s) { + Logging.instance.log( + "${_getMessageFromException(e)}\n$s", + level: LogLevel.Error, + ); + throw Exception(_getMessageFromException(e)); + } + } + + Future<bool> verifyPassphrase(String passphrase) async { + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + final keyBlob = DB.instance.get<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + ); + await box.close(); + + if (keyBlob == null) { + // no passphrase key blob found so any passphrase is technically bad + return false; + } + + try { + await StorageCryptoHandler.fromExisting(passphrase, keyBlob); + // existing passphrase matches key blob + return true; + } catch (e, s) { + Logging.instance.log( + "${_getMessageFromException(e)}\n$s", + level: LogLevel.Warning, + ); + // password is wrong or some other error + return false; + } + } + + Future<bool> changePassphrase( + String passphraseOld, + String passphraseNew, + ) async { + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + final keyBlob = DB.instance.get<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + ); + await box.close(); + + if (keyBlob == null) { + // no passphrase key blob found so any passphrase is technically bad + return false; + } + + if (!(await verifyPassphrase(passphraseOld))) { + return false; + } + + try { + await _handler!.resetPassphrase(passphraseNew); + + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + await DB.instance.put<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + value: await _handler!.getKeyBlob(), + ); + await box.close(); + + // successfully updated passphrase + return true; + } catch (e, s) { + Logging.instance.log( + "${_getMessageFromException(e)}\n$s", + level: LogLevel.Warning, + ); + return false; + } + } + + Future<bool> hasPassword() async { + final keyBlob = DB.instance.get<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + ); + return keyBlob != null; + } +} diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 3f578c669..8a5941194 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -6,12 +6,15 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart' as epic; import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as firo; +import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart' + as ltc; import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; import 'package:stackwallet/services/coins/particl/particl_wallet.dart' as particl; +import 'package:stackwallet/utilities/util.dart'; enum Coin { bitcoin, @@ -19,29 +22,32 @@ enum Coin { dogecoin, epicCash, firo, + litecoin, monero, particl, wownero, - namecoin, /// + /// /// bitcoinTestNet, + litecoinTestNet, bitcoincashTestnet, dogecoinTestNet, firoTestNet, } -// remove firotestnet for now -const int kTestNetCoinCount = 3; +final int kTestNetCoinCount = Util.isDesktop ? 5 : 4; extension CoinExt on Coin { String get prettyName { switch (this) { case Coin.bitcoin: return "Bitcoin"; + case Coin.litecoin: + return "Litecoin"; case Coin.bitcoincash: return "Bitcoin Cash"; case Coin.dogecoin: @@ -60,6 +66,8 @@ extension CoinExt on Coin { return "Namecoin"; case Coin.bitcoinTestNet: return "tBitcoin"; + case Coin.litecoinTestNet: + return "tLitecoin"; case Coin.bitcoincashTestnet: return "tBitcoin Cash"; case Coin.firoTestNet: @@ -73,6 +81,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "BTC"; + case Coin.litecoin: + return "LTC"; case Coin.bitcoincash: return "BCH"; case Coin.dogecoin: @@ -91,6 +101,8 @@ extension CoinExt on Coin { return "NMC"; case Coin.bitcoinTestNet: return "tBTC"; + case Coin.litecoinTestNet: + return "tLTC"; case Coin.bitcoincashTestnet: return "tBCH"; case Coin.firoTestNet: @@ -104,6 +116,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "bitcoin"; + case Coin.litecoin: + return "litecoin"; case Coin.bitcoincash: return "bitcoincash"; case Coin.dogecoin: @@ -123,8 +137,10 @@ extension CoinExt on Coin { return "namecoin"; case Coin.bitcoinTestNet: return "bitcoin"; + case Coin.litecoinTestNet: + return "litecoin"; case Coin.bitcoincashTestnet: - return "bitcoincash"; + return "bchtest"; case Coin.firoTestNet: return "firo"; case Coin.dogecoinTestNet: @@ -135,12 +151,14 @@ extension CoinExt on Coin { bool get isElectrumXCoin { switch (this) { case Coin.bitcoin: + case Coin.litecoin: case Coin.bitcoincash: case Coin.dogecoin: case Coin.firo: case Coin.namecoin: case Coin.particl: case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: @@ -159,6 +177,10 @@ extension CoinExt on Coin { case Coin.bitcoinTestNet: return btc.MINIMUM_CONFIRMATIONS; + case Coin.litecoin: + case Coin.litecoinTestNet: + return ltc.MINIMUM_CONFIRMATIONS; + case Coin.bitcoincash: case Coin.bitcoincashTestnet: return bch.MINIMUM_CONFIRMATIONS; @@ -195,6 +217,10 @@ Coin coinFromPrettyName(String name) { case "bitcoin": return Coin.bitcoin; + case "Litecoin": + case "litecoin": + return Coin.litecoin; + case "Bitcoincash": case "bitcoincash": case "Bitcoin Cash": @@ -229,6 +255,12 @@ Coin coinFromPrettyName(String name) { case "bitcoinTestNet": return Coin.bitcoinTestNet; + case "Litecoin Testnet": + case "tlitecoin": + case "litecoinTestNet": + case "tLitecoin": + return Coin.litecoinTestNet; + case "Bitcoincash Testnet": case "tBitcoin Cash": case "Bitcoin Cash Testnet": @@ -252,7 +284,10 @@ Coin coinFromPrettyName(String name) { default: throw ArgumentError.value( - name, "name", "No Coin enum value with that prettyName"); + name, + "name", + "No Coin enum value with that prettyName", + ); } } @@ -260,6 +295,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { switch (ticker.toLowerCase()) { case "btc": return Coin.bitcoin; + case "ltc": + return Coin.litecoin; case "bch": return Coin.bitcoincash; case "doge": @@ -274,6 +311,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.namecoin; case "part": return Coin.particl; + case "tltc": + return Coin.litecoinTestNet; case "tbtc": return Coin.bitcoinTestNet; case "tbch": diff --git a/lib/utilities/flutter_secure_storage_interface.dart b/lib/utilities/flutter_secure_storage_interface.dart index f8163ae49..2d9b19050 100644 --- a/lib/utilities/flutter_secure_storage_interface.dart +++ b/lib/utilities/flutter_secure_storage_interface.dart @@ -1,6 +1,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:isar/isar.dart'; +import 'package:stack_wallet_backup/secure_storage.dart'; +import 'package:stackwallet/models/isar/models/encrypted_string_value.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; + +abstract class SecureStorageInterface { + dynamic get store; -abstract class FlutterSecureStorageInterface { Future<void> write({ required String key, required String? value, @@ -33,10 +39,82 @@ abstract class FlutterSecureStorageInterface { }); } -class SecureStorageWrapper implements FlutterSecureStorageInterface { - final FlutterSecureStorage secureStore; +class DesktopSecureStore { + final StorageCryptoHandler handler; + late final Isar isar; - const SecureStorageWrapper(this.secureStore); + DesktopSecureStore(this.handler); + + Future<void> init() async { + isar = await Isar.open( + [EncryptedStringValueSchema], + directory: (await StackFileSystem.applicationIsarDirectory()).path, + inspector: false, + name: "desktopStore", + ); + } + + Future<String?> read({ + required String key, + }) async { + final value = + await isar.encryptedStringValues.filter().keyEqualTo(key).findFirst(); + + // value does not exist; + if (value == null) { + return null; + } + + return await handler.decryptValue(key, value.value); + } + + Future<void> write({ + required String key, + required String? value, + }) async { + if (value == null) { + // here we assume that a value is to be deleted + await isar.writeTxn(() async { + await isar.encryptedStringValues.deleteByKey(key); + }); + } else { + // otherwise created encrypted object value + final object = EncryptedStringValue(); + object.key = key; + object.value = await handler.encryptValue(key, value); + + // store object value + await isar.writeTxn(() async { + await isar.encryptedStringValues.put(object); + }); + } + } + + Future<void> delete({ + required String key, + }) async { + await isar.writeTxn(() async { + await isar.encryptedStringValues.deleteByKey(key); + }); + } +} + +/// all *Options params ignored on desktop +class SecureStorageWrapper implements SecureStorageInterface { + final dynamic _store; + final bool _isDesktop; + + @override + dynamic get store => _store; + + const SecureStorageWrapper({ + required dynamic store, + required bool isDesktop, + }) : assert(isDesktop + ? store is DesktopSecureStore + : store is FlutterSecureStorage), + _store = store, + _isDesktop = isDesktop; @override Future<String?> read({ @@ -47,16 +125,20 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { WebOptions? webOptions, MacOsOptions? mOptions, WindowsOptions? wOptions, - }) { - return secureStore.read( - key: key, - iOptions: iOptions, - aOptions: aOptions, - lOptions: lOptions, - webOptions: webOptions, - mOptions: mOptions, - wOptions: wOptions, - ); + }) async { + if (_isDesktop) { + return await (_store as DesktopSecureStore).read(key: key); + } else { + return await (_store as FlutterSecureStorage).read( + key: key, + iOptions: iOptions, + aOptions: aOptions, + lOptions: lOptions, + webOptions: webOptions, + mOptions: mOptions, + wOptions: wOptions, + ); + } } @override @@ -69,17 +151,21 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { WebOptions? webOptions, MacOsOptions? mOptions, WindowsOptions? wOptions, - }) { - return secureStore.write( - key: key, - value: value, - iOptions: iOptions, - aOptions: aOptions, - lOptions: lOptions, - webOptions: webOptions, - mOptions: mOptions, - wOptions: wOptions, - ); + }) async { + if (_isDesktop) { + return await (_store as DesktopSecureStore).write(key: key, value: value); + } else { + return await (_store as FlutterSecureStorage).write( + key: key, + value: value, + iOptions: iOptions, + aOptions: aOptions, + lOptions: lOptions, + webOptions: webOptions, + mOptions: mOptions, + wOptions: wOptions, + ); + } } @override @@ -92,20 +178,24 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { MacOsOptions? mOptions, WindowsOptions? wOptions, }) async { - await secureStore.delete( - key: key, - iOptions: iOptions, - aOptions: aOptions, - lOptions: lOptions, - webOptions: webOptions, - mOptions: mOptions, - wOptions: wOptions, - ); + if (_isDesktop) { + return (_store as DesktopSecureStore).delete(key: key); + } else { + return await (_store as FlutterSecureStorage).delete( + key: key, + iOptions: iOptions, + aOptions: aOptions, + lOptions: lOptions, + webOptions: webOptions, + mOptions: mOptions, + wOptions: wOptions, + ); + } } } // Mock class for testing purposes -class FakeSecureStorage implements FlutterSecureStorageInterface { +class FakeSecureStorage implements SecureStorageInterface { final Map<String, String?> _store = {}; int _interactions = 0; int get interactions => _interactions; @@ -161,4 +251,7 @@ class FakeSecureStorage implements FlutterSecureStorageInterface { _deletes++; _store.remove(key); } + + @override + dynamic get store => throw UnimplementedError(); } diff --git a/lib/utilities/format.dart b/lib/utilities/format.dart index f0eafdf94..136ec5b95 100644 --- a/lib/utilities/format.dart +++ b/lib/utilities/format.dart @@ -4,24 +4,32 @@ import 'package:decimal/decimal.dart'; import 'package:intl/number_symbols.dart'; import 'package:intl/number_symbols_data.dart' show numberFormatSymbols; import 'package:stackwallet/utilities/constants.dart'; - -import 'enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; abstract class Format { - static Decimal satoshisToAmount(int sats) => - (Decimal.fromInt(sats) / Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces); - - /// - static String satoshiAmountToPrettyString(int sats, String locale) { - final amount = satoshisToAmount(sats); - return localizedStringAsFixed( - value: amount, locale: locale, decimalPlaces: Constants.decimalPlaces); + static Decimal satoshisToAmount(int sats, {required Coin coin}) { + return (Decimal.fromInt(sats) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); } - static int decimalAmountToSatoshis(Decimal amount) { - final value = - (Decimal.fromInt(Constants.satsPerCoin) * amount).floor().toBigInt(); + /// + static String satoshiAmountToPrettyString( + int sats, String locale, Coin coin) { + final amount = satoshisToAmount(sats, coin: coin); + return localizedStringAsFixed( + value: amount, + locale: locale, + decimalPlaces: Constants.decimalPlacesForCoin(coin), + ); + } + + static int decimalAmountToSatoshis(Decimal amount, Coin coin) { + final value = (Decimal.fromInt(Constants.satsPerCoin(coin)) * amount) + .floor() + .toBigInt(); return value.toInt(); } diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 246291053..6b4b9821a 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -37,6 +37,7 @@ class Prefs extends ChangeNotifier { _gotoWalletOnStartup = await _getGotoWalletOnStartup(); _startupWalletId = await _getStartupWalletId(); _externalCalls = await _getHasExternalCalls(); + _familiarity = await _getHasFamiliarity(); _initialized = true; } @@ -328,6 +329,27 @@ class Prefs extends ChangeNotifier { false; } + // familiarity + + int _familiarity = 0; + + int get familiarity => _familiarity; + + set familiarity(int familiarity) { + if (_familiarity != familiarity) { + DB.instance.put<dynamic>( + boxName: DB.boxNamePrefs, key: "familiarity", value: familiarity); + _familiarity = familiarity; + notifyListeners(); + } + } + + Future<int> _getHasFamiliarity() async { + return await DB.instance.get<dynamic>( + boxName: DB.boxNamePrefs, key: "familiarity") as int? ?? + 0; + } + // show testnet coins bool _showTestNetCoins = false; diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart new file mode 100644 index 000000000..5177f1973 --- /dev/null +++ b/lib/utilities/stack_file_system.dart @@ -0,0 +1,66 @@ +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; + +abstract class StackFileSystem { + static Future<Directory> applicationRootDirectory() async { + Directory appDirectory; + + // todo: can merge and do same as regular linux home dir? + if (Logging.isArmLinux) { + appDirectory = await getApplicationDocumentsDirectory(); + appDirectory = Directory("${appDirectory.path}/.stackwallet"); + } else if (Platform.isLinux) { + appDirectory = Directory("${Platform.environment['HOME']}/.stackwallet"); + } else if (Platform.isWindows) { + // TODO: windows root .stackwallet dir location + throw Exception("Unsupported platform"); + } else if (Platform.isMacOS) { + // currently run in ipad mode?? + throw Exception("Unsupported platform"); + } else if (Platform.isIOS) { + // todo: check if we need different behaviour here + if (Util.isDesktop) { + appDirectory = await getLibraryDirectory(); + } else { + appDirectory = await getLibraryDirectory(); + } + } else if (Platform.isAndroid) { + appDirectory = await getApplicationDocumentsDirectory(); + } else { + throw Exception("Unsupported platform"); + } + if (!appDirectory.existsSync()) { + await appDirectory.create(recursive: true); + } + return appDirectory; + } + + static Future<Directory> applicationIsarDirectory() async { + final root = await applicationRootDirectory(); + if (Util.isDesktop) { + final dir = Directory("${root.path}/isar"); + if (!dir.existsSync()) { + await dir.create(); + } + return dir; + } else { + return root; + } + } + + static Future<Directory> applicationHiveDirectory() async { + final root = await applicationRootDirectory(); + if (Util.isDesktop) { + final dir = Directory("${root.path}/hive"); + if (!dir.existsSync()) { + await dir.create(); + } + return dir; + } else { + return root; + } + } +} diff --git a/lib/utilities/test_monero_node_connection.dart b/lib/utilities/test_monero_node_connection.dart index 7cb01e8b1..5e35f9a03 100644 --- a/lib/utilities/test_monero_node_connection.dart +++ b/lib/utilities/test_monero_node_connection.dart @@ -1,26 +1,121 @@ import 'dart:convert'; +import 'dart:io'; -import 'package:http/http.dart' as http; +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; -Future<bool> testMoneroNodeConnection(Uri uri) async { +class MoneroNodeConnectionResponse { + final X509Certificate? cert; + final String? url; + final int? port; + final bool success; + + MoneroNodeConnectionResponse(this.cert, this.url, this.port, this.success); +} + +Future<MoneroNodeConnectionResponse> testMoneroNodeConnection( + Uri uri, + bool allowBadX509Certificate, +) async { + final client = HttpClient(); + MoneroNodeConnectionResponse? badCertResponse; try { - final client = http.Client(); - final response = await client - .post( - uri, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({"jsonrpc": "2.0", "id": "0", "method": "get_info"}), - ) - .timeout(const Duration(milliseconds: 1200), - onTimeout: () async => http.Response('Error', 408)); + client.badCertificateCallback = (cert, url, port) { + if (allowBadX509Certificate) { + return true; + } - final result = jsonDecode(response.body); + if (badCertResponse == null) { + badCertResponse = MoneroNodeConnectionResponse(cert, url, port, false); + } else { + return false; + } + + return false; + }; + + final request = await client.postUrl(uri); + + final body = utf8.encode( + jsonEncode({ + "jsonrpc": "2.0", + "id": "0", + "method": "get_info", + }), + ); + + request.headers.add( + 'Content-Length', + body.length.toString(), + preserveHeaderCase: true, + ); + request.headers.set( + 'Content-Type', + 'application/json', + preserveHeaderCase: true, + ); + + request.add(body); + + final response = await request.close(); + final result = await response.transform(utf8.decoder).join(); // TODO: json decoded without error so assume connection exists? // or we can check for certain values in the response to decide - return true; + return MoneroNodeConnectionResponse(null, null, null, true); } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Warning); - return false; + if (badCertResponse != null) { + return badCertResponse!; + } else { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + return MoneroNodeConnectionResponse(null, null, null, false); + } + } finally { + client.close(force: true); } } + +Future<bool> showBadX509CertificateDialog( + X509Certificate cert, + String url, + int port, + BuildContext context, +) async { + final chars = Format.uint8listToString(cert.sha1) + .toUpperCase() + .characters + .toList(growable: false); + + String sha1 = chars.sublist(0, 2).join(); + for (int i = 2; i < chars.length; i += 2) { + sha1 += ":${chars.sublist(i, i + 2).join()}"; + } + + final result = await showDialog<bool>( + context: context, + barrierDismissible: false, + builder: (context) { + return StackDialog( + title: "Untrusted X509Certificate", + message: "SHA1:\n$sha1", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: PrimaryButton( + label: "Trust", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ); + + return result ?? false; +} diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index c3a6929fe..c9dd15e1d 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -15,6 +15,12 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 20, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -32,6 +38,12 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 18, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 18, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -49,6 +61,12 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -66,6 +84,12 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -83,6 +107,12 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -100,6 +130,12 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -117,6 +153,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -134,6 +176,12 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -151,6 +199,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -168,6 +222,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -185,6 +245,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -202,6 +268,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -219,6 +291,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -228,6 +306,32 @@ class STextStyles { } } + static TextStyle labelExtraExtraSmall(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textFieldActiveSearchIconRight, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 14 / 14, + ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textFieldActiveSearchIconRight, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 14 / 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textFieldActiveSearchIconRight, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 14 / 14, + ); + } + } + static TextStyle label700(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: @@ -236,6 +340,12 @@ class STextStyles { fontWeight: FontWeight.w700, fontSize: 12, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w700, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -253,6 +363,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).infoItemLabel, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemLabel, @@ -270,6 +386,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -287,6 +409,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -305,6 +433,13 @@ class STextStyles { fontSize: 14, height: 1.5, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -324,6 +459,13 @@ class STextStyles { fontSize: 14, height: 1.5, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -342,6 +484,12 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 14, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -359,6 +507,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).accentColorRed, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorRed, @@ -376,6 +530,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).infoItemIcons, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemIcons, @@ -393,6 +553,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).accentColorBlue, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorBlue, @@ -410,6 +576,12 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 12, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -427,6 +599,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -444,6 +622,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -461,6 +645,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 10, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textError, + fontWeight: FontWeight.w500, + fontSize: 10, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textError, @@ -478,6 +668,12 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 10, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 10, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -489,6 +685,32 @@ class STextStyles { // Desktop + static TextStyle desktopH1(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 40, + height: 40 / 40, + ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 40, + height: 40 / 40, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 40, + height: 40 / 40, + ); + } + } + static TextStyle desktopH2(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: @@ -498,6 +720,13 @@ class STextStyles { fontSize: 32, height: 32 / 32, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 32, + height: 32 / 32, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -517,6 +746,13 @@ class STextStyles { fontSize: 24, height: 24 / 24, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 24, + height: 24 / 24, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -536,6 +772,13 @@ class STextStyles { fontSize: 20, height: 30 / 20, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -555,6 +798,13 @@ class STextStyles { fontSize: 20, height: 30 / 20, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 30 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -574,6 +824,13 @@ class STextStyles { fontSize: 20, height: 28 / 20, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 28 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -593,6 +850,13 @@ class STextStyles { fontSize: 24, height: 33 / 24, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 24, + height: 33 / 24, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -612,6 +876,13 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -631,6 +902,13 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -650,6 +928,13 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -669,6 +954,13 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondaryDisabled, @@ -679,6 +971,32 @@ class STextStyles { } } + static TextStyle desktopTextSmall(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 18, + height: 27 / 18, + ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 18, + height: 27 / 18, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 18, + height: 27 / 18, + ); + } + } + static TextStyle desktopTextExtraSmall(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: @@ -688,6 +1006,13 @@ class STextStyles { fontSize: 16, height: 24 / 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -707,6 +1032,13 @@ class STextStyles { fontSize: 14, height: 21 / 14, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 21 / 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -717,6 +1049,32 @@ class STextStyles { } } + static TextStyle desktopTextExtraExtraSmall600(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 14, + height: 21 / 14, + ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 14, + height: 21 / 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 14, + height: 21 / 14, + ); + } + } + static TextStyle desktopButtonSmallSecondaryEnabled(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: @@ -726,6 +1084,13 @@ class STextStyles { fontSize: 16, height: 24 / 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -745,6 +1110,13 @@ class STextStyles { fontSize: 20, height: 30 / 20, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -764,6 +1136,13 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark.withOpacity(0.8), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.8), @@ -783,6 +1162,13 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -802,6 +1188,13 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark.withOpacity(0.5), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.5), @@ -821,6 +1214,13 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -839,6 +1239,12 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 8, ); + case ThemeType.oceanBreeze: + return GoogleFonts.roboto( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 8, + ); case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).textDark, @@ -856,6 +1262,12 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 26, ); + case ThemeType.oceanBreeze: + return GoogleFonts.roboto( + color: _theme(context).numberTextDefault, + fontWeight: FontWeight.w400, + fontSize: 26, + ); case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).numberTextDefault, @@ -874,6 +1286,13 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 12, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w400, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, @@ -893,6 +1312,13 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 4512dabfc..8de0954e8 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -1,16 +1,20 @@ -import 'dart:ui'; - +import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; enum ThemeType { light, dark, + oceanBreeze, } abstract class StackColorTheme { ThemeType get themeType; Color get background; + Color get backgroundAppBar; + + Gradient? get gradientBackground; + Color get overlay; Color get accentColorBlue; @@ -181,6 +185,7 @@ class CoinThemeColor { const CoinThemeColor(); Color get bitcoin => const Color(0xFFFCC17B); + Color get litecoin => const Color(0xFF7FA6E1); Color get bitcoincash => const Color(0xFF7BCFB8); Color get firo => const Color(0xFFFF897A); Color get dogecoin => const Color(0xFFFFE079); @@ -195,6 +200,9 @@ class CoinThemeColor { case Coin.bitcoin: case Coin.bitcoinTestNet: return bitcoin; + case Coin.litecoin: + case Coin.litecoinTestNet: + return litecoin; case Coin.bitcoincash: case Coin.bitcoincashTestnet: return bitcoincash; diff --git a/lib/utilities/theme/dark_colors.dart b/lib/utilities/theme/dark_colors.dart index e7c4e51db..d55581921 100644 --- a/lib/utilities/theme/dark_colors.dart +++ b/lib/utilities/theme/dark_colors.dart @@ -7,6 +7,11 @@ class DarkColors extends StackColorTheme { @override Color get background => const Color(0xFF2A2D34); + @override + Color get backgroundAppBar => background; + @override + Gradient? get gradientBackground => null; + @override Color get overlay => const Color(0xFF111215); diff --git a/lib/utilities/theme/light_colors.dart b/lib/utilities/theme/light_colors.dart index ea3a7cb92..1303d0b75 100644 --- a/lib/utilities/theme/light_colors.dart +++ b/lib/utilities/theme/light_colors.dart @@ -7,11 +7,16 @@ class LightColors extends StackColorTheme { @override Color get background => const Color(0xFFF7F7F7); + @override + Color get backgroundAppBar => background; + @override + Gradient? get gradientBackground => null; + @override Color get overlay => const Color(0xFF111215); @override - Color get accentColorBlue => const Color(0xFF4C86E9); + Color get accentColorBlue => const Color(0xFF0052DF); @override Color get accentColorGreen => const Color(0xFF4CC0A0); @override diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart new file mode 100644 index 000000000..8c4259bb9 --- /dev/null +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -0,0 +1,318 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; + +class OceanBreezeColors extends StackColorTheme { + @override + ThemeType get themeType => ThemeType.oceanBreeze; + + @override + Color get background => Colors.transparent; + @override + Color get backgroundAppBar => const Color(0xFFF3F7FA); + @override + Gradient? get gradientBackground => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFF3F7FA), + Color(0xFFE8F2F9), + ], + ); + + @override + Color get overlay => const Color(0xFF111215); + + @override + Color get accentColorBlue => const Color(0xFF077CBE); + @override + Color get accentColorGreen => const Color(0xFF00A591); + @override + Color get accentColorYellow => const Color(0xFFF4C517); + @override + Color get accentColorRed => const Color(0xFFD1382D); + @override + Color get accentColorOrange => const Color(0xFFFF985F); + @override + Color get accentColorDark => const Color(0xFF227386); + + @override + Color get shadow => const Color(0x0F2D3132); + + @override + Color get textDark => const Color(0xFF232323); + @override + Color get textDark2 => const Color(0xFF333333); + @override + Color get textDark3 => const Color(0xFF696B6C); + @override + Color get textSubtitle1 => const Color(0xFF7E8284); + @override + Color get textSubtitle2 => const Color(0xFF919393); + @override + Color get textSubtitle3 => const Color(0xFFB0B2B2); + @override + Color get textSubtitle4 => const Color(0xFFD1D3D3); + @override + Color get textSubtitle5 => const Color(0xFFDEDFE1); + @override + Color get textSubtitle6 => const Color(0xFFF1F1F1); + @override + Color get textWhite => const Color(0xFFFFFFFF); + @override + Color get textFavoriteCard => const Color(0xFF232323); + @override + Color get textError => const Color(0xFF8D0006); + + // button background + @override + Color get buttonBackPrimary => const Color(0xFF227386); + @override + Color get buttonBackSecondary => const Color(0xFFC2DAE2); + @override + Color get buttonBackPrimaryDisabled => const Color(0xFFBDD5DB); + @override + Color get buttonBackSecondaryDisabled => const Color(0xFFBDBDBD); + @override + Color get buttonBackBorder => const Color(0xFF227386); + @override + Color get buttonBackBorderDisabled => const Color(0xFFBDD5DB); + + @override + Color get numberBackDefault => const Color(0xFFFFFFFF); + @override + Color get numpadBackDefault => const Color(0xFF227386); + @override + Color get bottomNavBack => const Color(0xFFFFFFFF); + + // button text/element + @override + Color get buttonTextPrimary => const Color(0xFFFFFFFF); + @override + Color get buttonTextSecondary => const Color(0xFF232323); + @override + Color get buttonTextPrimaryDisabled => const Color(0xFFFFFFFF); + @override + Color get buttonTextSecondaryDisabled => const Color(0xFFBDD5DB); + @override + Color get buttonTextBorder => const Color(0xFF227386); + @override + Color get buttonTextDisabled => const Color(0xFFFFFFFF); + @override + Color get buttonTextBorderless => const Color(0xFF056EC6); + @override + Color get buttonTextBorderlessDisabled => const Color(0xFFB6B6B6); + @override + Color get numberTextDefault => const Color(0xFF232323); + @override + Color get numpadTextDefault => const Color(0xFFFFFFFF); + @override + Color get bottomNavText => const Color(0xFF232323); + + // switch + @override + Color get switchBGOn => const Color(0xFF056EC6); + @override + Color get switchBGOff => const Color(0xFFCCDBF9); + @override + Color get switchBGDisabled => const Color(0xFFC5C6C9); + @override + Color get switchCircleOn => const Color(0xFFDAE2FF); + @override + Color get switchCircleOff => const Color(0xFFFBFCFF); + @override + Color get switchCircleDisabled => const Color(0xFFFBFCFF); + + // step indicator background + @override + Color get stepIndicatorBGCheck => const Color(0xFFCDD9FF); + @override + Color get stepIndicatorBGNumber => const Color(0xFFCDD9FF); + @override + Color get stepIndicatorBGInactive => const Color(0xFFA6C7D1); + @override + Color get stepIndicatorBGLines => const Color(0xFF90B8DC); + @override + Color get stepIndicatorBGLinesInactive => const Color(0xFFBCD4EA); + @override + Color get stepIndicatorIconText => const Color(0xFF005BAF); + @override + Color get stepIndicatorIconNumber => const Color(0xFF005BAF); + @override + Color get stepIndicatorIconInactive => const Color(0xFFD4DFFF); + + // checkbox + @override + Color get checkboxBGChecked => const Color(0xFF056EC6); + @override + Color get checkboxBorderEmpty => const Color(0xFF8C8F90); + @override + Color get checkboxBGDisabled => const Color(0xFFB0C9ED); + @override + Color get checkboxIconChecked => const Color(0xFFFFFFFF); + @override + Color get checkboxIconDisabled => const Color(0xFFFFFFFF); + @override + Color get checkboxTextLabel => const Color(0xFF232323); + + // snack bar + @override + Color get snackBarBackSuccess => const Color(0xFFADD6D2); + @override + Color get snackBarBackError => const Color(0xFFF6C7C3); + @override + Color get snackBarBackInfo => const Color(0xFFCCD7FF); + @override + Color get snackBarTextSuccess => const Color(0xFF075547); + @override + Color get snackBarTextError => const Color(0xFF8D0006); + @override + Color get snackBarTextInfo => const Color(0xFF002569); + + // icons + @override + Color get bottomNavIconBack => const Color(0xFFA7C7CF); + @override + Color get bottomNavIconIcon => const Color(0xFF227386); + + @override + Color get topNavIconPrimary => const Color(0xFF227386); + @override + Color get topNavIconGreen => const Color(0xFF00A591); + @override + Color get topNavIconYellow => const Color(0xFFFDD33A); + @override + Color get topNavIconRed => const Color(0xFFEA4649); + + @override + Color get settingsIconBack => const Color(0xFFE0E3E3); + @override + Color get settingsIconIcon => const Color(0xFF232323); + @override + Color get settingsIconBack2 => const Color(0xFF80D2C8); + @override + Color get settingsIconElement => const Color(0xFF00A591); + + // text field + @override + Color get textFieldActiveBG => const Color(0xFFD3E3E7); + @override + Color get textFieldDefaultBG => const Color(0xFFD8E7EB); + @override + Color get textFieldErrorBG => const Color(0xFFF6C7C3); + @override + Color get textFieldSuccessBG => const Color(0xFFADD6D2); + + @override + Color get textFieldActiveSearchIconLeft => const Color(0xFF86898C); + @override + Color get textFieldDefaultSearchIconLeft => const Color(0xFF86898C); + @override + Color get textFieldErrorSearchIconLeft => const Color(0xFF8D0006); + @override + Color get textFieldSuccessSearchIconLeft => const Color(0xFF006C4D); + + @override + Color get textFieldActiveText => const Color(0xFF232323); + @override + Color get textFieldDefaultText => const Color(0xFF86898C); + @override + Color get textFieldErrorText => const Color(0xFF000000); + @override + Color get textFieldSuccessText => const Color(0xFF000000); + + @override + Color get textFieldActiveLabel => const Color(0xFF86898C); + @override + Color get textFieldErrorLabel => const Color(0xFF8D0006); + @override + Color get textFieldSuccessLabel => const Color(0xFF077C6E); + + @override + Color get textFieldActiveSearchIconRight => const Color(0xFF388192); + @override + Color get textFieldDefaultSearchIconRight => const Color(0xFF388192); + @override + Color get textFieldErrorSearchIconRight => const Color(0xFF8D0006); + @override + Color get textFieldSuccessSearchIconRight => const Color(0xFF077C6E); + + // settings item level2 + @override + Color get settingsItem2ActiveBG => const Color(0xFFFFFFFF); + @override + Color get settingsItem2ActiveText => const Color(0xFF232323); + @override + Color get settingsItem2ActiveSub => const Color(0xFF8C8F90); + + // radio buttons + @override + Color get radioButtonIconBorder => const Color(0xFF056EC6); + @override + Color get radioButtonIconBorderDisabled => const Color(0xFF8C8D97); + @override + Color get radioButtonBorderEnabled => const Color(0xFF056EC6); + @override + Color get radioButtonBorderDisabled => const Color(0xFF8C8D97); + @override + Color get radioButtonIconCircle => const Color(0xFF056EC6); + @override + Color get radioButtonIconEnabled => const Color(0xFF056EC6); + @override + Color get radioButtonTextEnabled => const Color(0xFF42444B); + @override + Color get radioButtonTextDisabled => const Color(0xFF42444B); + @override + Color get radioButtonLabelEnabled => const Color(0xFF8C8F90); + @override + Color get radioButtonLabelDisabled => const Color(0xFF8C8F90); + + // info text + @override + Color get infoItemBG => const Color(0xFFFFFFFF); + @override + Color get infoItemLabel => const Color(0xFF838788); + @override + Color get infoItemText => const Color(0xFF232323); + @override + Color get infoItemIcons => const Color(0xFF056EC6); + + // popup + @override + Color get popupBG => const Color(0xFFFFFFFF); + + // currency list + @override + Color get currencyListItemBG => const Color(0xFFF0F5F7); + + // bottom nav + @override + Color get stackWalletBG => const Color(0xFFFFFFFF); + @override + Color get stackWalletMid => const Color(0xFFFFFFFF); + @override + Color get stackWalletBottom => const Color(0xFF232323); + @override + Color get bottomNavShadow => const Color(0xFF388192); + + @override + Color get favoriteStarActive => const Color(0xFFF4C517); + @override + Color get favoriteStarInactive => const Color(0xFFB0B2B2); + + @override + Color get splash => const Color(0xFF8E9192); + @override + Color get highlight => const Color(0xFFA9ACAC); + @override + Color get warningForeground => const Color(0xFF232323); + @override + Color get warningBackground => const Color(0xFFF6C7C3); + @override + Color get loadingOverlayTextColor => const Color(0xFFF7F7F7); + @override + Color get myStackContactIconBG => const Color(0xFFD8E7EB); + @override + Color get textConfirmTotalAmount => const Color(0xFF232323); + @override + Color get textSelectedWordTableItem => const Color(0xFF232323); +} diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 2a1ee7ce9..9764128e4 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -6,6 +6,9 @@ class StackColors extends ThemeExtension<StackColors> { final ThemeType themeType; final Color background; + final Color backgroundAppBar; + final Gradient? gradientBackground; + final Color overlay; final Color accentColorBlue; @@ -173,6 +176,8 @@ class StackColors extends ThemeExtension<StackColors> { StackColors({ required this.themeType, required this.background, + required this.backgroundAppBar, + required this.gradientBackground, required this.overlay, required this.accentColorBlue, required this.accentColorGreen, @@ -307,6 +312,8 @@ class StackColors extends ThemeExtension<StackColors> { return StackColors( themeType: colorTheme.themeType, background: colorTheme.background, + backgroundAppBar: colorTheme.backgroundAppBar, + gradientBackground: colorTheme.gradientBackground, overlay: colorTheme.overlay, accentColorBlue: colorTheme.accentColorBlue, accentColorGreen: colorTheme.accentColorGreen, @@ -444,6 +451,8 @@ class StackColors extends ThemeExtension<StackColors> { ThemeExtension<StackColors> copyWith({ ThemeType? themeType, Color? background, + Color? backgroundAppBar, + Gradient? gradientBackground, Color? overlay, Color? accentColorBlue, Color? accentColorGreen, @@ -576,6 +585,8 @@ class StackColors extends ThemeExtension<StackColors> { return StackColors( themeType: themeType ?? this.themeType, background: background ?? this.background, + backgroundAppBar: backgroundAppBar ?? this.backgroundAppBar, + gradientBackground: gradientBackground ?? this.gradientBackground, overlay: overlay ?? this.overlay, accentColorBlue: accentColorBlue ?? this.accentColorBlue, accentColorGreen: accentColorGreen ?? this.accentColorGreen, @@ -755,11 +766,17 @@ class StackColors extends ThemeExtension<StackColors> { return StackColors( themeType: other.themeType, + gradientBackground: other.gradientBackground, background: Color.lerp( background, other.background, t, )!, + backgroundAppBar: Color.lerp( + backgroundAppBar, + other.backgroundAppBar, + t, + )!, overlay: Color.lerp( overlay, other.overlay, @@ -1406,6 +1423,9 @@ class StackColors extends ThemeExtension<StackColors> { case Coin.bitcoin: case Coin.bitcoinTestNet: return _coin.bitcoin; + case Coin.litecoin: + case Coin.litecoinTestNet: + return _coin.litecoin; case Coin.bitcoincash: case Coin.bitcoincashTestnet: return _coin.bitcoincash; @@ -1467,6 +1487,20 @@ class StackColors extends ThemeExtension<StackColors> { } } + ButtonStyle? getDeleteEnabledButtonColor(BuildContext context) => + Theme.of(context).textButtonTheme.style?.copyWith( + backgroundColor: MaterialStateProperty.all<Color>( + textFieldErrorBG, + ), + ); + + ButtonStyle? getDeleteDisabledButtonColor(BuildContext context) => + Theme.of(context).textButtonTheme.style?.copyWith( + backgroundColor: MaterialStateProperty.all<Color>( + buttonBackSecondaryDisabled, + ), + ); + ButtonStyle? getPrimaryEnabledButtonColor(BuildContext context) => Theme.of(context).textButtonTheme.style?.copyWith( backgroundColor: MaterialStateProperty.all<Color>( @@ -1521,5 +1555,8 @@ class StackColors extends ThemeExtension<StackColors> { backgroundColor: MaterialStateProperty.all<Color>( background, ), + overlayColor: MaterialStateProperty.all<Color>( + Colors.transparent, + ), ); } diff --git a/lib/utilities/util.dart b/lib/utilities/util.dart index 8a98787f2..2940b6d40 100644 --- a/lib/utilities/util.dart +++ b/lib/utilities/util.dart @@ -3,7 +3,22 @@ import 'dart:io'; import 'package:flutter/material.dart'; abstract class Util { + static Directory? libraryPath; + static double? screenWidth; + static bool get isDesktop { + // special check for running on linux based phones + if (Platform.isLinux && screenWidth != null && screenWidth! < 800) { + return false; + } + + // special check for running under ipad mode in macos + if (Platform.isIOS && + libraryPath != null && + !libraryPath!.path.contains("/var/mobile/")) { + return true; + } + return Platform.isLinux || Platform.isMacOS || Platform.isWindows; } diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index 7a2fca19f..dfa655f86 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -1,6 +1,7 @@ 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/pages/address_book_views/subviews/contact_popup.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -8,12 +9,22 @@ 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/conditional_parent.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddressBookCard extends ConsumerStatefulWidget { - const AddressBookCard({Key? key, required this.contactId}) : super(key: key); + const AddressBookCard({ + Key? key, + required this.contactId, + this.indicatorDown, + this.desktopSendFrom = true, + }) : super(key: key); final String contactId; + final ExpandableState? indicatorDown; + final bool desktopSendFrom; @override ConsumerState<AddressBookCard> createState() => _AddressBookCardState(); @@ -21,20 +32,29 @@ class AddressBookCard extends ConsumerStatefulWidget { class _AddressBookCardState extends ConsumerState<AddressBookCard> { late final String contactId; + late final bool isDesktop; + late final bool desktopSendFrom; @override void initState() { contactId = widget.contactId; - + desktopSendFrom = widget.desktopSendFrom; + isDesktop = Util.isDesktop; super.initState(); } @override Widget build(BuildContext context) { - // final isTiny = SizingUtilities.isTinyWidth(context); + // provider hack to prevent trying to update widget with deleted contact + Contact? _contact; + try { + _contact = ref.watch(addressBookServiceProvider + .select((value) => value.getContactById(contactId))); + } catch (_) { + return Container(); + } - final contact = ref.watch(addressBookServiceProvider - .select((value) => value.getContactById(contactId))); + final contact = _contact!; final List<Coin> coins = []; for (var element in contact.addresses) { @@ -51,82 +71,112 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { } } - return RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showDialog<void>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => ContactPopUp( - contactId: contact.id, + return ConditionalParent( + condition: !isDesktop, + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: contact.id == "default" + ? Theme.of(context) + .extension<StackColors>()! + .myStackContactIconBG + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: contact.id == "default" - ? Theme.of(context) - .extension<StackColors>()! - .myStackContactIconBG - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(32), - ), - child: contact.id == "default" + child: contact.id == "default" + ? Center( + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 20, + ), + ) + : contact.emojiChar != null ? Center( - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 20, - ), + child: Text(contact.emojiChar!), ) - : contact.emojiChar != null - ? Center( - child: Text(contact.emojiChar!), - ) - : Center( - child: SvgPicture.asset( - Assets.svg.user, - width: 18, - ), - ), + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), + ), + const SizedBox( + width: 12, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + if (isDesktop && !desktopSendFrom) const Spacer(), + if (isDesktop) + Text( + coinsString, + style: STextStyles.label(context), + ), + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + contact.name, + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 4, + ), + Text( + coinsString, + style: STextStyles.label(context), + ), + ], + ), + if (isDesktop && desktopSendFrom) const Spacer(), + if (isDesktop && desktopSendFrom) + SvgPicture.asset( + widget.indicatorDown == ExpandableState.collapsed + ? Assets.svg.chevronDown + : Assets.svg.chevronUp, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textSubtitle2, + ), + ], + ), + builder: (child) => RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showDialog<void>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => ContactPopUp( + contactId: contact.id, ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - contact.name, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 4, - ), - Text( - coinsString, - style: STextStyles.label(context), - ), - ], - ) - ], + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: child, ), ), ), diff --git a/lib/widgets/background.dart b/lib/widgets/background.dart new file mode 100644 index 000000000..67ff44f55 --- /dev/null +++ b/lib/widgets/background.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; + +class Background extends StatelessWidget { + const Background({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + Color? color; + + switch (Theme.of(context).extension<StackColors>()!.themeType) { + case ThemeType.light: + case ThemeType.dark: + color = Theme.of(context).extension<StackColors>()!.background; + break; + case ThemeType.oceanBreeze: + color = null; + break; + } + + final bgAsset = Assets.svg.background(context); + + return Container( + decoration: BoxDecoration( + color: color, + gradient: + Theme.of(context).extension<StackColors>()!.gradientBackground, + ), + child: ConditionalParent( + condition: bgAsset != null, + builder: (child) => Stack( + children: [ + Positioned.fill( + child: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).size.height * (1 / 8), + bottom: MediaQuery.of(context).size.height * (1 / 12), + ), + child: SvgPicture.asset( + bgAsset!, + fit: BoxFit.fill, + ), + ), + ), + Positioned.fill( + child: child, + ), + ], + ), + child: child, + ), + ); + } +} diff --git a/lib/widgets/conditional_parent.dart b/lib/widgets/conditional_parent.dart new file mode 100644 index 000000000..e8c60884a --- /dev/null +++ b/lib/widgets/conditional_parent.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class ConditionalParent extends StatelessWidget { + const ConditionalParent({ + Key? key, + required this.condition, + required this.builder, + required this.child, + }) : super(key: key); + + final bool condition; + final Widget Function(Widget) builder; + final Widget child; + + @override + Widget build(BuildContext context) { + if (condition) { + return builder(child); + } else { + return child; + } + } +} + +class BranchedParent extends StatelessWidget { + const BranchedParent({ + Key? key, + required this.condition, + required this.conditionBranchBuilder, + required this.otherBranchBuilder, + required this.children, + }) : super(key: key); + + final bool condition; + final Widget Function(List<Widget>) conditionBranchBuilder; + final Widget Function(List<Widget>) otherBranchBuilder; + final List<Widget> children; + + @override + Widget build(BuildContext context) { + if (condition) { + return conditionBranchBuilder(children); + } else { + return otherBranchBuilder(children); + } + } +} diff --git a/lib/widgets/custom_buttons/app_bar_icon_button.dart b/lib/widgets/custom_buttons/app_bar_icon_button.dart index eb926112a..9edc1ca5f 100644 --- a/lib/widgets/custom_buttons/app_bar_icon_button.dart +++ b/lib/widgets/custom_buttons/app_bar_icon_button.dart @@ -51,10 +51,14 @@ class AppBarBackButton extends StatelessWidget { Key? key, this.onPressed, this.isCompact = false, + this.size, + this.iconSize, }) : super(key: key); final VoidCallback? onPressed; final bool isCompact; + final double? size; + final double? iconSize; @override Widget build(BuildContext context) { @@ -67,19 +71,20 @@ class AppBarBackButton extends StatelessWidget { ) : const EdgeInsets.all(10), child: AppBarIconButton( - size: isDesktop - ? isCompact - ? 42 - : 56 - : 32, + size: size ?? + (isDesktop + ? isCompact + ? 42 + : 56 + : 32), color: isDesktop ? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG : Theme.of(context).extension<StackColors>()!.background, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, - width: isCompact ? 18 : 24, - height: isCompact ? 18 : 24, + width: iconSize ?? (isCompact ? 18 : 24), + height: iconSize ?? (isCompact ? 18 : 24), color: Theme.of(context).extension<StackColors>()!.topNavIconPrimary, ), onPressed: onPressed ?? Navigator.of(context).pop, diff --git a/lib/widgets/custom_buttons/blue_text_button.dart b/lib/widgets/custom_buttons/blue_text_button.dart index 18757ab93..a87d1e6b2 100644 --- a/lib/widgets/custom_buttons/blue_text_button.dart +++ b/lib/widgets/custom_buttons/blue_text_button.dart @@ -5,11 +5,18 @@ import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; class BlueTextButton extends ConsumerStatefulWidget { - const BlueTextButton({Key? key, required this.text, this.onTap}) - : super(key: key); + const BlueTextButton({ + Key? key, + required this.text, + this.onTap, + this.enabled = true, + this.textSize, + }) : super(key: key); final String text; final VoidCallback? onTap; + final bool enabled; + final double? textSize; @override ConsumerState<BlueTextButton> createState() => _BlueTextButtonState(); @@ -17,38 +24,42 @@ class BlueTextButton extends ConsumerStatefulWidget { class _BlueTextButtonState extends ConsumerState<BlueTextButton> with SingleTickerProviderStateMixin { - late AnimationController controller; - late Animation<dynamic> animation; + AnimationController? controller; + Animation<dynamic>? animation; late Color color; @override void initState() { - color = ref.read(colorThemeProvider.state).state.buttonTextBorderless; - controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 100), - ); - animation = ColorTween( - begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless, - end: ref - .read(colorThemeProvider.state) - .state - .buttonTextBorderless - .withOpacity(0.4), - ).animate(controller); + if (widget.enabled) { + color = ref.read(colorThemeProvider.state).state.buttonTextBorderless; + controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + animation = ColorTween( + begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless, + end: ref + .read(colorThemeProvider.state) + .state + .buttonTextBorderless + .withOpacity(0.4), + ).animate(controller!); - animation.addListener(() { - setState(() { - color = animation.value as Color; + animation!.addListener(() { + setState(() { + color = animation!.value as Color; + }); }); - }); + } else { + color = ref.read(colorThemeProvider.state).state.textSubtitle1; + } super.initState(); } @override void dispose() { - controller.dispose(); + controller?.dispose(); super.dispose(); } @@ -58,12 +69,21 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton> textAlign: TextAlign.center, text: TextSpan( text: widget.text, - style: STextStyles.link2(context).copyWith(color: color), - recognizer: TapGestureRecognizer() - ..onTap = () { - widget.onTap?.call(); - controller.forward().then((value) => controller.reverse()); - }, + style: widget.textSize == null + ? STextStyles.link2(context).copyWith( + color: color, + ) + : STextStyles.link2(context).copyWith( + color: color, + fontSize: widget.textSize, + ), + recognizer: widget.enabled + ? (TapGestureRecognizer() + ..onTap = () { + widget.onTap?.call(); + controller?.forward().then((value) => controller?.reverse()); + }) + : null, ), ); } diff --git a/lib/widgets/desktop/custom_text_button.dart b/lib/widgets/desktop/custom_text_button.dart index b96a697b8..90b75c459 100644 --- a/lib/widgets/desktop/custom_text_button.dart +++ b/lib/widgets/desktop/custom_text_button.dart @@ -1,6 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/util.dart'; +enum ButtonHeight { + xxs, + xs, + s, + m, + l, + xl, + xxl, +} + class CustomTextButtonBase extends StatelessWidget { const CustomTextButtonBase({ Key? key, diff --git a/lib/widgets/desktop/delete_button.dart b/lib/widgets/desktop/delete_button.dart new file mode 100644 index 000000000..e64c85f34 --- /dev/null +++ b/lib/widgets/desktop/delete_button.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +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/custom_text_button.dart'; + +class DeleteButton extends StatelessWidget { + const DeleteButton({ + Key? key, + this.width, + this.height, + this.label, + this.onPressed, + this.enabled = true, + this.desktopMed = false, + }) : super(key: key); + + final double? width; + final double? height; + final String? label; + final VoidCallback? onPressed; + final bool enabled; + final bool desktopMed; + + TextStyle getStyle(bool isDesktop, BuildContext context) { + if (isDesktop) { + if (desktopMed) { + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.accentColorRed + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + } else { + return enabled + ? STextStyles.desktopButtonSecondaryEnabled(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.accentColorRed) + : STextStyles.desktopButtonSecondaryDisabled(context); + } + } else { + return STextStyles.button(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.accentColorRed + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + } + } + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return CustomTextButtonBase( + height: desktopMed ? 56 : height, + width: width, + textButton: TextButton( + onPressed: enabled ? onPressed : null, + style: enabled + ? Theme.of(context) + .extension<StackColors>()! + .getDeleteEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getDeleteDisabledButtonColor(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.trash, + width: 20, + height: 20, + color: enabled + ? Theme.of(context).extension<StackColors>()!.accentColorRed + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ), + if (label != null) + const SizedBox( + width: 10, + ), + if (label != null) + Text( + label!, + style: getStyle(isDesktop, context), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/desktop/desktop_app_bar.dart b/lib/widgets/desktop/desktop_app_bar.dart index 1c825382c..bbad0385a 100644 --- a/lib/widgets/desktop/desktop_app_bar.dart +++ b/lib/widgets/desktop/desktop_app_bar.dart @@ -11,6 +11,7 @@ class DesktopAppBar extends StatefulWidget { this.trailing, this.background = Colors.transparent, required this.isCompactHeight, + this.useSpacers = true, }) : super(key: key); final Widget? leading; @@ -18,6 +19,7 @@ class DesktopAppBar extends StatefulWidget { final Widget? trailing; final Color background; final bool isCompactHeight; + final bool useSpacers; @override State<DesktopAppBar> createState() => _DesktopAppBarState(); @@ -33,11 +35,15 @@ class _DesktopAppBarState extends State<DesktopAppBar> { items.add(widget.leading!); } - items.add(const Spacer()); + if (widget.useSpacers) { + items.add(const Spacer()); + } if (widget.center != null) { items.add(widget.center!); - items.add(const Spacer()); + if (widget.useSpacers) { + items.add(const Spacer()); + } } if (widget.trailing != null) { diff --git a/lib/widgets/desktop/desktop_dialog.dart b/lib/widgets/desktop/desktop_dialog.dart index 5ada3a545..59c59c575 100644 --- a/lib/widgets/desktop/desktop_dialog.dart +++ b/lib/widgets/desktop/desktop_dialog.dart @@ -2,9 +2,16 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class DesktopDialog extends StatelessWidget { - const DesktopDialog({Key? key, this.child}) : super(key: key); + const DesktopDialog({ + Key? key, + this.child, + this.maxWidth = 641, + this.maxHeight = 474, + }) : super(key: key); final Widget? child; + final double maxWidth; + final double maxHeight; @override Widget build(BuildContext context) { @@ -13,11 +20,12 @@ class DesktopDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 641, - maxHeight: 474, + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, ), child: Material( + color: Colors.transparent, borderRadius: BorderRadius.circular( 20, ), diff --git a/lib/widgets/desktop/desktop_scaffold.dart b/lib/widgets/desktop/desktop_scaffold.dart index 439289518..51fa9f3a5 100644 --- a/lib/widgets/desktop/desktop_scaffold.dart +++ b/lib/widgets/desktop/desktop_scaffold.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; class DesktopScaffold extends StatelessWidget { const DesktopScaffold({ @@ -18,15 +19,17 @@ class DesktopScaffold extends StatelessWidget { return Material( color: background ?? Theme.of(context).extension<StackColors>()!.background, - child: Column( - // crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (appBar != null) appBar!, - if (body != null) - Expanded( - child: body!, - ), - ], + child: Background( + child: Column( + // crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (appBar != null) appBar!, + if (body != null) + Expanded( + child: body!, + ), + ], + ), ), ); } @@ -50,17 +53,18 @@ class MasterScaffold extends StatelessWidget { Widget build(BuildContext context) { if (isDesktop) { return DesktopScaffold( - background: background ?? - Theme.of(context).extension<StackColors>()!.background, + background: background, appBar: appBar, body: body, ); } else { - return Scaffold( - backgroundColor: background ?? - Theme.of(context).extension<StackColors>()!.background, - appBar: appBar as PreferredSizeWidget?, - body: body, + return Background( + child: Scaffold( + backgroundColor: background ?? + Theme.of(context).extension<StackColors>()!.background, + appBar: appBar as PreferredSizeWidget?, + body: body, + ), ); } } diff --git a/lib/widgets/desktop/living_stack_icon.dart b/lib/widgets/desktop/living_stack_icon.dart new file mode 100644 index 000000000..7afc8f8d2 --- /dev/null +++ b/lib/widgets/desktop/living_stack_icon.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; + +class LivingStackIcon extends StatefulWidget { + const LivingStackIcon({Key? key, this.onPressed,}) : super(key: key); + + final VoidCallback? onPressed; + + @override + State<LivingStackIcon> createState() => _LivingStackIconState(); +} + +class _LivingStackIconState extends State<LivingStackIcon> { + bool _hovering = false; + + late final VoidCallback? onPressed; + + @override + void initState() { + onPressed = widget.onPressed; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 76, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: GestureDetector( + onTap: () => onPressed?.call(), + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: _hovering ? 1.2 : 1, + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/desktop/primary_button.dart b/lib/widgets/desktop/primary_button.dart index 6034cc08b..9441168e7 100644 --- a/lib/widgets/desktop/primary_button.dart +++ b/lib/widgets/desktop/primary_button.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; +export 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + class PrimaryButton extends StatelessWidget { const PrimaryButton({ Key? key, @@ -13,6 +15,7 @@ class PrimaryButton extends StatelessWidget { this.icon, this.onPressed, this.enabled = true, + this.buttonHeight, }) : super(key: key); final double? width; @@ -21,13 +24,100 @@ class PrimaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; + final ButtonHeight? buttonHeight; + + TextStyle getStyle(bool isDesktop, BuildContext context) { + if (isDesktop) { + if (buttonHeight == null) { + return enabled + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.desktopButtonDisabled(context); + } + + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + return STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ); + + case ButtonHeight.m: + case ButtonHeight.l: + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ); + + case ButtonHeight.xl: + case ButtonHeight.xxl: + return enabled + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.desktopButtonDisabled(context); + } + } else { + return STextStyles.button(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ); + } + } + + double? _getHeight() { + if (buttonHeight == null) { + return height; + } + + if (Util.isDesktop) { + switch (buttonHeight!) { + case ButtonHeight.xxs: + return 32; + case ButtonHeight.xs: + return 37; + case ButtonHeight.s: + return 40; + case ButtonHeight.m: + return 48; + case ButtonHeight.l: + return 56; + case ButtonHeight.xl: + return 70; + case ButtonHeight.xxl: + return 96; + } + } else { + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + case ButtonHeight.m: + return 28; + case ButtonHeight.l: + return 30; + case ButtonHeight.xl: + return 46; + case ButtonHeight.xxl: + return 56; + } + } + } @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; return CustomTextButtonBase( - height: height, + height: _getHeight(), width: width, textButton: TextButton( onPressed: enabled ? onPressed : null, @@ -49,19 +139,7 @@ class PrimaryButton extends StatelessWidget { if (label != null) Text( label!, - style: isDesktop - ? enabled - ? STextStyles.desktopButtonEnabled(context) - : STextStyles.desktopButtonDisabled(context) - : STextStyles.button(context).copyWith( - color: enabled - ? Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimaryDisabled, - ), + style: getStyle(isDesktop, context), ), ], ), diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index 2a88e548d..62bd900dd 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; +export 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + class SecondaryButton extends StatelessWidget { const SecondaryButton({ Key? key, @@ -13,6 +15,7 @@ class SecondaryButton extends StatelessWidget { this.icon, this.onPressed, this.enabled = true, + this.buttonHeight, }) : super(key: key); final double? width; @@ -21,13 +24,103 @@ class SecondaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; + final ButtonHeight? buttonHeight; + + TextStyle getStyle(bool isDesktop, BuildContext context) { + if (isDesktop) { + if (buttonHeight == null) { + return enabled + ? STextStyles.desktopButtonSecondaryEnabled(context) + : STextStyles.desktopButtonSecondaryDisabled(context); + } + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + return STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + + case ButtonHeight.m: + case ButtonHeight.l: + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + + case ButtonHeight.xl: + case ButtonHeight.xxl: + return enabled + ? STextStyles.desktopButtonSecondaryEnabled(context) + : STextStyles.desktopButtonSecondaryDisabled(context); + } + } else { + return STextStyles.button(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextSecondary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + } + } + + double? _getHeight() { + if (buttonHeight == null) { + return height; + } + + if (Util.isDesktop) { + switch (buttonHeight!) { + case ButtonHeight.xxs: + return 32; + case ButtonHeight.xs: + return 37; + case ButtonHeight.s: + return 40; + case ButtonHeight.m: + return 48; + case ButtonHeight.l: + return 56; + case ButtonHeight.xl: + return 70; + case ButtonHeight.xxl: + return 96; + } + } else { + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + case ButtonHeight.m: + return 28; + case ButtonHeight.l: + return 30; + case ButtonHeight.xl: + return 46; + case ButtonHeight.xxl: + return 56; + } + } + } @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; return CustomTextButtonBase( - height: height, + height: _getHeight(), width: width, textButton: TextButton( onPressed: enabled ? onPressed : null, @@ -49,19 +142,7 @@ class SecondaryButton extends StatelessWidget { if (label != null) Text( label!, - style: isDesktop - ? enabled - ? STextStyles.desktopButtonSecondaryEnabled(context) - : STextStyles.desktopButtonSecondaryDisabled(context) - : STextStyles.button(context).copyWith( - color: enabled - ? Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondaryDisabled, - ), + style: getStyle(isDesktop, context), ), ], ), diff --git a/lib/widgets/desktop/simple_desktop_dialog.dart b/lib/widgets/desktop/simple_desktop_dialog.dart new file mode 100644 index 000000000..cd066c221 --- /dev/null +++ b/lib/widgets/desktop/simple_desktop_dialog.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.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'; + +class SimpleDesktopDialog extends StatelessWidget { + const SimpleDesktopDialog({ + Key? key, + required this.title, + required this.message, + }) : super(key: key); + + final String title; + final String message; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const Spacer(), + Text( + message, + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ), + ], + ) + ], + ), + ); + } +} diff --git a/lib/widgets/emoji_select_sheet.dart b/lib/widgets/emoji_select_sheet.dart index 85a90fec8..ecb6d1a1b 100644 --- a/lib/widgets/emoji_select_sheet.dart +++ b/lib/widgets/emoji_select_sheet.dart @@ -1,11 +1,19 @@ import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.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/desktop/secondary_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'; -class EmojiSelectSheet extends ConsumerWidget { +class EmojiSelectSheet extends ConsumerStatefulWidget { const EmojiSelectSheet({ Key? key, }) : super(key: key); @@ -15,72 +23,188 @@ class EmojiSelectSheet extends ConsumerWidget { final double minimumEmojiSpacing = 25; @override - Widget build(BuildContext context, WidgetRef ref) { - final size = MediaQuery.of(context).size; - final double maxHeight = size.height * 0.60; - final double availableWidth = size.width - (2 * horizontalPadding); - final int emojisPerRow = + ConsumerState<EmojiSelectSheet> createState() => _EmojiSelectSheetState(); +} + +class _EmojiSelectSheetState extends ConsumerState<EmojiSelectSheet> { + final isDesktop = Util.isDesktop; + + late final TextEditingController _searchController; + late final FocusNode _searchFocusNode; + late final double horizontalPadding = 24; + late final double emojiSize = 24; + late final double minimumEmojiSpacing = 25; + + String _searchTerm = ""; + + List<Emoji> filtered(String text) { + if (text.isEmpty) { + return Emoji.all(); + } + + text = text.toLowerCase(); + + return Emoji.all() + .where((e) => e.keywords + .where( + (e) => e.contains(text), + ) + .isNotEmpty) + .toList(growable: false); + } + + @override + void initState() { + _searchController = TextEditingController(); + _searchFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = isDesktop ? const Size(600, 700) : MediaQuery.of(context).size; + final maxHeight = size.height * (isDesktop ? 0.6 : 0.9); + final availableWidth = size.width - (2 * horizontalPadding); + final emojisPerRow = ((availableWidth - emojiSize) ~/ (emojiSize + minimumEmojiSpacing)) + 1; - final itemCount = Emoji.all().length; - - return Container( - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: LimitedBox( + maxHeight: maxHeight, + child: Padding( + padding: EdgeInsets.only( + left: horizontalPadding, + right: horizontalPadding, + top: 10, + bottom: 0, + ), + child: child, + ), ), ), - child: LimitedBox( - maxHeight: maxHeight, - child: Padding( - padding: EdgeInsets.only( - left: horizontalPadding, - right: horizontalPadding, - top: 10, - bottom: 0, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isDesktop) + Center( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + width: 60, + height: 4, + ), + ), + if (!isDesktop) + const SizedBox( + height: 36, + ), + Text( + "Select emoji", + style: isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH2(context), + textAlign: TextAlign.left, ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + SizedBox( + height: isDesktop ? 16 : 12, + ), + Material( + color: Colors.transparent, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => _searchTerm = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, ), ), - width: 60, - height: 4, + 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: 36, - ), - Text( - "Select emoji", - style: STextStyles.pageTitleH2(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 16, - ), - Flexible( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: GridView.builder( + ), + ), + SizedBox( + height: isDesktop ? 28 : 16, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Builder( + builder: (context) { + final emojis = filtered(_searchTerm); + final itemCount = emojis.length; + return GridView.builder( itemCount: itemCount, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: emojisPerRow, ), itemBuilder: (context, index) { - final emoji = Emoji.all()[index]; + final emoji = emojis[index]; return GestureDetector( onTap: () { Navigator.of(context).pop(emoji); @@ -92,22 +216,39 @@ class EmojiSelectSheet extends ConsumerWidget { ), child: Padding( padding: const EdgeInsets.all(8.0), - child: Text(emoji.char), + child: Text( + emoji.char, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : null, + ), ), ), ); }, - ), - ) - ], - ), - ), - const SizedBox( - height: 24, - ), - ], + ); + }, + ), + ) + ], + ), ), - ), + SizedBox( + height: isDesktop ? 20 : 24, + ), + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SecondaryButton( + label: "Cancel", + width: 248, + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ], + ), + ], ), ); } diff --git a/lib/widgets/expandable.dart b/lib/widgets/expandable.dart index ddae2201d..737f4ce7d 100644 --- a/lib/widgets/expandable.dart +++ b/lib/widgets/expandable.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; enum ExpandableState { - expanded, collapsed, + expanded, +} + +class ExpandableController { + VoidCallback? toggle; + ExpandableState state = ExpandableState.collapsed; } class Expandable extends StatefulWidget { @@ -14,6 +19,7 @@ class Expandable extends StatefulWidget { this.animation, this.animationDurationMultiplier = 1.0, this.onExpandChanged, + this.controller, }) : super(key: key); final Widget header; @@ -22,6 +28,7 @@ class Expandable extends StatefulWidget { final Animation<double>? animation; final double animationDurationMultiplier; final void Function(ExpandableState)? onExpandChanged; + final ExpandableController? controller; @override State<Expandable> createState() => _ExpandableState(); @@ -31,19 +38,28 @@ class _ExpandableState extends State<Expandable> with TickerProviderStateMixin { late final AnimationController animationController; late final Animation<double> animation; late final Duration duration; + late final ExpandableController? controller; + + ExpandableState _toggleState = ExpandableState.collapsed; Future<void> toggle() async { if (animation.isDismissed) { await animationController.forward(); - widget.onExpandChanged?.call(ExpandableState.collapsed); + _toggleState = ExpandableState.expanded; + widget.onExpandChanged?.call(_toggleState); } else if (animation.isCompleted) { await animationController.reverse(); - widget.onExpandChanged?.call(ExpandableState.expanded); + _toggleState = ExpandableState.collapsed; + widget.onExpandChanged?.call(_toggleState); } + controller?.state = _toggleState; } @override void initState() { + controller = widget.controller; + controller?.toggle = toggle; + duration = Duration( milliseconds: (500 * widget.animationDurationMultiplier).toInt(), ); @@ -73,11 +89,14 @@ class _ExpandableState extends State<Expandable> with TickerProviderStateMixin { return Column( mainAxisSize: MainAxisSize.min, children: [ - GestureDetector( - onTap: toggle, - child: Container( - color: Colors.transparent, - child: widget.header, + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: toggle, + child: Container( + color: Colors.transparent, + child: widget.header, + ), ), ), SizeTransition( diff --git a/lib/widgets/hover_text_field.dart b/lib/widgets/hover_text_field.dart new file mode 100644 index 000000000..475d6c2ec --- /dev/null +++ b/lib/widgets/hover_text_field.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; + +class HoverTextField extends StatefulWidget { + const HoverTextField({ + Key? key, + this.controller, + this.focusNode, + this.readOnly = false, + this.enabled, + this.onTap, + this.onChanged, + this.onEditingComplete, + this.style, + this.onDone, + }) : super(key: key); + + final TextEditingController? controller; + final FocusNode? focusNode; + final bool readOnly; + final bool? enabled; + final GestureTapCallback? onTap; + final ValueChanged<String>? onChanged; + final VoidCallback? onEditingComplete; + final TextStyle? style; + final VoidCallback? onDone; + + @override + State<HoverTextField> createState() => _HoverTextFieldState(); +} + +class _HoverTextFieldState extends State<HoverTextField> { + late final TextEditingController? controller; + late final FocusNode? focusNode; + late bool readOnly; + late bool? enabled; + late final GestureTapCallback? onTap; + late final ValueChanged<String>? onChanged; + late final VoidCallback? onEditingComplete; + late final TextStyle? style; + late final VoidCallback? onDone; + + final InputBorder inputBorder = OutlineInputBorder( + borderSide: const BorderSide( + width: 0, + color: Colors.transparent, + ), + borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), + ); + + @override + void initState() { + controller = widget.controller; + focusNode = widget.focusNode ?? FocusNode(); + readOnly = widget.readOnly; + enabled = widget.enabled; + onChanged = widget.onChanged; + style = widget.style; + onTap = widget.onTap; + onEditingComplete = widget.onEditingComplete; + onDone = widget.onDone; + + focusNode!.addListener(() { + if (!focusNode!.hasPrimaryFocus && !readOnly) { + setState(() { + readOnly = true; + }); + onDone?.call(); + } + }); + super.initState(); + } + + @override + void dispose() { + controller?.dispose(); + focusNode?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextField( + autocorrect: !Util.isDesktop, + enableSuggestions: !Util.isDesktop, + controller: controller, + focusNode: focusNode, + readOnly: readOnly, + enabled: enabled, + onTap: () { + setState(() { + readOnly = false; + }); + onTap?.call(); + }, + onChanged: onChanged, + onEditingComplete: () { + setState(() { + readOnly = true; + }); + onEditingComplete?.call(); + onDone?.call(); + }, + style: style, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 12, + ), + border: inputBorder, + focusedBorder: inputBorder, + disabledBorder: inputBorder, + enabledBorder: inputBorder, + errorBorder: inputBorder, + fillColor: readOnly + ? Colors.transparent + : Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, + ), + ); + } +} diff --git a/lib/widgets/icon_widgets/copy_icon.dart b/lib/widgets/icon_widgets/copy_icon.dart new file mode 100644 index 000000000..9f82a8066 --- /dev/null +++ b/lib/widgets/icon_widgets/copy_icon.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class CopyIcon extends StatelessWidget { + const CopyIcon({ + Key? key, + this.width = 18, + this.height = 18, + this.color, + }) : super(key: key); + + final double width; + final double height; + final Color? color; + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.copy, + width: width, + height: height, + color: color ?? Theme.of(context).extension<StackColors>()!.textDark3, + ); + } +} diff --git a/lib/widgets/icon_widgets/pencil_icon.dart b/lib/widgets/icon_widgets/pencil_icon.dart new file mode 100644 index 000000000..cb14f1cbf --- /dev/null +++ b/lib/widgets/icon_widgets/pencil_icon.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class PencilIcon extends StatelessWidget { + const PencilIcon({ + Key? key, + this.width = 18, + this.height = 18, + this.color, + }) : super(key: key); + + final double width; + final double height; + final Color? color; + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.pencil, + width: width, + height: height, + color: color ?? Theme.of(context).extension<StackColors>()!.textDark3, + ); + } +} diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 1f0287013..fb8260b24 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -1,15 +1,31 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; 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/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +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/blue_text_button.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/node_options_sheet.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; class NodeCard extends ConsumerStatefulWidget { const NodeCard({ @@ -30,6 +46,147 @@ class NodeCard extends ConsumerStatefulWidget { class _NodeCardState extends ConsumerState<NodeCard> { String _status = "Disconnected"; late final String nodeId; + bool _advancedIsExpanded = false; + + Future<void> _notifyWalletsOfUpdatedNode(WidgetRef ref) async { + final managers = ref + .read(walletsChangeNotifierProvider) + .managers + .where((e) => e.coin == widget.coin); + final prefs = ref.read(prefsChangeNotifierProvider); + + switch (prefs.syncType) { + case SyncingType.currentWalletOnly: + for (final manager in managers) { + if (manager.isActiveWallet) { + manager.updateNode(true); + } else { + manager.updateNode(false); + } + } + break; + case SyncingType.selectedWalletsAtStartup: + final List<String> walletIdsToSync = prefs.walletIdsSyncOnStartup; + for (final manager in managers) { + if (walletIdsToSync.contains(manager.walletId)) { + manager.updateNode(true); + } else { + manager.updateNode(false); + } + } + break; + case SyncingType.allWalletsOnStartup: + for (final manager in managers) { + manager.updateNode(true); + } + break; + } + } + + Future<bool> _testConnection( + NodeModel node, + BuildContext context, + WidgetRef ref, + ) async { + bool testPassed = false; + + switch (widget.coin) { + case Coin.epicCash: + try { + final String uriString = "${node.host}:${node.port}/v1/version"; + + testPassed = await testEpicBoxNodeConnection(Uri.parse(uriString)); + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } + break; + + case Coin.monero: + case Coin.wownero: + try { + final uri = Uri.parse(node.host); + if (uri.scheme.startsWith("http")) { + final String path = uri.path.isEmpty ? "/json_rpc" : uri.path; + + String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; + + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = await testMoneroNodeConnection( + Uri.parse(uriString), true); + testPassed = response.success; + } + } + } else { + testPassed = response.success; + } + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } + + break; + + case Coin.bitcoin: + case Coin.litecoin: + case Coin.dogecoin: + case Coin.firo: + case Coin.bitcoinTestNet: + case Coin.firoTestNet: + case Coin.dogecoinTestNet: + case Coin.bitcoincash: + case Coin.litecoinTestNet: + case Coin.namecoin: + case Coin.bitcoincashTestnet: + final client = ElectrumX( + host: node.host, + port: node.port, + useSSL: node.useSSL, + failovers: [], + prefs: ref.read(prefsChangeNotifierProvider), + ); + + try { + testPassed = await client.ping(); + } catch (_) { + testPassed = false; + } + + break; + } + + if (testPassed) { + // showFloatingFlushBar( + // type: FlushBarType.success, + // message: "Server ping success", + // context: context, + // ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + iconAsset: Assets.svg.circleAlert, + message: "Could not connect to node", + context: context, + ), + ); + } + + return testPassed; + } @override void initState() { @@ -50,91 +207,177 @@ class _NodeCardState extends ConsumerState<NodeCard> { _status = "Disconnected"; } + final isDesktop = Util.isDesktop; + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - builder: (_) => NodeOptionsSheet( - nodeId: nodeId, - coin: widget.coin, - popBackToRoute: widget.popBackToRoute, + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + child: ConditionalParent( + condition: !isDesktop, + builder: (child) { + return RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), + onPressed: () { + showModalBottomSheet<void>( + backgroundColor: Colors.transparent, + context: context, + builder: (_) => NodeOptionsSheet( + nodeId: nodeId, + coin: widget.coin, + popBackToRoute: widget.popBackToRoute, + ), + ); + }, + child: child, ); }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: _node.name == DefaultNodes.defaultName - ? Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary - : Theme.of(context) - .extension<StackColors>()! - .infoItemIcons - .withOpacity(0.2), - borderRadius: BorderRadius.circular(100), + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Expandable( + onExpandChanged: (state) { + setState(() { + _advancedIsExpanded = state == ExpandableState.expanded; + }); + }, + header: child, + body: Padding( + padding: const EdgeInsets.only( + bottom: 24, ), - child: Center( - child: SvgPicture.asset( - Assets.svg.node, - height: 11, - width: 14, - color: _node.name == DefaultNodes.defaultName + child: Row( + children: [ + const SizedBox( + width: 66, + ), + BlueTextButton( + text: "Connect", + enabled: _status == "Disconnected", + onTap: () async { + final canConnect = + await _testConnection(_node, context, ref); + if (!canConnect) { + return; + } + + await ref + .read(nodeServiceChangeNotifierProvider) + .setPrimaryNodeFor( + coin: widget.coin, + node: _node, + shouldNotifyListeners: true, + ); + + await _notifyWalletsOfUpdatedNode(ref); + }, + ), + const SizedBox( + width: 48, + ), + BlueTextButton( + text: "Details", + onTap: () { + Navigator.of(context).pushNamed( + NodeDetailsView.routeName, + arguments: Tuple3( + widget.coin, + widget.nodeId, + widget.popBackToRoute, + ), + ); + }, + ), + ], + ), + ), + ); + }, + child: Padding( + padding: EdgeInsets.all(isDesktop ? 16 : 12), + child: Row( + children: [ + Container( + width: isDesktop ? 40 : 24, + height: isDesktop ? 40 : 24, + decoration: BoxDecoration( + color: _node.id.startsWith(DefaultNodes.defaultNodeIdPrefix) ? Theme.of(context) .extension<StackColors>()! - .accentColorDark + .buttonBackSecondary : Theme.of(context) .extension<StackColors>()! - .infoItemIcons, + .infoItemIcons + .withOpacity(0.2), + borderRadius: BorderRadius.circular(100), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + height: isDesktop ? 18 : 11, + width: isDesktop ? 20 : 14, + color: + _node.id.startsWith(DefaultNodes.defaultNodeIdPrefix) + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), ), ), - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _node.name, - style: STextStyles.titleBold12(context), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _node.name, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 2, + ), + Text( + _status, + style: STextStyles.label(context), + ), + ], + ), + const Spacer(), + if (!isDesktop) + SvgPicture.asset( + Assets.svg.network, + color: _status == "Connected" + ? Theme.of(context) + .extension<StackColors>()! + .accentColorGreen + : Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + width: 20, + height: 20, ), - const SizedBox( - height: 2, - ), - Text( - _status, - style: STextStyles.label(context), - ), - ], - ), - const Spacer(), - SvgPicture.asset( - Assets.svg.network, - color: _status == "Connected" - ? Theme.of(context) + if (isDesktop) + SvgPicture.asset( + _advancedIsExpanded + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) .extension<StackColors>()! - .accentColorGreen - : Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - width: 20, - height: 20, - ), - ], + .textSubtitle1, + ), + ], + ), ), ), ), diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index effe97097..1ef9b07fe 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -93,7 +93,29 @@ class NodeOptionsSheet extends ConsumerWidget { String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + // if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = + await testMoneroNodeConnection(Uri.parse(uriString), true); + testPassed = response.success; + } + // } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); @@ -102,12 +124,14 @@ class NodeOptionsSheet extends ConsumerWidget { break; case Coin.bitcoin: + case Coin.litecoin: case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: case Coin.bitcoincash: + case Coin.litecoinTestNet: case Coin.namecoin: case Coin.bitcoincashTestnet: final client = ElectrumX( @@ -210,7 +234,8 @@ class NodeOptionsSheet extends ConsumerWidget { width: 32, height: 32, decoration: BoxDecoration( - color: node.name == DefaultNodes.defaultName + color: node.id + .startsWith(DefaultNodes.defaultNodeIdPrefix) ? Theme.of(context) .extension<StackColors>()! .textSubtitle4 @@ -225,7 +250,8 @@ class NodeOptionsSheet extends ConsumerWidget { Assets.svg.node, height: 15, width: 19, - color: node.name == DefaultNodes.defaultName + color: node.id.startsWith( + DefaultNodes.defaultNodeIdPrefix) ? Theme.of(context) .extension<StackColors>()! .accentColorDark diff --git a/lib/widgets/rounded_date_picker/LICENSE b/lib/widgets/rounded_date_picker/LICENSE new file mode 100644 index 000000000..58665fbd2 --- /dev/null +++ b/lib/widgets/rounded_date_picker/LICENSE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at: + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart new file mode 100644 index 000000000..6d7f775cd --- /dev/null +++ b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart @@ -0,0 +1,332 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; +import 'package:flutter_rounded_date_picker/src/flutter_rounded_button_action.dart'; +import 'package:flutter_rounded_date_picker/src/material_rounded_date_picker_style.dart'; +import 'package:flutter_rounded_date_picker/src/material_rounded_year_picker_style.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_date_picker_header.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_day_picker.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_month_picker.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_year_picker.dart'; +import 'package:stackwallet/utilities/util.dart'; + +/// +/// This file uses code taken from https://github.com/benznest/flutter_rounded_date_picker +/// + +class FlutterRoundedDatePickerDialog extends StatefulWidget { + const FlutterRoundedDatePickerDialog( + {Key? key, + this.height, + required this.initialDate, + required this.firstDate, + required this.lastDate, + this.selectableDayPredicate, + required this.initialDatePickerMode, + required this.era, + this.locale, + required this.borderRadius, + this.imageHeader, + this.description = "", + this.fontFamily, + this.textNegativeButton, + this.textPositiveButton, + this.textActionButton, + this.onTapActionButton, + this.styleDatePicker, + this.styleYearPicker, + this.customWeekDays, + this.builderDay, + this.listDateDisabled, + this.onTapDay, + this.onMonthChange}) + : super(key: key); + + final DateTime initialDate; + final DateTime firstDate; + final DateTime lastDate; + final SelectableDayPredicate? selectableDayPredicate; + final DatePickerMode initialDatePickerMode; + + /// double height. + final double? height; + + /// Custom era year. + final EraMode era; + final Locale? locale; + + /// Border + final double borderRadius; + + /// Header; + final ImageProvider? imageHeader; + final String description; + + /// Font + final String? fontFamily; + + /// Button + final String? textNegativeButton; + final String? textPositiveButton; + final String? textActionButton; + + final VoidCallback? onTapActionButton; + + /// Style + final MaterialRoundedDatePickerStyle? styleDatePicker; + final MaterialRoundedYearPickerStyle? styleYearPicker; + + /// Custom Weekday + final List<String>? customWeekDays; + + final BuilderDayOfDatePicker? builderDay; + + final List<DateTime>? listDateDisabled; + final OnTapDay? onTapDay; + + final Function? onMonthChange; + + @override + _FlutterRoundedDatePickerDialogState createState() => + _FlutterRoundedDatePickerDialogState(); +} + +class _FlutterRoundedDatePickerDialogState + extends State<FlutterRoundedDatePickerDialog> { + @override + void initState() { + super.initState(); + _selectedDate = widget.initialDate; + _mode = widget.initialDatePickerMode; + } + + bool _announcedInitialDate = false; + + late MaterialLocalizations localizations; + late TextDirection textDirection; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + localizations = MaterialLocalizations.of(context); + textDirection = Directionality.of(context); + if (!_announcedInitialDate) { + _announcedInitialDate = true; + SemanticsService.announce( + localizations.formatFullDate(_selectedDate), + textDirection, + ); + } + } + + late DateTime _selectedDate; + late DatePickerMode _mode; + final GlobalKey _pickerKey = GlobalKey(); + + void _vibrate() { + switch (Theme.of(context).platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + HapticFeedback.vibrate(); + break; + case TargetPlatform.iOS: + default: + break; + } + } + + void _handleModeChanged(DatePickerMode mode) { + _vibrate(); + setState(() { + _mode = mode; + if (_mode == DatePickerMode.day) { + SemanticsService.announce( + localizations.formatMonthYear(_selectedDate), + textDirection, + ); + } else { + SemanticsService.announce( + localizations.formatYear(_selectedDate), + textDirection, + ); + } + }); + } + + Future<void> _handleYearChanged(DateTime value) async { + if (value.isBefore(widget.firstDate)) { + value = widget.firstDate; + } else if (value.isAfter(widget.lastDate)) { + value = widget.lastDate; + } + if (value == _selectedDate) return; + + if (widget.onMonthChange != null) await widget.onMonthChange!(value); + + _vibrate(); + setState(() { + _mode = DatePickerMode.day; + _selectedDate = value; + }); + } + + void _handleDayChanged(DateTime value) { + _vibrate(); + setState(() { + _selectedDate = value; + }); + } + + void _handleCancel() { + Navigator.of(context).pop(); + } + + void _handleOk() { + Navigator.of(context).pop(_selectedDate); + } + + Widget _buildPicker() { + switch (_mode) { + case DatePickerMode.year: + return FlutterRoundedYearPicker( + key: _pickerKey, + selectedDate: _selectedDate, + onChanged: (DateTime date) async => await _handleYearChanged(date), + firstDate: widget.firstDate, + lastDate: widget.lastDate, + era: widget.era, + fontFamily: widget.fontFamily, + style: widget.styleYearPicker, + ); + case DatePickerMode.day: + default: + return FlutterRoundedMonthPicker( + key: _pickerKey, + selectedDate: _selectedDate, + onChanged: _handleDayChanged, + firstDate: widget.firstDate, + lastDate: widget.lastDate, + era: widget.era, + locale: widget.locale, + selectableDayPredicate: widget.selectableDayPredicate, + fontFamily: widget.fontFamily, + style: widget.styleDatePicker, + borderRadius: widget.borderRadius, + customWeekDays: widget.customWeekDays, + builderDay: widget.builderDay, + listDateDisabled: widget.listDateDisabled, + onTapDay: widget.onTapDay, + onMonthChange: widget.onMonthChange); + } + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final Widget picker = _buildPicker(); + final isDesktop = Util.isDesktop; + + final Widget actions = FlutterRoundedButtonAction( + textButtonNegative: widget.textNegativeButton, + textButtonPositive: widget.textPositiveButton, + onTapButtonNegative: _handleCancel, + onTapButtonPositive: _handleOk, + textActionButton: widget.textActionButton, + onTapButtonAction: widget.onTapActionButton, + localizations: localizations, + textStyleButtonNegative: widget.styleDatePicker?.textStyleButtonNegative, + textStyleButtonPositive: widget.styleDatePicker?.textStyleButtonPositive, + textStyleButtonAction: widget.styleDatePicker?.textStyleButtonAction, + borderRadius: widget.borderRadius, + paddingActionBar: widget.styleDatePicker?.paddingActionBar, + background: widget.styleDatePicker?.backgroundActionBar, + ); + + Color backgroundPicker = theme.dialogBackgroundColor; + if (_mode == DatePickerMode.day) { + backgroundPicker = widget.styleDatePicker?.backgroundPicker ?? + theme.dialogBackgroundColor; + } else { + backgroundPicker = widget.styleYearPicker?.backgroundPicker ?? + theme.dialogBackgroundColor; + } + + final Dialog dialog = Dialog( + child: OrientationBuilder( + builder: (BuildContext context, Orientation orientation) { + final Widget header = FlutterRoundedDatePickerHeader( + selectedDate: _selectedDate, + mode: _mode, + onModeChanged: _handleModeChanged, + orientation: orientation, + era: widget.era, + borderRadius: widget.borderRadius, + imageHeader: widget.imageHeader, + description: widget.description, + fontFamily: widget.fontFamily, + style: widget.styleDatePicker); + switch (orientation) { + case Orientation.landscape: + return Container( + height: isDesktop ? 600 : null, + width: isDesktop ? 700 : null, + decoration: BoxDecoration( + color: backgroundPicker, + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + Flexible(flex: 1, child: header), + Flexible( + flex: 2, // have the picker take up 2/3 of the dialog width + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + SizedBox( + height: isDesktop ? 530 : null, + width: isDesktop ? 700 : null, + child: picker), + actions, + ], + ), + ), + ], + ), + ); + case Orientation.portrait: + default: + return Container( + decoration: BoxDecoration( + color: backgroundPicker, + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + header, + if (widget.height == null) + Flexible(child: picker) + else + SizedBox( + height: widget.height, + child: picker, + ), + actions, + ], + ), + ); + } + }), + ); + + return Theme( + data: theme.copyWith(dialogBackgroundColor: Colors.transparent), + child: dialog, + ); + } +} diff --git a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart new file mode 100644 index 000000000..5f576f480 --- /dev/null +++ b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart @@ -0,0 +1,216 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +// import 'package:flutter_rounded_date_picker/src/dialogs/flutter_rounded_date_picker_dialog.dart'; +import 'package:flutter_rounded_date_picker/src/era_mode.dart'; +import 'package:flutter_rounded_date_picker/src/material_rounded_date_picker_style.dart'; +import 'package:flutter_rounded_date_picker/src/material_rounded_year_picker_style.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_day_picker.dart'; +import 'package:stackwallet/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart'; + +/// +/// This file uses code taken from https://github.com/benznest/flutter_rounded_date_picker +/// + +// Examples can assume: +// BuildContext context; + +/// Initial display mode of the date picker dialog. +/// +/// Date picker UI mode for either showing a list of available years or a +/// monthly calendar initially in the dialog shown by calling [showDatePicker]. +/// + +// Shows the selected date in large font and toggles between year and day mode + +/// Signature for predicating dates for enabled date selections. +/// +/// See [showDatePicker]. +typedef SelectableDayPredicate = bool Function(DateTime day); + +/// Shows a dialog containing a material design date picker. +/// +/// The returned [Future] resolves to the date selected by the user when the +/// user closes the dialog. If the user cancels the dialog, null is returned. +/// +/// An optional [selectableDayPredicate] function can be passed in to customize +/// the days to enable for selection. If provided, only the days that +/// [selectableDayPredicate] returned true for will be selectable. +/// +/// An optional [initialDatePickerMode] argument can be used to display the +/// date picker initially in the year or month+day picker mode. It defaults +/// to month+day, and must not be null. +/// +/// An optional [locale] argument can be used to set the locale for the date +/// picker. It defaults to the ambient locale provided by [Localizations]. +/// +/// An optional [textDirection] argument can be used to set the text direction +/// (RTL or LTR) for the date picker. It defaults to the ambient text direction +/// provided by [Directionality]. If both [locale] and [textDirection] are not +/// null, [textDirection] overrides the direction chosen for the [locale]. +/// +/// The [context] argument is passed to [showDialog], the documentation for +/// which discusses how it is used. +/// +/// The [builder] parameter can be used to wrap the dialog widget +/// to add inherited widgets like [Theme]. +/// +/// {@tool sample} +/// Show a date picker with the dark theme. +/// +/// ```dart +/// Future<DateTime> selectedDate = showDatePicker( +/// context: context, +/// initialDate: DateTime.now(), +/// firstDate: DateTime(2018), +/// lastDate: DateTime(2030), +/// builder: (BuildContext context, Widget child) { +/// return Theme( +/// data: ThemeData.dark(), +/// child: child, +/// ); +/// }, +/// ); +/// ``` +/// {@end-tool} +/// +/// The [context], [initialDate], [firstDate], and [lastDate] parameters must +/// not be null. +/// +/// See also: +/// +/// * [showTimePicker], which shows a dialog that contains a material design +/// time picker. +/// * [DayPicker], which displays the days of a given month and allows +/// choosing a day. +/// * [MonthPicker], which displays a scrollable list of months to allow +/// picking a month. +/// * [YearPicker], which displays a scrollable list of years to allow picking +/// a year. +/// + +Future<DateTime?> showRoundedDatePicker( + {required BuildContext context, + double? height, + DateTime? initialDate, + DateTime? firstDate, + DateTime? lastDate, + SelectableDayPredicate? selectableDayPredicate, + DatePickerMode initialDatePickerMode = DatePickerMode.day, + Locale? locale, + TextDirection? textDirection, + ThemeData? theme, + double borderRadius = 16, + EraMode era = EraMode.CHRIST_YEAR, + ImageProvider? imageHeader, + String description = "", + String? fontFamily, + bool barrierDismissible = false, + Color background = Colors.transparent, + String? textNegativeButton, + String? textPositiveButton, + String? textActionButton, + VoidCallback? onTapActionButton, + MaterialRoundedDatePickerStyle? styleDatePicker, + MaterialRoundedYearPickerStyle? styleYearPicker, + List<String>? customWeekDays, + BuilderDayOfDatePicker? builderDay, + List<DateTime>? listDateDisabled, + OnTapDay? onTapDay, + Function? onMonthChange}) async { + initialDate ??= DateTime.now(); + firstDate ??= DateTime(initialDate.year - 1); + lastDate ??= DateTime(initialDate.year + 1); + theme ??= ThemeData(); + + assert( + !initialDate.isBefore(firstDate), + 'initialDate must be on or after firstDate', + ); + assert( + !initialDate.isAfter(lastDate), + 'initialDate must be on or before lastDate', + ); + assert( + !firstDate.isAfter(lastDate), + 'lastDate must be on or after firstDate', + ); + assert( + selectableDayPredicate == null || selectableDayPredicate(initialDate), + 'Provided initialDate must satisfy provided selectableDayPredicate', + ); + assert( + (onTapActionButton != null && textActionButton != null) || + onTapActionButton == null, + "If you provide onLeftBtn, you must provide leftBtn", + ); + assert(debugCheckHasMaterialLocalizations(context)); + + Widget child = GestureDetector( + onTap: () { + if (!barrierDismissible) { + Navigator.pop(context); + } + }, + child: Container( + color: background, + child: GestureDetector( + onTap: () { + // + }, + child: FlutterRoundedDatePickerDialog( + height: height, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + selectableDayPredicate: selectableDayPredicate, + initialDatePickerMode: initialDatePickerMode, + era: era, + locale: locale, + borderRadius: borderRadius, + imageHeader: imageHeader, + description: description, + fontFamily: fontFamily, + textNegativeButton: textNegativeButton, + textPositiveButton: textPositiveButton, + textActionButton: textActionButton, + onTapActionButton: onTapActionButton, + styleDatePicker: styleDatePicker, + styleYearPicker: styleYearPicker, + customWeekDays: customWeekDays, + builderDay: builderDay, + listDateDisabled: listDateDisabled, + onTapDay: onTapDay, + onMonthChange: onMonthChange, + ), + ), + ), + ); + + if (textDirection != null) { + child = Directionality( + textDirection: textDirection, + child: child, + ); + } + + if (locale != null) { + child = Localizations.override( + context: context, + locale: locale, + child: child, + ); + } + + return await showDialog<DateTime>( + context: context, + barrierDismissible: barrierDismissible, + builder: (_) => Theme(data: theme!, child: child), + ); +} diff --git a/lib/widgets/rounded_white_container.dart b/lib/widgets/rounded_white_container.dart index a574dc1ce..1173e95b1 100644 --- a/lib/widgets/rounded_white_container.dart +++ b/lib/widgets/rounded_white_container.dart @@ -28,8 +28,8 @@ class RoundedWhiteContainer extends StatelessWidget { radiusMultiplier: radiusMultiplier, width: width, height: height, - child: child, borderColor: borderColor, + child: child, ); } } diff --git a/lib/widgets/stack_dialog.dart b/lib/widgets/stack_dialog.dart index be1d51596..ea2638264 100644 --- a/lib/widgets/stack_dialog.dart +++ b/lib/widgets/stack_dialog.dart @@ -1,6 +1,7 @@ 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'; class StackDialogBase extends StatelessWidget { const StackDialogBase({ @@ -17,7 +18,8 @@ class StackDialogBase extends StatelessWidget { return Padding( padding: const EdgeInsets.all(16), child: Column( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: + !Util.isDesktop ? MainAxisAlignment.end : MainAxisAlignment.center, children: [ Material( borderRadius: BorderRadius.circular( @@ -179,10 +181,16 @@ class StackOkDialog extends StatelessWidget { ), Expanded( child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - onOkPressed?.call("OK"); - }, + onPressed: !Util.isDesktop + ? () { + Navigator.of(context).pop(); + onOkPressed?.call("OK"); + } + : () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + // onOkPressed?.call("OK"); + }, style: Theme.of(context) .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), diff --git a/lib/widgets/stack_text_field.dart b/lib/widgets/stack_text_field.dart index 9858c18db..1f1e9f8de 100644 --- a/lib/widgets/stack_text_field.dart +++ b/lib/widgets/stack_text_field.dart @@ -4,7 +4,11 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; InputDecoration standardInputDecoration( - String? labelText, FocusNode textFieldFocusNode, BuildContext context) { + String? labelText, + FocusNode textFieldFocusNode, + BuildContext context, { + bool desktopMed = false, +}) { final isDesktop = Util.isDesktop; return InputDecoration( @@ -13,10 +17,20 @@ InputDecoration standardInputDecoration( ? Theme.of(context).extension<StackColors>()!.textFieldActiveBG : Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, labelStyle: isDesktop - ? STextStyles.desktopTextFieldLabel(context) + ? desktopMed + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText) + : STextStyles.desktopTextFieldLabel(context) : STextStyles.fieldLabel(context), hintStyle: isDesktop - ? STextStyles.desktopTextFieldLabel(context) + ? desktopMed + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText) + : STextStyles.desktopTextFieldLabel(context) : STextStyles.fieldLabel(context), enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, diff --git a/lib/widgets/table_view/table_view.dart b/lib/widgets/table_view/table_view.dart index 7e8693f0d..8c2d470bd 100644 --- a/lib/widgets/table_view/table_view.dart +++ b/lib/widgets/table_view/table_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/widgets/table_view/table_view_row.dart'; class TableView extends StatefulWidget { const TableView({ @@ -9,7 +8,7 @@ class TableView extends StatefulWidget { this.shrinkWrap = false, }) : super(key: key); - final List<TableViewRow> rows; + final List<Widget> rows; final double rowSpacing; final bool shrinkWrap; diff --git a/lib/widgets/table_view/table_view_row.dart b/lib/widgets/table_view/table_view_row.dart index e95eb68bd..9c3175efe 100644 --- a/lib/widgets/table_view/table_view_row.dart +++ b/lib/widgets/table_view/table_view_row.dart @@ -3,7 +3,7 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/table_view/table_view_cell.dart'; -class TableViewRow extends StatelessWidget { +class TableViewRow extends StatefulWidget { const TableViewRow({ Key? key, required this.cells, @@ -17,40 +17,66 @@ class TableViewRow extends StatelessWidget { final List<TableViewCell> cells; final Widget? expandingChild; - final Decoration? decoration; + final BoxDecoration? decoration; final void Function(ExpandableState)? onExpandChanged; final EdgeInsetsGeometry padding; final double spacing; final CrossAxisAlignment crossAxisAlignment; + @override + State<TableViewRow> createState() => _TableViewRowState(); +} + +class _TableViewRowState extends State<TableViewRow> { + late final List<TableViewCell> cells; + late final Widget? expandingChild; + late final BoxDecoration? decoration; + late final void Function(ExpandableState)? onExpandChanged; + late final EdgeInsetsGeometry padding; + late final double spacing; + late final CrossAxisAlignment crossAxisAlignment; + + bool _hovering = false; + + @override + void initState() { + cells = widget.cells; + expandingChild = widget.expandingChild; + decoration = widget.decoration; + onExpandChanged = widget.onExpandChanged; + padding = widget.padding; + spacing = widget.spacing; + crossAxisAlignment = widget.crossAxisAlignment; + super.initState(); + } + @override Widget build(BuildContext context) { return Container( - decoration: decoration, + decoration: !_hovering + ? decoration + : decoration?.copyWith( + boxShadow: [ + Theme.of(context).extension<StackColors>()!.standardBoxShadow, + Theme.of(context).extension<StackColors>()!.standardBoxShadow, + ], + ), child: expandingChild == null - ? Padding( - padding: padding, - child: Row( - crossAxisAlignment: crossAxisAlignment, - children: [ - for (int i = 0; i < cells.length; i++) ...[ - if (i != 0 && i != cells.length) - SizedBox( - width: spacing, - ), - Expanded( - flex: cells[i].flex, - child: cells[i], - ), - ], - ], - ), - ) - : Expandable( - onExpandChanged: onExpandChanged, - header: Padding( + ? MouseRegion( + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: Padding( padding: padding, child: Row( + crossAxisAlignment: crossAxisAlignment, children: [ for (int i = 0; i < cells.length; i++) ...[ if (i != 0 && i != cells.length) @@ -65,6 +91,38 @@ class TableViewRow extends StatelessWidget { ], ), ), + ) + : Expandable( + onExpandChanged: onExpandChanged, + header: MouseRegion( + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: Padding( + padding: padding, + child: Row( + children: [ + for (int i = 0; i < cells.length; i++) ...[ + if (i != 0 && i != cells.length) + SizedBox( + width: spacing, + ), + Expanded( + flex: cells[i].flex, + child: cells[i], + ), + ], + ], + ), + ), + ), body: Column( children: [ Container( diff --git a/lib/widgets/textfields/exchange_textfield.dart b/lib/widgets/textfields/exchange_textfield.dart new file mode 100644 index 000000000..399d077c4 --- /dev/null +++ b/lib/widgets/textfields/exchange_textfield.dart @@ -0,0 +1,384 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +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/widgets/loading_indicator.dart'; + +class ExchangeTextField extends StatefulWidget { + const ExchangeTextField({ + Key? key, + this.borderRadius = 0, + this.background, + required this.controller, + this.buttonColor, + required this.focusNode, + this.buttonContent, + required this.textStyle, + this.onButtonTap, + this.onChanged, + this.onSubmitted, + this.onTap, + required this.isWalletCoin, + this.image, + this.ticker, + this.readOnly = false, + }) : super(key: key); + + final double borderRadius; + final Color? background; + final Color? buttonColor; + final Widget? buttonContent; + final TextEditingController controller; + final FocusNode focusNode; + final TextStyle textStyle; + final VoidCallback? onTap; + final VoidCallback? onButtonTap; + final void Function(String)? onChanged; + final void Function(String)? onSubmitted; + + final bool isWalletCoin; + final bool readOnly; + final String? image; + final String? ticker; + + @override + State<ExchangeTextField> createState() => _ExchangeTextFieldState(); +} + +class _ExchangeTextFieldState extends State<ExchangeTextField> { + late final TextEditingController controller; + late final FocusNode focusNode; + late final TextStyle textStyle; + + late final double borderRadius; + + late final Color? background; + late final Color? buttonColor; + late final Widget? buttonContent; + late final VoidCallback? onButtonTap; + late final VoidCallback? onTap; + late final void Function(String)? onChanged; + late final void Function(String)? onSubmitted; + + @override + void initState() { + borderRadius = widget.borderRadius; + background = widget.background; + buttonColor = widget.buttonColor; + controller = widget.controller; + focusNode = widget.focusNode; + buttonContent = widget.buttonContent; + textStyle = widget.textStyle; + onButtonTap = widget.onButtonTap; + onChanged = widget.onChanged; + onSubmitted = widget.onSubmitted; + onTap = widget.onTap; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(borderRadius), + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TextField( + style: textStyle, + controller: controller, + focusNode: focusNode, + onChanged: onChanged, + onTap: onTap, + enableSuggestions: false, + autocorrect: false, + readOnly: widget.readOnly, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + left: 12, + ), + hintText: "0", + hintStyle: STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + ), + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + ), + ), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => onButtonTap?.call(), + child: Container( + decoration: BoxDecoration( + color: buttonColor, + borderRadius: BorderRadius.horizontal( + right: Radius.circular( + borderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Row( + children: [ + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + ), + child: Builder( + builder: (context) { + final image = widget.image; + + if (image != null && image.isNotEmpty) { + return Center( + child: SvgPicture.network( + image, + height: 18, + placeholderBuilder: (_) => Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + 18, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + 18, + ), + child: const LoadingIndicator(), + ), + ), + ), + ); + } else { + return Container( + width: 18, + height: 18, + decoration: BoxDecoration( + // color: Theme.of(context).extension<StackColors>()!.accentColorDark + borderRadius: BorderRadius.circular(18), + ), + child: SvgPicture.asset( + Assets.svg.circleQuestion, + width: 18, + height: 18, + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + ), + ); + } + }, + ), + ), + const SizedBox( + width: 6, + ), + Text( + widget.ticker?.toUpperCase() ?? "-", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + if (!widget.isWalletCoin) + const SizedBox( + width: 6, + ), + if (!widget.isWalletCoin) + SvgPicture.asset( + Assets.svg.chevronDown, + width: 5, + height: 2.5, + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +// experimental UNUSED +// class ExchangeTextField extends StatefulWidget { +// const ExchangeTextField({ +// Key? key, +// this.borderRadius = 0, +// this.background, +// required this.controller, +// this.buttonColor, +// required this.focusNode, +// this.buttonContent, +// required this.textStyle, +// this.onButtonTap, +// this.onChanged, +// this.onSubmitted, +// }) : super(key: key); +// +// final double borderRadius; +// final Color? background; +// final Color? buttonColor; +// final Widget? buttonContent; +// final TextEditingController controller; +// final FocusNode focusNode; +// final TextStyle textStyle; +// final VoidCallback? onButtonTap; +// final void Function(String)? onChanged; +// final void Function(String)? onSubmitted; +// +// @override +// State<ExchangeTextField> createState() => _ExchangeTextFieldState(); +// } +// +// class _ExchangeTextFieldState extends State<ExchangeTextField> { +// late final TextEditingController controller; +// late final FocusNode focusNode; +// late final TextStyle textStyle; +// +// late final double borderRadius; +// +// late final Color? background; +// late final Color? buttonColor; +// late final Widget? buttonContent; +// late final VoidCallback? onButtonTap; +// late final void Function(String)? onChanged; +// late final void Function(String)? onSubmitted; +// +// @override +// void initState() { +// borderRadius = widget.borderRadius; +// background = widget.background; +// buttonColor = widget.buttonColor; +// controller = widget.controller; +// focusNode = widget.focusNode; +// buttonContent = widget.buttonContent; +// textStyle = widget.textStyle; +// onButtonTap = widget.onButtonTap; +// onChanged = widget.onChanged; +// onSubmitted = widget.onSubmitted; +// +// super.initState(); +// } +// +// @override +// Widget build(BuildContext context) { +// return Container( +// decoration: BoxDecoration( +// color: background, +// borderRadius: BorderRadius.circular(borderRadius), +// ), +// child: IntrinsicHeight( +// child: Row( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// children: [ +// Expanded( +// child: MouseRegion( +// cursor: SystemMouseCursors.text, +// child: GestureDetector( +// onTap: () { +// // +// }, +// child: Padding( +// padding: const EdgeInsets.only( +// left: 16, +// top: 18, +// bottom: 17, +// ), +// child: IgnorePointer( +// ignoring: true, +// child: EditableText( +// controller: controller, +// focusNode: focusNode, +// style: textStyle, +// onChanged: onChanged, +// onSubmitted: onSubmitted, +// onEditingComplete: () => print("lol"), +// autocorrect: false, +// enableSuggestions: false, +// keyboardType: const TextInputType.numberWithOptions( +// signed: false, +// decimal: true, +// ), +// inputFormatters: [ +// // regex to validate a crypto amount with 8 decimal places +// TextInputFormatter.withFunction((oldValue, +// newValue) => +// RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') +// .hasMatch(newValue.text) +// ? newValue +// : oldValue), +// ], +// cursorColor: textStyle.color ?? +// Theme.of(context).backgroundColor, +// backgroundCursorColor: background ?? Colors.transparent, +// ), +// ), +// ), +// ), +// ), +// ), +// MouseRegion( +// cursor: SystemMouseCursors.click, +// child: GestureDetector( +// onTap: () => onButtonTap?.call(), +// child: Container( +// decoration: BoxDecoration( +// color: buttonColor, +// borderRadius: BorderRadius.horizontal( +// right: Radius.circular( +// borderRadius, +// ), +// ), +// ), +// child: Padding( +// padding: const EdgeInsets.symmetric( +// horizontal: 16, +// ), +// child: buttonContent, +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// } +// } diff --git a/lib/widgets/trade_card.dart b/lib/widgets/trade_card.dart index ba07b9576..5a14a0777 100644 --- a/lib/widgets/trade_card.dart +++ b/lib/widgets/trade_card.dart @@ -7,6 +7,8 @@ import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class TradeCard extends ConsumerWidget { @@ -48,68 +50,85 @@ class TradeCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: onTap, - child: RoundedWhiteContainer( - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - ), - child: Center( - child: SvgPicture.asset( - _fetchIconAssetForStatus( - trade.status, - context, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: onTap, + child: RoundedWhiteContainer( + padding: + isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + ), + child: Center( + child: SvgPicture.asset( + _fetchIconAssetForStatus( + trade.status, + context, + ), + width: 32, + height: 32, ), - width: 32, - height: 32, ), ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - Text( - "${Decimal.tryParse(trade.payInAmount) ?? "..."} ${trade.payInCurrency.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - const SizedBox( - height: 2, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - trade.exchangeName, - style: STextStyles.label(context), - ), - Text( - Format.extractDateFrom( - trade.timestamp.millisecondsSinceEpoch ~/ 1000), - style: STextStyles.label(context), - ), - ], - ), - ], + const SizedBox( + width: 12, ), - ) - ], + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + Text( + "${isDesktop ? "-" : ""}${Decimal.tryParse(trade.payInAmount) ?? "..."} ${trade.payInCurrency.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + const SizedBox( + height: 2, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!isDesktop) + Text( + trade.exchangeName, + style: STextStyles.label(context), + ), + Text( + Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + style: STextStyles.label(context), + ), + if (isDesktop) + Text( + trade.exchangeName, + style: STextStyles.label(context), + ), + ], + ), + ], + ), + ) + ], + ), ), ), ); diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index cb737ae08..4389573c3 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -9,10 +9,11 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_deta import 'package:stackwallet/providers/providers.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/desktop/desktop_dialog.dart'; import 'package:tuple/tuple.dart'; class TransactionCard extends ConsumerStatefulWidget { @@ -100,6 +101,15 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { .select((value) => value.getPrice(coin))) .item1; + String prefix = ""; + if (Util.isDesktop) { + if (_transaction.txType == "Sent") { + prefix = "-"; + } else if (_transaction.txType == "Received") { + prefix = "+"; + } + } + return Material( color: Theme.of(context).extension<StackColors>()!.popupBG, elevation: 0, @@ -126,14 +136,31 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { )); return; } - unawaited(Navigator.of(context).pushNamed( - TransactionDetailsView.routeName, - arguments: Tuple3( - _transaction, - coin, - walletId, - ), - )); + 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.all(8), @@ -170,13 +197,9 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { fit: BoxFit.scaleDown, child: Builder( builder: (_) { - final amount = coin == Coin.monero - ? (_transaction.amount ~/ 10000) - : coin == Coin.wownero - ? (_transaction.amount ~/ 1000) - : _transaction.amount; + final amount = _transaction.amount; return Text( - "${Format.satoshiAmountToPrettyString(amount, locale)} ${coin.ticker}", + "$prefix${Format.satoshiAmountToPrettyString(amount, locale, coin)} ${coin.ticker}", style: STextStyles.itemSubtitle12_600(context), ); @@ -214,17 +237,12 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { fit: BoxFit.scaleDown, 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( - "${Format.localizedStringAsFixed( - value: Format.satoshisToAmount(value) * + "$prefix${Format.localizedStringAsFixed( + value: Format.satoshisToAmount(value, + coin: coin) * price, locale: locale, decimalPlaces: 2, diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index 4840e9b01..5bb51e2e6 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -14,10 +14,12 @@ class WalletInfoRow extends ConsumerWidget { Key? key, required this.walletId, this.onPressed, + this.padding = const EdgeInsets.all(0), }) : super(key: key); final String walletId; final VoidCallback? onPressed; + final EdgeInsets padding; @override Widget build(BuildContext context, WidgetRef ref) { @@ -25,74 +27,90 @@ class WalletInfoRow extends ConsumerWidget { .watch(walletsChangeNotifierProvider.notifier) .getManagerProvider(walletId)); - return Row( - children: Util.isDesktop - ? [ - Expanded( - flex: 4, - child: Row( - children: [ - WalletInfoCoinIcon(coin: manager.coin), - const SizedBox( - width: 12, + if (Util.isDesktop) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onPressed, + child: Padding( + padding: padding, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Expanded( + flex: 4, + child: Row( + children: [ + WalletInfoCoinIcon(coin: manager.coin), + const SizedBox( + width: 12, + ), + Text( + manager.walletName, + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], ), - Text( - manager.walletName, - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), + ), + Expanded( + flex: 4, + child: WalletInfoRowBalanceFuture( + walletId: walletId, ), - ], - ), - ), - Expanded( - flex: 4, - child: WalletInfoRowBalanceFuture( - walletId: walletId, - ), - ), - Expanded( - flex: 6, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SvgPicture.asset( - Assets.svg.chevronRight, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ) - ], - ), - ) - ] - : [ - WalletInfoCoinIcon(coin: manager.coin), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), + ), + Expanded( + flex: 6, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SvgPicture.asset( + Assets.svg.chevronRight, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ) + ], ), - const SizedBox( - height: 2, - ), - WalletInfoRowBalanceFuture(walletId: walletId), - ], - ), + ) + ], ), - ], - ); + ), + ), + ), + ); + } else { + return Row( + children: [ + WalletInfoCoinIcon(coin: manager.coin), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 2, + ), + WalletInfoRowBalanceFuture(walletId: walletId), + ], + ), + ), + ], + ); + } } } diff --git a/linux/my_application.cc b/linux/my_application.cc index 280895e03..d342c1506 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -47,7 +47,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "Stack Wallet"); } - gtk_window_set_default_size(window, 720, 1280); + gtk_window_set_default_size(window, 1220, 500); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f19..96d3fee1a 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "info": { + "version": 1, + "author": "xcode" }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 3c4935a7c..55efad011 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index ed4cc1642..c8ba3b1a2 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 483be6138..e85f01418 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bcbf36df2..bfb090fc6 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 9c0a65286..877b87dc3 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index e71a72613..f24fbe65d 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 8a31fe2dd..b81789038 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/pubspec.lock b/pubspec.lock index 2a680284c..f12b25487 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.1.11" args: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.9.0" + version: "2.8.2" barcode_scan2: dependency: "direct main" description: @@ -190,7 +190,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -198,13 +205,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -274,7 +288,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.2.0" cross_file: dependency: transitive description: @@ -428,7 +442,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" ffi: dependency: "direct main" description: @@ -487,7 +501,7 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.3" + version: "0.11.0" flutter_libepiccash: dependency: "direct main" description: @@ -543,7 +557,7 @@ packages: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "2.2.11" + version: "2.2.9" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -816,6 +830,13 @@ packages: relative: true source: path version: "0.0.1" + lint: + dependency: transitive + description: + name: lint + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" lints: dependency: transitive description: @@ -843,28 +864,28 @@ packages: name: lottie url: "https://pub.dartlang.org" source: hosted - version: "1.4.3" + version: "1.4.2" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: @@ -976,7 +997,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -1230,7 +1251,7 @@ packages: source: hosted version: "3.0.1" shared_preferences: - dependency: "direct main" + dependency: transitive description: name: shared_preferences url: "https://pub.dartlang.org" @@ -1352,7 +1373,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -1364,8 +1385,8 @@ packages: dependency: "direct main" description: path: "." - ref: b7b184ec36466f2a24104a7056de88881cb0c1e9 - resolved-ref: b7b184ec36466f2a24104a7056de88881cb0c1e9 + ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d" + resolved-ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d" url: "https://github.com/cypherstack/stack_wallet_backup.git" source: git version: "0.0.1" @@ -1396,7 +1417,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" string_validator: dependency: "direct main" description: @@ -1410,42 +1431,42 @@ packages: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.4" + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.12" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.16" + version: "0.4.13" time: dependency: transitive description: name: time url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.2" timezone: dependency: transitive description: @@ -1487,7 +1508,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" universal_io: dependency: transitive description: @@ -1571,7 +1592,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "9.0.0" + version: "8.2.2" wakelock: dependency: "direct main" description: @@ -1687,5 +1708,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=2.18.0 <3.0.0" - flutter: ">=3.3.0" + dart: ">=2.17.5 <3.0.0" + flutter: ">=3.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 3858c5a70..8cfce39cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.9+79 +version: 1.5.21+93 environment: sdk: ">=2.17.0 <3.0.0" @@ -54,7 +54,7 @@ dependencies: stack_wallet_backup: git: url: https://github.com/cypherstack/stack_wallet_backup.git - ref: b7b184ec36466f2a24104a7056de88881cb0c1e9 + ref: 011dc9ce3d29f5fdeeaf711d58b5122f055c146d # Utility plugins # provider: ^6.0.1 @@ -126,7 +126,7 @@ dependencies: pointycastle: ^3.6.0 package_info_plus: ^1.4.2 lottie: ^1.3.0 - shared_preferences: ^2.0.15 +# shared_preferences: ^2.0.15 file_picker: ^5.0.1 connectivity_plus: 2.3.6+1 # document_file_save_plus: ^1.0.5 @@ -141,7 +141,7 @@ dev_dependencies: integration_test: sdk: flutter build_runner: ^2.1.7 - flutter_launcher_icons: ^0.9.3 + flutter_launcher_icons: ^0.11.0 hive_generator: ^1.1.2 dependency_validator: ^3.1.2 hive_test: ^1.0.1 @@ -160,6 +160,13 @@ flutter_icons: image_path_android: assets/icon/app_icon_alpha.png image_path_ios: assets/icon/icon.png remove_alpha_ios: true + windows: + generate: true + image_path: assets/icon/icon.png + icon_size: 48 # min:48, max:256, default: 48 + macos: + generate: true + image_path: assets/icon/icon.png flutter_native_splash: image: assets/images/splash.png @@ -196,17 +203,17 @@ flutter: - assets/images/monero.png - assets/images/wownero.png - assets/images/firo.png + - assets/images/litecoin.png - assets/images/doge.png - assets/images/bitcoin.png - assets/images/epic-cash.png - assets/images/bitcoincash.png - assets/images/namecoin.png + - assets/images/glasses.png + - assets/images/glasses-hidden.png - assets/svg/plus.svg - assets/svg/gear.svg - assets/svg/bell.svg - - assets/svg/light/bell-new.svg - - assets/svg/dark/bell-new.svg - - assets/svg/stack-icon1.svg - assets/svg/arrow-left-fa.svg - assets/svg/copy-fa.svg - assets/svg/star.svg @@ -219,10 +226,7 @@ flutter: - assets/svg/bars.svg - assets/svg/filter.svg - assets/svg/pending.svg - - assets/svg/dark/exchange-2.svg - - assets/svg/light/exchange-2.svg - assets/svg/signal-stream.svg - - assets/svg/buy-coins-icon.svg - assets/svg/Ellipse-43.svg - assets/svg/Ellipse-42.svg - assets/svg/arrow-rotate.svg @@ -231,10 +235,13 @@ flutter: - assets/svg/gear-3.svg - assets/svg/swap.svg - assets/svg/chevron-down.svg + - assets/svg/chevron-up.svg - assets/svg/lock-keyhole.svg + - assets/svg/lock-open.svg - assets/svg/rotate-exclamation.svg - assets/svg/folder-down.svg - assets/svg/network-wired.svg + - assets/svg/network-wired-2.svg - assets/svg/address-book.svg - assets/svg/address-book2.svg - assets/svg/arrow-right.svg @@ -262,25 +269,7 @@ flutter: - assets/svg/ellipsis-vertical1.svg - assets/svg/dice-alt.svg - assets/svg/circle-arrow-up-right2.svg - - assets/svg/dark/tx-exchange-icon.svg - - assets/svg/light/tx-exchange-icon.svg - - assets/svg/dark/tx-exchange-icon-pending.svg - - assets/svg/light/tx-exchange-icon-pending.svg - - assets/svg/dark/tx-exchange-icon-failed.svg - - assets/svg/light/tx-exchange-icon-failed.svg - assets/svg/loader.svg - - assets/svg/dark/tx-icon-send.svg - - assets/svg/light/tx-icon-send.svg - - assets/svg/dark/tx-icon-send-pending.svg - - assets/svg/light/tx-icon-send-pending.svg - - assets/svg/dark/tx-icon-send-failed.svg - - assets/svg/light/tx-icon-send-failed.svg - - assets/svg/dark/tx-icon-receive.svg - - assets/svg/light/tx-icon-receive.svg - - assets/svg/dark/tx-icon-receive-pending.svg - - assets/svg/light/tx-icon-receive-pending.svg - - assets/svg/dark/tx-icon-receive-failed.svg - - assets/svg/light/tx-icon-receive-failed.svg - assets/svg/add-backup.svg - assets/svg/auto-backup.svg - assets/svg/restore-backup.svg @@ -294,8 +283,26 @@ flutter: - assets/svg/Polygon.svg - assets/svg/persona-easy-1.svg - assets/svg/persona-incognito-1.svg + - assets/svg/Button.svg + - assets/svg/enabled-button.svg + - assets/svg/lock-circle.svg + - assets/svg/dollar-sign-circle.svg + - assets/svg/language-circle.svg + - assets/svg/rotate-circle.svg + - assets/svg/sun-circle.svg + - assets/svg/node-circle.svg + - assets/svg/address-book-desktop.svg + - assets/svg/about-desktop.svg + - assets/svg/exchange-desktop.svg + - assets/svg/wallet-desktop.svg + - assets/svg/exit-desktop.svg + - assets/svg/keys.svg + - assets/svg/arrow-down.svg + - assets/svg/plus-circle.svg + - assets/svg/configuration.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg + - assets/svg/coin_icons/Litecoin.svg - assets/svg/coin_icons/Bitcoincash.svg - assets/svg/coin_icons/Dogecoin.svg - assets/svg/coin_icons/EpicCash.svg @@ -318,10 +325,63 @@ flutter: - assets/svg/message-question-1.svg - assets/svg/drd-icon.svg - assets/svg/box-auto.svg + - assets/svg/framed-address-book.svg + - assets/svg/framed-gear.svg # exchange icons - assets/svg/exchange_icons/change_now_logo_1.svg - assets/svg/exchange_icons/simpleswap-icon.svg + # theme selectors + - assets/svg/dark-theme.svg + - assets/svg/light-mode.svg + - assets/svg/ocean-breeze-theme.svg + + # light theme specific + - assets/svg/light/tx-exchange-icon.svg + - assets/svg/light/tx-exchange-icon-pending.svg + - assets/svg/light/tx-exchange-icon-failed.svg + - assets/svg/light/tx-icon-send.svg + - assets/svg/light/tx-icon-send-pending.svg + - assets/svg/light/tx-icon-send-failed.svg + - assets/svg/light/tx-icon-receive.svg + - assets/svg/light/tx-icon-receive-pending.svg + - assets/svg/light/tx-icon-receive-failed.svg + - assets/svg/light/exchange-2.svg + - assets/svg/light/bell-new.svg + - assets/svg/light/stack-icon1.svg + - assets/svg/light/buy-coins-icon.svg + + # dark theme specific + - assets/svg/dark/tx-exchange-icon.svg + - assets/svg/dark/tx-exchange-icon-pending.svg + - assets/svg/dark/tx-exchange-icon-failed.svg + - assets/svg/dark/tx-icon-send.svg + - assets/svg/dark/tx-icon-send-pending.svg + - assets/svg/dark/tx-icon-send-failed.svg + - assets/svg/dark/tx-icon-receive.svg + - assets/svg/dark/tx-icon-receive-pending.svg + - assets/svg/dark/tx-icon-receive-failed.svg + - assets/svg/dark/exchange-2.svg + - assets/svg/dark/bell-new.svg + - assets/svg/dark/stack-icon1.svg + - assets/svg/dark/buy-coins-icon.svg + + # light theme specific + - assets/svg/oceanBreeze/tx-exchange-icon.svg + - assets/svg/oceanBreeze/tx-exchange-icon-pending.svg + - assets/svg/oceanBreeze/tx-exchange-icon-failed.svg + - assets/svg/oceanBreeze/tx-icon-send.svg + - assets/svg/oceanBreeze/tx-icon-send-pending.svg + - assets/svg/oceanBreeze/tx-icon-send-failed.svg + - assets/svg/oceanBreeze/tx-icon-receive.svg + - assets/svg/oceanBreeze/tx-icon-receive-pending.svg + - assets/svg/oceanBreeze/tx-icon-receive-failed.svg + - assets/svg/oceanBreeze/exchange-2.svg + - assets/svg/oceanBreeze/bell-new.svg + - assets/svg/oceanBreeze/stack-icon1.svg + - assets/svg/oceanBreeze/buy-coins-icon.svg + - assets/svg/oceanBreeze/bg.svg + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see @@ -351,4 +411,4 @@ import_sorter: ignored_files: # Optional, defaults to [] - \/test\/* - \/crypto_plugins\/* - - \/integration_test\/* \ No newline at end of file + - \/integration_test\/* diff --git a/scripts/linux/build_secure_storage_deps.sh b/scripts/linux/build_secure_storage_deps.sh index 5ed032b1a..69b452e2d 100755 --- a/scripts/linux/build_secure_storage_deps.sh +++ b/scripts/linux/build_secure_storage_deps.sh @@ -1,25 +1,37 @@ #!/bin/bash LINUX_DIRECTORY=$(pwd) -mkdir build +mkdir -p build # Build JsonCPP -cd build -git clone https://github.com/open-source-parsers/jsoncpp.git -cd jsoncpp +cd build || exit +if ! [ -x "$(command -v git)" ]; then + echo 'Error: git is not installed.' >&2 + exit 1 +fi +git -C jsoncpp pull || git clone https://github.com/open-source-parsers/jsoncpp.git jsoncpp +cd jsoncpp || exit git checkout 1.7.4 -mkdir build -cd build +mkdir -p build +cd build || exit cmake -DCMAKE_BUILD_TYPE=release -DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=ON -DARCHIVE_INSTALL_DIR=. -G "Unix Makefiles" .. -make -j$(nproc) +make -j"$(nproc)" -cd $LINUX_DIRECTORY +cd "$LINUX_DIRECTORY" || exit # Build libSecret # sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl # sudo apt install python3-pip -#pip3 install --user meson --upgrade +#pip3 install --user meson markdown tomli --upgrade # pip3 install --user gi-docgen -cd build -git clone https://gitlab.gnome.org/GNOME/libsecret.git -cd libsecret +cd build || exit +git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret +cd libsecret || exit +if ! [ -x "$(command -v meson)" ]; then + echo 'Error: meson is not installed.' >&2 + exit 1 +fi meson _build +if ! [ -x "$(command -v ninja)" ]; then + echo 'Error: ninja is not installed.' >&2 + exit 1 +fi ninja -C _build diff --git a/test/address_book_service_test.dart b/test/address_book_service_test.dart index 1059f7fc3..c5effd223 100644 --- a/test/address_book_service_test.dart +++ b/test/address_book_service_test.dart @@ -94,19 +94,19 @@ void main() { test("get contacts", () { final service = AddressBookService(); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("get addressBookEntries", () async { final service = AddressBookService(); expect((await service.addressBookEntries).toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("search contacts", () async { final service = AddressBookService(); final results = await service.search("j"); - expect(results.toString(), [contactA, contactB].toString()); + expect(results.toString(), [contactB, contactA].toString()); final results2 = await service.search("ja"); expect(results2.toString(), [contactB].toString()); @@ -118,7 +118,7 @@ void main() { expect(results4.toString(), <Contact>[].toString()); final results5 = await service.search(""); - expect(results5.toString(), [contactA, contactB, contactC].toString()); + expect(results5.toString(), [contactC, contactB, contactA].toString()); final results6 = await service.search("epic address"); expect(results6.toString(), [contactC].toString()); @@ -140,7 +140,7 @@ void main() { expect(result, false); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("edit contact", () async { @@ -149,14 +149,14 @@ void main() { expect(await service.editContact(editedContact), true); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, editedContact, contactC].toString()); + [contactC, contactA, editedContact].toString()); }); test("remove existing contact", () async { final service = AddressBookService(); await service.removeContact(contactB.id); expect(service.contacts.length, 2); - expect(service.contacts.toString(), [contactA, contactC].toString()); + expect(service.contacts.toString(), [contactC, contactA].toString()); }); test("remove non existing contact", () async { @@ -164,7 +164,7 @@ void main() { await service.removeContact("some id"); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); tearDown(() async { diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 1e3e70a0a..a45cdd402 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -678,6 +678,14 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: _i4.Future<void>.value(), ) as _i4.Future<void>); @override + _i4.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i4.Future<bool>.value(false), + ) as _i4.Future<bool>); + @override void addListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, diff --git a/test/electrumx_test.mocks.dart b/test/electrumx_test.mocks.dart index fa48ea2fd..9f29ae1e5 100644 --- a/test/electrumx_test.mocks.dart +++ b/test/electrumx_test.mocks.dart @@ -399,6 +399,14 @@ class MockPrefs extends _i1.Mock implements _i4.Prefs { returnValueForMissingStub: _i3.Future<void>.value(), ) as _i3.Future<void>); @override + _i3.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i3.Future<bool>.value(false), + ) as _i3.Future<bool>); + @override void addListener(_i8.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, diff --git a/test/flutter_secure_storage_interface_test.dart b/test/flutter_secure_storage_interface_test.dart index 90dfdcf13..a421b14c1 100644 --- a/test/flutter_secure_storage_interface_test.dart +++ b/test/flutter_secure_storage_interface_test.dart @@ -13,7 +13,7 @@ void main() { when(secureStore.write(key: "testKey", value: "some value")) .thenAnswer((_) async => null); - final wrapper = SecureStorageWrapper(secureStore); + final wrapper = SecureStorageWrapper(store: secureStore, isDesktop: false); await expectLater( () async => await wrapper.write(key: "testKey", value: "some value"), @@ -27,7 +27,7 @@ void main() { final secureStore = MockFlutterSecureStorage(); when(secureStore.read(key: "testKey")) .thenAnswer((_) async => "some value"); - final wrapper = SecureStorageWrapper(secureStore); + final wrapper = SecureStorageWrapper(store: secureStore, isDesktop: false); final result = await wrapper.read(key: "testKey"); @@ -40,7 +40,7 @@ void main() { test("SecureStorageWrapper delete", () async { final secureStore = MockFlutterSecureStorage(); when(secureStore.delete(key: "testKey")).thenAnswer((_) async {}); - final wrapper = SecureStorageWrapper(secureStore); + final wrapper = SecureStorageWrapper(store: secureStore, isDesktop: false); await expectLater( () async => await wrapper.delete(key: "testKey"), returnsNormally); diff --git a/test/formet_test.dart b/test/formet_test.dart index 4f7136cd4..e27293114 100644 --- a/test/formet_test.dart +++ b/test/formet_test.dart @@ -1,54 +1,64 @@ import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; void main() { group("satoshisToAmount", () { test("12345", () { - expect(Format.satoshisToAmount(12345), Decimal.parse("0.00012345")); + expect(Format.satoshisToAmount(12345, coin: Coin.bitcoin), + Decimal.parse("0.00012345")); }); test("100012345", () { - expect(Format.satoshisToAmount(100012345), Decimal.parse("1.00012345")); + expect(Format.satoshisToAmount(100012345, coin: Coin.bitcoin), + Decimal.parse("1.00012345")); }); test("0", () { - expect(Format.satoshisToAmount(0), Decimal.zero); + expect(Format.satoshisToAmount(0, coin: Coin.bitcoin), Decimal.zero); }); test("1000000000", () { - expect(Format.satoshisToAmount(1000000000), Decimal.parse("10")); + expect(Format.satoshisToAmount(1000000000, coin: Coin.bitcoin), + Decimal.parse("10")); }); }); group("satoshiAmountToPrettyString", () { const locale = "en_US"; test("12345", () { - expect(Format.satoshiAmountToPrettyString(12345, locale), "0.00012345"); + expect(Format.satoshiAmountToPrettyString(12345, locale, Coin.bitcoin), + "0.00012345"); }); test("100012345", () { expect( - Format.satoshiAmountToPrettyString(100012345, locale), "1.00012345"); + Format.satoshiAmountToPrettyString(100012345, locale, Coin.bitcoin), + "1.00012345"); }); test("123450000", () { expect( - Format.satoshiAmountToPrettyString(123450000, locale), "1.23450000"); + Format.satoshiAmountToPrettyString(123450000, locale, Coin.bitcoin), + "1.23450000"); }); test("1230045000", () { - expect(Format.satoshiAmountToPrettyString(1230045000, locale), + expect( + Format.satoshiAmountToPrettyString(1230045000, locale, Coin.bitcoin), "12.30045000"); }); test("1000000000", () { - expect(Format.satoshiAmountToPrettyString(1000000000, locale), + expect( + Format.satoshiAmountToPrettyString(1000000000, locale, Coin.bitcoin), "10.00000000"); }); test("0", () { - expect(Format.satoshiAmountToPrettyString(0, locale), "0.00000000"); + expect(Format.satoshiAmountToPrettyString(0, locale, Coin.bitcoin), + "0.00000000"); }); }); diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index a07377309..d63dafb04 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -1,5 +1,5 @@ // Mocks generated by Mockito 5.3.2 from annotations -// in stackwallet/test/pages/send_view_test.dart. +// in stackwallet/test/pages/send_view/send_view_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes @@ -86,7 +86,7 @@ class _FakeManager_3 extends _i1.SmartFake implements _i6.Manager { } class _FakeFlutterSecureStorageInterface_4 extends _i1.SmartFake - implements _i7.FlutterSecureStorageInterface { + implements _i7.SecureStorageInterface { _FakeFlutterSecureStorageInterface_4( Object parent, Invocation parentInvocation, @@ -621,14 +621,13 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i7.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i7.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_4( this, Invocation.getter(#secureStorageInterface), ), - ) as _i7.FlutterSecureStorageInterface); + ) as _i7.SecureStorageInterface); @override List<_i19.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), @@ -890,6 +889,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i20.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i9.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1272,6 +1279,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i20.BitcoinWallet { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -1875,6 +1892,14 @@ class MockPrefs extends _i1.Mock implements _i17.Prefs { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i16.Future<bool>.value(false), + ) as _i16.Future<bool>); + @override void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, @@ -2623,4 +2648,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i13.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } diff --git a/test/price_test.dart b/test/price_test.dart index 6abbb6741..89300122e 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -26,7 +26,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -39,10 +39,10 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -53,7 +53,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -71,12 +71,12 @@ void main() { await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(cachedPrice.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); // verify only called once during filling of cache verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -87,7 +87,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -100,7 +100,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); test("no internet available", () async { @@ -108,7 +108,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenThrow(const SocketException( @@ -120,7 +120,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); tearDown(() async { diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 1d93023d5..0da12dbd0 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -350,6 +350,14 @@ class MockPrefs extends _i1.Mock implements _i3.Prefs { returnValueForMissingStub: _i7.Future<void>.value(), ) as _i7.Future<void>); @override + _i7.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i7.Future<bool>.value(false), + ) as _i7.Future<bool>); + @override void addListener(_i8.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index 1cd697e05..a33c4ef28 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -30,7 +30,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -309,14 +309,13 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i10.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i11.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 62d3c597d..3aed1dcb8 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -30,7 +30,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -309,14 +309,13 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i10.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i11.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index cf891e829..cd4986e13 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -84,7 +84,7 @@ class _FakeTransactionData_4 extends _i1.SmartFake } class _FakeFlutterSecureStorageInterface_5 extends _i1.SmartFake - implements _i6.FlutterSecureStorageInterface { + implements _i6.SecureStorageInterface { _FakeFlutterSecureStorageInterface_5( Object parent, Invocation parentInvocation, @@ -744,14 +744,14 @@ class MockManager extends _i1.Mock implements _i12.Manager { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i13.NodeService { @override - _i6.FlutterSecureStorageInterface get secureStorageInterface => + _i6.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_5( this, Invocation.getter(#secureStorageInterface), ), - ) as _i6.FlutterSecureStorageInterface); + ) as _i6.SecureStorageInterface); @override List<_i14.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index d80b97852..3ac5afcc7 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -29,7 +29,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -86,14 +86,13 @@ class _FakeTransactionData_4 extends _i1.SmartFake /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i6.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i7.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index 89820ee97..0f6447a9e 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -29,7 +29,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -86,14 +86,13 @@ class _FakeTransactionData_4 extends _i1.SmartFake /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i6.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i7.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart index 7e8ff731e..707da7345 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart @@ -25,7 +25,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -40,14 +40,13 @@ class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i3.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i4.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index 27ae85597..7eb0853dd 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -139,6 +139,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i6.CachedElectrumX { _i8.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i8.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i8.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i9.Coin? coin, diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.dart index 8a240a3dd..e33ffafdf 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.dart @@ -103,7 +103,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? testnetWallet; @@ -194,7 +194,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? mainnetWallet; @@ -363,7 +363,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? btc; @@ -428,7 +428,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? btc; @@ -640,7 +640,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? btc; diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index 99e2e6231..32a6c7195 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart index 4c392fe81..1c32802c9 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -1,4 +1,3 @@ -import 'package:bitcoindart/bitcoindart.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; @@ -61,11 +60,11 @@ void main() { }); }); - group("validate mainnet bitcoincash addresses", () { + group("mainnet bitcoincash addressType", () { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinCashWallet? mainnetWallet; @@ -137,10 +136,172 @@ void main() { verifyNoMoreInteractions(priceAPI); }); + test("P2PKH cashaddr with prefix", () { + expect( + mainnetWallet?.addressType( + address: + "bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + DerivePathType.bip44); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("P2PKH cashaddr without prefix", () { + expect( + mainnetWallet?.addressType( + address: "qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + DerivePathType.bip44); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("Multisig cashaddr with prefix", () { + expect( + () => mainnetWallet?.addressType( + address: + "bitcoincash:pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("Multisig cashaddr without prefix", () { + expect( + () => mainnetWallet?.addressType( + address: "pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("Multisig/P2SH address", () { + expect( + mainnetWallet?.addressType( + address: "3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"), + DerivePathType.bip49); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + }); + + group("validate mainnet bitcoincash addresses", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + BitcoinCashWallet? mainnetWallet; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + mainnetWallet = BitcoinCashWallet( + walletId: "validateAddressMainNet", + walletName: "validateAddressMainNet", + coin: Coin.bitcoincash, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("valid mainnet legacy/p2pkh address type", () { + expect( + mainnetWallet?.validateAddress("1DP3PUePwMa5CoZwzjznVKhzdLsZftjcAT"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet legacy/p2pkh cashaddr with prefix address type", () { + expect( + mainnetWallet?.validateAddress( + "bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet legacy/p2pkh cashaddr without prefix address type", () { + expect( + mainnetWallet + ?.validateAddress("qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("invalid legacy/p2pkh address type", () { + expect( + mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test( + "invalid cashaddr (is valid multisig but bitbox is broken for multisig)", + () { + expect( + mainnetWallet + ?.validateAddress("pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("multisig address should fail for bitbox", () { + expect( + mainnetWallet?.validateAddress("3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + test("invalid mainnet bitcoincash legacy/p2pkh address", () { expect( mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), - true); + false); expect(secureStore?.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -153,7 +314,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinCashWallet? bch; @@ -222,7 +383,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinCashWallet? bch; @@ -445,7 +606,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinCashWallet? bch; diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index a5a2018e7..bfc5f793b 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.dart index 7fcb1cdbd..7c1535ec5 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.dart @@ -97,7 +97,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; DogecoinWallet? mainnetWallet; @@ -196,7 +196,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; DogecoinWallet? doge; @@ -266,7 +266,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; DogecoinWallet? doge; @@ -489,7 +489,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; DogecoinWallet? doge; diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 944e5faea..f7220922e 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/coins/fake_coin_service_api.dart b/test/services/coins/fake_coin_service_api.dart index a3ae28a4b..c5f300c16 100644 --- a/test/services/coins/fake_coin_service_api.dart +++ b/test/services/coins/fake_coin_service_api.dart @@ -182,4 +182,10 @@ class FakeCoinServiceAPI extends CoinServiceAPI { // TODO: implement generateNewAddress throw UnimplementedError(); } + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } } diff --git a/test/services/coins/firo/firo_wallet_test.mocks.dart b/test/services/coins/firo/firo_wallet_test.mocks.dart index 4fe6af52a..2d32cef48 100644 --- a/test/services/coins/firo/firo_wallet_test.mocks.dart +++ b/test/services/coins/firo/firo_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/coins/manager_test.mocks.dart b/test/services/coins/manager_test.mocks.dart index 0c20b752d..9a958702f 100644 --- a/test/services/coins/manager_test.mocks.dart +++ b/test/services/coins/manager_test.mocks.dart @@ -117,6 +117,14 @@ class MockFiroWallet extends _i1.Mock implements _i7.FiroWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i4.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override _i2.TransactionNotificationTracker get txTracker => (super.noSuchMethod( Invocation.getter(#txTracker), returnValue: _FakeTransactionNotificationTracker_0( @@ -375,6 +383,16 @@ class MockFiroWallet extends _i1.Mock implements _i7.FiroWallet { returnValue: false, ) as bool); @override + _i8.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i8.Future<void>.value(), + returnValueForMissingStub: _i8.Future<void>.value(), + ) as _i8.Future<void>); + @override _i8.Future<bool> testNetworkConnection() => (super.noSuchMethod( Invocation.method( #testNetworkConnection, diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart new file mode 100644 index 000000000..d6d600e36 --- /dev/null +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -0,0 +1,236 @@ +import 'dart:core'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/node.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/monero_wallet.dart'; +import 'package:flutter_libmonero/core/key_service.dart'; +import 'package:flutter_libmonero/core/wallet_creation_service.dart'; +import 'package:flutter_libmonero/monero/monero.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:stackwallet/services/wallets.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; + +import 'monero_wallet_test_data.dart'; + +FakeSecureStorage? storage; +WalletService? walletService; +KeyService? keysStorage; +MoneroWalletBase? walletBase; +late WalletCreationService _walletCreationService; +dynamic _walletInfoSource; +Wallets? walletsService; + +String path = ''; + +String name = 'namee${Random().nextInt(10000000)}'; +int nettype = 0; +WalletType type = WalletType.monero; + +@GenerateMocks([]) +void main() async { + storage = FakeSecureStorage(); + keysStorage = KeyService(storage!); + WalletInfo walletInfo = WalletInfo.external( + id: '', + name: '', + type: type, + isRecovery: false, + restoreHeight: 0, + date: DateTime.now(), + path: '', + address: '', + dirPath: ''); + late WalletCredentials credentials; + + monero.onStartup(); + + bool hiveAdaptersRegistered = false; + + group("Mainnet tests", () { + setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', name); + + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = monero + .createMoneroWalletService(_walletInfoSource as Box<WalletInfo>); + } + + try { + // if (name?.isEmpty ?? true) { + // name = await generateName(); + // } + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + credentials = + // // creating a new wallet + // monero.createMoneroNewWalletCredentials( + // name: name, language: "English"); + // restoring a previous wallet + monero.createMoneroRestoreWalletFromSeedCredentials( + name: name, height: 2580000, mnemonic: testMnemonic); + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + } + }); + + test("Test mainnet address generation from seed", () async { + final wallet = await + // _walletCreationService.create(credentials); + _walletCreationService.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + //print(walletInfo.address); + + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as MoneroWalletBase; + //print("${walletBase?.seed}"); + + expect(await walletBase!.validateAddress(walletInfo.address ?? ''), true); + + // print(walletBase); + // loggerPrint(walletBase.toString()); + // loggerPrint("name: ${walletBase!.name} seed: ${walletBase!.seed} id: " + // "${walletBase!.id} walletinfo: ${toStringForinfo(walletBase!.walletInfo)} type: ${walletBase!.type} balance: " + // "${walletBase!.balance.entries.first.value.available} currency: ${walletBase!.currency}"); + + expect(walletInfo.address, mainnetTestData[0][0]); + expect( + await walletBase!.getTransactionAddress(0, 0), mainnetTestData[0][0]); + expect( + await walletBase!.getTransactionAddress(0, 1), mainnetTestData[0][1]); + expect( + await walletBase!.getTransactionAddress(0, 2), mainnetTestData[0][2]); + expect( + await walletBase!.getTransactionAddress(1, 0), mainnetTestData[1][0]); + expect( + await walletBase!.getTransactionAddress(1, 1), mainnetTestData[1][1]); + expect( + await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]); + + expect(await walletBase!.validateAddress(''), false); + expect( + await walletBase!.validateAddress( + '4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), + true); + expect( + await walletBase!.validateAddress( + '4asdfkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gpjkl'), + false); + expect( + await walletBase!.validateAddress( + '8AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), + false); + expect( + await walletBase!.validateAddress( + '84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), + true); + expect( + await walletBase!.validateAddress( + '8asdfuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenjkl'), + false); + expect( + await walletBase!.validateAddress( + '44kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), + false); + }); + }); + /* + // Not needed; only folder created, wallet files not saved yet. TODO test saving and deleting wallet files and make sure to clean up leftover folder afterwards + group("Mainnet wallet deletion test", () { + test("Test mainnet wallet existence", () { + expect(monero_wallet_manager.isWalletExistSync(path: path), true); + }); + + test("Test mainnet wallet deletion", () { + // Remove wallet from wallet service + walletService?.remove(name); + walletsService?.removeWallet(walletId: name); + expect(monero_wallet_manager.isWalletExistSync(path: path), false); + }); + }); + + group("Mainnet node tests", () { + test("Test mainnet node connection", () async { + await walletBase?.connectToNode( + node: Node( + uri: "monero-stagenet.stackwallet.com:38081", + type: WalletType.moneroStageNet)); + await walletBase!.rescan( + height: + credentials.height); // Probably shouldn't be rescanning from 0... + await walletBase!.getNodeHeight(); + int height = await walletBase!.getNodeHeight(); + print('height: $height'); + bool connected = await walletBase!.isConnected(); + print('connected: $connected'); + + //expect... + }); + }); + */ + + // TODO test deletion of wallets ... and delete them +} + +Future<String> pathForWalletDir( + {required String name, required WalletType type}) async { + Directory root = (await getApplicationDocumentsDirectory()); + if (Platform.isIOS) { + root = (await getLibraryDirectory()); + } + final prefix = walletTypeToString(type).toLowerCase(); + final walletsDir = Directory('${root.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; +} + +Future<String> pathForWallet( + {required String name, required WalletType type}) async => + await pathForWalletDir(name: name, type: type) + .then((path) => path + '/$name'); diff --git a/test/services/coins/monero/monero_wallet_test_data.dart b/test/services/coins/monero/monero_wallet_test_data.dart new file mode 100644 index 000000000..dc0a0f4cb --- /dev/null +++ b/test/services/coins/monero/monero_wallet_test_data.dart @@ -0,0 +1,14 @@ +String testMnemonic = + 'agreed aquarium wallets uptight karate wonders afoot guys itself nucleus reduce lamb fully fewest bimonthly dazed skulls magically mocked fugitive imbalance saga calamity dialect itself'; +var mainnetTestData = [ + [ + '4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn', + '82WsoLmbZt3BPwJMF5PfT8GitThJzUq3FFoSQyr4fKfJdxZebgY3mHPcnAqTBA3FFwZRGxC4ZDwkfE1VVULPa55x3xXgCbj', + '84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy' + ], + [ + '86SF44CsTBYU3vk1X7nGBbQnrUSknGbd6Uw8a9hUUgy3KBeXTDvk3pm8upMzZKw17m3mLPEzbcPp5WLpYVoHR5PKNVtFrHH', + '8Aa9LNGdBHwYUMsy6M9ZVXMEkTBZyEDT7aQmY32trCxbU6dwkZJSCSbcpyL7UiTB9QXXosomZtJYvUJ296vTNX5yQ81KaA2', + '85C5zZRcaD89PKmXEwjcYMVAUqoH5rrAXe3GokvSupXnDmccYvZagz5Qem7bQLteEw4iFEJ9oRk9BNfjTi4K2cyTJbTMMPT' + ] +]; diff --git a/test/services/coins/namecoin/namecoin_wallet_test.dart b/test/services/coins/namecoin/namecoin_wallet_test.dart index f6bc1b065..46afd06bd 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.dart @@ -103,7 +103,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; NamecoinWallet? mainnetWallet; @@ -132,7 +132,7 @@ void main() { mainnetWallet?.addressType( address: "N673DDbjPcrNgJmrhJ1xQXF9LLizQzvjEs"), DerivePathType.bip44); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -144,7 +144,7 @@ void main() { mainnetWallet?.addressType( address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v"), DerivePathType.bip84); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -156,7 +156,7 @@ void main() { () => mainnetWallet?.addressType( address: "tb1qzzlm6mnc8k54mx6akehl8p9ray8r439va5ndyq"), throwsArgumentError); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -168,7 +168,7 @@ void main() { () => mainnetWallet?.addressType( address: "mpMk94ETazqonHutyC1v6ajshgtP8oiFKU"), throwsArgumentError); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -180,7 +180,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; NamecoinWallet? nmc; @@ -208,7 +208,7 @@ void main() { when(client?.ping()).thenAnswer((_) async => false); final bool? result = await nmc?.testNetworkConnection(); expect(result, false); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verify(client?.ping()).called(1); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -219,7 +219,7 @@ void main() { when(client?.ping()).thenThrow(Exception); final bool? result = await nmc?.testNetworkConnection(); expect(result, false); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verify(client?.ping()).called(1); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -230,7 +230,7 @@ void main() { when(client?.ping()).thenAnswer((_) async => true); final bool? result = await nmc?.testNetworkConnection(); expect(result, true); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verify(client?.ping()).called(1); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -245,7 +245,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; NamecoinWallet? nmc; @@ -271,7 +271,7 @@ void main() { test("get networkType main", () async { expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -289,7 +289,7 @@ void main() { secureStore: secureStore, ); expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -297,7 +297,7 @@ void main() { test("get cryptoCurrency", () async { expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -305,7 +305,7 @@ void main() { test("get coinName", () async { expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -313,7 +313,7 @@ void main() { test("get coinTicker", () async { expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -323,7 +323,7 @@ void main() { expect(Coin.namecoin, Coin.namecoin); nmc?.walletName = "new name"; expect(nmc?.walletName, "new name"); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -338,7 +338,7 @@ void main() { expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 12345), 4628); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -372,7 +372,7 @@ void main() { verify(client?.estimateFee(blocks: 1)).called(1); verify(client?.estimateFee(blocks: 5)).called(1); verify(client?.estimateFee(blocks: 20)).called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -409,7 +409,7 @@ void main() { verify(client?.estimateFee(blocks: 1)).called(1); verify(client?.estimateFee(blocks: 5)).called(1); verify(client?.estimateFee(blocks: 20)).called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -440,7 +440,7 @@ void main() { // verify(client?.estimateFee(blocks: 1)).called(1); // verify(client?.estimateFee(blocks: 5)).called(1); // verify(client?.estimateFee(blocks: 20)).called(1); - // expect(secureStore?.interactions, 0); + // expect(secureStore.interactions, 0); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); // verifyNoMoreInteractions(tracker); @@ -457,7 +457,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; NamecoinWallet? nmc; @@ -504,7 +504,7 @@ void main() { // test("initializeWallet no network", () async { // when(client?.ping()).thenAnswer((_) async => false); // expect(await nmc?.initializeWallet(), false); - // expect(secureStore?.interactions, 0); + // expect(secureStore.interactions, 0); // verify(client?.ping()).called(1); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -515,7 +515,7 @@ void main() { // when(client?.ping()).thenThrow(Exception("Network connection failed")); // final wallets = await Hive.openBox(testWalletId); // expect(await nmc?.initializeExisting(), false); - // expect(secureStore?.interactions, 0); + // expect(secureStore.interactions, 0); // verify(client?.ping()).called(1); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -539,7 +539,7 @@ void main() { expectLater(() => nmc?.initializeExisting(), throwsA(isA<Exception>())) .then((_) { - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); // verify(client?.ping()).called(1); // verify(client?.getServerFeatures()).called(1); verifyNoMoreInteractions(client); @@ -560,13 +560,13 @@ void main() { "hash_function": "sha256", "services": [] }); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_mnemonic", value: "some mnemonic"); final wallets = await Hive.openBox(testWalletId); expectLater(() => nmc?.initializeExisting(), throwsA(isA<Exception>())) .then((_) { - expect(secureStore?.interactions, 1); + expect(secureStore.interactions, 1); // verify(client?.ping()).called(1); // verify(client?.getServerFeatures()).called(1); verifyNoMoreInteractions(client); @@ -603,7 +603,7 @@ void main() { verify(client?.getServerFeatures()).called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -623,7 +623,7 @@ void main() { "services": [] }); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_mnemonic", value: "some mnemonic words"); bool hasThrown = false; @@ -640,7 +640,7 @@ void main() { verify(client?.getServerFeatures()).called(1); - expect(secureStore?.interactions, 2); + expect(secureStore.interactions, 2); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -691,10 +691,10 @@ void main() { verify(client?.getBatchHistory(args: historyBatchArgs4)).called(1); verify(client?.getBatchHistory(args: historyBatchArgs5)).called(1); - expect(secureStore?.interactions, 20); - expect(secureStore?.writes, 7); - expect(secureStore?.reads, 13); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 20); + expect(secureStore.writes, 7); + expect(secureStore.reads, 13); + expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -814,10 +814,10 @@ void main() { true); } - expect(secureStore?.interactions, 14); - expect(secureStore?.writes, 7); - expect(secureStore?.reads, 7); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 14); + expect(secureStore.writes, 7); + expect(secureStore.reads, 7); + expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -911,17 +911,17 @@ void main() { final preChangeIndexP2SH = await wallet.get('changeIndexP2SH'); final preChangeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); final preUtxoData = await wallet.get('latest_utxo_model'); - final preReceiveDerivationsStringP2PKH = await secureStore?.read( + final preReceiveDerivationsStringP2PKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2PKH"); - final preChangeDerivationsStringP2PKH = await secureStore?.read( - key: "${testWalletId}_changeDerivationsP2PKH"); - final preReceiveDerivationsStringP2SH = await secureStore?.read( - key: "${testWalletId}_receiveDerivationsP2SH"); + final preChangeDerivationsStringP2PKH = + await secureStore.read(key: "${testWalletId}_changeDerivationsP2PKH"); + final preReceiveDerivationsStringP2SH = + await secureStore.read(key: "${testWalletId}_receiveDerivationsP2SH"); final preChangeDerivationsStringP2SH = - await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); - final preReceiveDerivationsStringP2WPKH = await secureStore?.read( + await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH"); + final preReceiveDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2WPKH"); - final preChangeDerivationsStringP2WPKH = await secureStore?.read( + final preChangeDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_changeDerivationsP2WPKH"); // destroy the data that the rescan will fix @@ -943,17 +943,17 @@ void main() { await wallet.put('changeIndexP2PKH', 123); await wallet.put('changeIndexP2SH', 123); await wallet.put('changeIndexP2WPKH', 123); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_receiveDerivationsP2PKH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_changeDerivationsP2PKH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_receiveDerivationsP2SH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_changeDerivationsP2SH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_receiveDerivationsP2WPKH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_changeDerivationsP2WPKH", value: "{}"); bool hasThrown = false; @@ -980,17 +980,17 @@ void main() { final changeIndexP2SH = await wallet.get('changeIndexP2SH'); final changeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); final utxoData = await wallet.get('latest_utxo_model'); - final receiveDerivationsStringP2PKH = await secureStore?.read( + final receiveDerivationsStringP2PKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2PKH"); - final changeDerivationsStringP2PKH = await secureStore?.read( - key: "${testWalletId}_changeDerivationsP2PKH"); - final receiveDerivationsStringP2SH = await secureStore?.read( - key: "${testWalletId}_receiveDerivationsP2SH"); + final changeDerivationsStringP2PKH = + await secureStore.read(key: "${testWalletId}_changeDerivationsP2PKH"); + final receiveDerivationsStringP2SH = + await secureStore.read(key: "${testWalletId}_receiveDerivationsP2SH"); final changeDerivationsStringP2SH = - await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); - final receiveDerivationsStringP2WPKH = await secureStore?.read( + await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH"); + final receiveDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2WPKH"); - final changeDerivationsStringP2WPKH = await secureStore?.read( + final changeDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_changeDerivationsP2WPKH"); expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); @@ -1082,9 +1082,9 @@ void main() { // // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); - expect(secureStore?.writes, 25); - expect(secureStore?.reads, 32); - expect(secureStore?.deletes, 6); + expect(secureStore.writes, 25); + expect(secureStore.reads, 32); + expect(secureStore.deletes, 6); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -1182,17 +1182,17 @@ void main() { final preChangeIndexP2SH = await wallet.get('changeIndexP2SH'); final preChangeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); final preUtxoData = await wallet.get('latest_utxo_model'); - final preReceiveDerivationsStringP2PKH = await secureStore?.read( + final preReceiveDerivationsStringP2PKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2PKH"); - final preChangeDerivationsStringP2PKH = await secureStore?.read( - key: "${testWalletId}_changeDerivationsP2PKH"); - final preReceiveDerivationsStringP2SH = await secureStore?.read( - key: "${testWalletId}_receiveDerivationsP2SH"); + final preChangeDerivationsStringP2PKH = + await secureStore.read(key: "${testWalletId}_changeDerivationsP2PKH"); + final preReceiveDerivationsStringP2SH = + await secureStore.read(key: "${testWalletId}_receiveDerivationsP2SH"); final preChangeDerivationsStringP2SH = - await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); - final preReceiveDerivationsStringP2WPKH = await secureStore?.read( + await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH"); + final preReceiveDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2WPKH"); - final preChangeDerivationsStringP2WPKH = await secureStore?.read( + final preChangeDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_changeDerivationsP2WPKH"); when(client?.getBatchHistory(args: historyBatchArgs0)) @@ -1222,17 +1222,17 @@ void main() { final changeIndexP2SH = await wallet.get('changeIndexP2SH'); final changeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); final utxoData = await wallet.get('latest_utxo_model'); - final receiveDerivationsStringP2PKH = await secureStore?.read( + final receiveDerivationsStringP2PKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2PKH"); - final changeDerivationsStringP2PKH = await secureStore?.read( - key: "${testWalletId}_changeDerivationsP2PKH"); - final receiveDerivationsStringP2SH = await secureStore?.read( - key: "${testWalletId}_receiveDerivationsP2SH"); + final changeDerivationsStringP2PKH = + await secureStore.read(key: "${testWalletId}_changeDerivationsP2PKH"); + final receiveDerivationsStringP2SH = + await secureStore.read(key: "${testWalletId}_receiveDerivationsP2SH"); final changeDerivationsStringP2SH = - await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); - final receiveDerivationsStringP2WPKH = await secureStore?.read( + await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH"); + final receiveDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2WPKH"); - final changeDerivationsStringP2WPKH = await secureStore?.read( + final changeDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_changeDerivationsP2WPKH"); expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); @@ -1296,9 +1296,9 @@ void main() { verify(cachedClient?.clearSharedTransactionCache(coin: Coin.namecoin)) .called(1); - expect(secureStore?.writes, 19); - expect(secureStore?.reads, 32); - expect(secureStore?.deletes, 12); + expect(secureStore.writes, 19); + expect(secureStore.reads, 32); + expect(secureStore.deletes, 12); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -1366,21 +1366,21 @@ void main() { height: 4000); // modify addresses to properly mock data to build a tx - final rcv44 = await secureStore?.read( + final rcv44 = await secureStore.read( key: testWalletId + "_receiveDerivationsP2PKH"); - await secureStore?.write( + await secureStore.write( key: testWalletId + "_receiveDerivationsP2PKH", value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); - final rcv49 = await secureStore?.read( - key: testWalletId + "_receiveDerivationsP2SH"); - await secureStore?.write( + final rcv49 = + await secureStore.read(key: testWalletId + "_receiveDerivationsP2SH"); + await secureStore.write( key: testWalletId + "_receiveDerivationsP2SH", value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); - final rcv84 = await secureStore?.read( + final rcv84 = await secureStore.read( key: testWalletId + "_receiveDerivationsP2WPKH"); - await secureStore?.write( + await secureStore.write( key: testWalletId + "_receiveDerivationsP2WPKH", value: rcv84?.replaceFirst( "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", @@ -1436,10 +1436,10 @@ void main() { true); } - expect(secureStore?.interactions, 20); - expect(secureStore?.writes, 10); - expect(secureStore?.reads, 10); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 20); + expect(secureStore.writes, 10); + expect(secureStore.reads, 10); + expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -1456,7 +1456,7 @@ void main() { expect(didThrow, true); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -1472,7 +1472,7 @@ void main() { expect(didThrow, true); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -1492,7 +1492,7 @@ void main() { rawTx: "a string", requestID: anyNamed("requestID"))) .called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -1513,7 +1513,7 @@ void main() { rawTx: "a string", requestID: anyNamed("requestID"))) .called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -1538,7 +1538,7 @@ void main() { rawTx: "a string", requestID: anyNamed("requestID"))) .called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -1658,10 +1658,10 @@ void main() { true); } - expect(secureStore?.interactions, 14); - expect(secureStore?.writes, 7); - expect(secureStore?.reads, 7); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 14); + expect(secureStore.writes, 7); + expect(secureStore.reads, 7); + expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -1726,10 +1726,10 @@ void main() { verify(client?.getBatchHistory(args: map)).called(1); } - expect(secureStore?.interactions, 14); - expect(secureStore?.writes, 7); - expect(secureStore?.reads, 7); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 14); + expect(secureStore.writes, 7); + expect(secureStore.reads, 7); + expect(secureStore.deletes, 0); // verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index a81c27fe0..91c3e5bfa 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart new file mode 100644 index 000000000..637a40b81 --- /dev/null +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -0,0 +1,369 @@ +import 'dart:core'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/node.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_wownero/wownero_wallet.dart'; +import 'package:flutter_libmonero/core/key_service.dart'; +import 'package:flutter_libmonero/core/wallet_creation_service.dart'; +import 'package:flutter_libmonero/wownero/wownero.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; + +import 'wownero_wallet_test_data.dart'; + +FakeSecureStorage? storage; +WalletService? walletService; +KeyService? keysStorage; +WowneroWalletBase? walletBase; +late WalletCreationService _walletCreationService; +dynamic _walletInfoSource; + +String path = ''; + +String name = ''; +int nettype = 0; +WalletType type = WalletType.wownero; + +@GenerateMocks([]) +void main() async { + storage = FakeSecureStorage(); + keysStorage = KeyService(storage!); + WalletInfo walletInfo = WalletInfo.external( + id: '', + name: '', + type: type, + isRecovery: false, + restoreHeight: 0, + date: DateTime.now(), + path: '', + address: '', + dirPath: ''); + late WalletCredentials credentials; + + wownero.onStartup(); + + bool hiveAdaptersRegistered = false; + + group("Wownero 14 word seed generation", () { + setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', name); + + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = wownero + .createWowneroWalletService(_walletInfoSource as Box<WalletInfo>); + } + + bool hasThrown = false; + try { + name = 'namee${Random().nextInt(10000000)}'; + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + credentials = wownero.createWowneroNewWalletCredentials( + name: name, + language: "English", + seedWordsLength: 14); // TODO catch failure + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + hasThrown = true; + } + expect(hasThrown, false); + }); + + test("Wownero 14 word seed address generation", () async { + final wallet = await _walletCreationService.create(credentials); + // TODO validate mnemonic + walletInfo.address = wallet.walletAddresses.address; + + bool hasThrown = false; + try { + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + + expect( + await walletBase! + .validateAddress(wallet.walletAddresses.address ?? ''), + true); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + // Address validation + expect(await walletBase!.validateAddress(''), false); + expect( + await walletBase!.validateAddress( + 'Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi'), + true); + expect( + await walletBase!.validateAddress( + 'WasdfHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmjkl'), + false); + + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + }); + + // TODO delete left over wallet file with name: name + }); + + group("Wownero 14 word seed restoration", () { + setUp(() async { + bool hasThrown = false; + try { + name = 'namee${Random().nextInt(10000000)}'; + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( + name: name, + height: 465760, + mnemonic: testMnemonic14); // TODO catch failure + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + hasThrown = true; + } + expect(hasThrown, false); + }); + + test("Wownero 14 word seed address generation", () async { + final wallet = await _walletCreationService.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + + bool hasThrown = false; + try { + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + + expect(walletInfo.address, mainnetTestData14[0][0]); + expect(await walletBase!.getTransactionAddress(0, 0), + mainnetTestData14[0][0]); + expect(await walletBase!.getTransactionAddress(0, 1), + mainnetTestData14[0][1]); + expect(await walletBase!.getTransactionAddress(0, 2), + mainnetTestData14[0][2]); + expect(await walletBase!.getTransactionAddress(1, 0), + mainnetTestData14[1][0]); + expect(await walletBase!.getTransactionAddress(1, 1), + mainnetTestData14[1][1]); + expect(await walletBase!.getTransactionAddress(1, 2), + mainnetTestData14[1][2]); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + }); + + // TODO delete left over wallet file with name: name + }); + + group("Wownero 25 word seed generation", () { + setUp(() async { + bool hasThrown = false; + try { + name = 'namee${Random().nextInt(10000000)}'; + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + credentials = wownero.createWowneroNewWalletCredentials( + name: name, + language: "English", + seedWordsLength: 25); // TODO catch failure + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + hasThrown = true; + } + expect(hasThrown, false); + }); + + test("Wownero 25 word seed address generation", () async { + final wallet = await _walletCreationService.create(credentials); + // TODO validate mnemonic + walletInfo.address = wallet.walletAddresses.address; + + bool hasThrown = false; + try { + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + + // TODO validate + //expect(walletInfo.address, mainnetTestData14[0][0]); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + }); + + // TODO delete left over wallet file with name: name + }); + + group("Wownero 25 word seed restoration", () { + setUp(() async { + bool hasThrown = false; + try { + name = 'namee${Random().nextInt(10000000)}'; + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( + name: name, + height: 465760, + mnemonic: testMnemonic25); // TODO catch failure + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + hasThrown = true; + } + expect(hasThrown, false); + }); + + test("Wownero 25 word seed address generation", () async { + final wallet = await _walletCreationService.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + + bool hasThrown = false; + try { + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + + expect(walletInfo.address, mainnetTestData25[0][0]); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + }); + + // TODO delete left over wallet file with name: name + }); +} + +Future<String> pathForWalletDir( + {required String name, required WalletType type}) async { + Directory root = (await getApplicationDocumentsDirectory()); + if (Platform.isIOS) { + root = (await getLibraryDirectory()); + } + final prefix = walletTypeToString(type).toLowerCase(); + final walletsDir = Directory('${root.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; +} + +Future<String> pathForWallet( + {required String name, required WalletType type}) async => + await pathForWalletDir(name: name, type: type) + .then((path) => path + '/$name'); diff --git a/test/services/coins/wownero/wownero_wallet_test_data.dart b/test/services/coins/wownero/wownero_wallet_test_data.dart new file mode 100644 index 000000000..7f27fc486 --- /dev/null +++ b/test/services/coins/wownero/wownero_wallet_test_data.dart @@ -0,0 +1,22 @@ +String testMnemonic14 = + 'weather cruise school such silly profit clerk wage reduce obtain ill sand episode shadow'; +var mainnetTestData14 = [ + [ + 'Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi', + 'WW3K54QzmMFB1uTZh3LVvgQYqANLmX1FkJHLJ4sU1E7BQmp8nGizyBnjNXSgsjCa4BQ3Rw3GG5jw1ByUkaUjSywm2KmHAbFvK', + 'WW3e3F51KAojcSW2G5WimmE1WVFsbBHc6HppZFBa6dNiEn21cThXzdGGDbpv89aTKXSRSPSFaetK6HgCozYawaYz2knUi9Hmn' + ], + [ + 'WW2nx7MFruyN2CcXnGnMbDdvqsyZUGQthLWKYPkQ4iM9XCE54RyWVjNjgopryUbyi9WKzYhHDai2wENbh1Jh1UHa28CL72TYt', + 'WW34p57QBMoD6MEZVTu5u9R7G3KeYqvN4eYbvHLYsgbWXpLe992fBvVB7ANJNvaGmPg2uwY5oKjwKbpo4fDU6cGS231PmvXrZ', + 'WW2KQLLt6gjC9gRsC4NGehbAZX6UPU7sK89UQFwSg3NKj3MXPwnjh5BiJVqYYNQb6JNsfa7oP7eDjLagtLa2H6YP11RhUNQqw' + ] +]; + +String testMnemonic25 = + 'myth byline benches sadness nylon tamper guide giving match angled lurk rally makeup alarms river soapy dolphin woven ticket maul examine public luggage mammal alarms'; +var mainnetTestData25 = [ + [ + 'Wo3piMnt1ztjLktFJNsfs9ce6N1tyHk7DB93cNqTGPJ7To3RS7W2q5DdxgQAG5E6RQXQhchQD7ip8WWL3fD8Ww5K2XmAXYxta' + ] +]; diff --git a/test/services/node_service_test.dart b/test/services/node_service_test.dart index 81f8ca6ed..cea30be2d 100644 --- a/test/services/node_service_test.dart +++ b/test/services/node_service_test.dart @@ -141,7 +141,8 @@ void main() { ); setUp(() async { - await NodeService().updateDefaults(); + await NodeService(secureStorageInterface: FakeSecureStorage()) + .updateDefaults(); }); test("setPrimaryNodeFor and getPrimaryNodeFor", () async { diff --git a/test/services/wallets_service_test.dart b/test/services/wallets_service_test.dart index 1cb712759..9cd808b7c 100644 --- a/test/services/wallets_service_test.dart +++ b/test/services/wallets_service_test.dart @@ -32,7 +32,7 @@ void main() { }); test("get walletNames", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect((await service.walletNames).toString(), '{wallet_id: WalletInfo: {"name":"My Firo Wallet","id":"wallet_id","coin":"bitcoin"}, wallet_id2: WalletInfo: {"name":"wallet2","id":"wallet_id2","coin":"bitcoin"}}'); }); @@ -40,13 +40,13 @@ void main() { test("get null wallet names", () async { final wallets = await Hive.openBox<dynamic>('wallets'); await wallets.put('names', null); - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.walletNames, <String, WalletInfo>{}); expect((await service.walletNames).toString(), '{}'); }); test("rename wallet to same name", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.renameWallet( from: "My Firo Wallet", @@ -58,7 +58,7 @@ void main() { }); test("rename wallet to new name", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.renameWallet( from: "My Firo Wallet", @@ -71,7 +71,7 @@ void main() { }); test("attempt rename wallet to another existing name", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.renameWallet( from: "My Firo Wallet", @@ -83,7 +83,7 @@ void main() { }); test("add new wallet name", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.addNewWallet( name: "wallet3", coin: Coin.bitcoin, shouldNotifyListeners: false), @@ -92,7 +92,7 @@ void main() { }); test("add duplicate wallet name fails", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.addNewWallet( name: "wallet2", coin: Coin.bitcoin, shouldNotifyListeners: false), @@ -103,27 +103,27 @@ void main() { test("check for duplicates when null names", () async { final wallets = await Hive.openBox<dynamic>('wallets'); await wallets.put('names', null); - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.checkForDuplicate("anything"), false); }); test("check for duplicates when some names with no matches", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.checkForDuplicate("anything"), false); }); test("check for duplicates when some names with a match", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.checkForDuplicate("wallet2"), true); }); test("get existing wallet id", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.getWalletId("wallet2"), "wallet_id2"); }); test("get non existent wallet id", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expectLater(await service.getWalletId("wallet 99"), null); }); diff --git a/test/services/wallets_service_test.mocks.dart b/test/services/wallets_service_test.mocks.dart index c553bbf93..19d525196 100644 --- a/test/services/wallets_service_test.mocks.dart +++ b/test/services/wallets_service_test.mocks.dart @@ -3,12 +3,12 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i3; -import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i2; +import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' - as _i3; + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -21,43 +21,24 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeFlutterSecureStorage_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorage { - _FakeFlutterSecureStorage_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [SecureStorageWrapper]. /// /// See the documentation for Mockito's code generation for more information. class MockSecureStorageWrapper extends _i1.Mock - implements _i3.SecureStorageWrapper { + implements _i2.SecureStorageWrapper { MockSecureStorageWrapper() { _i1.throwOnMissingStub(this); } @override - _i2.FlutterSecureStorage get secureStore => (super.noSuchMethod( - Invocation.getter(#secureStore), - returnValue: _FakeFlutterSecureStorage_0( - this, - Invocation.getter(#secureStore), - ), - ) as _i2.FlutterSecureStorage); - @override - _i4.Future<String?> read({ + _i3.Future<String?> read({ required String? key, - _i2.IOSOptions? iOptions, - _i2.AndroidOptions? aOptions, - _i2.LinuxOptions? lOptions, - _i2.WebOptions? webOptions, - _i2.MacOsOptions? mOptions, - _i2.WindowsOptions? wOptions, + _i4.IOSOptions? iOptions, + _i4.AndroidOptions? aOptions, + _i4.LinuxOptions? lOptions, + _i4.WebOptions? webOptions, + _i4.MacOsOptions? mOptions, + _i4.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method( @@ -73,18 +54,18 @@ class MockSecureStorageWrapper extends _i1.Mock #wOptions: wOptions, }, ), - returnValue: _i4.Future<String?>.value(), - ) as _i4.Future<String?>); + returnValue: _i3.Future<String?>.value(), + ) as _i3.Future<String?>); @override - _i4.Future<void> write({ + _i3.Future<void> write({ required String? key, required String? value, - _i2.IOSOptions? iOptions, - _i2.AndroidOptions? aOptions, - _i2.LinuxOptions? lOptions, - _i2.WebOptions? webOptions, - _i2.MacOsOptions? mOptions, - _i2.WindowsOptions? wOptions, + _i4.IOSOptions? iOptions, + _i4.AndroidOptions? aOptions, + _i4.LinuxOptions? lOptions, + _i4.WebOptions? webOptions, + _i4.MacOsOptions? mOptions, + _i4.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method( @@ -101,18 +82,18 @@ class MockSecureStorageWrapper extends _i1.Mock #wOptions: wOptions, }, ), - returnValue: _i4.Future<void>.value(), - returnValueForMissingStub: _i4.Future<void>.value(), - ) as _i4.Future<void>); + returnValue: _i3.Future<void>.value(), + returnValueForMissingStub: _i3.Future<void>.value(), + ) as _i3.Future<void>); @override - _i4.Future<void> delete({ + _i3.Future<void> delete({ required String? key, - _i2.IOSOptions? iOptions, - _i2.AndroidOptions? aOptions, - _i2.LinuxOptions? lOptions, - _i2.WebOptions? webOptions, - _i2.MacOsOptions? mOptions, - _i2.WindowsOptions? wOptions, + _i4.IOSOptions? iOptions, + _i4.AndroidOptions? aOptions, + _i4.LinuxOptions? lOptions, + _i4.WebOptions? webOptions, + _i4.MacOsOptions? mOptions, + _i4.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method( @@ -128,7 +109,7 @@ class MockSecureStorageWrapper extends _i1.Mock #wOptions: wOptions, }, ), - returnValue: _i4.Future<void>.value(), - returnValueForMissingStub: _i4.Future<void>.value(), - ) as _i4.Future<void>); + returnValue: _i3.Future<void>.value(), + returnValueForMissingStub: _i3.Future<void>.value(), + ) as _i3.Future<void>); } diff --git a/test/widget_tests/address_book_card_test.dart b/test/widget_tests/address_book_card_test.dart index ef031eb4e..07b1387df 100644 --- a/test/widget_tests/address_book_card_test.dart +++ b/test/widget_tests/address_book_card_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -11,6 +13,7 @@ import 'package:stackwallet/services/address_book_service.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/address_book_card.dart'; import 'address_book_card_test.mocks.dart'; @@ -24,16 +27,18 @@ void main() { testWidgets('test returns Contact Address Entry', (widgetTester) async { final service = MockAddressBookService(); - when(service.getContactById("default")) - .thenAnswer((realInvocation) => Contact( - name: "John Doe", - addresses: [ - const ContactAddressEntry( - coin: Coin.bitcoincash, - address: "some bch address", - label: "Bills") - ], - isFavorite: true)); + when(service.getContactById("default")).thenAnswer( + (realInvocation) => Contact( + name: "John Doe", + addresses: [ + const ContactAddressEntry( + coin: Coin.bitcoincash, + address: "some bch address", + label: "Bills") + ], + isFavorite: true, + ), + ); await widgetTester.pumpWidget( ProviderScope( @@ -61,6 +66,11 @@ void main() { expect(find.text("BCH"), findsOneWidget); expect(find.text(Coin.bitcoincash.ticker), findsOneWidget); - await widgetTester.tap(find.byType(RawMaterialButton)); + if (Platform.isIOS || Platform.isAndroid) { + await widgetTester.tap(find.byType(RawMaterialButton)); + expect(find.byType(ContactPopUp), findsOneWidget); + } else if (Util.isDesktop) { + expect(find.byType(RawMaterialButton), findsNothing); + } }); } diff --git a/test/widget_tests/emoji_select_sheet_test.dart b/test/widget_tests/emoji_select_sheet_test.dart index 368a1d99b..aec05d580 100644 --- a/test/widget_tests/emoji_select_sheet_test.dart +++ b/test/widget_tests/emoji_select_sheet_test.dart @@ -1,8 +1,8 @@ import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mockingjay/mockingjay.dart' as mockingjay; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart' as mockingjay; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/emoji_select_sheet.dart'; @@ -43,15 +43,21 @@ void main() { ], ), home: mockingjay.MockNavigatorProvider( - navigator: navigator, child: emojiSelectSheet), + navigator: navigator, + child: Column( + children: const [ + Expanded(child: emojiSelectSheet), + ], + ), + ), ), ), ); - final gestureDetector = find.byType(GestureDetector).first; + final gestureDetector = find.byType(GestureDetector).at(5); expect(gestureDetector, findsOneWidget); - final emoji = Emoji.all()[0]; + final emoji = Emoji.byChar("😅"); await tester.tap(gestureDetector); await tester.pumpAndSettle(); diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 7d1d864df..50f906c3c 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -166,7 +166,7 @@ class _FakeElectrumXNode_11 extends _i1.SmartFake } class _FakeFlutterSecureStorageInterface_12 extends _i1.SmartFake - implements _i12.FlutterSecureStorageInterface { + implements _i12.SecureStorageInterface { _FakeFlutterSecureStorageInterface_12( Object parent, Invocation parentInvocation, @@ -681,6 +681,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1063,6 +1071,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -1380,14 +1398,13 @@ class MockLocaleService extends _i1.Mock implements _i20.LocaleService { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i3.NodeService { @override - _i12.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i12.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_12( this, Invocation.getter(#secureStorageInterface), ), - ) as _i12.FlutterSecureStorageInterface); + ) as _i12.SecureStorageInterface); @override List<_i21.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), @@ -2291,4 +2308,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i13.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } diff --git a/test/widget_tests/node_card_test.dart b/test/widget_tests/node_card_test.dart index 2728fc304..22e0661bf 100644 --- a/test/widget_tests/node_card_test.dart +++ b/test/widget_tests/node_card_test.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/node_card.dart'; import 'package:stackwallet/widgets/node_options_sheet.dart'; @@ -190,13 +191,22 @@ void main() { await tester.tap(find.byType(NodeCard)); await tester.pumpAndSettle(); - expect(find.text("Connect"), findsOneWidget); - expect(find.text("Details"), findsOneWidget); - expect(find.byType(NodeOptionsSheet), findsOneWidget); - expect(find.byType(Text), findsNWidgets(7)); + if (Util.isDesktop) { + expect(find.text("Connect"), findsNothing); + expect(find.text("Details"), findsNothing); + + verify(nodeService.getPrimaryNodeFor(coin: Coin.bitcoin)).called(1); + verify(nodeService.getNodeById(id: "node id")).called(1); + } else { + expect(find.text("Connect"), findsOneWidget); + expect(find.text("Details"), findsOneWidget); + expect(find.byType(NodeOptionsSheet), findsOneWidget); + expect(find.byType(Text), findsNWidgets(7)); + + verify(nodeService.getPrimaryNodeFor(coin: Coin.bitcoin)).called(2); + verify(nodeService.getNodeById(id: "node id")).called(2); + } - verify(nodeService.getPrimaryNodeFor(coin: Coin.bitcoin)).called(2); - verify(nodeService.getNodeById(id: "node id")).called(2); verify(nodeService.addListener(any)).called(1); verifyNoMoreInteractions(nodeService); diff --git a/test/widget_tests/node_card_test.mocks.dart b/test/widget_tests/node_card_test.mocks.dart index 673182ceb..2bb32b58d 100644 --- a/test/widget_tests/node_card_test.mocks.dart +++ b/test/widget_tests/node_card_test.mocks.dart @@ -25,7 +25,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -44,14 +44,13 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i4.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 173edf1bf..c9e4e2bb8 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -77,7 +77,7 @@ class _FakeManager_3 extends _i1.SmartFake implements _i6.Manager { } class _FakeFlutterSecureStorageInterface_4 extends _i1.SmartFake - implements _i7.FlutterSecureStorageInterface { + implements _i7.SecureStorageInterface { _FakeFlutterSecureStorageInterface_4( Object parent, Invocation parentInvocation, @@ -573,6 +573,14 @@ class MockPrefs extends _i1.Mock implements _i11.Prefs { returnValueForMissingStub: _i10.Future<void>.value(), ) as _i10.Future<void>); @override + _i10.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i10.Future<bool>.value(false), + ) as _i10.Future<bool>); + @override void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, @@ -615,14 +623,14 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i7.FlutterSecureStorageInterface get secureStorageInterface => + _i7.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_4( this, Invocation.getter(#secureStorageInterface), ), - ) as _i7.FlutterSecureStorageInterface); + ) as _i7.SecureStorageInterface); @override List<_i16.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index b4a0e9cc7..5a47436ff 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -666,6 +666,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i18.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1048,6 +1056,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i18.BitcoinWallet { returnValueForMissingStub: _i15.Future<void>.value(), ) as _i15.Future<void>); @override + _i15.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i15.Future<void>.value(), + returnValueForMissingStub: _i15.Future<void>.value(), + ) as _i15.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -2013,4 +2031,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i12.CoinServiceAPI { ), returnValue: _i15.Future<bool>.value(false), ) as _i15.Future<bool>); + @override + _i15.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i15.Future<void>.value(), + returnValueForMissingStub: _i15.Future<void>.value(), + ) as _i15.Future<void>); } diff --git a/test/widget_tests/transaction_card_test.dart b/test/widget_tests/transaction_card_test.dart index 3f46794bd..f28c5f81d 100644 --- a/test/widget_tests/transaction_card_test.dart +++ b/test/widget_tests/transaction_card_test.dart @@ -1,12 +1,13 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockingjay/mockingjay.dart' as mockingjay; import 'package:mockito/annotations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockito/mockito.dart'; import 'package:stackwallet/models/models.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; @@ -15,13 +16,12 @@ import 'package:stackwallet/services/locale_service.dart'; import 'package:stackwallet/services/notes_service.dart'; import 'package:stackwallet/services/price_service.dart'; import 'package:stackwallet/services/wallets.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/listenable_map.dart'; import 'package:stackwallet/utilities/prefs.dart'; -import 'package:stackwallet/widgets/transaction_card.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/transaction_card.dart'; import 'package:tuple/tuple.dart'; import 'transaction_card_test.mocks.dart'; @@ -104,7 +104,9 @@ void main() { // final title = find.text("Sent"); // final price1 = find.text("0.00 USD"); - final amount = find.text("1.00000000 FIRO"); + final amount = Util.isDesktop + ? find.text("-1.00000000 FIRO") + : find.text("1.00000000 FIRO"); final icon = find.byIcon(FeatherIcons.arrowUp); @@ -113,7 +115,7 @@ void main() { expect(amount, findsOneWidget); // expect(icon, findsOneWidget); // - await tester.pumpAndSettle(Duration(seconds: 2)); + await tester.pumpAndSettle(const Duration(seconds: 2)); // // final price2 = find.text("\$10.00"); // expect(price2, findsOneWidget); @@ -206,7 +208,7 @@ void main() { expect(amount, findsOneWidget); // expect(icon, findsOneWidget); // - await tester.pumpAndSettle(Duration(seconds: 2)); + await tester.pumpAndSettle(const Duration(seconds: 2)); // // final price2 = find.text("\$10.00"); // expect(price2, findsOneWidget); @@ -288,12 +290,14 @@ void main() { ); final title = find.text("Receiving"); - final amount = find.text("1.00000000 FIRO"); + final amount = Util.isDesktop + ? find.text("+1.00000000 FIRO") + : find.text("1.00000000 FIRO"); expect(title, findsOneWidget); expect(amount, findsOneWidget); - await tester.pumpAndSettle(Duration(seconds: 2)); + await tester.pumpAndSettle(const Duration(seconds: 2)); verify(mockLocaleService.addListener(any)).called(1); @@ -383,16 +387,20 @@ void main() { verify(mockLocaleService.addListener(any)).called(1); - verify(mockPrefs.currency).called(1); - verify(mockLocaleService.locale).called(1); + verify(mockPrefs.currency).called(2); + verify(mockLocaleService.locale).called(4); verify(wallet.coin.ticker).called(1); verifyNoMoreInteractions(wallet); verifyNoMoreInteractions(mockLocaleService); - mockingjay - .verify(() => navigator.pushNamed("/transactionDetails", - arguments: Tuple3(tx, Coin.firo, "wallet id"))) - .called(1); + if (Util.isDesktop) { + expect(find.byType(TransactionDetailsView), findsOneWidget); + } else { + mockingjay + .verify(() => navigator.pushNamed("/transactionDetails", + arguments: Tuple3(tx, Coin.firo, "wallet id"))) + .called(1); + } }); } diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index df696801a..f258a402b 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -1108,6 +1108,16 @@ class MockCoinServiceAPI extends _i1.Mock implements _i7.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } /// A class which mocks [FiroWallet]. @@ -1127,6 +1137,14 @@ class MockFiroWallet extends _i1.Mock implements _i19.FiroWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override _i10.TransactionNotificationTracker get txTracker => (super.noSuchMethod( Invocation.getter(#txTracker), returnValue: _FakeTransactionNotificationTracker_8( @@ -1386,6 +1404,16 @@ class MockFiroWallet extends _i1.Mock implements _i19.FiroWallet { returnValue: false, ) as bool); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override _i16.Future<bool> testNetworkConnection() => (super.noSuchMethod( Invocation.method( #testNetworkConnection, @@ -2312,6 +2340,14 @@ class MockPrefs extends _i1.Mock implements _i17.Prefs { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i16.Future<bool>.value(false), + ) as _i16.Future<bool>); + @override void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index ec4f90e22..e323911d4 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -429,6 +429,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i17.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -811,6 +819,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i17.BitcoinWallet { returnValueForMissingStub: _i14.Future<void>.value(), ) as _i14.Future<void>); @override + _i14.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i14.Future<void>.value(), + returnValueForMissingStub: _i14.Future<void>.value(), + ) as _i14.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index e5dc17bc4..a249c997d 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -165,7 +165,7 @@ class _FakeElectrumXNode_11 extends _i1.SmartFake } class _FakeFlutterSecureStorageInterface_12 extends _i1.SmartFake - implements _i12.FlutterSecureStorageInterface { + implements _i12.SecureStorageInterface { _FakeFlutterSecureStorageInterface_12( Object parent, Invocation parentInvocation, @@ -680,6 +680,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1062,6 +1070,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -1317,14 +1335,13 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i3.NodeService { @override - _i12.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i12.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_12( this, Invocation.getter(#secureStorageInterface), ), - ) as _i12.FlutterSecureStorageInterface); + ) as _i12.SecureStorageInterface); @override List<_i20.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), @@ -2228,4 +2245,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i13.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 2b7bedb15..820fbd96d 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -165,7 +165,7 @@ class _FakeElectrumXNode_11 extends _i1.SmartFake } class _FakeFlutterSecureStorageInterface_12 extends _i1.SmartFake - implements _i12.FlutterSecureStorageInterface { + implements _i12.SecureStorageInterface { _FakeFlutterSecureStorageInterface_12( Object parent, Invocation parentInvocation, @@ -680,6 +680,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1062,6 +1070,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -1317,14 +1335,13 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i3.NodeService { @override - _i12.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i12.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_12( this, Invocation.getter(#secureStorageInterface), ), - ) as _i12.FlutterSecureStorageInterface); + ) as _i12.SecureStorageInterface); @override List<_i20.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), @@ -2228,4 +2245,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i13.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20caf..eee73d91b 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ