Merge remote-tracking branch 'origin/simplex' into ui-fixes

This commit is contained in:
ryleedavis 2023-01-25 15:29:03 -07:00
commit 83cd1759be
54 changed files with 4889 additions and 227 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 336.41 337.42"><defs><style>.cls-1{fill:#f0b90b;stroke:#f0b90b;}</style></defs><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M168.2.71l41.5,42.5L105.2,147.71l-41.5-41.5Z"/><path class="cls-1" d="M231.2,63.71l41.5,42.5L105.2,273.71l-41.5-41.5Z"/><path class="cls-1" d="M42.2,126.71l41.5,42.5-41.5,41.5L.7,169.21Z"/><path class="cls-1" d="M294.2,126.71l41.5,42.5L168.2,336.71l-41.5-41.5Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 528 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2500 2500"><title>cosmos-atom-logo</title><circle cx="1250" cy="1250" r="1250" style="fill:#2e3148"/><circle cx="1250" cy="1250" r="725.31" style="fill:#1b1e36"/><path d="M1252.57,159.47c-134.93,0-244.34,489.4-244.34,1093.11s109.41,1093.11,244.34,1093.11,244.34-489.4,244.34-1093.11S1387.5,159.47,1252.57,159.47ZM1269.44,2284c-15.43,20.58-30.86,5.14-30.86,5.14-62.14-72-93.21-205.76-93.21-205.76-108.69-349.79-82.82-1100.82-82.82-1100.82,51.08-596.24,144-737.09,175.62-768.36a19.29,19.29,0,0,1,24.74-2c45.88,32.51,84.36,168.47,84.36,168.47,113.63,421.81,103.34,817.9,103.34,817.9,10.29,344.65-56.94,730.45-56.94,730.45C1341.92,2222.22,1269.44,2284,1269.44,2284Z" style="fill:#6f7390"/><path d="M2200.72,708.59c-67.18-117.08-546.09,31.58-1070,332s-893.47,638.89-826.34,755.92,546.09-31.58,1070-332,893.47-638.89,826.34-755.92h0ZM366.36,1780.45c-25.72-3.24-19.91-24.38-19.91-24.38C378,1666.36,478.4,1572.84,478.4,1572.84c249.43-268.36,913.79-619.65,913.79-619.65,542.54-252.42,711.06-241.77,753.81-230a19.29,19.29,0,0,1,14,20.58c-5.14,56-104.17,157-104.17,157C1746.71,1209.36,1398,1397.58,1398,1397.58c-293.83,180.5-661.93,314.09-661.93,314.09-280.09,100.93-369.7,68.78-369.7,68.78h0Z" style="fill:#6f7390"/><path d="M2198.35,1800.41c67.7-116.77-300.93-456.79-823-759.47S374.43,587.76,306.79,704.73s300.93,456.79,823.3,759.47S2130.71,1917.39,2198.35,1800.41ZM351.65,749.85c-10-23.71,11.11-29.42,11.11-29.42C456.22,702.78,587.5,743,587.5,743c357.15,81.33,994,480.25,994,480.25,490.33,343.11,565.53,494.24,576.8,537.14a19.29,19.29,0,0,1-10.7,22.43c-51.13,23.41-188.07-11.47-188.07-11.47-422.07-113.17-759.62-320.52-759.62-320.52-303.29-163.58-603.19-415.28-603.19-415.28-227.88-191.87-245-285.44-245-285.44Z" style="fill:#6f7390"/><circle cx="1250" cy="1250" r="128.6" style="fill:#b7b9c8"/><ellipse cx="1777.26" cy="756.17" rx="74.59" ry="77.16" style="fill:#b7b9c8"/><ellipse cx="552.98" cy="1018.52" rx="74.59" ry="77.16" style="fill:#b7b9c8"/><ellipse cx="1098.25" cy="1965.02" rx="74.59" ry="77.16" style="fill:#b7b9c8"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 444.44 444.44" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<path fill="#F5AC37" fill-rule="nonzero" d="M222.22 0c122.74,0 222.22,99.5 222.22,222.22 0,122.74 -99.48,222.22 -222.22,222.22 -122.72,0 -222.22,-99.49 -222.22,-222.22 0,-122.72 99.5,-222.22 222.22,-222.22z"/>
<path fill="#FEFEFD" fill-rule="nonzero" d="M230.41 237.91l84.44 0c1.8,0 2.65,0 2.78,-2.36 0.69,-8.59 0.69,-17.23 0,-25.83 0,-1.67 -0.83,-2.36 -2.64,-2.36l-168.05 0c-2.08,0 -2.64,0.69 -2.64,2.64l0 24.72c0,3.19 0,3.19 3.33,3.19l82.78 0zm77.79 -59.44c0.24,-0.63 0.24,-1.32 0,-1.94 -1.41,-3.07 -3.08,-6 -5.02,-8.75 -2.92,-4.7 -6.36,-9.03 -10.28,-12.92 -1.85,-2.35 -3.99,-4.46 -6.39,-6.25 -12.02,-10.23 -26.31,-17.47 -41.67,-21.11 -7.75,-1.74 -15.67,-2.57 -23.61,-2.5l-74.58 0c-2.08,0 -2.36,0.83 -2.36,2.64l0 49.3c0,2.08 0,2.64 2.64,2.64l160.27 0c0,0 1.39,-0.28 1.67,-1.11l-0.68 0zm0 88.33c-2.36,-0.26 -4.74,-0.26 -7.1,0l-154.02 0c-2.08,0 -2.78,0 -2.78,2.78l0 48.2c0,2.22 0,2.78 2.78,2.78l71.11 0c3.4,0.26 6.8,0.02 10.13,-0.69 10.32,-0.74 20.47,-2.98 30.15,-6.67 3.52,-1.22 6.92,-2.81 10.13,-4.72l0.97 0c16.67,-8.67 30.21,-22.29 38.75,-39.01 0,0 0.97,-2.1 -0.12,-2.65zm-191.81 78.75l0 -0.83 0 -32.36 0 -10.97 0 -32.64c0,-1.81 0,-2.08 -2.22,-2.08l-30.14 0c-1.67,0 -2.36,0 -2.36,-2.22l0 -26.39 32.22 0c1.8,0 2.5,0 2.5,-2.36l0 -26.11c0,-1.67 0,-2.08 -2.22,-2.08l-30.14 0c-1.67,0 -2.36,0 -2.36,-2.22l0 -24.44c0,-1.53 0,-1.94 2.22,-1.94l29.86 0c2.08,0 2.64,0 2.64,-2.64l0 -74.86c0,-2.22 0,-2.78 2.78,-2.78l104.16 0c7.56,0.3 15.07,1.13 22.5,2.5 15.31,2.83 30.02,8.3 43.47,16.11 8.92,5.25 17.13,11.59 24.44,18.89 5.5,5.71 10.46,11.89 14.86,18.47 4.37,6.67 8,13.8 10.85,21.25 0.35,1.94 2.21,3.25 4.15,2.92l24.86 0c3.19,0 3.19,0 3.33,3.06l0 22.78c0,2.22 -0.83,2.78 -3.06,2.78l-19.17 0c-1.94,0 -2.5,0 -2.36,2.5 0.76,8.46 0.76,16.95 0,25.41 0,2.36 0,2.64 2.65,2.64l21.93 0c0.97,1.25 0,2.5 0,3.76 0.14,1.61 0.14,3.24 0,4.85l0 16.81c0,2.36 -0.69,3.06 -2.78,3.06l-26.25 0c-1.83,-0.35 -3.61,0.82 -4.03,2.64 -6.25,16.25 -16.25,30.82 -29.17,42.5 -4.72,4.25 -9.68,8.25 -14.86,11.94 -5.56,3.2 -10.97,6.53 -16.67,9.17 -10.49,4.72 -21.49,8.2 -32.78,10.41 -10.72,1.92 -21.59,2.79 -32.5,2.64l-96.39 0 0 -0.14z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513.4 416.8"><defs><style>.cls-1{fill:#008de4;}</style></defs><title>d</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M336.25,0H149.35l-15.5,86.6,168.7.2c83.1,0,107.6,30.2,106.9,80.2-.4,25.6-11.5,69-16.3,83.1-12.8,37.5-39.1,80.2-137.7,80.1l-164-.1L76,416.8h186.5c65.8,0,93.7-7.7,123.4-21.3,65.7-30.5,104.8-95.3,120.5-179.9C529.65,89.6,500.65,0,336.25,0"/><path class="cls-1" d="M68.7,164.9c-49,0-56,31.9-60.6,51.2C2,241.3,0,251.6,0,251.6H191.4c49,0,56-31.9,60.6-51.2,6.1-25.2,8.1-35.5,8.1-35.5Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 621 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.2 50"><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M16.6,0,4.9,16.1,0,39.9,16.6,50,33.2,39.9,28.2,16ZM2.7,38.8,6.4,20.7l8.4,25.5ZM7.6,16.6l9-12.4,9,12.4-9,27.2ZM18.3,46.2l8.4-25.5,3.7,18.1Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 322 B

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 784.37 1277.39" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_1421394342400">
<g>
<polygon fill="#343434" fill-rule="nonzero" points="392.07,0 383.5,29.11 383.5,873.74 392.07,882.29 784.13,650.54 "/>
<polygon fill="#8C8C8C" fill-rule="nonzero" points="392.07,0 -0,650.54 392.07,882.29 392.07,472.33 "/>
<polygon fill="#3C3C3B" fill-rule="nonzero" points="392.07,956.52 387.24,962.41 387.24,1263.28 392.07,1277.38 784.37,724.89 "/>
<polygon fill="#8C8C8C" fill-rule="nonzero" points="392.07,1277.38 392.07,956.52 -0,724.89 "/>
<polygon fill="#141414" fill-rule="nonzero" points="392.07,882.29 784.13,650.54 392.07,472.33 "/>
<polygon fill="#393939" fill-rule="nonzero" points="0,650.54 392.07,882.29 392.07,472.33 "/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,11 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 82.6 82.6"><title>litecoin-ltc-logo</title><circle cx="41.3" cy="41.3" r="36.83" style="fill:#fff"/><path d="M41.3,0A41.3,41.3,0,1,0,82.6,41.3h0A41.18,41.18,0,0,0,41.54,0ZM42,42.7,37.7,57.2h23a1.16,1.16,0,0,1,1.2,1.12v.38l-2,6.9a1.49,1.49,0,0,1-1.5,1.1H23.2l5.9-20.1-6.6,2L24,44l6.6-2,8.3-28.2a1.51,1.51,0,0,1,1.5-1.1h8.9a1.16,1.16,0,0,1,1.2,1.12v.38L43.5,38l6.6-2-1.4,4.8Z" style="fill:#345d9d"/></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>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 489 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 424"><defs><style>.cls-1{fill:#23292f;}</style></defs><title>x</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M437,0h74L357,152.48c-55.77,55.19-146.19,55.19-202,0L.94,0H75L192,115.83a91.11,91.11,0,0,0,127.91,0Z"/><path class="cls-1" d="M74.05,424H0L155,270.58c55.77-55.19,146.19-55.19,202,0L512,424H438L320,307.23a91.11,91.11,0,0,0-127.91,0Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 472 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236.36 200"><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M203,26.16l-28.46,14.5-137.43,70a82.49,82.49,0,0,1-.7-10.69A81.87,81.87,0,0,1,158.2,28.6l16.29-8.3,2.43-1.24A100,100,0,0,0,18.18,100q0,3.82.29,7.61a18.19,18.19,0,0,1-9.88,17.58L0,129.57V150l25.29-12.89,0,0,8.19-4.18,8.07-4.11v0L186.43,55l16.28-8.29,33.65-17.15V9.14Z"/><path d="M236.36,50,49.78,145,33.5,153.31,0,170.38v20.41l33.27-16.95,28.46-14.5L199.3,89.24A83.45,83.45,0,0,1,200,100,81.87,81.87,0,0,1,78.09,171.36l-1,.53-17.66,9A100,100,0,0,0,218.18,100c0-2.57-.1-5.14-.29-7.68a18.2,18.2,0,0,1,9.87-17.58l8.6-4.38Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 705 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 339.43 295.27"><title>tether-usdt-logo</title><path d="M62.15,1.45l-61.89,130a2.52,2.52,0,0,0,.54,2.94L167.95,294.56a2.55,2.55,0,0,0,3.53,0L338.63,134.4a2.52,2.52,0,0,0,.54-2.94l-61.89-130A2.5,2.5,0,0,0,275,0H64.45a2.5,2.5,0,0,0-2.3,1.45h0Z" style="fill:#50af95;fill-rule:evenodd"/><path d="M191.19,144.8v0c-1.2.09-7.4,0.46-21.23,0.46-11,0-18.81-.33-21.55-0.46v0c-42.51-1.87-74.24-9.27-74.24-18.13s31.73-16.25,74.24-18.15v28.91c2.78,0.2,10.74.67,21.74,0.67,13.2,0,19.81-.55,21-0.66v-28.9c42.42,1.89,74.08,9.29,74.08,18.13s-31.65,16.24-74.08,18.12h0Zm0-39.25V79.68h59.2V40.23H89.21V79.68H148.4v25.86c-48.11,2.21-84.29,11.74-84.29,23.16s36.18,20.94,84.29,23.16v82.9h42.78V151.83c48-2.21,84.12-11.73,84.12-23.14s-36.09-20.93-84.12-23.15h0Zm0,0h0Z" style="fill:#fff;fill-rule:evenodd"/></svg>

After

Width:  |  Height:  |  Size: 874 B

View file

@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.cls-1{fill:#ff060a;}</style></defs><title>tron</title><g id="tron"><path class="cls-1" d="M61.55,19.28c-3-2.77-7.15-7-10.53-10l-.2-.14a3.82,3.82,0,0,0-1.11-.62l0,0C41.56,7,3.63-.09,2.89,0a1.4,1.4,0,0,0-.58.22L2.12.37a2.23,2.23,0,0,0-.52.84l-.05.13v.71l0,.11C5.82,14.05,22.68,53,26,62.14c.2.62.58,1.8,1.29,1.86h.16c.38,0,2-2.14,2-2.14S58.41,26.74,61.34,23a9.46,9.46,0,0,0,1-1.48A2.41,2.41,0,0,0,61.55,19.28ZM36.88,23.37,49.24,13.12l7.25,6.68Zm-4.8-.67L10.8,5.26l34.43,6.35ZM34,27.27l21.78-3.51-24.9,30ZM7.91,7,30.3,26,27.06,53.78Z"/></g></svg>

After

Width:  |  Height:  |  Size: 651 B

View file

@ -33,6 +33,7 @@ class DB {
static const String boxNameDBInfo = "dbInfo"; static const String boxNameDBInfo = "dbInfo";
static const String boxNameTheme = "theme"; static const String boxNameTheme = "theme";
static const String boxNameDesktopData = "desktopData"; static const String boxNameDesktopData = "desktopData";
static const String boxNameBuys = "buysBox";
String boxNameTxCache({required Coin coin}) => "${coin.name}_txCache"; String boxNameTxCache({required Coin coin}) => "${coin.name}_txCache";
String boxNameSetCache({required Coin coin}) => String boxNameSetCache({required Coin coin}) =>

View file

@ -41,6 +41,7 @@ import 'package:stackwallet/providers/global/trades_service_provider.dart';
import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart';
import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/route_generator.dart';
// import 'package:stackwallet/services/buy/buy_data_loading_service.dart';
import 'package:stackwallet/services/debug_service.dart'; import 'package:stackwallet/services/debug_service.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.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/services/exchange/exchange_data_loading_service.dart';
@ -292,10 +293,14 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
// unawaited(_nodeService.updateCommunityNodes()); // unawaited(_nodeService.updateCommunityNodes());
// run without awaiting // run without awaiting
if (Constants.enableExchange && if (ref.read(prefsChangeNotifierProvider).externalCalls &&
ref.read(prefsChangeNotifierProvider).externalCalls &&
await ref.read(prefsChangeNotifierProvider).isExternalCallsSet()) { await ref.read(prefsChangeNotifierProvider).isExternalCallsSet()) {
unawaited(ExchangeDataLoadingService().loadAll(ref)); if (Constants.enableExchange) {
unawaited(ExchangeDataLoadingService().loadAll(ref));
}
// if (Constants.enableBuy) {
// unawaited(BuyDataLoadingService().loadAll(ref));
// }
} }
if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) { if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) {
@ -312,6 +317,11 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
break; break;
} }
} }
// ref
// .read(prefsChangeNotifierProvider)
// .userID; // Just reading the ref should set it if it's not already set
// We shouldn't need to do this, instead only generating an ID when (or if) the userID is looked up when creating a quote
} catch (e, s) { } catch (e, s) {
Logger.print("$e $s", normalLength: false); Logger.print("$e $s", normalLength: false);
} }

View file

@ -0,0 +1,25 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stackwallet/services/buy/buy.dart';
class BuyFormState extends ChangeNotifier {
Buy? _buy;
Buy? get buy => _buy;
set buy(Buy? value) {
_buy = value;
// _onBuyTypeChanged();
}
bool reversed = false;
Future<void> updateEstimate({
required bool shouldNotifyListeners,
required bool reversed,
}) async {
// TODO implement updating estimaate based on changed selected crypto, fiat, etc
}
Future<void> swap({dynamic? market}) async {
// TODO implement swapping values on FiatOrCrypto toggle (or whatever it's called)
}
}

View file

@ -0,0 +1,61 @@
class Crypto {
/// Crypto ticker
final String ticker;
/// Crypto name
final String name;
/// Crypto network
final String? network;
/// Crypto contract address
final String? contractAddress;
Crypto({
required this.ticker,
required this.name,
required this.network,
required this.contractAddress,
});
factory Crypto.fromJson(Map<String, dynamic> json) {
try {
return Crypto(
ticker: "${json['ticker']}",
name: "${json['name']}",
network: "${json['network']}",
contractAddress: "${json['contractAddress']}",
);
} catch (e) {
rethrow;
}
}
Map<String, dynamic> toJson() {
final map = {
"ticker": ticker,
"name": name,
"network": network,
"contractAddress": contractAddress,
};
return map;
}
Crypto copyWith({
String? ticker,
String? name,
}) {
return Crypto(
ticker: ticker ?? this.ticker,
name: name ?? this.name,
network: network ?? this.network,
contractAddress: contractAddress ?? this.contractAddress,
);
}
@override
String toString() {
return "Crypto: ${toJson()}";
}
}

View file

@ -0,0 +1,64 @@
import 'package:decimal/decimal.dart';
class Fiat {
/// Fiat ticker
final String ticker;
/// Fiat name
final String name;
/// Fiat name
final Decimal min_amount;
/// Fiat name
final Decimal max_amount;
Fiat(
{required this.ticker,
required this.name,
required this.min_amount,
required this.max_amount});
factory Fiat.fromJson(Map<String, dynamic> json) {
try {
return Fiat(
ticker: "${json['ticker']}",
name: "${json['name']}", // TODO nameFromTicker
min_amount: Decimal.parse("${json['min_amount'] ?? 0}"),
max_amount: Decimal.parse("${json['max_amount'] ?? 0}"),
);
} catch (e) {
rethrow;
}
}
Map<String, dynamic> toJson() {
final map = {
"ticker": ticker,
"name": name,
"min_amount": min_amount,
"max_amount": max_amount,
};
return map;
}
Fiat copyWith({
String? ticker,
String? name,
Decimal? min_amount,
Decimal? max_amount,
}) {
return Fiat(
ticker: ticker ?? this.ticker,
name: name ?? this.name,
min_amount: min_amount ?? this.min_amount,
max_amount: max_amount ?? this.max_amount,
);
}
@override
String toString() {
return "Fiat: ${toJson()}";
}
}

View file

@ -0,0 +1,17 @@
import 'package:stackwallet/models/buy/response_objects/quote.dart';
class SimplexOrder {
final SimplexQuote quote;
late final String paymentId;
late final String orderId;
late final String userId;
// TODO remove after userIds are sourced from isar/storage
SimplexOrder({
required this.quote,
required this.paymentId,
required this.orderId,
required this.userId,
});
}

View file

@ -0,0 +1,73 @@
import 'dart:ui';
import 'package:stackwallet/utilities/logger.dart';
class Pair {
final String from;
final String fromNetwork;
final String to;
final String toNetwork;
final bool fixedRate;
final bool floatingRate;
Pair({
required this.from,
required this.fromNetwork,
required this.to,
required this.toNetwork,
required this.fixedRate,
required this.floatingRate,
});
factory Pair.fromMap(Map<String, dynamic> map) {
try {
return Pair(
from: map["from"] as String,
fromNetwork: map["fromNetwork"] as String,
to: map["to"] as String,
toNetwork: map["toNetwork"] as String,
fixedRate: map["fixedRate"] as bool,
floatingRate: map["floatingRate"] as bool,
);
} catch (e, s) {
Logging.instance.log("Pair.fromMap(): $e\n$s", level: LogLevel.Error);
rethrow;
}
}
Map<String, dynamic> toMap() {
return {
"from": from,
"fromNetwork": fromNetwork,
"to": to,
"toNetwork": toNetwork,
"fixedRate": fixedRate,
"floatingRate": floatingRate,
};
}
@override
bool operator ==(other) =>
other is Pair &&
from == other.from &&
fromNetwork == other.fromNetwork &&
to == other.to &&
toNetwork == other.toNetwork &&
fixedRate == other.fixedRate &&
floatingRate == other.floatingRate;
@override
int get hashCode => hashValues(
from,
fromNetwork,
to,
toNetwork,
fixedRate,
floatingRate,
);
@override
String toString() => "Pair: ${toMap()}";
}

View file

@ -0,0 +1,26 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/buy/response_objects/crypto.dart';
import 'package:stackwallet/models/buy/response_objects/fiat.dart';
class SimplexQuote {
final Crypto crypto;
final Fiat fiat;
late final Decimal youPayFiatPrice;
late final Decimal youReceiveCryptoAmount;
late final String id;
late final String receivingAddress;
late final bool buyWithFiat;
SimplexQuote({
required this.crypto,
required this.fiat,
required this.youPayFiatPrice,
required this.youReceiveCryptoAmount,
required this.id,
required this.receivingAddress,
required this.buyWithFiat,
});
}

View file

@ -0,0 +1,32 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/buy/response_objects/crypto.dart';
import 'package:stackwallet/models/buy/response_objects/fiat.dart';
import 'package:stackwallet/models/buy/response_objects/quote.dart';
// import 'package:stackwallet/models/buy/response_objects/pair.dart';
class Simplex {
List<Crypto> supportedCryptos = [];
List<Fiat> supportedFiats = [];
SimplexQuote quote = SimplexQuote(
crypto: Crypto.fromJson({'ticker': 'BTC', 'name': 'Bitcoin', 'image': ''}),
fiat: Fiat.fromJson(
{'ticker': 'USD', 'name': 'United States Dollar', 'image': ''}),
youPayFiatPrice: Decimal.parse("100"),
youReceiveCryptoAmount: Decimal.parse("1.0238917"),
id: "someID",
receivingAddress: '',
buyWithFiat: true,
);
void updateSupportedCryptos(List<Crypto> newCryptos) {
supportedCryptos = newCryptos;
}
void updateSupportedFiats(List<Fiat> newFiats) {
supportedFiats = newFiats;
}
void updateQuote(SimplexQuote newQuote) {
quote = newQuote;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/buy/response_objects/order.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/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/rounded_white_container.dart';
class BuyOrderDetailsView extends StatefulWidget {
const BuyOrderDetailsView({
Key? key,
required this.order,
}) : super(key: key);
final SimplexOrder order;
static const String routeName = "/buyOrderDetails";
@override
State<BuyOrderDetailsView> createState() => _BuyOrderDetailsViewState();
}
class _BuyOrderDetailsViewState extends State<BuyOrderDetailsView> {
final isDesktop = Util.isDesktop;
@override
Widget build(BuildContext context) {
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>()!.backgroundAppBar,
leading: const AppBarBackButton(),
title: Text(
"Order details",
style: STextStyles.navBarTitle(context),
),
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(4),
child: child,
),
),
),
),
);
},
),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Simplex order",
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 16,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Purchase ID",
style: STextStyles.label(context),
),
Text(
widget.order.paymentId,
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"User ID",
style: STextStyles.label(context),
),
Text(
widget.order.userId,
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Quote ID",
style: STextStyles.label(context),
),
Text(
widget.order.quote.id,
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Quoted cost",
style: STextStyles.label(context),
),
Text(
"${widget.order.quote.youPayFiatPrice.toStringAsFixed(2)} ${widget.order.quote.fiat.ticker.toUpperCase()}",
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
// RoundedWhiteContainer(
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// "You pay with",
// style: STextStyles.label(context),
// ),
// Text(
// widget.quote.fiat.name,
// style: STextStyles.label(context).copyWith(
// color: Theme.of(context).extension<StackColors>()!.textDark,
// ),
// ),
// ],
// ),
// ),
// const SizedBox(
// height: 8,
// ),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Quoted amount",
style: STextStyles.label(context),
),
Text(
"${widget.order.quote.youReceiveCryptoAmount} ${widget.order.quote.crypto.ticker.toUpperCase()}",
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Receiving ${widget.order.quote.crypto.ticker.toUpperCase()} address",
style: STextStyles.label(context),
),
Text(
"${widget.order.quote.receivingAddress} ",
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Provider",
style: STextStyles.label(context),
),
SizedBox(
width: 64,
height: 32,
child: SvgPicture.asset(
Assets.buy.simplexLogo(context),
),
),
],
),
),
const SizedBox(
height: 24,
),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(
"This information is not saved,\nscreenshot it now for your records",
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
textAlign: TextAlign.center,
),
]),
const Spacer(),
PrimaryButton(
label: "Dismiss",
onPressed: () {
Navigator.of(context, rootNavigator: isDesktop).pop();
},
)
],
),
);
}
}

View file

@ -0,0 +1,233 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart';
import 'package:stackwallet/models/buy/response_objects/quote.dart';
import 'package:stackwallet/pages/buy_view/sub_widgets/buy_warning_popup.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/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/rounded_white_container.dart';
class BuyQuotePreviewView extends StatefulWidget {
const BuyQuotePreviewView({
Key? key,
required this.quote,
}) : super(key: key);
final SimplexQuote quote;
static const String routeName = "/buyQuotePreview";
@override
State<BuyQuotePreviewView> createState() => _BuyQuotePreviewViewState();
}
class _BuyQuotePreviewViewState extends State<BuyQuotePreviewView> {
final isDesktop = Util.isDesktop;
Future<void> _buyWarning() async {
await showDialog<void>(
context: context,
builder: (context) => BuyWarningPopup(
quote: widget.quote,
),
);
}
@override
Widget build(BuildContext context) {
Locale locale = Localizations.localeOf(context);
var format = NumberFormat.simpleCurrency(locale: locale.toString());
// See https://stackoverflow.com/a/67055685
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>()!.backgroundAppBar,
leading: const AppBarBackButton(),
title: Text(
"Preview quote",
style: STextStyles.navBarTitle(context),
),
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(4),
child: child,
),
),
),
),
);
},
),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Buy ${widget.quote.crypto.ticker.toUpperCase()}",
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 16,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"You pay",
style: STextStyles.label(context),
),
Text(
"${format.simpleCurrencySymbol(widget.quote.fiat.ticker.toUpperCase())}${widget.quote.youPayFiatPrice.toStringAsFixed(2)} ${widget.quote.fiat.ticker.toUpperCase()}",
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
// RoundedWhiteContainer(
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// "You pay with",
// style: STextStyles.label(context),
// ),
// Text(
// widget.quote.fiat.name,
// style: STextStyles.label(context).copyWith(
// color: Theme.of(context).extension<StackColors>()!.textDark,
// ),
// ),
// ],
// ),
// ),
// const SizedBox(
// height: 8,
// ),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"You receive",
style: STextStyles.label(context),
),
Text(
"${widget.quote.youReceiveCryptoAmount} ${widget.quote.crypto.ticker.toUpperCase()}",
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Receiving ${widget.quote.crypto.ticker.toUpperCase()} address",
style: STextStyles.label(context),
),
Text(
"${widget.quote.receivingAddress} ",
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Quote ID",
style: STextStyles.label(context),
),
Text(
widget.quote.id,
style: STextStyles.label(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
],
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Provider",
style: STextStyles.label(context),
),
SizedBox(
width: 64,
height: 32,
child: SvgPicture.asset(
Assets.buy.simplexLogo(context),
),
),
],
),
),
const SizedBox(
height: 8,
),
const Spacer(),
PrimaryButton(
label: "Buy",
onPressed: _buyWarning,
)
],
),
);
}
}

View file

@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/pages/buy_view/buy_form.dart';
class BuyView extends StatefulWidget { class BuyView extends StatefulWidget {
const BuyView({Key? key}) : super(key: key); const BuyView({Key? key}) : super(key: key);
static const String routeName = "/stackBuyView";
@override @override
State<BuyView> createState() => _BuyViewState(); State<BuyView> createState() => _BuyViewState();
} }
@ -11,39 +13,16 @@ class BuyView extends StatefulWidget {
class _BuyViewState extends State<BuyView> { class _BuyViewState extends State<BuyView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
//todo: check if print needed debugPrint("BUILD: $runtimeType");
// debugPrint("BUILD: BuyView");
return SafeArea( return const SafeArea(
child: Center( child: Padding(
child: SingleChildScrollView( padding: EdgeInsets.only(
child: Column( left: 16,
children: [ right: 16,
Center( top: 16,
child: Text(
"Coming soon",
style: STextStyles.pageTitleH1(context),
),
),
],
),
), ),
// child: Column( child: BuyForm(),
// children: [
// Container(
// color: Colors.green,
// child: Text("BuyView"),
// ),
// Container(
// color: Colors.green,
// child: Text("BuyView"),
// ),
// Spacer(),
// Container(
// color: Colors.green,
// child: Text("BuyView"),
// ),
// ],
// ),
), ),
); );
} }

View file

@ -0,0 +1,132 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/buy/response_objects/order.dart';
import 'package:stackwallet/models/buy/response_objects/quote.dart';
import 'package:stackwallet/pages/buy_view/buy_order_details.dart';
import 'package:stackwallet/services/buy/buy_response.dart';
import 'package:stackwallet/services/buy/simplex/simplex_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/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';
import 'package:stackwallet/widgets/stack_dialog.dart';
class BuyWarningPopup extends StatelessWidget {
BuyWarningPopup({
Key? key,
required this.quote,
this.order,
}) : super(key: key);
final SimplexQuote quote;
SimplexOrder? order;
Future<BuyResponse<SimplexOrder>> newOrder(SimplexQuote quote) async {
return SimplexAPI.instance.newOrder(quote);
}
Future<BuyResponse<bool>> redirect(SimplexOrder order) async {
return SimplexAPI.instance.redirect(order);
}
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
Future<void> _buyInvoice() async {
await showDialog<void>(
context: context,
// useRootNavigator: isDesktop,
builder: (context) {
return isDesktop
? DesktopDialog(
maxHeight: 700,
maxWidth: 580,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Order details",
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: BuyOrderDetailsView(
order: order as SimplexOrder,
),
),
),
],
),
),
),
],
),
)
: BuyOrderDetailsView(
order: order as SimplexOrder,
);
});
}
return StackDialog(
title: "Buy ${quote.crypto.ticker}",
message: "This purchase is provided and fulfilled by Simplex by nuvei "
"(a third party). You will be taken to their website. Please follow "
"their instructions.",
leftButton: SecondaryButton(
label: "Cancel",
onPressed: Navigator.of(context, rootNavigator: isDesktop).pop,
),
rightButton: PrimaryButton(
label: "Continue",
onPressed: () async {
BuyResponse<SimplexOrder> order = await newOrder(quote);
await redirect(order.value as SimplexOrder).then((_response) async {
this.order = order.value as SimplexOrder;
Navigator.of(context, rootNavigator: isDesktop).pop();
Navigator.of(context, rootNavigator: isDesktop).pop();
await _buyInvoice();
});
},
),
icon: SizedBox(
width: 64,
height: 32,
child: SvgPicture.asset(
Assets.buy.simplexLogo(context),
),
),
);
}
}

View file

@ -0,0 +1,262 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/buy/response_objects/crypto.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/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/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class CryptoSelectionView extends StatefulWidget {
const CryptoSelectionView({
Key? key,
required this.coins,
}) : super(key: key);
final List<Crypto> coins;
@override
State<CryptoSelectionView> createState() => _CryptoSelectionViewState();
}
class _CryptoSelectionViewState extends State<CryptoSelectionView> {
late TextEditingController _searchController;
final _searchFocusNode = FocusNode();
late final List<Crypto> coins;
late List<Crypto> _coins;
void filter(String text) {
setState(() {
_coins = [
...coins.where((e) =>
e.name.toLowerCase().contains(text.toLowerCase()) ||
e.ticker.toLowerCase().contains(text.toLowerCase()))
];
});
}
@override
void initState() {
_searchController = TextEditingController();
coins = [...widget.coins];
coins.sort(
(a, b) => a.ticker.toLowerCase().compareTo(b.ticker.toLowerCase()));
for (Coin coin in Coin.values.reversed) {
int index = coins.indexWhere((element) =>
element.ticker.toLowerCase() == coin.ticker.toLowerCase());
if (index > 0) {
final currency = coins.removeAt(index);
coins.insert(0, currency);
}
}
_coins = [...coins];
super.initState();
}
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext 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 {
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 buy",
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(
autofocus: isDesktop,
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,
),
),
),
const SizedBox(
height: 10,
),
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: _coins.length,
itemBuilder: (builderContext, index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: GestureDetector(
onTap: () {
Navigator.of(context).pop(_coins[index]);
},
child: RoundedWhiteContainer(
child: Row(
children: [
SizedBox(
width: 24,
height: 24,
child: getIconForTicker(_coins[index].ticker)),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_coins[index].name,
style: STextStyles.largeMedium14(context),
),
const SizedBox(
height: 2,
),
Text(
_coins[index].ticker.toUpperCase(),
style: STextStyles.smallMed12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
],
),
),
),
);
},
),
),
),
],
),
);
}
}
bool isStackCoin(String? ticker) {
if (ticker == null) return false;
try {
coinFromTickerCaseInsensitive(ticker);
return true;
} on ArgumentError catch (_) {
return false;
}
}
Widget? getIconForTicker(String ticker) {
String? iconAsset = isStackCoin(ticker)
? Assets.svg.iconFor(coin: coinFromTickerCaseInsensitive(ticker))
: Assets.svg.buyIconFor(ticker);
return (iconAsset != null)
? SvgPicture.asset(iconAsset, height: 20, width: 20)
: null;
}

View file

@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart';
import 'package:stackwallet/models/buy/response_objects/fiat.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/fiat_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/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FiatSelectionView extends StatefulWidget {
const FiatSelectionView({
Key? key,
required this.fiats,
}) : super(key: key);
final List<Fiat> fiats;
@override
State<FiatSelectionView> createState() => _FiatSelectionViewState();
}
class _FiatSelectionViewState extends State<FiatSelectionView> {
late TextEditingController _searchController;
final _searchFocusNode = FocusNode();
late final List<Fiat> fiats;
late List<Fiat> _fiats;
void filter(String text) {
setState(() {
_fiats = [
...fiats.where((e) =>
e.name.toLowerCase().contains(text.toLowerCase()) ||
e.ticker.toLowerCase().contains(text.toLowerCase()))
];
});
}
@override
void initState() {
_searchController = TextEditingController();
fiats = [...widget.fiats];
fiats.sort(
(a, b) => a.ticker.toLowerCase().compareTo(b.ticker.toLowerCase()));
for (Fiats fiat in Fiats.values.reversed) {
int index = fiats.indexWhere((element) =>
element.ticker.toLowerCase() == fiat.ticker.toLowerCase());
if (index > 0) {
final currency = fiats.removeAt(index);
fiats.insert(0, currency);
}
}
_fiats = [...fiats];
super.initState();
}
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Locale locale = Localizations.localeOf(context);
var format = NumberFormat.simpleCurrency(locale: locale.toString());
// See https://stackoverflow.com/a/67055685
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 currency with which to pay",
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(
autofocus: isDesktop,
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,
),
),
),
const SizedBox(
height: 10,
),
Text(
"All currencies",
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: _fiats.length,
itemBuilder: (builderContext, index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: GestureDetector(
onTap: () {
Navigator.of(context).pop(_fiats[index]);
},
child: RoundedWhiteContainer(
child: Row(
children: [
Container(
padding: const EdgeInsets.all(7.5),
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.highlight,
borderRadius: BorderRadius.circular(4),
),
child: Text(
format.simpleCurrencySymbol(
_fiats[index].ticker.toUpperCase()),
style: STextStyles.subtitle(context).apply(
fontSizeFactor: (1 /
format
.simpleCurrencySymbol(_fiats[index]
.ticker
.toUpperCase())
.length * // Couldn't get pow() working here
format
.simpleCurrencySymbol(_fiats[index]
.ticker
.toUpperCase())
.length)),
textAlign: TextAlign.center,
),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_fiats[index].name,
style: STextStyles.largeMedium14(context),
),
const SizedBox(
height: 2,
),
Text(
_fiats[index].ticker.toUpperCase(),
style: STextStyles.smallMed12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
],
),
),
),
);
},
),
),
),
],
),
);
}
}

View file

@ -61,6 +61,8 @@ class _ConfirmChangeNowSendViewState
late final String routeOnSuccessName; late final String routeOnSuccessName;
late final Trade trade; late final Trade trade;
final isDesktop = Util.isDesktop;
Future<void> _attemptSend(BuildContext context) async { Future<void> _attemptSend(BuildContext context) async {
unawaited( unawaited(
showDialog<void>( showDialog<void>(
@ -227,8 +229,6 @@ class _ConfirmChangeNowSendViewState
final managerProvider = ref.watch(walletsChangeNotifierProvider final managerProvider = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManagerProvider(walletId))); .select((value) => value.getManagerProvider(walletId)));
final isDesktop = Util.isDesktop;
return ConditionalParent( return ConditionalParent(
condition: !isDesktop, condition: !isDesktop,
builder: (child) { builder: (child) {
@ -238,7 +238,7 @@ class _ConfirmChangeNowSendViewState
Theme.of(context).extension<StackColors>()!.background, Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar( appBar: AppBar(
backgroundColor: backgroundColor:
Theme.of(context).extension<StackColors>()!.background, Theme.of(context).extension<StackColors>()!.backgroundAppBar,
leading: AppBarBackButton( leading: AppBarBackButton(
onPressed: () async { onPressed: () async {
// if (FocusScope.of(context).hasFocus) { // if (FocusScope.of(context).hasFocus) {

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/buy_view/buy_view.dart';
import 'package:stackwallet/pages/exchange_view/exchange_loading_overlay.dart'; import 'package:stackwallet/pages/exchange_view/exchange_loading_overlay.dart';
import 'package:stackwallet/pages/exchange_view/exchange_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_view.dart';
import 'package:stackwallet/pages/home_view/sub_widgets/home_view_button_bar.dart'; import 'package:stackwallet/pages/home_view/sub_widgets/home_view_button_bar.dart';
@ -45,6 +46,7 @@ class _HomeViewState extends ConsumerState<HomeView> {
bool _exitEnabled = false; bool _exitEnabled = false;
final _exchangeDataLoadingService = ExchangeDataLoadingService(); final _exchangeDataLoadingService = ExchangeDataLoadingService();
// final _buyDataLoadingService = BuyDataLoadingService();
Future<bool> _onWillPop() async { Future<bool> _onWillPop() async {
// go to home view when tapping back on the main exchange view // go to home view when tapping back on the main exchange view
@ -92,6 +94,26 @@ class _HomeViewState extends ConsumerState<HomeView> {
} }
} }
// void _loadSimplexData() {
// // unawaited future
// if (ref.read(prefsChangeNotifierProvider).externalCalls) {
// _buyDataLoadingService.loadAll(ref);
// } else {
// Logging.instance.log("User does not want to use external calls",
// level: LogLevel.Info);
// }
// }
bool _lock = false;
Future<void> _animateToPage(int index) async {
await _pageController.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.decelerate,
);
}
@override @override
void initState() { void initState() {
_pageController = PageController(); _pageController = PageController();
@ -106,7 +128,14 @@ class _HomeViewState extends ConsumerState<HomeView> {
), ),
], ],
), ),
// const BuyView(), if (Constants.enableBuy)
// Stack(
// children: [
const BuyView(),
// BuyLoadingOverlayView(
// unawaitedLoad: _loadSimplexData,
// ),
// ],
]; ];
ref.read(notificationsProvider).startCheckingWatchedNotifications(); ref.read(notificationsProvider).startCheckingWatchedNotifications();
@ -301,35 +330,31 @@ class _HomeViewState extends ConsumerState<HomeView> {
builder: (_, _ref, __) { builder: (_, _ref, __) {
_ref.listen(homeViewPageIndexStateProvider, _ref.listen(homeViewPageIndexStateProvider,
(previous, next) { (previous, next) {
if (next is int) { if (next is int && next >= 0 && next <= 2) {
if (next == 1) { if (next == 1) {
_exchangeDataLoadingService.loadAll(ref); _exchangeDataLoadingService.loadAll(ref);
} }
if (next >= 0 && next <= 1) { // if (next == 2) {
_pageController.animateToPage( // _buyDataLoadingService.loadAll(ref);
next, // }
duration: const Duration(milliseconds: 300),
curve: Curves.decelerate, _lock = true;
); _animateToPage(next).then((value) => _lock = false);
}
} }
}); });
return PageView( return PageView(
controller: _pageController, controller: _pageController,
children: _children, children: _children,
onPageChanged: (pageIndex) { onPageChanged: (pageIndex) {
ref.read(homeViewPageIndexStateProvider.state).state = if (!_lock) {
pageIndex; ref.read(homeViewPageIndexStateProvider.state).state =
pageIndex;
}
}, },
); );
}, },
), ),
), ),
// Expanded(
// child: HomeStack(
// children: _children,
// ),
// ),
], ],
), ),
), ),

View file

@ -127,37 +127,48 @@ class _HomeViewButtonBarState extends ConsumerState<HomeViewButtonBar> {
), ),
), ),
), ),
// TODO: Do not delete this code. const SizedBox(
// only temporarily disabled width: 8,
// SizedBox( ),
// width: 8, Expanded(
// ), child: TextButton(
// Expanded( style: selectedIndex == 2
// child: TextButton( ? Theme.of(context)
// style: ButtonStyle( .extension<StackColors>()!
// minimumSize: MaterialStateProperty.all<Size>(Size(46, 36)), .getPrimaryEnabledButtonStyle(context)!
// backgroundColor: MaterialStateProperty.all<Color>( .copyWith(
// selectedIndex == 2 minimumSize:
// ? CFColors.stackAccent MaterialStateProperty.all<Size>(const Size(46, 36)),
// : CFColors.disabledButton, )
// ), : Theme.of(context)
// ), .extension<StackColors>()!
// onPressed: () { .getSecondaryEnabledButtonStyle(context)!
// FocusScope.of(context).unfocus(); .copyWith(
// if (selectedIndex != 2) { minimumSize:
// ref.read(homeViewPageIndexStateProvider.state).state = 2; MaterialStateProperty.all<Size>(const Size(46, 36)),
// } ),
// }, onPressed: () async {
// child: Text( FocusScope.of(context).unfocus();
// "Buy", if (selectedIndex != 2) {
// style: STextStyles.button(context).copyWith( ref.read(homeViewPageIndexStateProvider.state).state = 2;
// fontSize: 14, }
// color: // await BuyDataLoadingService().loadAll(ref);
// selectedIndex == 2 ? CFColors.light1 : Theme.of(context).extension<StackColors>()!.accentColorDark },
// ), child: Text(
// ), "Buy",
// ), style: STextStyles.button(context).copyWith(
// ), fontSize: 14,
color: selectedIndex == 2
? Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary
: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
),
),
),
], ],
); );
} }

View file

@ -232,6 +232,8 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> {
if (isEasy) { if (isEasy) {
unawaited(ExchangeDataLoadingService() unawaited(ExchangeDataLoadingService()
.loadAll(ref)); .loadAll(ref));
// unawaited(
// BuyDataLoadingService().loadAll(ref));
ref ref
.read(priceAnd24hChangeNotifierProvider) .read(priceAnd24hChangeNotifierProvider)
.start(true); .start(true);

View file

@ -412,39 +412,47 @@ class _WalletNavigationBarState extends State<WalletNavigationBar> {
const SizedBox( const SizedBox(
width: 12, width: 12,
), ),
// TODO: Do not delete this code. RawMaterialButton(
// only temporarily disabled constraints: const BoxConstraints(
// Spacer( minWidth: 66,
// flex: 2, ),
// ), onPressed: widget.onBuyPressed,
// GestureDetector( splashColor:
// onTap: onBuyPressed, Theme.of(context).extension<StackColors>()!.highlight,
// child: Container( shape: RoundedRectangleBorder(
// color: Colors.transparent, borderRadius: BorderRadius.circular(
// child: Padding( widget.height / 2.0,
// padding: const EdgeInsets.symmetric(vertical: 2.0), ),
// child: Column( ),
// crossAxisAlignment: CrossAxisAlignment.center, child: Container(
// children: [ color: Colors.transparent,
// Spacer(), child: Padding(
// SvgPicture.asset( padding: const EdgeInsets.symmetric(vertical: 2.0),
// Assets.svg.buy, child: Column(
// width: 24, crossAxisAlignment: CrossAxisAlignment.center,
// height: 24, children: [
// ), const Spacer(),
// SizedBox( SvgPicture.asset(
// height: 4, Assets.svg.buyDesktop,
// ), width: 24,
// Text( height: 24,
// "Buy", ),
// style: STextStyles.buttonSmall(context), const SizedBox(
// ), height: 4,
// Spacer(), ),
// ], Text(
// ), "Buy",
// ), style: STextStyles.buttonSmall(context),
// ), ),
// ), const Spacer(),
],
),
),
),
),
const SizedBox(
width: 12,
),
], ],
), ),
), ),

View file

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/pages/buy_view/buy_form.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 DesktopBuyView extends StatefulWidget {
const DesktopBuyView({Key? key}) : super(key: key);
static const String routeName = "/desktopBuyView";
@override
State<DesktopBuyView> createState() => _DesktopBuyViewState();
}
class _DesktopBuyViewState extends State<DesktopBuyView> {
@override
Widget build(BuildContext context) {
return DesktopScaffold(
appBar: DesktopAppBar(
isCompactHeight: true,
leading: Padding(
padding: const EdgeInsets.only(
left: 24,
),
child: Text(
"Buy crypto",
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(
// "Coming soon",
// style: STextStyles.desktopTextExtraExtraSmall(context),
// ),
const SizedBox(
height: 16,
),
const RoundedWhiteContainer(
padding: EdgeInsets.all(24),
child: BuyForm(),
),
],
),
),
const SizedBox(
width: 16,
),
// Expanded(
// child: Row(
// children: const [
// Expanded(
// child: DesktopTradeHistory(),
// ),
// ],
// ),
// ),
],
),
),
);
}
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_buy/desktop_buy_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_menu.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart';
@ -56,6 +57,11 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> {
onGenerateRoute: RouteGenerator.generateRoute, onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: DesktopExchangeView.routeName, initialRoute: DesktopExchangeView.routeName,
), ),
DesktopMenuItemId.buy: const Navigator(
key: Key("desktopBuyHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: DesktopBuyView.routeName,
),
DesktopMenuItemId.notifications: const Navigator( DesktopMenuItemId.notifications: const Navigator(
key: Key("desktopNotificationsHomeKey"), key: Key("desktopNotificationsHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute, onGenerateRoute: RouteGenerator.generateRoute,

View file

@ -12,6 +12,7 @@ import 'package:stackwallet/widgets/desktop/living_stack_icon.dart';
enum DesktopMenuItemId { enum DesktopMenuItemId {
myStack, myStack,
exchange, exchange,
buy,
notifications, notifications,
addressBook, addressBook,
settings, settings,
@ -42,6 +43,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
double _width = expandedWidth; double _width = expandedWidth;
// final _buyDataLoadingService = BuyDataLoadingService();
void updateSelectedMenuItem(DesktopMenuItemId idKey) { void updateSelectedMenuItem(DesktopMenuItemId idKey) {
widget.onSelectionWillChange?.call(idKey); widget.onSelectionWillChange?.call(idKey);
@ -73,6 +76,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
DMIController(), DMIController(),
DMIController(), DMIController(),
DMIController(), DMIController(),
DMIController(),
]; ];
super.initState(); super.initState();
@ -157,13 +161,24 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
const SizedBox( const SizedBox(
height: 2, height: 2,
), ),
DesktopMenuItem(
duration: duration,
icon: const DesktopBuyIcon(),
label: "Buy crypto",
value: DesktopMenuItemId.buy,
onChanged: updateSelectedMenuItem,
controller: controllers[2],
),
const SizedBox(
height: 2,
),
DesktopMenuItem( DesktopMenuItem(
duration: duration, duration: duration,
icon: const DesktopNotificationsIcon(), icon: const DesktopNotificationsIcon(),
label: "Notifications", label: "Notifications",
value: DesktopMenuItemId.notifications, value: DesktopMenuItemId.notifications,
onChanged: updateSelectedMenuItem, onChanged: updateSelectedMenuItem,
controller: controllers[2], controller: controllers[3],
), ),
const SizedBox( const SizedBox(
height: 2, height: 2,
@ -174,7 +189,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
label: "Address Book", label: "Address Book",
value: DesktopMenuItemId.addressBook, value: DesktopMenuItemId.addressBook,
onChanged: updateSelectedMenuItem, onChanged: updateSelectedMenuItem,
controller: controllers[3], controller: controllers[4],
), ),
const SizedBox( const SizedBox(
height: 2, height: 2,
@ -185,7 +200,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
label: "Settings", label: "Settings",
value: DesktopMenuItemId.settings, value: DesktopMenuItemId.settings,
onChanged: updateSelectedMenuItem, onChanged: updateSelectedMenuItem,
controller: controllers[4], controller: controllers[5],
), ),
const SizedBox( const SizedBox(
height: 2, height: 2,
@ -196,7 +211,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
label: "Support", label: "Support",
value: DesktopMenuItemId.support, value: DesktopMenuItemId.support,
onChanged: updateSelectedMenuItem, onChanged: updateSelectedMenuItem,
controller: controllers[5], controller: controllers[6],
), ),
const SizedBox( const SizedBox(
height: 2, height: 2,
@ -207,7 +222,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
label: "About", label: "About",
value: DesktopMenuItemId.about, value: DesktopMenuItemId.about,
onChanged: updateSelectedMenuItem, onChanged: updateSelectedMenuItem,
controller: controllers[6], controller: controllers[7],
), ),
const Spacer(), const Spacer(),
DesktopMenuItem( DesktopMenuItem(
@ -221,7 +236,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
// exit(0); // exit(0);
SystemNavigator.pop(); SystemNavigator.pop();
}, },
controller: controllers[7], controller: controllers[8],
), ),
], ],
), ),

View file

@ -55,6 +55,26 @@ class DesktopExchangeIcon extends ConsumerWidget {
} }
} }
class DesktopBuyIcon extends ConsumerWidget {
const DesktopBuyIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return SvgPicture.asset(
Assets.svg.buyDesktop,
width: 20,
height: 20,
color: DesktopMenuItemId.buy ==
ref.watch(currentDesktopMenuItemProvider.state).state
? Theme.of(context).extension<StackColors>()!.accentColorDark
: Theme.of(context)
.extension<StackColors>()!
.accentColorDark
.withOpacity(0.8),
);
}
}
class DesktopNotificationsIcon extends ConsumerWidget { class DesktopNotificationsIcon extends ConsumerWidget {
const DesktopNotificationsIcon({Key? key}) : super(key: key); const DesktopNotificationsIcon({Key? key}) : super(key: key);

View file

@ -6,8 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.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/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart';
@ -26,8 +24,6 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.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/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.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/assets.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -44,8 +40,6 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/hover_text_field.dart'; import 'package:stackwallet/widgets/hover_text_field.dart';
import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_white_container.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 /// [eventBus] should only be set during testing
class DesktopWalletView extends ConsumerStatefulWidget { class DesktopWalletView extends ConsumerStatefulWidget {
@ -70,8 +64,6 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
late final bool _shouldDisableAutoSyncOnLogOut; late final bool _shouldDisableAutoSyncOnLogOut;
final _cnLoadingService = ExchangeDataLoadingService();
Future<void> onBackPressed() async { Future<void> onBackPressed() async {
await _logout(); await _logout();
if (mounted) { if (mounted) {
@ -96,87 +88,6 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
ref.read(managerProvider.notifier).isActiveWallet = false; ref.read(managerProvider.notifier).isActiveWallet = false;
} }
void _loadCNData() {
// unawaited future
if (ref.read(prefsChangeNotifierProvider).externalCalls) {
_cnLoadingService.loadAll(ref,
coin: ref
.read(walletsChangeNotifierProvider)
.getManager(widget.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(widget.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;
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(
widget.walletId,
coin,
_loadCNData,
),
),
);
}
}
}
Future<void> attemptAnonymize() async { Future<void> attemptAnonymize() async {
final managerProvider = ref final managerProvider = ref
.read(walletsChangeNotifierProvider) .read(walletsChangeNotifierProvider)

View file

@ -0,0 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/buy/buy_form_state.dart';
final buyFormStateProvider = ChangeNotifierProvider<BuyFormState>(
(ref) => BuyFormState(),
);

View file

@ -0,0 +1,11 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
enum SimplexLoadStatus {
waiting,
loading,
success,
failed,
}
final simplexLoadStatusStateProvider =
StateProvider<SimplexLoadStatus>((ref) => SimplexLoadStatus.waiting);

View file

@ -0,0 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/buy/simplex/simplex.dart';
final simplexProvider = Provider<Simplex>(
(ref) => Simplex(),
);

View file

@ -1,3 +1,6 @@
export './buy/buy_form_state_provider.dart';
export './buy/simplex_initial_load_status.dart';
export './buy/simplex_provider.dart';
export './exchange/available_changenow_currencies_provider.dart'; export './exchange/available_changenow_currencies_provider.dart';
export './exchange/available_simpleswap_currencies_provider.dart'; export './exchange/available_simpleswap_currencies_provider.dart';
export './exchange/changenow_initial_load_status.dart'; export './exchange/changenow_initial_load_status.dart';

View file

@ -2,6 +2,7 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/buy/response_objects/quote.dart';
import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/contact_address_entry.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart';
@ -21,6 +22,7 @@ import 'package:stackwallet/pages/address_book_views/subviews/address_book_filte
import 'package:stackwallet/pages/address_book_views/subviews/contact_details_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/contact_details_view.dart';
import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_address_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_address_view.dart';
import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart';
import 'package:stackwallet/pages/buy_view/buy_quote_preview.dart';
import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart';
import 'package:stackwallet/pages/exchange_view/edit_trade_note_view.dart'; import 'package:stackwallet/pages/exchange_view/edit_trade_note_view.dart';
import 'package:stackwallet/pages/exchange_view/exchange_loading_overlay.dart'; import 'package:stackwallet/pages/exchange_view/exchange_loading_overlay.dart';
@ -87,6 +89,8 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear
import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart';
import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart';
// import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_buys_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_buy/desktop_buy_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
@ -1045,6 +1049,20 @@ class RouteGenerator {
} }
return _routeError("${settings.name} invalid args: ${args.toString()}"); return _routeError("${settings.name} invalid args: ${args.toString()}");
case BuyQuotePreviewView.routeName:
if (args is SimplexQuote) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => BuyQuotePreviewView(
quote: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
// == Desktop specific routes ============================================ // == Desktop specific routes ============================================
case CreatePasswordView.routeName: case CreatePasswordView.routeName:
if (args is bool) { if (args is bool) {
@ -1107,6 +1125,12 @@ class RouteGenerator {
builder: (_) => const DesktopExchangeView(), builder: (_) => const DesktopExchangeView(),
settings: RouteSettings(name: settings.name)); settings: RouteSettings(name: settings.name));
case DesktopBuyView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const DesktopBuyView(),
settings: RouteSettings(name: settings.name));
case DesktopAllTradesView.routeName: case DesktopAllTradesView.routeName:
return getRoute( return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute, shouldUseMaterialRoute: useMaterialPageRoute,

44
lib/services/buy/buy.dart Normal file
View file

@ -0,0 +1,44 @@
abstract class Buy {
String get name;
// Future<BuyResponse<List<Currency>>> getAllCurrencies(bool fixedRate);
//
// Future<BuyResponse<List<Pair>>> getPairsFor(
// String currency,
// bool fixedRate,
// );
//
// Future<BuyResponse<List<Pair>>> getAllPairs(bool fixedRate);
//
// Future<BuyResponse<Trade>> getTrade(String tradeId);
// Future<BuyResponse<Trade>> updateTrade(Trade trade);
//
// Future<BuyResponse<List<Trade>>> getTrades();
//
// Future<BuyResponse<Range>> getRange(
// String from,
// String to,
// bool fixedRate,
// );
//
// Future<BuyResponse<Estimate>> getEstimate(
// String from,
// String to,
// Decimal amount,
// bool fixedRate,
// bool reversed,
// );
//
// Future<BuyResponse<Trade>> createTrade({
// required String from,
// required String to,
// required bool fixedRate,
// required Decimal amount,
// required String addressTo,
// String? extraId,
// required String addressRefund,
// required String refundExtraId,
// String? rateId,
// required bool reversed,
// });
}

View file

@ -0,0 +1,62 @@
// import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:stackwallet/providers/providers.dart';
// import 'package:stackwallet/services/buy/simplex/simplex_api.dart';
// import 'package:stackwallet/utilities/logger.dart';
//
// class BuyDataLoadingService {
// Future<void> loadAll(WidgetRef ref) async {
// try {
// await Future.wait([
// _loadSimplexCurrencies(ref),
// ]);
// } catch (e, s) {
// Logging.instance.log("BuyDataLoadingService.loadAll failed: $e\n$s",
// level: LogLevel.Error);
// }
// }
//
// Future<void> _loadSimplexCurrencies(WidgetRef ref) async {
// bool error = false;
// // if (ref.read(simplexLoadStatusStateProvider.state).state ==
// // SimplexLoadStatus.loading) {
// // // already in progress so just
// // return;
// // }
//
// ref.read(simplexLoadStatusStateProvider.state).state =
// SimplexLoadStatus.loading;
//
// final response = await SimplexAPI.instance.getSupported();
//
// if (response.value != null) {
// ref
// .read(supportedSimplexCurrenciesProvider)
// .updateSupportedCryptos(response.value!.item1);
// } else {
// error = true;
// Logging.instance.log(
// "_loadSimplexCurrencies: $response",
// level: LogLevel.Warning,
// );
// }
//
// if (response.value != null) {
// ref
// .read(supportedSimplexCurrenciesProvider)
// .updateSupportedFiats(response.value!.item2);
// } else {
// error = true;
// Logging.instance.log(
// "_loadSimplexCurrencies: $response",
// level: LogLevel.Warning,
// );
// }
//
// if (error) {
// // _loadSimplexCurrencies() again?
// } else {
// ref.read(simplexLoadStatusStateProvider.state).state =
// SimplexLoadStatus.success;
// }
// }
// }

View file

@ -0,0 +1,24 @@
enum BuyExceptionType { generic, serializeResponseError }
class BuyException implements Exception {
String errorMessage;
BuyExceptionType type;
BuyException(this.errorMessage, this.type);
@override
String toString() {
return errorMessage;
}
}
class BuyResponse<T> {
final T? value;
final BuyException? exception;
BuyResponse({this.value, this.exception});
@override
String toString() {
return "{error: $exception, value: $value}";
}
}

View file

@ -0,0 +1,332 @@
// TODO use _buildUri
import 'dart:async';
import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/models/buy/response_objects/crypto.dart';
import 'package:stackwallet/models/buy/response_objects/fiat.dart';
import 'package:stackwallet/models/buy/response_objects/order.dart';
import 'package:stackwallet/models/buy/response_objects/quote.dart';
import 'package:stackwallet/services/buy/buy_response.dart';
import 'package:stackwallet/utilities/enums/fiat_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:url_launcher/url_launcher.dart';
class SimplexAPI {
static const String authority = "sandbox-api.stackwallet.com";
// static const String authority = "localhost";
static const String scheme = authority == "localhost" ? "http" : "https";
final _prefs = Prefs.instance;
SimplexAPI._();
static final SimplexAPI _instance = SimplexAPI._();
static SimplexAPI get instance => _instance;
/// set this to override using standard http client. Useful for testing
http.Client? client;
Uri _buildUri(String path, Map<String, String>? params) {
if (scheme == "http") {
return Uri.http(authority, path, params);
}
return Uri.https(authority, path, params);
}
Future<BuyResponse<List<Crypto>>> getSupportedCryptos() async {
try {
Map<String, String> headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
Map<String, String> data = {
'ROUTE': 'supported_cryptos',
};
Uri url = _buildUri('api.php', data);
var res = await http.post(url, headers: headers);
if (res.statusCode != 200) {
throw Exception(
'getAvailableCurrencies exception: statusCode= ${res.statusCode}');
}
final jsonArray = jsonDecode(res.body); // TODO handle if invalid json
return _parseSupportedCryptos(jsonArray);
} catch (e, s) {
Logging.instance.log("getAvailableCurrencies exception: $e\n$s",
level: LogLevel.Error);
return BuyResponse(
exception: BuyException(
e.toString(),
BuyExceptionType.generic,
),
);
}
}
BuyResponse<List<Crypto>> _parseSupportedCryptos(dynamic jsonArray) {
try {
List<Crypto> cryptos = [];
List<Fiat> fiats = [];
for (final crypto in jsonArray as List) {
// TODO validate jsonArray
cryptos.add(Crypto.fromJson({
'ticker': "${crypto['ticker_symbol']}",
'name': crypto['name'],
'network': "${crypto['network']}",
'contractAddress': "${crypto['contractAddress']}",
'image': "",
}));
}
return BuyResponse(value: cryptos);
} catch (e, s) {
Logging.instance
.log("_parseSupported exception: $e\n$s", level: LogLevel.Error);
return BuyResponse(
exception: BuyException(
e.toString(),
BuyExceptionType.generic,
),
);
}
}
Future<BuyResponse<List<Fiat>>> getSupportedFiats() async {
try {
Map<String, String> headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
Map<String, String> data = {
'ROUTE': 'supported_fiats',
};
Uri url = _buildUri('api.php', data);
var res = await http.post(url, headers: headers);
if (res.statusCode != 200) {
throw Exception(
'getAvailableCurrencies exception: statusCode= ${res.statusCode}');
}
final jsonArray = jsonDecode(res.body); // TODO validate json
return _parseSupportedFiats(jsonArray);
} catch (e, s) {
Logging.instance.log("getAvailableCurrencies exception: $e\n$s",
level: LogLevel.Error);
return BuyResponse(
exception: BuyException(
e.toString(),
BuyExceptionType.generic,
),
);
}
}
BuyResponse<List<Fiat>> _parseSupportedFiats(dynamic jsonArray) {
try {
List<Crypto> cryptos = [];
List<Fiat> fiats = [];
for (final fiat in jsonArray as List) {
if (isSimplexFiat("${fiat['ticker_symbol']}")) {
// TODO validate list
fiats.add(Fiat.fromJson({
'ticker': "${fiat['ticker_symbol']}",
'name': fiatFromTickerCaseInsensitive("${fiat['ticker_symbol']}")
.prettyName,
'min_amount': "${fiat['min_amount']}",
'max_amount': "${fiat['max_amount']}",
'image': "",
}));
} // TODO handle else
}
return BuyResponse(value: fiats);
} catch (e, s) {
Logging.instance
.log("_parseSupported exception: $e\n$s", level: LogLevel.Error);
return BuyResponse(
exception: BuyException(
e.toString(),
BuyExceptionType.generic,
),
);
}
}
Future<BuyResponse<SimplexQuote>> getQuote(SimplexQuote quote) async {
try {
await _prefs.init();
String? userID = _prefs.userID;
Map<String, String> headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
Map<String, String> data = {
'ROUTE': 'quote',
'CRYPTO_TICKER': quote.crypto.ticker.toUpperCase(),
'FIAT_TICKER': quote.fiat.ticker.toUpperCase(),
'REQUESTED_TICKER': quote.buyWithFiat
? quote.fiat.ticker.toUpperCase()
: quote.crypto.ticker.toUpperCase(),
'REQUESTED_AMOUNT': quote.buyWithFiat
? "${quote.youPayFiatPrice}"
: "${quote.youReceiveCryptoAmount}",
};
if (userID != null) {
data['USER_ID'] = userID;
}
Uri url = _buildUri('api.php', data);
var res = await http.get(url, headers: headers);
if (res.statusCode != 200) {
throw Exception('getQuote exception: statusCode= ${res.statusCode}');
}
final jsonArray = jsonDecode(res.body);
jsonArray['quote'] = quote; // Add and pass this on
return _parseQuote(jsonArray);
} catch (e, s) {
Logging.instance.log("getQuote exception: $e\n$s", level: LogLevel.Error);
return BuyResponse(
exception: BuyException(
e.toString(),
BuyExceptionType.generic,
),
);
}
}
BuyResponse<SimplexQuote> _parseQuote(dynamic jsonArray) {
try {
String cryptoAmount = "${jsonArray['digital_money']['amount']}";
SimplexQuote quote = jsonArray['quote'] as SimplexQuote;
final SimplexQuote _quote = SimplexQuote(
crypto: quote.crypto,
fiat: quote.fiat,
youPayFiatPrice: quote.buyWithFiat
? quote.youPayFiatPrice
: Decimal.parse("${jsonArray['fiat_money']['base_amount']}"),
youReceiveCryptoAmount:
Decimal.parse("${jsonArray['digital_money']['amount']}"),
id: jsonArray['quote_id'] as String,
receivingAddress: quote.receivingAddress,
buyWithFiat: quote.buyWithFiat,
);
return BuyResponse(value: _quote);
} catch (e, s) {
Logging.instance
.log("_parseQuote exception: $e\n$s", level: LogLevel.Error);
return BuyResponse(
exception: BuyException(
e.toString(),
BuyExceptionType.generic,
),
);
}
}
Future<BuyResponse<SimplexOrder>> newOrder(SimplexQuote quote) async {
// Calling Simplex's API manually:
// curl --request POST \
// --url https://sandbox.test-simplexcc.com/wallet/merchant/v2/payments/partner/data \
// --header 'Authorization: ApiKey $apiKey' \
// --header 'accept: application/json' \
// --header 'content-type: application/json' \
// -d '{"account_details": {"app_provider_id": "$publicKey", "app_version_id": "123", "app_end_user_id": "01e7a0b9-8dfc-4988-a28d-84a34e5f0a63", "signup_login": {"timestamp": "1994-11-05T08:15:30-05:00", "ip": "207.66.86.226"}}, "transaction_details": {"payment_details": {"quote_id": "3b58f4b4-ed6f-447c-b96a-ffe97d7b6803", "payment_id": "baaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "order_id": "789", "original_http_ref_url": "https://stackwallet.com/simplex", "destination_wallet": {"currency": "BTC", "address": "bc1qjvj9ca8gdsv3g58yrzrk6jycvgnjh9uj35rja2"}}}}'
try {
await _prefs.init();
String? userID = _prefs.userID;
int? signupEpoch = _prefs.signupEpoch;
Map<String, String> headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
Map<String, String> data = {
'ROUTE': 'order',
'QUOTE_ID': quote.id,
'ADDRESS': quote.receivingAddress,
'CRYPTO_TICKER': quote.crypto.ticker.toUpperCase(),
};
if (userID != null) {
data['USER_ID'] = userID;
}
if (signupEpoch != null && signupEpoch != 0) {
DateTime date = DateTime.fromMillisecondsSinceEpoch(signupEpoch * 1000);
data['SIGNUP_TIMESTAMP'] =
date.toIso8601String() + timeZoneFormatter(date.timeZoneOffset);
}
Uri url = _buildUri('api.php', data);
print(data);
var res = await http.get(url, headers: headers);
if (res.statusCode != 200) {
throw Exception('newOrder exception: statusCode= ${res.statusCode}');
}
final jsonArray = jsonDecode(res.body); // TODO check if valid json
SimplexOrder _order = SimplexOrder(
quote: quote,
paymentId: "${jsonArray['paymentId']}",
orderId: "${jsonArray['orderId']}",
userId: "${jsonArray['userId']}",
);
return BuyResponse(value: _order);
} catch (e, s) {
Logging.instance.log("newOrder exception: $e\n$s", level: LogLevel.Error);
return BuyResponse(
exception: BuyException(
e.toString(),
BuyExceptionType.generic,
),
);
}
}
Future<BuyResponse<bool>> redirect(SimplexOrder order) async {
try {
Map<String, String> headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
Map<String, String> data = {
'ROUTE': 'redirect',
'PAYMENT_ID': order.paymentId,
};
Uri url = _buildUri('api.php', data);
bool status = await launchUrl(
url,
mode: LaunchMode.externalApplication,
);
return BuyResponse(value: status);
} catch (e, s) {
Logging.instance.log("newOrder exception: $e\n$s", level: LogLevel.Error);
return BuyResponse(
exception: BuyException(
e.toString(),
BuyExceptionType.generic,
));
}
}
bool isSimplexFiat(String ticker) {
try {
fiatFromTickerCaseInsensitive(ticker);
return true;
} on ArgumentError catch (_) {
return false;
}
}
// See https://github.com/dart-lang/sdk/issues/43391#issuecomment-1229656422
String timeZoneFormatter(Duration offset) =>
"${offset.isNegative ? "-" : "+"}${offset.inHours.abs().toString().padLeft(2, "0")}:${(offset.inMinutes - offset.inHours * 60).abs().toString().padLeft(2, "0")}";
}

View file

@ -9,6 +9,7 @@ abstract class Assets {
static const lottie = _ANIMATIONS(); static const lottie = _ANIMATIONS();
static const socials = _SOCIALS(); static const socials = _SOCIALS();
static const exchange = _EXCHANGE(); static const exchange = _EXCHANGE();
static const buy = _BUY();
} }
class _SOCIALS { class _SOCIALS {
@ -27,6 +28,25 @@ class _EXCHANGE {
String get simpleSwap => "assets/svg/exchange_icons/simpleswap-icon.svg"; String get simpleSwap => "assets/svg/exchange_icons/simpleswap-icon.svg";
} }
class _BUY {
const _BUY();
// TODO: switch this to something like
// String buy(BuildContext context) =>
// "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/buy.svg";
String get buy => "assets/svg/light/buy-coins-icon.svg";
String simplexLogo(BuildContext context) {
return (Theme.of(context).extension<StackColors>()!.themeType ==
ThemeType.dark ||
Theme.of(context).extension<StackColors>()!.themeType ==
ThemeType
.oledBlack) // TODO make sure this cover OLED black, too
? "assets/svg/buy/Simplex-Nuvei-Logo-light.svg"
: "assets/svg/buy/Simplex-Nuvei-Logo.svg";
}
}
class _SVG { class _SVG {
const _SVG(); const _SVG();
String? background(BuildContext context) { String? background(BuildContext context) {
@ -169,6 +189,7 @@ class _SVG {
String get anonymizeFailed => "assets/svg/tx-icon-anonymize-failed.svg"; String get anonymizeFailed => "assets/svg/tx-icon-anonymize-failed.svg";
String get addressBookDesktop => "assets/svg/address-book-desktop.svg"; String get addressBookDesktop => "assets/svg/address-book-desktop.svg";
String get exchangeDesktop => "assets/svg/exchange-desktop.svg"; String get exchangeDesktop => "assets/svg/exchange-desktop.svg";
String get buyDesktop => "assets/svg/light/buy-coins-icon.svg";
String get aboutDesktop => "assets/svg/about-desktop.svg"; String get aboutDesktop => "assets/svg/about-desktop.svg";
String get walletDesktop => "assets/svg/wallet-desktop.svg"; String get walletDesktop => "assets/svg/wallet-desktop.svg";
String get exitDesktop => "assets/svg/exit-desktop.svg"; String get exitDesktop => "assets/svg/exit-desktop.svg";
@ -189,6 +210,17 @@ class _SVG {
String get namecoin => "assets/svg/coin_icons/Namecoin.svg"; String get namecoin => "assets/svg/coin_icons/Namecoin.svg";
String get particl => "assets/svg/coin_icons/Particl.svg"; String get particl => "assets/svg/coin_icons/Particl.svg";
String get cosmos => "assets/svg/coin_icons/Cosmos.svg";
String get binanceusd => "assets/svg/coin_icons/BinanceUSD.svg";
String get dai => "assets/svg/coin_icons/Dai.svg";
String get dash => "assets/svg/coin_icons/Dash.svg";
String get eos => "assets/svg/coin_icons/EOS.svg";
String get ethereum => "assets/svg/coin_icons/Ethereum.svg";
String get tron => "assets/svg/coin_icons/Tron.svg";
String get tether => "assets/svg/coin_icons/Tether.svg";
String get stellar => "assets/svg/coin_icons/Stellar.svg";
String get ripple => "assets/svg/coin_icons/Ripple.svg";
String get chevronRight => "assets/svg/chevron-right.svg"; String get chevronRight => "assets/svg/chevron-right.svg";
String get minimize => "assets/svg/minimize.svg"; String get minimize => "assets/svg/minimize.svg";
String get walletFa => "assets/svg/wallet-fa.svg"; String get walletFa => "assets/svg/wallet-fa.svg";
@ -236,6 +268,33 @@ class _SVG {
return dogecoinTestnet; return dogecoinTestnet;
} }
} }
String? buyIconFor(String ticker) {
switch (ticker.toLowerCase()) {
case 'atom':
return cosmos;
case 'busd':
return binanceusd;
case 'dai':
return dai;
case 'dash':
return dash;
case 'eos':
return eos;
case 'eth':
return ethereum;
case 'trx':
return tron;
case 'usdt':
return tether;
case 'xlm':
return stellar;
case 'xrp':
return ripple;
default:
return null;
}
}
} }
class _PNG { class _PNG {

View file

@ -21,6 +21,7 @@ abstract class Constants {
} }
static bool enableExchange = Util.isDesktop || !Platform.isIOS; static bool enableExchange = Util.isDesktop || !Platform.isIOS;
static bool enableBuy = true; // true for development, TODO change to "Util.isDesktop || !Platform.isIOS;" as above or even just = enableExchange
//TODO: correct for monero? //TODO: correct for monero?
static const int _satsPerCoinMonero = 1000000000000; static const int _satsPerCoinMonero = 1000000000000;

View file

@ -337,8 +337,6 @@ Coin coinFromTickerCaseInsensitive(String ticker) {
return Coin.particl; return Coin.particl;
case "tltc": case "tltc":
return Coin.litecoinTestNet; return Coin.litecoinTestNet;
case "part":
return Coin.particl;
case "tbtc": case "tbtc":
return Coin.bitcoinTestNet; return Coin.bitcoinTestNet;
case "tbch": case "tbch":

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/enums/languages_enum.dart'; import 'package:stackwallet/utilities/enums/languages_enum.dart';
import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
import 'package:uuid/uuid.dart';
class Prefs extends ChangeNotifier { class Prefs extends ChangeNotifier {
Prefs._(); Prefs._();
@ -38,6 +39,8 @@ class Prefs extends ChangeNotifier {
_startupWalletId = await _getStartupWalletId(); _startupWalletId = await _getStartupWalletId();
_externalCalls = await _getHasExternalCalls(); _externalCalls = await _getHasExternalCalls();
_familiarity = await _getHasFamiliarity(); _familiarity = await _getHasFamiliarity();
_userId = await _getUserId();
_signupEpoch = await _getSignupEpoch();
_initialized = true; _initialized = true;
} }
@ -602,4 +605,45 @@ class Prefs extends ChangeNotifier {
} }
return true; return true;
} }
String? _userId;
String? get userID => _userId;
Future<String?> _getUserId() async {
String? userID = await DB.instance
.get<dynamic>(boxName: DB.boxNamePrefs, key: "userID") as String?;
if (userID == null) {
userID = const Uuid().v4();
await saveUserID(userID);
}
return userID;
}
Future<void> saveUserID(String userId) async {
_userId = userId;
await DB.instance
.put<dynamic>(boxName: DB.boxNamePrefs, key: "userID", value: _userId);
// notifyListeners();
}
int? _signupEpoch;
int? get signupEpoch => _signupEpoch;
Future<int?> _getSignupEpoch() async {
int? signupEpoch = await DB.instance
.get<dynamic>(boxName: DB.boxNamePrefs, key: "signupEpoch") as int?;
if (signupEpoch == null) {
signupEpoch = DateTime.now().millisecondsSinceEpoch ~/
Duration.millisecondsPerSecond;
await saveSignupEpoch(signupEpoch);
}
return signupEpoch;
}
Future<void> saveSignupEpoch(int signupEpoch) async {
_signupEpoch = signupEpoch;
await DB.instance.put<dynamic>(
boxName: DB.boxNamePrefs, key: "signupEpoch", value: _signupEpoch);
// notifyListeners();
}
} }

View file

@ -320,6 +320,17 @@ flutter:
- assets/svg/coin_icons/Wownero.svg - assets/svg/coin_icons/Wownero.svg
- assets/svg/coin_icons/Namecoin.svg - assets/svg/coin_icons/Namecoin.svg
- assets/svg/coin_icons/Particl.svg - assets/svg/coin_icons/Particl.svg
# buy coin icons
- assets/svg/coin_icons/Cosmos.svg
- assets/svg/coin_icons/BinanceUSD.svg
- assets/svg/coin_icons/Dai.svg
- assets/svg/coin_icons/Dash.svg
- assets/svg/coin_icons/EOS.svg
- assets/svg/coin_icons/Ethereum.svg
- assets/svg/coin_icons/Tron.svg
- assets/svg/coin_icons/Tether.svg
- assets/svg/coin_icons/Stellar.svg
- assets/svg/coin_icons/Ripple.svg
# lottie animations # lottie animations
- assets/lottie/test.json - assets/lottie/test.json
- assets/lottie/test2.json - assets/lottie/test2.json
@ -425,6 +436,10 @@ flutter:
- assets/svg/fruitSorbet/buy-coins-icon.svg - assets/svg/fruitSorbet/buy-coins-icon.svg
- assets/svg/fruitSorbet/bg.svg - assets/svg/fruitSorbet/bg.svg
# buy
- assets/svg/buy/Simplex-Nuvei-Logo.svg
- assets/svg/buy/Simplex-Nuvei-Logo-light.svg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware. # https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see # For details regarding adding assets from package dependencies, see