From 4b553a422c6ed0f79bbcf430c528a8d426dbb98b Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:38:32 +0200 Subject: [PATCH 01/52] basic wallet components --- assets/animations/correct.json | 1 + assets/animations/error-warn.json | 1 + assets/animations/error.json | 2074 ++++++++++ assets/animations/loading.json | 1 + assets/animations/nfc-mood.json | 1 + assets/animations/phone-scanning.json | 1 + assets/animations/qr-code.json | 1 + assets/animations/read-nfc-done.json | 3430 +++++++++++++++++ lib/main.dart | 7 + .../components/wallet/sheet_send_receive.dart | 68 + .../wallet/wallet_accounts_card.dart | 184 + .../wallet/wallet_actions_strip.dart | 103 + .../wallet/wallet_friends_strip.dart | 82 + .../routes/nostr/nostr_drawer.dart | 6 +- .../routes/wallet/wallet_dashboard.dart | 35 + .../routes/wallet/wallet_mints.dart | 14 + .../routes/wallet/wallet_navigation.dart | 164 + .../routes/wallet/wallet_qr_scan.dart | 63 + .../routes/wallet/wallet_receive.dart | 14 + pubspec.lock | 8 +- pubspec.yaml | 11 +- 21 files changed, 6254 insertions(+), 15 deletions(-) create mode 100644 assets/animations/correct.json create mode 100644 assets/animations/error-warn.json create mode 100644 assets/animations/error.json create mode 100644 assets/animations/loading.json create mode 100644 assets/animations/nfc-mood.json create mode 100644 assets/animations/phone-scanning.json create mode 100644 assets/animations/qr-code.json create mode 100644 assets/animations/read-nfc-done.json create mode 100644 lib/presentation_layer/components/wallet/sheet_send_receive.dart create mode 100644 lib/presentation_layer/components/wallet/wallet_accounts_card.dart create mode 100644 lib/presentation_layer/components/wallet/wallet_actions_strip.dart create mode 100644 lib/presentation_layer/components/wallet/wallet_friends_strip.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_dashboard.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_mints.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_navigation.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_qr_scan.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_receive.dart diff --git a/assets/animations/correct.json b/assets/animations/correct.json new file mode 100644 index 00000000..742fe7e6 --- /dev/null +++ b/assets/animations/correct.json @@ -0,0 +1 @@ +{"v":"5.6.7","fr":60,"ip":0,"op":180,"w":1200,"h":1200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"check","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[575.25,603.465,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":114,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":130,"s":[105,105,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":136,"s":[95,95,100]},{"t":142,"s":[100,100,100]}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Excite - Skew - Transform","np":8,"mn":"Pseudo/BNCA2506f0b33","ix":1,"en":1,"ef":[{"ty":7,"nm":"Enable","mn":"Pseudo/BNCA2506f0b33-0001","ix":1,"v":{"a":0,"k":1,"ix":1}},{"ty":6,"nm":"Properties","mn":"Pseudo/BNCA2506f0b33-0002","ix":2,"v":0},{"ty":0,"nm":"Overshoot","mn":"Pseudo/BNCA2506f0b33-0003","ix":3,"v":{"a":0,"k":10,"ix":3,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":0,"nm":"Bounce","mn":"Pseudo/BNCA2506f0b33-0004","ix":4,"v":{"a":0,"k":15,"ix":4,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":0,"nm":"Friction","mn":"Pseudo/BNCA2506f0b33-0005","ix":5,"v":{"a":0,"k":40,"ix":5,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":6,"nm":"","mn":"Pseudo/BNCA2506f0b33-0006","ix":6,"v":0}]},{"ty":5,"nm":"Excite - Scale - Transform","np":8,"mn":"Pseudo/BNCA2506f0b33","ix":2,"en":1,"ef":[{"ty":7,"nm":"Enable","mn":"Pseudo/BNCA2506f0b33-0001","ix":1,"v":{"a":0,"k":1,"ix":1}},{"ty":6,"nm":"Properties","mn":"Pseudo/BNCA2506f0b33-0002","ix":2,"v":0},{"ty":0,"nm":"Overshoot","mn":"Pseudo/BNCA2506f0b33-0003","ix":3,"v":{"a":0,"k":10,"ix":3,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":0,"nm":"Bounce","mn":"Pseudo/BNCA2506f0b33-0004","ix":4,"v":{"a":0,"k":15,"ix":4,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":0,"nm":"Friction","mn":"Pseudo/BNCA2506f0b33-0005","ix":5,"v":{"a":0,"k":40,"ix":5,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":6,"nm":"","mn":"Pseudo/BNCA2506f0b33-0006","ix":6,"v":0}]},{"ty":5,"nm":"Excite - Position - Transform","np":8,"mn":"Pseudo/BNCA2506f0b33","ix":3,"en":1,"ef":[{"ty":7,"nm":"Enable","mn":"Pseudo/BNCA2506f0b33-0001","ix":1,"v":{"a":0,"k":1,"ix":1}},{"ty":6,"nm":"Properties","mn":"Pseudo/BNCA2506f0b33-0002","ix":2,"v":0},{"ty":0,"nm":"Overshoot","mn":"Pseudo/BNCA2506f0b33-0003","ix":3,"v":{"a":0,"k":10,"ix":3,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":0,"nm":"Bounce","mn":"Pseudo/BNCA2506f0b33-0004","ix":4,"v":{"a":0,"k":15,"ix":4,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":0,"nm":"Friction","mn":"Pseudo/BNCA2506f0b33-0005","ix":5,"v":{"a":0,"k":40,"ix":5,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":6,"nm":"","mn":"Pseudo/BNCA2506f0b33-0006","ix":6,"v":0}]},{"ty":5,"nm":"Excite - Anchor Point - Transform","np":8,"mn":"Pseudo/BNCA2506f0b33","ix":4,"en":1,"ef":[{"ty":7,"nm":"Enable","mn":"Pseudo/BNCA2506f0b33-0001","ix":1,"v":{"a":0,"k":1,"ix":1}},{"ty":6,"nm":"Properties","mn":"Pseudo/BNCA2506f0b33-0002","ix":2,"v":0},{"ty":0,"nm":"Overshoot","mn":"Pseudo/BNCA2506f0b33-0003","ix":3,"v":{"a":0,"k":10,"ix":3,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":0,"nm":"Bounce","mn":"Pseudo/BNCA2506f0b33-0004","ix":4,"v":{"a":0,"k":15,"ix":4,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":0,"nm":"Friction","mn":"Pseudo/BNCA2506f0b33-0005","ix":5,"v":{"a":0,"k":40,"ix":5,"x":"var $bm_rt;\n$bm_rt = clamp(value, 0, 100);"}},{"ty":6,"nm":"","mn":"Pseudo/BNCA2506f0b33-0006","ix":6,"v":0}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[214,-137],[-68,130],[-164,34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":56,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2,"x":"var $bm_rt;\nvar enable, amp, freq, decay, n, t, v;\ntry {\n $bm_rt = enable = effect('Excite - Position - Transform')('Pseudo/BNCA2506f0b33-0001');\n if (enable == 0) {\n $bm_rt = value;\n } else {\n amp = $bm_div(effect('Excite - Position - Transform')('Pseudo/BNCA2506f0b33-0003'), 2.5);\n freq = $bm_div(effect('Excite - Position - Transform')('Pseudo/BNCA2506f0b33-0004'), 20);\n decay = $bm_div(effect('Excite - Position - Transform')('Pseudo/BNCA2506f0b33-0005'), 20);\n n = 0, 0 < numKeys && (n = nearestKey(time).index, key(n).time > time && n--), t = 0 === n ? 0 : $bm_sub(time, key(n).time), $bm_rt = 0 < n ? (v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10))), $bm_sum(value, $bm_div($bm_mul($bm_mul($bm_div(v, 100), amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))))) : value;\n }\n} catch (err) {\n $bm_rt = value = value;\n}"},"a":{"a":0,"k":[0,0],"ix":1,"x":"var $bm_rt;\nvar enable, amp, freq, decay, n, t, v;\ntry {\n $bm_rt = enable = effect('Excite - Anchor Point - Transform')('Pseudo/BNCA2506f0b33-0001');\n if (enable == 0) {\n $bm_rt = value;\n } else {\n amp = $bm_div(effect('Excite - Anchor Point - Transform')('Pseudo/BNCA2506f0b33-0003'), 2.5);\n freq = $bm_div(effect('Excite - Anchor Point - Transform')('Pseudo/BNCA2506f0b33-0004'), 20);\n decay = $bm_div(effect('Excite - Anchor Point - Transform')('Pseudo/BNCA2506f0b33-0005'), 20);\n n = 0, 0 < numKeys && (n = nearestKey(time).index, key(n).time > time && n--), t = 0 === n ? 0 : $bm_sub(time, key(n).time), $bm_rt = 0 < n ? (v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10))), $bm_sum(value, $bm_div($bm_mul($bm_mul($bm_div(v, 100), amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))))) : value;\n }\n} catch (err) {\n $bm_rt = value = value;\n}"},"s":{"a":0,"k":[99,99],"ix":3,"x":"var $bm_rt;\nvar enable, amp, freq, decay, n, t, v;\ntry {\n $bm_rt = enable = effect('Excite - Scale - Transform')('Pseudo/BNCA2506f0b33-0001');\n if (enable == 0) {\n $bm_rt = value;\n } else {\n amp = $bm_div(effect('Excite - Scale - Transform')('Pseudo/BNCA2506f0b33-0003'), 2.5);\n freq = $bm_div(effect('Excite - Scale - Transform')('Pseudo/BNCA2506f0b33-0004'), 20);\n decay = $bm_div(effect('Excite - Scale - Transform')('Pseudo/BNCA2506f0b33-0005'), 20);\n n = 0, 0 < numKeys && (n = nearestKey(time).index, key(n).time > time && n--), t = 0 === n ? 0 : $bm_sub(time, key(n).time), $bm_rt = 0 < n ? (v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10))), $bm_sum(value, $bm_div($bm_mul($bm_mul($bm_div(v, 100), amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))))) : value;\n }\n} catch (err) {\n $bm_rt = value = value;\n}"},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4,"x":"var $bm_rt;\nvar enable, amp, freq, decay, n, t, v;\ntry {\n $bm_rt = enable = effect('Excite - Skew - Transform')('Pseudo/BNCA2506f0b33-0001');\n if (enable == 0) {\n $bm_rt = value;\n } else {\n amp = $bm_div(effect('Excite - Skew - Transform')('Pseudo/BNCA2506f0b33-0003'), 2.5);\n freq = $bm_div(effect('Excite - Skew - Transform')('Pseudo/BNCA2506f0b33-0004'), 20);\n decay = $bm_div(effect('Excite - Skew - Transform')('Pseudo/BNCA2506f0b33-0005'), 20);\n n = 0, 0 < numKeys && (n = nearestKey(time).index, key(n).time > time && n--), t = 0 === n ? 0 : $bm_sub(time, key(n).time), $bm_rt = 0 < n ? (v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10))), $bm_sum(value, $bm_div($bm_mul($bm_mul($bm_div(v, 100), amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))))) : value;\n }\n} catch (err) {\n $bm_rt = value = value;\n}"},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":42,"s":[100]},{"t":58,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":1800,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"circle - stroke","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[100]},{"t":36,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[617.721,614.58,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[172.277,0],[0,-172.277],[-172.277,0],[0,172.277]],"o":[[-172.277,0],[0,172.277],[172.277,0],[0,-172.277]],"v":[[0,-311.936],[-311.936,0],[0,311.936],[311.936,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.22745098039215686,0.788235294117647,0.40784313725490196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-17.721,-14.58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[130.536,130.536],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[100]},{"t":23,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":2,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":1800,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"circle - bg","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[617.585,614.469,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":19,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":33,"s":[114.99999999999999,114.99999999999999,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":40,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":45,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":114,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":130,"s":[110.00000000000001,110.00000000000001,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":136,"s":[95,95,100]},{"t":142,"s":[105,105,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[172.277,0],[0,-172.277],[-172.277,0],[0,172.277]],"o":[[-172.277,0],[0,172.277],[172.277,0],[0,-172.277]],"v":[[0,-311.936],[-311.936,0],[0,311.936],[311.936,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.22745098039215686,0.788235294117647,0.40784313725490196,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-17.721,-14.58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[130.536,130.536],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"burst","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[612.759,610.498,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[72,72,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[172.277,0],[0,-172.277],[-172.277,0],[0,172.277]],"o":[[-172.277,0],[0,172.277],[172.277,0],[0,-172.277]],"v":[[0,-311.936],[-311.936,0],[0,311.936],[311.936,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.22745098039215686,0.788235294117647,0.40784313725490196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":30,"s":[40]},{"t":68,"s":[0]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":0,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":0,"ix":2}},{"n":"o","nm":"offset","v":{"a":0,"k":0,"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-17.721,-14.58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":26,"s":[167.536,167.536]},{"t":68,"s":[251.536,251.536]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":26,"s":[0]},{"t":34,"s":[100]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"burst 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[612.759,610.498,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[72,72,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[172.277,0],[0,-172.277],[-172.277,0],[0,172.277]],"o":[[-172.277,0],[0,172.277],[172.277,0],[0,-172.277]],"v":[[0,-311.936],[-311.936,0],[0,311.936],[311.936,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.22745098039215686,0.788235294117647,0.40784313725490196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":122,"s":[40]},{"t":170,"s":[0]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":0,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":0,"ix":2}},{"n":"o","nm":"offset","v":{"a":0,"k":0,"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-17.721,-14.58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":118,"s":[167.536,167.536]},{"t":170,"s":[251.536,251.536]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":124,"s":[0]},{"t":132,"s":[100]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/animations/error-warn.json b/assets/animations/error-warn.json new file mode 100644 index 00000000..d5173f13 --- /dev/null +++ b/assets/animations/error-warn.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":80,"w":500,"h":500,"nm":"exclamação animation","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"exclamation","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":7,"s":[258.4,231.36,0],"to":[0,-1.06,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[258.4,225,0],"to":[0,0,0],"ti":[0,-1.06,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":13,"s":[258.4,231.36,0],"to":[0,1.06,0],"ti":[0,1.06,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41,"s":[258.4,231.36,0],"to":[0,-1.06,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":44,"s":[258.4,225,0],"to":[0,0,0],"ti":[0,-1.06,0]},{"t":47,"s":[258.4,231.36,0]}],"ix":2},"a":{"a":0,"k":[16.613,83.692,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":7,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[93,93,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":13,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":41,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":44,"s":[93,93,100]},{"t":47,"s":[90,90,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[9.037,0],[0,0],[0,9.038],[-9.037,0],[0,-9.037]],"o":[[0,0],[-9.037,0],[0,-9.037],[9.037,0],[0,9.038]],"v":[[0,16.363],[0,16.363],[-16.363,0],[0,-16.363],[16.363,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.613,150.771],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.134,0],[0,0],[0,8.134],[0,0],[-8.133,0],[0,-8.134],[0,0]],"o":[[0,0],[-8.133,0],[0,0],[0,-8.134],[8.134,0],[0,0],[0,8.134]],"v":[[0,59.999],[0,59.999],[-14.727,45.271],[-14.727,-45.271],[0,-59.999],[14.727,-45.271],[14.727,45.271]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.613,60.249],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":80,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"stroke circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[258.4,231.36,0],"ix":2},"a":{"a":0,"k":[146,146,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-77.872,0],[0,-77.872],[77.872,0],[0,77.872]],"o":[[77.872,0],[0,77.872],[-77.872,0],[0,-77.872]],"v":[[0,-141],[141,0],[0,141],[-141,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.952999997606,0.340999977261,0.301999978458,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[146,146],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":30,"s":[90,90]},{"t":78,"s":[120,120]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[90]},{"t":79,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"stroke circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[258.4,231.36,0],"ix":2},"a":{"a":0,"k":[146,146,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-77.872,0],[0,-77.872],[77.872,0],[0,77.872]],"o":[[77.872,0],[0,77.872],[-77.872,0],[0,-77.872]],"v":[[0,-141],[141,0],[0,141],[-141,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.952999997606,0.340999977261,0.301999978458,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[146,146],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[90,90]},{"t":48,"s":[120,120]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[90]},{"t":49,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[258.4,231.36,0],"ix":2},"a":{"a":0,"k":[130.82,130.82,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-72.112,0],[0,-72.112],[72.111,0],[0,72.111]],"o":[[72.111,0],[0,72.111],[-72.112,0],[0,-72.112]],"v":[[0,-130.57],[130.57,0],[0,130.57],[-130.57,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.952999997606,0.340999977261,0.301999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[130.82,130.82],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/animations/error.json b/assets/animations/error.json new file mode 100644 index 00000000..c3095b05 --- /dev/null +++ b/assets/animations/error.json @@ -0,0 +1,2074 @@ +{ + "v": "4.6.3", + "fr": 24, + "ip": 0, + "op": 21, + "w": 320, + "h": 320, + "nm": "checklist", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Shape Layer 13", + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 300 }, + "p": { "a": 0, "k": [160, 159.5, 0] }, + "a": { "a": 0, "k": [0, -34, 0] }, + "s": { "a": 0, "k": [100, 100, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667], "y": [1, 1] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p667_1_0p167_0p167", "0p667_1_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 17 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p667_1_0p167_0p167", + "t": 6, + "s": [-8.142, -92.147], + "e": [-7.675, -162.544], + "to": [0.07779947668314, -11.7327470779419], + "ti": [-0.07779947668314, 11.7327470779419] + }, + { "t": 17 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [83.981, 100], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p667_1_0p167_0p167"], + "t": 6, + "s": [20.367], + "e": [6.367] + }, + { "t": 17 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 2", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + }, + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.833, 0.833], "y": [0.833, 0.833] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p833_0p833_0p167_0p167", "0p833_0p833_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 21 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p833_0p833_0p167_0p167", + "t": 6, + "s": [16.585, -99.759], + "e": [28.521, -187.495], + "to": [1.9892578125, -14.6227216720581], + "ti": [-1.9892578125, 14.6227216720581] + }, + { "t": 21 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [97.419, 116], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p833_0p833_0p167_0p167"], + "t": 6, + "s": [14.733], + "e": [8.733] + }, + { "t": 21 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 2, + "mn": "ADBE Vector Group" + } + ], + "ip": 6, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "Shape Layer 12", + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 250 }, + "p": { "a": 0, "k": [160, 159.5, 0] }, + "a": { "a": 0, "k": [0, -34, 0] }, + "s": { "a": 0, "k": [100, 100, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667], "y": [1, 1] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p667_1_0p167_0p167", "0p667_1_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 17 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p667_1_0p167_0p167", + "t": 6, + "s": [-8.142, -92.147], + "e": [-7.675, -162.544], + "to": [0.07779947668314, -11.7327470779419], + "ti": [-0.07779947668314, 11.7327470779419] + }, + { "t": 17 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [83.981, 100], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p667_1_0p167_0p167"], + "t": 6, + "s": [20.367], + "e": [6.367] + }, + { "t": 17 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 2", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + }, + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.833, 0.833], "y": [0.833, 0.833] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p833_0p833_0p167_0p167", "0p833_0p833_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 21 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p833_0p833_0p167_0p167", + "t": 6, + "s": [16.585, -99.759], + "e": [28.521, -187.495], + "to": [1.9892578125, -14.6227216720581], + "ti": [-1.9892578125, 14.6227216720581] + }, + { "t": 21 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [97.419, 116], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p833_0p833_0p167_0p167"], + "t": 6, + "s": [14.733], + "e": [8.733] + }, + { "t": 21 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 2, + "mn": "ADBE Vector Group" + } + ], + "ip": 6, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "Shape Layer 11", + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 200 }, + "p": { "a": 0, "k": [160, 159.5, 0] }, + "a": { "a": 0, "k": [0, -34, 0] }, + "s": { "a": 0, "k": [100, 100, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667], "y": [1, 1] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p667_1_0p167_0p167", "0p667_1_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 17 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p667_1_0p167_0p167", + "t": 6, + "s": [-8.142, -92.147], + "e": [-7.675, -162.544], + "to": [0.07779947668314, -11.7327470779419], + "ti": [-0.07779947668314, 11.7327470779419] + }, + { "t": 17 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [83.981, 100], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p667_1_0p167_0p167"], + "t": 6, + "s": [20.367], + "e": [6.367] + }, + { "t": 17 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 2", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + }, + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.833, 0.833], "y": [0.833, 0.833] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p833_0p833_0p167_0p167", "0p833_0p833_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 21 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p833_0p833_0p167_0p167", + "t": 6, + "s": [16.585, -99.759], + "e": [28.521, -187.495], + "to": [1.9892578125, -14.6227216720581], + "ti": [-1.9892578125, 14.6227216720581] + }, + { "t": 21 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [97.419, 116], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p833_0p833_0p167_0p167"], + "t": 6, + "s": [14.733], + "e": [8.733] + }, + { "t": 21 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 2, + "mn": "ADBE Vector Group" + } + ], + "ip": 6, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "Shape Layer 10", + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 150 }, + "p": { "a": 0, "k": [160, 159.5, 0] }, + "a": { "a": 0, "k": [0, -34, 0] }, + "s": { "a": 0, "k": [100, 100, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667], "y": [1, 1] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p667_1_0p167_0p167", "0p667_1_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 17 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p667_1_0p167_0p167", + "t": 6, + "s": [-8.142, -92.147], + "e": [-7.675, -162.544], + "to": [0.07779947668314, -11.7327470779419], + "ti": [-0.07779947668314, 11.7327470779419] + }, + { "t": 17 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [83.981, 100], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p667_1_0p167_0p167"], + "t": 6, + "s": [20.367], + "e": [6.367] + }, + { "t": 17 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 2", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + }, + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.833, 0.833], "y": [0.833, 0.833] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p833_0p833_0p167_0p167", "0p833_0p833_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 21 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p833_0p833_0p167_0p167", + "t": 6, + "s": [16.585, -99.759], + "e": [28.521, -187.495], + "to": [1.9892578125, -14.6227216720581], + "ti": [-1.9892578125, 14.6227216720581] + }, + { "t": 21 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [97.419, 116], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p833_0p833_0p167_0p167"], + "t": 6, + "s": [14.733], + "e": [8.733] + }, + { "t": 21 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 2, + "mn": "ADBE Vector Group" + } + ], + "ip": 6, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "Shape Layer 9", + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 100 }, + "p": { "a": 0, "k": [160, 159.5, 0] }, + "a": { "a": 0, "k": [0, -34, 0] }, + "s": { "a": 0, "k": [100, 100, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667], "y": [1, 1] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p667_1_0p167_0p167", "0p667_1_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 17 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p667_1_0p167_0p167", + "t": 6, + "s": [-8.142, -92.147], + "e": [-7.675, -162.544], + "to": [0.07779947668314, -11.7327470779419], + "ti": [-0.07779947668314, 11.7327470779419] + }, + { "t": 17 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [83.981, 100], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p667_1_0p167_0p167"], + "t": 6, + "s": [20.367], + "e": [6.367] + }, + { "t": 17 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 2", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + }, + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.833, 0.833], "y": [0.833, 0.833] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p833_0p833_0p167_0p167", "0p833_0p833_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 21 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p833_0p833_0p167_0p167", + "t": 6, + "s": [16.585, -99.759], + "e": [28.521, -187.495], + "to": [1.9892578125, -14.6227216720581], + "ti": [-1.9892578125, 14.6227216720581] + }, + { "t": 21 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [97.419, 116], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p833_0p833_0p167_0p167"], + "t": 6, + "s": [14.733], + "e": [8.733] + }, + { "t": 21 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 2, + "mn": "ADBE Vector Group" + } + ], + "ip": 6, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 7, + "ty": 4, + "nm": "Shape Layer 8", + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 50 }, + "p": { "a": 0, "k": [160, 159.5, 0] }, + "a": { "a": 0, "k": [0, -34, 0] }, + "s": { "a": 0, "k": [100, 100, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667], "y": [1, 1] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p667_1_0p167_0p167", "0p667_1_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 17 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p667_1_0p167_0p167", + "t": 6, + "s": [-8.142, -92.147], + "e": [-7.675, -162.544], + "to": [0.07779947668314, -11.7327470779419], + "ti": [-0.07779947668314, 11.7327470779419] + }, + { "t": 17 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [83.981, 100], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p667_1_0p167_0p167"], + "t": 6, + "s": [20.367], + "e": [6.367] + }, + { "t": 17 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 2", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + }, + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.833, 0.833], "y": [0.833, 0.833] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p833_0p833_0p167_0p167", "0p833_0p833_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 21 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p833_0p833_0p167_0p167", + "t": 6, + "s": [16.585, -99.759], + "e": [28.521, -187.495], + "to": [1.9892578125, -14.6227216720581], + "ti": [-1.9892578125, 14.6227216720581] + }, + { "t": 21 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [97.419, 116], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p833_0p833_0p167_0p167"], + "t": 6, + "s": [14.733], + "e": [8.733] + }, + { "t": 21 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 2, + "mn": "ADBE Vector Group" + } + ], + "ip": 6, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 8, + "ty": 4, + "nm": "Shape Layer 7", + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [160, 159.5, 0] }, + "a": { "a": 0, "k": [0, -34, 0] }, + "s": { "a": 0, "k": [100, 100, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667], "y": [1, 1] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p667_1_0p167_0p167", "0p667_1_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 17 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p667_1_0p167_0p167", + "t": 6, + "s": [-8.142, -92.147], + "e": [-7.675, -162.544], + "to": [0.07779947668314, -11.7327470779419], + "ti": [-0.07779947668314, 11.7327470779419] + }, + { "t": 17 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [83.981, 100], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p667_1_0p167_0p167"], + "t": 6, + "s": [20.367], + "e": [6.367] + }, + { "t": 17 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 2", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + }, + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.833, 0.833], "y": [0.833, 0.833] }, + "o": { "x": [0.167, 0.167], "y": [0.167, 0.167] }, + "n": ["0p833_0p833_0p167_0p167", "0p833_0p833_0p167_0p167"], + "t": 6, + "s": [15.021, 15.021], + "e": [0, 0] + }, + { "t": 21 } + ] + }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 17 } + ] + }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "n": "0p833_0p833_0p167_0p167", + "t": 6, + "s": [16.585, -99.759], + "e": [28.521, -187.495], + "to": [1.9892578125, -14.6227216720581], + "ti": [-1.9892578125, 14.6227216720581] + }, + { "t": 21 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [97.419, 116], "ix": 3 }, + "r": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "n": ["0p833_0p833_0p167_0p167"], + "t": 6, + "s": [14.733], + "e": [8.733] + }, + { "t": 21 } + ], + "ix": 6 + }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 2, + "mn": "ADBE Vector Group" + } + ], + "ip": 6, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 9, + "ty": 4, + "nm": "Shape Layer 5", + "parent": 11, + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [0.8, -0.641, 0] }, + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [7.39, 7.39, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [-6.977, 7], + [6.973, -7] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [-6.977, 7], + [6.973, -7] + ], + "c": false + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [0, 0], + "ix": 2 + }, + "a": { + "a": 0, + "k": [0, 0], + "ix": 1 + }, + "s": { + "a": 0, + "k": [100, 100], + "ix": 3 + }, + "r": { + "a": 0, + "k": -90, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "mm", + "mm": 2, + "nm": "Merge Paths 1", + "mn": "ADBE Vector Filter - Merge", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [1, 1, 1, 1], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 4, + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [0, 0], + "ix": 2 + }, + "a": { + "a": 0, + "k": [0, 0], + "ix": 1 + }, + "s": { + "a": 0, + "k": [200, 200], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Combined Shape", + "np": 4, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 7, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 10, + "ty": 4, + "nm": "Shape Layer 6", + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 4, + "s": [50], + "e": [0] + }, + { "t": 14 } + ] + }, + "r": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [160, 160, 0] }, + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 0.667] }, + "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0.333] }, + "n": ["0p667_1_0p333_0", "0p667_1_0p333_0", "0p667_0p667_0p333_0p333"], + "t": 4, + "s": [100, 100, 100], + "e": [1085, 1085, 100] + }, + { "t": 14 } + ] + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { "a": 0, "k": [19.779, 19.779] }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.8274509, 0.1843137, 0.1843137, 1] }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-0.068, 0.036], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + } + ], + "ip": 4, + "op": 22, + "st": -23, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 11, + "ty": 4, + "nm": "Shape Layer 4", + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 6, + "s": [30], + "e": [100] + }, + { "t": 9 } + ] + }, + "r": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [160.312, 161.188, 0] }, + "a": { "a": 0, "k": [0.812, -0.562, 0] }, + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 0.667] }, + "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0.333] }, + "n": ["0p667_1_0p333_0", "0p667_1_0p333_0", "0p667_0p667_0p333_0p333"], + "t": 6, + "s": [100, 100, 100], + "e": [1087, 1087, 100] + }, + { + "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 0.667] }, + "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0.333] }, + "n": ["0p667_1_0p333_0", "0p667_1_0p333_0", "0p667_0p667_0p333_0p333"], + "t": 11, + "s": [1087, 1087, 100], + "e": [866, 866, 100] + }, + { + "i": { "x": [0.833, 0.833, 0.833], "y": [0.833, 0.833, 0.833] }, + "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0.333] }, + "n": ["0p833_0p833_0p333_0", "0p833_0p833_0p333_0", "0p833_0p833_0p333_0p333"], + "t": 13, + "s": [866, 866, 100], + "e": [878, 878, 100] + }, + { "t": 16 } + ] + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { "a": 0, "k": [10.068, 10.068] }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.8274509, 0.1843137, 0.1843137, 1] }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0.784, -0.716], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + } + ], + "ip": 6, + "op": 22, + "st": -19, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 12, + "ty": 4, + "nm": "Shape Layer 3", + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [161, 160, 0] }, + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 0.667] }, + "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0.333] }, + "n": ["0p667_1_0p333_0", "0p667_1_0p333_0", "0p667_0p667_0p333_0p333"], + "t": 3, + "s": [100, 100, 100], + "e": [224, 224, 100] + }, + { + "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 0.667] }, + "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0.333] }, + "n": ["0p667_1_0p333_0", "0p667_1_0p333_0", "0p667_0p667_0p333_0p333"], + "t": 4, + "s": [224, 224, 100], + "e": [476, 476, 100] + }, + { "t": 8 } + ] + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { "a": 0, "k": [6.009, 6.009] }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "st", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 4, + "s": [0.8274509, 0.1843137, 0.1843137, 1], + "e": [0.8274509, 0.1843137, 0.1843137, 1] + }, + { "t": 8 } + ] + }, + "o": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 4, + "s": [0], + "e": [100] + }, + { "t": 5 } + ] + }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 4, + "s": [3], + "e": [0] + }, + { "t": 8 } + ] + }, + "lc": 1, + "lj": 1, + "ml": 4, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke" + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.8274509, 0.1843137, 0.1843137, 1] }, + "o": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 3, + "s": [100], + "e": [99] + }, + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "n": ["0p667_1_0p333_0"], + "t": 4, + "s": [99], + "e": [0] + }, + { "t": 5 } + ] + }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-0.338, 0.065], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [649.112, 649.112], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 2", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + } + ], + "ip": 3, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + }, + { + "ddd": 0, + "ind": 13, + "ty": 4, + "nm": "Shape Layer 2", + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [160.142, 159.987, 0] }, + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [377.603, 377.603, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { "a": 0, "k": [22.315, 22.315] }, + "p": { "a": 0, "k": [0, 0] }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse" + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.8352941, 0.8352941, 0.8352941, 1] }, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 1 }, + "lc": 1, + "lj": 1, + "ml": 4, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke" + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1, 1] }, + "o": { "a": 0, "k": 100 }, + "r": 1, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill" + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-0.038, 0.003], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "ix": 1, + "mn": "ADBE Vector Group" + } + ], + "ip": -21, + "op": 22, + "st": -21, + "bm": 0, + "sr": 1 + } + ] +} diff --git a/assets/animations/loading.json b/assets/animations/loading.json new file mode 100644 index 00000000..b0e2812b --- /dev/null +++ b/assets/animations/loading.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":30,"ip":0,"op":60,"w":300,"h":300,"nm":"loading_6","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60,"s":[360]}],"ix":10},"p":{"a":0,"k":[150.00000000000003,150.00000000000003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[30.000000000000004,30.000000000000004,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[300,300],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":60,"s":[99]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[1]},{"t":50,"s":[100]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60,"s":[3]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":30,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150.00000000000003,150.00000000000003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[30.000000000000004,30.000000000000004,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[300,300],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.4470588235294118,0.4470588235294118,0.4470588235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/animations/nfc-mood.json b/assets/animations/nfc-mood.json new file mode 100644 index 00000000..82a94a27 --- /dev/null +++ b/assets/animations/nfc-mood.json @@ -0,0 +1 @@ +{"v":"5.4.4","fr":25,"ip":0,"op":137,"w":612,"h":382,"nm":"read_card","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 2","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[274,-75,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":137,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"elips NFC ","parent":1,"sr":1,"ks":{"o":{"a":0,"k":60,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[264.25,201,0],"e":[54,449,0],"to":[-35.042,41.333,0],"ti":[35.042,-41.333,0]},{"t":9}],"ix":2},"a":{"a":0,"k":[360,640,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[32.362,31.264,100],"e":[103.512,100,100]},{"t":9}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-48.877],[69.824,0],[0,48.877],[-69.825,0]],"o":[[0,48.877],[-69.825,0],[0,-48.877],[69.824,0]],"v":[[126.428,0],[0,88.5],[-126.428,0],[0,-88.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[341.4,452.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":200,"op":200,"st":0,"bm":0,"hidden":0},{"ddd":0,"ind":3,"ty":4,"nm":"wave 1","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[60],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":39,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":62,"s":[60],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":65,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":93,"s":[60],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":96,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":108,"s":[60],"e":[60]},{"t":123}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[44,449,0],"e":[54,449,0],"to":[1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[54,449,0],"e":[44,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":65,"s":[54,449,0],"e":[44,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":78,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":96,"s":[54,449,0],"e":[44,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":100,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":108,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"t":126}],"ix":2},"a":{"a":0,"k":[360,640,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-5.693],[2.915,-4.592]],"o":[[3.175,4.725],[0,5.439],[0,0]],"v":[[-2.436,-15.672],[2.436,0.312],[-2.028,15.672]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[295.136,453.031],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":9,"op":200,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"wave 2","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":12,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":20,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":32,"s":[60],"e":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":35,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":42,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":50,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":62,"s":[60],"e":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":65,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":73,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":81,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":93,"s":[60],"e":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":96,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":103,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":111,"s":[60],"e":[60]},{"t":123}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":12,"s":[44,449,0],"e":[54,449,0],"to":[1.667,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":20,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":35,"s":[54,449,0],"e":[44,449,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":50,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":65,"s":[54,449,0],"e":[44,449,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":73,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":81,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":96,"s":[54,449,0],"e":[44,449,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":103,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":111,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"t":126}],"ix":2},"a":{"a":0,"k":[360,640,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-10.142],[5.103,-8.255]],"o":[[5.542,8.493],[0,9.705],[0,0]],"v":[[-4.247,-28.018],[4.247,0.543],[-3.56,28.018]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[316.924,452.801],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":200,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"wave 3 ","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":32,"s":[60],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":35,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":45,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":53,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":62,"s":[60],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":65,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":76,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":93,"s":[60],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":96,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":106,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":114,"s":[60],"e":[60]},{"t":123}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[44,449,0],"e":[54,449,0],"to":[1.667,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":23,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[54,449,0],"e":[44,449,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":53,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":65,"s":[54,449,0],"e":[44,449,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":76,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":84,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":96,"s":[54,449,0],"e":[44,449,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":106,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":114,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"t":126}],"ix":2},"a":{"a":0,"k":[360,640,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-15.048],[7.571,-12.25]],"o":[[8.225,12.602],[0,14.401],[0,0]],"v":[[-6.302,-41.575],[6.302,0.804],[-5.281,41.575]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[340.155,452.539],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":15,"op":200,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"wave 4","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":18,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":26,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":32,"s":[60],"e":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":35,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":48,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":56,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":62,"s":[60],"e":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":65,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":79,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":87,"s":[60],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":93,"s":[60],"e":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":96,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":109,"s":[0],"e":[60]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":117,"s":[60],"e":[60]},{"t":123}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[44,449,0],"e":[54,449,0],"to":[1.667,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":26,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":35,"s":[54,449,0],"e":[44,449,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":48,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":56,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":65,"s":[54,449,0],"e":[44,449,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":79,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":87,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":96,"s":[54,449,0],"e":[44,449,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":109,"s":[44,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":117,"s":[54,449,0],"e":[54,449,0],"to":[0,0,0],"ti":[0,0,0]},{"t":126}],"ix":2},"a":{"a":0,"k":[360,640,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-20.283],[10.205,-16.511]],"o":[[11.085,16.986],[0,19.41],[0,0]],"v":[[-8.495,-56.036],[8.494,1.083],[-7.118,56.036]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[364.934,452.259],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":18,"op":200,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/animations/phone-scanning.json b/assets/animations/phone-scanning.json new file mode 100644 index 00000000..bc053a3b --- /dev/null +++ b/assets/animations/phone-scanning.json @@ -0,0 +1 @@ +{"v":"5.6.3","fr":60,"ip":0,"op":120,"w":800,"h":600,"nm":"2-white-800x600","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"phone-speaker","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.655,-23,0],"ix":2},"a":{"a":0,"k":[0.655,-22.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.34,"y":1},"o":{"x":0.884,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-20.621,-23.25],[21.93,-23.25]],"c":false}]},{"i":{"x":0.34,"y":1},"o":{"x":0.884,"y":0},"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-14.496,-0.5],[15.805,-0.5]],"c":false}]},{"t":120,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-20.621,-23.25],[21.93,-23.25]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.884],"y":[0]},"t":0,"s":[6]},{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.884],"y":[0]},"t":60,"s":[4]},{"t":120,"s":[6]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"phone-screen","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-9.598,0],"ix":2},"a":{"a":0,"k":[0,-9.598,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.34,"y":1},"o":{"x":0.884,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-66.235,-9.598],[66.236,-9.598]],"c":false}]},{"i":{"x":0.34,"y":1},"o":{"x":0.884,"y":0},"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.86,12.277],[61.861,12.277]],"c":false}]},{"t":120,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-66.235,-9.598],[66.236,-9.598]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.884],"y":[0]},"t":0,"s":[8]},{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.884],"y":[0]},"t":60,"s":[4]},{"t":120,"s":[8]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-2.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"phone-mask","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[-467.836,-2.947,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-82.843,0],[0,-82.843],[82.843,0],[0,82.843]],"o":[[82.843,0],[0,82.843],[-82.843,0],[0,-82.843]],"v":[[-467.836,-152.947],[-317.836,-2.947],[-467.836,147.053],[-617.836,-2.947]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"phone-outline","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,414,0],"ix":2},"a":{"a":0,"k":[0,214,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[-82.843,0],[0,-82.843],[82.843,0],[0,82.843]],"o":[[82.843,0],[0,82.843],[-82.843,0],[0,-82.843]],"v":[[0.354,-150.342],[150.354,-0.342],[0.354,149.658],[-149.646,-0.342]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.34,"y":1},"o":{"x":0.884,"y":0},"t":0,"s":[{"i":[[8.455,0],[0,0],[0,8.455],[0,0],[-8.455,0],[0,0],[0,-8.455],[0,0]],"o":[[0,0],[-8.455,0],[0,0],[0,-8.455],[0,0],[8.455,0],[0,0],[0,8.455]],"v":[[53.691,210],[-53.691,210],[-69,194.691],[-69,-20.691],[-53.691,-36],[53.691,-36],[69,-20.691],[69,194.691]],"c":true}]},{"i":{"x":0.34,"y":1},"o":{"x":0.884,"y":0},"t":60,"s":[{"i":[[8.455,0],[0,0],[0,8.455],[0,0],[-8.455,0],[0,0],[0,-8.455],[0,0]],"o":[[0,0],[-8.455,0],[0,0],[0,-8.455],[0,0],[8.455,0],[0,0],[0,8.455]],"v":[[53.691,210],[-53.691,210],[-69,194.691],[-59,6.121],[-43.691,-9.188],[43.691,-9.188],[59,6.121],[69,194.691]],"c":true}]},{"t":120,"s":[{"i":[[8.455,0],[0,0],[0,8.455],[0,0],[-8.455,0],[0,0],[0,-8.455],[0,0]],"o":[[0,0],[-8.455,0],[0,0],[0,-8.455],[0,0],[8.455,0],[0,0],[0,8.455]],"v":[[53.691,210],[-53.691,210],[-69,194.691],[-69,-20.691],[-53.691,-36],[53.691,-36],[69,-20.691],[69,194.691]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"phone-shine","sr":1,"ks":{"o":{"a":0,"k":32,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[207,286.5,0],"ix":2},"a":{"a":0,"k":[7,86.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[-82.843,0],[0,-82.843],[82.843,0],[0,82.843]],"o":[[82.843,0],[0,82.843],[-82.843,0],[0,-82.843]],"v":[[0.354,-150.342],[150.354,-0.342],[0.354,149.658],[-149.646,-0.342]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.884,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[66,-12],[67,64.5],[68.75,-12.5],[66.164,-12.03]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":41,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-62.682,4.808],[67,138],[64.808,5.91],[-57.847,5.279]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-61.389,30.725],[67,138],[61.751,7.934],[-62.269,6.757]],"c":true}]},{"i":{"x":0.34,"y":1},"o":{"x":0.167,"y":0.167},"t":51,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-60.527,48.003],[67,138],[59.712,9.283],[-60.05,7.584]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.884,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-60.25,53],[67,138],[59,11],[-59.749,9.399]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-60.527,48.003],[67,138],[59.712,9.283],[-60.05,7.584]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":84,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-61.389,30.725],[67,138],[61.751,7.934],[-62.269,6.757]],"c":true}]},{"i":{"x":0.34,"y":1},"o":{"x":0.167,"y":0.167},"t":90,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-62.682,4.808],[67,138],[64.808,5.91],[-57.847,5.279]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[66,-12],[67,138],[68.75,-12.5],[66.164,-12.03]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"outline","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10,"x":"var $bm_rt;\n$bm_rt = transform.rotation;"},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[308,308],"ix":2,"x":"var $bm_rt;\n$bm_rt = content('Group 1').content('Ellipse Path 1').size;"},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.996078431372549,0.8666666666666667,0.7725490196078432,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":120,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"1-white","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[200,200,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":400,"h":400,"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/animations/qr-code.json b/assets/animations/qr-code.json new file mode 100644 index 00000000..991f8a8d --- /dev/null +++ b/assets/animations/qr-code.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.9","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":90,"w":300,"h":300,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 24","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[276.875,278,0],"ix":2},"a":{"a":0,"k":[-130.5,-133,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-130.5,-67],[-130.5,-133],[-64.5,-132.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0]},{"t":50,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-1,"op":90,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 23","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":270,"ix":10},"p":{"a":0,"k":[24.5,276.5,0],"ix":2},"a":{"a":0,"k":[-130.5,-133,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-130.5,-67],[-130.5,-133],[-64.5,-132.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[0]},{"t":45,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-5,"op":91,"st":-5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 22","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[276.875,24,0],"ix":2},"a":{"a":0,"k":[-130.5,-133,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-130.5,-67],[-130.5,-133],[-64.5,-132.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"t":40,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-10,"op":90,"st":-10,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 21","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[24.5,24,0],"ix":2},"a":{"a":0,"k":[-130.5,-133,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-130.5,-67],[-130.5,-133],[-64.5,-132.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"t":35,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-15,"op":90,"st":-15,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 20","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[11,-88],[11,-66.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 19","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-10.75,-66],[-10.5,-104.25],[32,-104.25],[32,-67.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 18","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[70.5,-17.5],[70.5,16],[88.875,16]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-108.75,-11.5],[14.75,-11.5],[15,15.25],[-109.25,15.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":25,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5,"op":95,"st":5,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 16","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":1,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[66.75,-31.75],[104.5,-31.75],[104.5,39.25]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"t":35,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":15,"op":105,"st":15,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-108.75,35.5],[-46.125,35.625]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":25,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5,"op":95,"st":5,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 13","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.5,48.5],[-9.438,93.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":30,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":100,"st":10,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-30,93.5],[-30,36],[-5,36]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[51.75,-36],[51.75,20.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[8.25,35.5],[80,35.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":25,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5,"op":95,"st":5,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-108.75,-33.625],[20,-33.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,2],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[148.75,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[33.75,48],[33.75,109.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"t":35,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":15,"op":105,"st":15,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Shape Layer 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-34,106],[13,106],[13,48.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-31.5,-108.75],[-31.5,-50.875],[34.5,-50.875],[34.5,20.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":30,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":100,"st":10,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[228.125,229.25,0],"ix":2},"a":{"a":0,"k":[-86,-79,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[9.125,9.125],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-86.438,-79.562],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[53,53],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-86,-79],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[228.125,72.25,0],"ix":2},"a":{"a":0,"k":[-86,-79,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[9.125,9.125],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-86.438,-79.562],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[53,53],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-86,-79],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":25,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5,"op":95,"st":5,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[72.125,229.25,0],"ix":2},"a":{"a":0,"k":[-86,-79,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[9.125,9.125],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-86.438,-79.562],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[53,53],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-86,-79],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[72.125,72.25,0],"ix":2},"a":{"a":0,"k":[-86,-79,0],"ix":1},"s":{"a":0,"k":[92,92,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[9.125,9.125],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-86.438,-79.562],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[53,53],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-86,-79],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":25,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim In Path","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5,"op":95,"st":5,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/animations/read-nfc-done.json b/assets/animations/read-nfc-done.json new file mode 100644 index 00000000..6f887f37 --- /dev/null +++ b/assets/animations/read-nfc-done.json @@ -0,0 +1,3430 @@ +{ + "v": "5.5.7", + "meta": { + "g": "LottieFiles AE 0.1.20", + "a": "", + "k": "", + "d": "", + "tc": "none" + }, + "fr": 29.9700012207031, + "ip": 0, + "op": 210.000008553475, + "w": 1080, + "h": 1080, + "nm": "wifi", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "wifi/sdk_elements_02 Outlines 3", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 205, + "s": [ + 100 + ] + }, + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 209, + "s": [ + 0 + ] + }, + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 235, + "s": [ + 100 + ] + }, + { + "t": 239.00000973467, + "s": [ + 0 + ] + } + ], + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 540, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 70.5, + 70.5, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + -38.66 + ], + [ + 38.66, + 0 + ], + [ + 0, + 38.66 + ], + [ + -38.66, + 0 + ] + ], + "o": [ + [ + 0, + 38.66 + ], + [ + -38.66, + 0 + ], + [ + 0, + -38.66 + ], + [ + 38.66, + 0 + ] + ], + "v": [ + [ + 70, + 0 + ], + [ + 0, + 70 + ], + [ + -70, + 0 + ], + [ + 0, + -70 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 0, + "k": 0, + "ix": 1 + }, + "e": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 59, + "s": [ + 0 + ] + }, + { + "t": 120.0000048877, + "s": [ + 100 + ] + } + ], + "ix": 2 + }, + "o": { + "a": 0, + "k": 0, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0, + 0.819607913494, + 0.007843137719, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 4, + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 70.25, + 70.25 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 5", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gs", + "o": { + "a": 0, + "k": 100, + "ix": 9 + }, + "w": { + "a": 0, + "k": 2, + "ix": 10 + }, + "g": { + "p": 2, + "k": { + "a": 0, + "k": [ + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0 + ], + "ix": 8 + } + }, + "s": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 4 + }, + "e": { + "a": 0, + "k": [ + 100, + 0 + ], + "ix": 5 + }, + "t": 1, + "lc": 1, + "lj": 1, + "ml": 4, + "ml2": { + "a": 0, + "k": 4, + "ix": 13 + }, + "bm": 0, + "nm": "Gradient Stroke 1", + "mn": "ADBE Vector Graphic - G-Stroke", + "hd": false + } + ], + "ip": 60.0000024438501, + "op": 210.000008553475, + "st": -31.0000012626559, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "wifi/sdk_elements_02 Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 540, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 70.5, + 70.5, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + -38.66 + ], + [ + 38.66, + 0 + ], + [ + 0, + 38.66 + ], + [ + -38.66, + 0 + ] + ], + "o": [ + [ + 0, + 38.66 + ], + [ + -38.66, + 0 + ], + [ + 0, + -38.66 + ], + [ + 38.66, + 0 + ] + ], + "v": [ + [ + 70, + 0 + ], + [ + 0, + 70 + ], + [ + -70, + 0 + ], + [ + 0, + -70 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.866666734219, + 0.866666734219, + 0.866666734219, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 4, + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 70.25, + 70.25 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 5", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gs", + "o": { + "a": 0, + "k": 100, + "ix": 9 + }, + "w": { + "a": 0, + "k": 2, + "ix": 10 + }, + "g": { + "p": 2, + "k": { + "a": 0, + "k": [ + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0 + ], + "ix": 8 + } + }, + "s": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 4 + }, + "e": { + "a": 0, + "k": [ + 100, + 0 + ], + "ix": 5 + }, + "t": 1, + "lc": 1, + "lj": 1, + "ml": 4, + "ml2": { + "a": 0, + "k": 4, + "ix": 13 + }, + "bm": 0, + "nm": "Gradient Stroke 1", + "mn": "ADBE Vector Graphic - G-Stroke", + "hd": false + } + ], + "ip": 0, + "op": 210.000008553475, + "st": -31.0000012626559, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "done", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 205, + "s": [ + 100 + ] + }, + { + "t": 209.000008512745, + "s": [ + 0 + ] + } + ], + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 549.483, + 573.493, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 33.965, + 33.965, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -87.63, + -28.839 + ], + [ + -9.979, + 47.039 + ], + [ + 156.105, + -125.833 + ] + ], + "c": false + }, + "ix": 2 + }, + "nm": "path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.179 + ], + "y": [ + 1.809 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 134, + "s": [ + 0 + ] + }, + { + "t": 151.000006150356, + "s": [ + 100 + ] + } + ], + "ix": 1 + }, + "e": { + "a": 0, + "k": 0, + "ix": 2 + }, + "o": { + "a": 0, + "k": 0, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "trimpaths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0, + 0.819607843137, + 0.007843137255, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 14, + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "stroke1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + -61.579, + -60.134 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": -0.173, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 120.0000048877, + "op": 210.000008553475, + "st": 120.0000048877, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "circ 2", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 120, + "s": [ + 0 + ] + }, + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 129, + "s": [ + 0 + ] + }, + { + "t": 134.000005457932, + "s": [ + 100 + ] + } + ], + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 543.399, + 533.989, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 47.836, + 47.836, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + -80.36, + 0 + ], + [ + 0, + -80.36 + ], + [ + 80.36, + 0 + ], + [ + 0, + 80.36 + ] + ], + "o": [ + [ + 80.36, + 0 + ], + [ + 0, + 80.36 + ], + [ + -80.36, + 0 + ], + [ + 0, + -80.36 + ] + ], + "v": [ + [ + 0, + -145.504 + ], + [ + 145.504, + 0 + ], + [ + 0, + 145.504 + ], + [ + -145.504, + 0 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 0, + "k": 100, + "ix": 1 + }, + "e": { + "a": 0, + "k": 0, + "ix": 2 + }, + "o": { + "a": 0, + "k": -56, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "trimpaths 2", + "mn": "ADBE Vector Filter - Trim", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + -8.496, + 11.504 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "group 2", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 120.0000048877, + "op": 210.000008553475, + "st": 120.0000048877, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "wifi/sdk_elements_02 Outlines 4", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 120, + "s": [ + 100 + ] + }, + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 125, + "s": [ + 100 + ] + }, + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 129, + "s": [ + 0 + ] + }, + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 205, + "s": [ + 0 + ] + }, + { + "t": 209.000008512745, + "s": [ + 0 + ] + } + ], + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 540, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 70.5, + 70.5, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 1.9, + -0.301 + ], + [ + -0.6, + -2 + ], + [ + -0.6, + -1.099 + ], + [ + 0, + -4 + ], + [ + 1.9, + -3.1 + ], + [ + -0.4, + -1.199 + ], + [ + -1, + -0.4 + ], + [ + -1.8, + 2.799 + ], + [ + 5.1, + 7.901 + ] + ], + "o": [ + [ + -2.3, + 0.399 + ], + [ + 0.1, + 0.5 + ], + [ + 1.9, + 3.101 + ], + [ + 0, + 4 + ], + [ + -1.5, + 2.4 + ], + [ + 0.3, + 1.101 + ], + [ + 2.1, + 0.899 + ], + [ + 5.1, + -7.901 + ], + [ + -1.8, + -2.699 + ] + ], + "v": [ + [ + -3.1, + -16.549 + ], + [ + -6.4, + -11.65 + ], + [ + -5, + -8.85 + ], + [ + -2.7, + -0.15 + ], + [ + -5, + 8.551 + ], + [ + -6.4, + 13.15 + ], + [ + -4, + 15.951 + ], + [ + 1.9, + 13.051 + ], + [ + 1.9, + -13.35 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0, + 0.819999964097, + 0.008000000785, + 1 + ], + "ix": 4 + }, + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 60, + "s": [ + 0 + ] + }, + { + "t": 75.0000030548126, + "s": [ + 100 + ] + } + ], + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 43.278, + 71.784 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 2.2, + 0 + ], + [ + 1, + -0.901 + ], + [ + -2.3, + -4.599 + ], + [ + 5.3, + -10.7 + ], + [ + -0.1, + -0.801 + ], + [ + -1.2, + -0.5 + ], + [ + -0.5, + 0.101 + ], + [ + -1.9, + 3.8 + ], + [ + 0, + 7.199 + ], + [ + 3.1, + 6.301 + ] + ], + "o": [ + [ + -1.3, + 0 + ], + [ + -1.9, + 1.799 + ], + [ + 5.2, + 10.5 + ], + [ + -1.4, + 2.901 + ], + [ + 0.3, + 1.3 + ], + [ + 0.5, + 0.2 + ], + [ + 1.9, + -0.299 + ], + [ + 3.3, + -6.7 + ], + [ + 0, + -7 + ], + [ + -1.9, + -4.099 + ] + ], + "v": [ + [ + -2.5, + -24 + ], + [ + -5.2, + -22.9 + ], + [ + -4.8, + -15.601 + ], + [ + -5, + 15.899 + ], + [ + -6.6, + 20.5 + ], + [ + -4.1, + 23.6 + ], + [ + -2.1, + 23.899 + ], + [ + 2.6, + 18.8 + ], + [ + 7.1, + 0 + ], + [ + 2.8, + -18.801 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0, + 0.819999964097, + 0.008000000785, + 1 + ], + "ix": 4 + }, + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 75, + "s": [ + 0 + ] + }, + { + "t": 90.0000036657751, + "s": [ + 100 + ] + } + ], + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 57.978, + 71.735 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 4.4, + 13.8 + ], + [ + 1.1, + 1 + ], + [ + 1.3, + -1.7 + ], + [ + -2.2, + -4.599 + ], + [ + -0.7, + -7.5 + ], + [ + 4.2, + -8.601 + ], + [ + 0.2, + -0.5 + ], + [ + -1.9, + -0.601 + ], + [ + -2.9, + 6.9 + ] + ], + "o": [ + [ + -1.4, + -4.2 + ], + [ + -1.6, + -1.5 + ], + [ + -1.3, + 1.899 + ], + [ + 3.4, + 6.801 + ], + [ + 0.9, + 9 + ], + [ + -1, + 2 + ], + [ + -0.6, + 1.8 + ], + [ + 2.9, + 0.899 + ], + [ + 5.9, + -13.6 + ] + ], + "v": [ + [ + 4.5, + -18.6 + ], + [ + -1.5, + -31.1 + ], + [ + -7.5, + -30.6 + ], + [ + -6.5, + -23.4 + ], + [ + -1.1, + -4.4 + ], + [ + -6.2, + 22.4 + ], + [ + -8.3, + 26.9 + ], + [ + -5.5, + 31.701 + ], + [ + 2.4, + 23.501 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0, + 0.819999964097, + 0.008000000785, + 1 + ], + "ix": 4 + }, + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 90, + "s": [ + 0 + ] + }, + { + "t": 105.000004276738, + "s": [ + 100 + ] + } + ], + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 75.178, + 71.835 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 3", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 3, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0.8, + 5.199 + ], + [ + 3.8, + 7.4 + ], + [ + 0.4, + 0.399 + ], + [ + 1.4, + -0.899 + ], + [ + 0, + -1.1 + ], + [ + -1.2, + -2.3 + ], + [ + 0, + -11.5 + ], + [ + 5.5, + -10.599 + ], + [ + -0.2, + -0.899 + ], + [ + -1.3, + -0.401 + ], + [ + -0.2, + 0 + ], + [ + -0.3, + 0 + ], + [ + -2.2, + 4.7 + ], + [ + -1.2, + 8.8 + ] + ], + "o": [ + [ + -1.3, + -8.4 + ], + [ + -0.9, + -1.901 + ], + [ + -1.1, + -1.101 + ], + [ + -1, + 0.601 + ], + [ + 0, + 0.299 + ], + [ + 5.3, + 10.7 + ], + [ + 0, + 11.901 + ], + [ + -1.4, + 2.601 + ], + [ + 0.3, + 1.5 + ], + [ + 0.6, + 0.2 + ], + [ + 0.2, + -0.101 + ], + [ + 1.2, + 0 + ], + [ + 3.7, + -7.7 + ], + [ + 0.5, + -4.799 + ] + ], + "v": [ + [ + 8.05, + -10.9 + ], + [ + 0.45, + -34.8 + ], + [ + -2.05, + -38.9 + ], + [ + -6.95, + -39.201 + ], + [ + -8.95, + -35.8 + ], + [ + -6.85, + -31.1 + ], + [ + 0.55, + -0.001 + ], + [ + -7.35, + 32.299 + ], + [ + -8.75, + 36.499 + ], + [ + -5.85, + 39.9 + ], + [ + -4.45, + 40.1 + ], + [ + -3.55, + 39.999 + ], + [ + 1.25, + 33.4 + ], + [ + 8.45, + 8.999 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0, + 0.819999964097, + 0.008000000785, + 1 + ], + "ix": 4 + }, + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 105, + "s": [ + 0 + ] + }, + { + "t": 120.0000048877, + "s": [ + 100 + ] + } + ], + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 91.028, + 71.635 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 4", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 4, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 60.0000024438501, + "op": 210.000008553475, + "st": -31.0000012626559, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "wifi/sdk_elements_02 Outlines 2", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 120, + "s": [ + 100 + ] + }, + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 125, + "s": [ + 100 + ] + }, + { + "t": 129.000005254278, + "s": [ + 0 + ] + } + ], + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 540, + 540, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 70.5, + 70.5, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 1.9, + -0.301 + ], + [ + -0.6, + -2 + ], + [ + -0.6, + -1.099 + ], + [ + 0, + -4 + ], + [ + 1.9, + -3.1 + ], + [ + -0.4, + -1.199 + ], + [ + -1, + -0.4 + ], + [ + -1.8, + 2.799 + ], + [ + 5.1, + 7.901 + ] + ], + "o": [ + [ + -2.3, + 0.399 + ], + [ + 0.1, + 0.5 + ], + [ + 1.9, + 3.101 + ], + [ + 0, + 4 + ], + [ + -1.5, + 2.4 + ], + [ + 0.3, + 1.101 + ], + [ + 2.1, + 0.899 + ], + [ + 5.1, + -7.901 + ], + [ + -1.8, + -2.699 + ] + ], + "v": [ + [ + -3.1, + -16.549 + ], + [ + -6.4, + -11.65 + ], + [ + -5, + -8.85 + ], + [ + -2.7, + -0.15 + ], + [ + -5, + 8.551 + ], + [ + -6.4, + 13.15 + ], + [ + -4, + 15.951 + ], + [ + 1.9, + 13.051 + ], + [ + 1.9, + -13.35 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.866957720588, + 0.866957720588, + 0.866957720588, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 43.278, + 71.784 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 2.2, + 0 + ], + [ + 1, + -0.901 + ], + [ + -2.3, + -4.599 + ], + [ + 5.3, + -10.7 + ], + [ + -0.1, + -0.801 + ], + [ + -1.2, + -0.5 + ], + [ + -0.5, + 0.101 + ], + [ + -1.9, + 3.8 + ], + [ + 0, + 7.199 + ], + [ + 3.1, + 6.301 + ] + ], + "o": [ + [ + -1.3, + 0 + ], + [ + -1.9, + 1.799 + ], + [ + 5.2, + 10.5 + ], + [ + -1.4, + 2.901 + ], + [ + 0.3, + 1.3 + ], + [ + 0.5, + 0.2 + ], + [ + 1.9, + -0.299 + ], + [ + 3.3, + -6.7 + ], + [ + 0, + -7 + ], + [ + -1.9, + -4.099 + ] + ], + "v": [ + [ + -2.5, + -24 + ], + [ + -5.2, + -22.9 + ], + [ + -4.8, + -15.601 + ], + [ + -5, + 15.899 + ], + [ + -6.6, + 20.5 + ], + [ + -4.1, + 23.6 + ], + [ + -2.1, + 23.899 + ], + [ + 2.6, + 18.8 + ], + [ + 7.1, + 0 + ], + [ + 2.8, + -18.801 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.866957720588, + 0.866957720588, + 0.866957720588, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 57.978, + 71.735 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 4.4, + 13.8 + ], + [ + 1.1, + 1 + ], + [ + 1.3, + -1.7 + ], + [ + -2.2, + -4.599 + ], + [ + -0.7, + -7.5 + ], + [ + 4.2, + -8.601 + ], + [ + 0.2, + -0.5 + ], + [ + -1.9, + -0.601 + ], + [ + -2.9, + 6.9 + ] + ], + "o": [ + [ + -1.4, + -4.2 + ], + [ + -1.6, + -1.5 + ], + [ + -1.3, + 1.899 + ], + [ + 3.4, + 6.801 + ], + [ + 0.9, + 9 + ], + [ + -1, + 2 + ], + [ + -0.6, + 1.8 + ], + [ + 2.9, + 0.899 + ], + [ + 5.9, + -13.6 + ] + ], + "v": [ + [ + 4.5, + -18.6 + ], + [ + -1.5, + -31.1 + ], + [ + -7.5, + -30.6 + ], + [ + -6.5, + -23.4 + ], + [ + -1.1, + -4.4 + ], + [ + -6.2, + 22.4 + ], + [ + -8.3, + 26.9 + ], + [ + -5.5, + 31.701 + ], + [ + 2.4, + 23.501 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.866957720588, + 0.866957720588, + 0.866957720588, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 75.178, + 71.835 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 3", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 3, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0.8, + 5.199 + ], + [ + 3.8, + 7.4 + ], + [ + 0.4, + 0.399 + ], + [ + 1.4, + -0.899 + ], + [ + 0, + -1.1 + ], + [ + -1.2, + -2.3 + ], + [ + 0, + -11.5 + ], + [ + 5.5, + -10.599 + ], + [ + -0.2, + -0.899 + ], + [ + -1.3, + -0.401 + ], + [ + -0.2, + 0 + ], + [ + -0.3, + 0 + ], + [ + -2.2, + 4.7 + ], + [ + -1.2, + 8.8 + ] + ], + "o": [ + [ + -1.3, + -8.4 + ], + [ + -0.9, + -1.901 + ], + [ + -1.1, + -1.101 + ], + [ + -1, + 0.601 + ], + [ + 0, + 0.299 + ], + [ + 5.3, + 10.7 + ], + [ + 0, + 11.901 + ], + [ + -1.4, + 2.601 + ], + [ + 0.3, + 1.5 + ], + [ + 0.6, + 0.2 + ], + [ + 0.2, + -0.101 + ], + [ + 1.2, + 0 + ], + [ + 3.7, + -7.7 + ], + [ + 0.5, + -4.799 + ] + ], + "v": [ + [ + 8.05, + -10.9 + ], + [ + 0.45, + -34.8 + ], + [ + -2.05, + -38.9 + ], + [ + -6.95, + -39.201 + ], + [ + -8.95, + -35.8 + ], + [ + -6.85, + -31.1 + ], + [ + 0.55, + -0.001 + ], + [ + -7.35, + 32.299 + ], + [ + -8.75, + 36.499 + ], + [ + -5.85, + 39.9 + ], + [ + -4.45, + 40.1 + ], + [ + -3.55, + 39.999 + ], + [ + 1.25, + 33.4 + ], + [ + 8.45, + 8.999 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.866957720588, + 0.866957720588, + 0.866957720588, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 91.028, + 71.635 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 4", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 4, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 210.000008553475, + "st": -31.0000012626559, + "bm": 0 + } + ], + "markers": [] +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 8c7be084..ff2e7c9f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,6 +37,7 @@ import 'presentation_layer/routes/nostr/settings/inital_route/inital_route_setti import 'presentation_layer/routes/nostr/settings/locale/locale_settings.dart'; import 'presentation_layer/routes/nostr/settings/moderation/moderation_settings.dart'; import 'presentation_layer/routes/nostr/settings/settings_page.dart'; +import 'presentation_layer/routes/wallet/wallet_navigation.dart'; import 'theme.dart' as theme; const devDeviceFrame = true; @@ -264,6 +265,12 @@ class MyApp extends ConsumerWidget { settings.arguments as StarterPackIdentifier, ), ); + case '/wallet': + return MaterialPageRoute( + builder: (context) => WalletNavigation( + title: "a", + ), + ); } assert(false, 'Need to implement ${settings.name}'); return null; diff --git a/lib/presentation_layer/components/wallet/sheet_send_receive.dart b/lib/presentation_layer/components/wallet/sheet_send_receive.dart new file mode 100644 index 00000000..01fcae82 --- /dev/null +++ b/lib/presentation_layer/components/wallet/sheet_send_receive.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +class WalletSheetSendReceive extends StatelessWidget { + const WalletSheetSendReceive( + {super.key, + required this.isHideBottomNavBar, + required this.pageChangeStream}); + + final Function(bool) isHideBottomNavBar; + final Stream pageChangeStream; + + @override + Widget build(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + child: Padding( + padding: EdgeInsets.only(top: 25), // const EdgeInsets.all(15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row(children: [ + SizedBox( + height: 85, + child: Column( + children: [ + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(7), + elevation: 0, + ), + child: + const Icon(Icons.file_upload_outlined, size: 40)), + const SizedBox(height: 5), + const Text("send") + ], + ), + ), + ]), + Row(children: [ + SizedBox( + height: 85, + child: Column( + children: [ + ElevatedButton( + onPressed: () { + print("unimplemented"); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(7), + elevation: 0, + ), + child: + const Icon(Icons.file_download_outlined, size: 40)), + const SizedBox(height: 5), + const Text("receive") + ], + ), + ), + ]), + ], + ), + ), + ); + } +} diff --git a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart new file mode 100644 index 00000000..65521292 --- /dev/null +++ b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; + +import 'package:lottie/lottie.dart'; +import 'package:intl/intl.dart'; + +class WalletAccountsCard extends StatefulWidget { + const WalletAccountsCard({super.key, required this.title}); + final String title; + + @override + State createState() => _AccountsCard(); +} + +class _AccountsCard extends State + with TickerProviderStateMixin { + late final AnimationController _nfcAnimController; + + final satFormat = NumberFormat("#,##0.##", "de_DE"); + final eurFormat = NumberFormat("#,##0.00", "de_DE"); + + @override + void initState() { + super.initState(); + + _nfcAnimController = AnimationController(vsync: this); + } + + @override + void dispose() { + _nfcAnimController.dispose(); + super.dispose(); + } + + @override + build(BuildContext context) { + return Column( + children: [ + Column( + children: [ + Container( + width: 370, + height: 210, + decoration: BoxDecoration( + // gradient: const RadialGradient( + // colors: [ + // Color.fromARGB(255, 7, 238, 176), + // Color.fromARGB(255, 11, 189, 243), + // ], + // stops: [ + // 0, + // 1, + // ], + // focal: Alignment.center, + // radius: 2, + // ), + border: Border.all( + width: 1, + color: Colors.white, + ), + // Make rounded corners + borderRadius: BorderRadius.circular(18.0), + ), + child: Container( + margin: const EdgeInsets.all(10.0), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "alias", + style: TextStyle(fontSize: 18), + ), + Container( + margin: const EdgeInsets.fromLTRB(10.0, 10, 0, 0), + child: const Text( + "main ", + style: TextStyle( + fontSize: 35, + color: Colors.white, + fontWeight: FontWeight.normal), + ), + ), + // const SizedBox(height: 5), + SizedBox( + width: MediaQuery.of(context).size.width * 0.65, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Text( + "₿", + style: TextStyle( + fontSize: 37, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Text( + '${satFormat.format(20.4)}', + style: const TextStyle( + fontSize: 37, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const Text( + "sat", + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + const SizedBox(height: 0), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Text( + "€", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.normal, + color: Colors.white60, + ), + ), + Text( + eurFormat.format(20.5), + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.white60, + ), + ), + const Text( + "eur", + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.normal, + color: Colors.white60, + ), + ), + ]), + ], + ), + ), + ], + ), + //lottie animation + + Positioned( + right: -50, + child: Transform( + transform: Matrix4.translationValues( + MediaQuery.of(context).size.width * 0, + -20.0, + -20.0), + child: Lottie.asset( + 'assets/animations/nfc-mood.json', + width: 150, + //fit: BoxFit.cover, + controller: _nfcAnimController, + onLoaded: (composition) { + _nfcAnimController.duration = composition.duration; + _nfcAnimController.repeat(); + _nfcAnimController.forward(); + }, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/presentation_layer/components/wallet/wallet_actions_strip.dart b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart new file mode 100644 index 00000000..affa3863 --- /dev/null +++ b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart @@ -0,0 +1,103 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../../../config/palette.dart'; + +class WalletActionsStrip extends StatelessWidget { + const WalletActionsStrip({super.key}); + + final myIconColor = Colors.white70; + final myTextColor = Colors.white60; + + _onScan() { + log("onScan"); + } + + _onReceive() { + log("onReceive"); + } + + _onPay(context) { + log("onPay"); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Container( + width: MediaQuery.of(context).size.width, + //height: 120, + //color: Colors.white12, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Container( + padding: const EdgeInsets.all(2), + child: _actionButton( + iconData: PhosphorIcons.barcode(), + onTab: () => _onScan(), + text: "scan", + ), + ), + Container( + padding: const EdgeInsets.all(2), + child: _actionButton( + iconData: PhosphorIcons.wallet(), + onTab: () => _onPay(context), + text: "pay", + ), + ), + Container( + padding: const EdgeInsets.all(2), + child: _actionButton( + iconData: PhosphorIcons.piggyBank(), + onTab: () => _onReceive(), + text: "receive"), + ), + Container( + padding: const EdgeInsets.all(2), + child: _actionButton( + iconData: PhosphorIcons.receipt(), + text: "history", + onTab: () {}), + ), + ], + ), + ), + ); + } +} + +Widget _actionButton({ + required Function onTab, + required IconData iconData, + required String text, + Color? iconColor, + Color? textColor, +}) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => onTab(), + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + fixedSize: const Size(50, 50), + padding: EdgeInsets.zero, + ), + child: Icon( + iconData, + color: iconColor ?? Palette.gray, + size: 23, + ), + ), + Text( + text, + style: TextStyle(color: textColor ?? Palette.gray), + ) + ], + ); +} diff --git a/lib/presentation_layer/components/wallet/wallet_friends_strip.dart b/lib/presentation_layer/components/wallet/wallet_friends_strip.dart new file mode 100644 index 00000000..0e407a93 --- /dev/null +++ b/lib/presentation_layer/components/wallet/wallet_friends_strip.dart @@ -0,0 +1,82 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; + +class WalletFriendsStrip extends StatelessWidget { + const WalletFriendsStrip({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + height: 130, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(10))), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(15, 4, 15, 0), + child: Text( + "friends", + style: TextStyle(color: Colors.white, fontSize: 24), + ), + ), + Container( + margin: const EdgeInsets.fromLTRB(15, 2, 15, 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + borderRadius: const BorderRadius.all(Radius.circular(10)), + onTap: () => {print("unimplemented")}, + child: Column( + children: const [ + CircleAvatar( + backgroundImage: NetworkImage( + "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg"), + radius: 30, + ), + Text( + "user1", + style: TextStyle(fontSize: 15), + ), + ], + ), + ), + const SizedBox(width: 15), + InkWell( + borderRadius: const BorderRadius.all(Radius.circular(10)), + onTap: () => {print("nimplemented;")}, + child: Column( + children: const [ + CircleAvatar( + backgroundImage: NetworkImage( + "https://www.venmond.com/demo/vendroid/img/avatar/big.jpg"), + radius: 30, + ), + Text( + "user2", + style: TextStyle(fontSize: 15), + ), + ], + ), + ), + const SizedBox(width: 15), + InkWell( + borderRadius: const BorderRadius.all(Radius.circular(50)), + onTap: () => {log("todo")}, + child: const CircleAvatar( + backgroundColor: Colors.white60, + radius: 30, + child: Icon(Icons.add), + ), + ), + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/presentation_layer/routes/nostr/nostr_drawer.dart b/lib/presentation_layer/routes/nostr/nostr_drawer.dart index aae45c2c..e08fdfed 100644 --- a/lib/presentation_layer/routes/nostr/nostr_drawer.dart +++ b/lib/presentation_layer/routes/nostr/nostr_drawer.dart @@ -271,11 +271,7 @@ class NostrDrawer extends ConsumerWidget { label: 'Payments', icon: 'assets/icons/lightning.svg', onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Not implemented yet'), - ), - ); + Navigator.pushNamed(context, '/wallet'); }), _drawerItem( label: 'Blocklist', diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart new file mode 100644 index 00000000..fe3dfb4c --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../components/wallet/wallet_actions_strip.dart'; +import '../../components/wallet/wallet_friends_strip.dart'; +import '../../components/wallet/wallet_accounts_card.dart'; + +class WalletDashboard extends ConsumerWidget { + const WalletDashboard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SafeArea( + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: const [ + SizedBox(height: 20), + WalletAccountsCard(title: "hi"), + SizedBox(height: 30), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: WalletFriendsStrip(), + ), + SizedBox(height: 30), + WalletActionsStrip(), + SizedBox(height: 30), + //PaymentHistoryShort(), + ]), + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_mints.dart b/lib/presentation_layer/routes/wallet/wallet_mints.dart new file mode 100644 index 00000000..cb9c1f67 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_mints.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class WalletMints extends ConsumerWidget { + const WalletMints({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: null, + body: ListView(children: [Text("WalletMints")]), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_navigation.dart b/lib/presentation_layer/routes/wallet/wallet_navigation.dart new file mode 100644 index 00000000..7b193338 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_navigation.dart @@ -0,0 +1,164 @@ +import 'dart:async'; + +import 'package:camelus/config/palette.dart'; +import 'package:flutter/material.dart'; + +import '../../components/app_bottom_navigation_bar/app_bottom_navigation_bar.dart'; +import '../../components/wallet/sheet_send_receive.dart'; +import 'wallet_dashboard.dart'; +import 'wallet_mints.dart'; +import 'wallet_qr_scan.dart'; +import 'wallet_receive.dart'; + +class WalletNavigation extends StatefulWidget { + const WalletNavigation({super.key, required this.title}); + + final String title; + + @override + State createState() => _WalletNavigationState(); +} + +class _WalletNavigationState extends State + with SingleTickerProviderStateMixin { + int _selectedIndex = 0; + late AnimationController animationController; + + late final PageController dashboardPageViewController; + + late final PageController mainPageViewController; + + final StreamController _pageChangeController = + StreamController.broadcast(); + + void _onItemLongPress(myContext) { + final previusIndex = _selectedIndex; + showModalBottomSheet( + backgroundColor: Colors.black, + //anchorPoint: Offset(50, 20), + //useRootNavigator: false, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + context: myContext, + builder: (context) => WalletSheetSendReceive( + isHideBottomNavBar: (isHideBottomNavBar) { + isHideBottomNavBar + ? animationController.forward() + : animationController.reverse(); + }, + pageChangeStream: _pageChangeController.stream.asBroadcastStream(), + ), + ).then( + (_) { + setState(() { + _selectedIndex = previusIndex; + }); + }, + ); + } + + void _onItemTapped( + int index, + myContext, + ) { + changePage(index); + } + + void _onPageSwipe(int index) { + changePage(index - 1); + } + + void changePage(int index) { + _pageChangeController.add(index); + switch (index) { + case 0: + { + //Navigator.pushNamed(context, '/home'); + setState(() { + _selectedIndex = index; + }); + _pageChangeController.add(index); + + break; + } + + case 1: + { + //Navigator.pushNamed(context, '/receive'); + setState(() { + _selectedIndex = index; + }); + _pageChangeController.add(index); + + break; + } + case 2: + { + setState(() { + _selectedIndex = index; + }); + _pageChangeController.add(index); + break; + } + case 3: + { + setState(() { + _selectedIndex = index; + }); + _pageChangeController.add(index); + break; + } + } + } + + @override + void initState() { + super.initState(); + animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 200)); + + dashboardPageViewController = PageController(); + + mainPageViewController = PageController(); + + animationController.forward(); + } + + @override + void dispose() { + animationController.dispose(); + dashboardPageViewController.dispose(); + mainPageViewController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Palette.background, + appBar: null, + body: PageView( + scrollDirection: Axis.horizontal, + controller: mainPageViewController, + onPageChanged: _onPageSwipe, + physics: const BouncingScrollPhysics(), + children: [ + PageView( + scrollDirection: Axis.vertical, + controller: dashboardPageViewController, + children: [ + WalletQrScan(), + WalletDashboard(), + ]), + WalletReceive(), + WalletMints(), + ], + ), + bottomNavigationBar: AppBottomNavigationBar( + pageController: mainPageViewController, // todo: replace with actual + )); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart new file mode 100644 index 00000000..8de78650 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class WalletQrScan extends StatefulWidget { + const WalletQrScan({Key? key}) : super(key: key); + + @override + State createState() => _QrScan(); +} + +class _QrScan extends State { + String qrcode = 'Unknown'; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Stack( + children: [ + Center( + child: Text("scanner here"), + ), + //fix outer border radius + Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + border: Border.all( + color: Colors.black, + width: 15, + ), + ), + ), +//rounded corners + GestureDetector( + onTap: () { + print("unimplemented"); + }, + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + border: Border.all( + color: Colors.black, + width: 20, + ), + borderRadius: BorderRadius.circular(40), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_receive.dart b/lib/presentation_layer/routes/wallet/wallet_receive.dart new file mode 100644 index 00000000..7f72f77a --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_receive.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class WalletReceive extends ConsumerWidget { + const WalletReceive({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: null, + body: ListView(children: [Text("WalletReceive")]), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index a589ea42..d0affc7a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1198,11 +1198,9 @@ packages: ndk: dependency: "direct main" description: - path: "packages/ndk" - ref: tmp-camelus-1-6-0 - resolved-ref: c634f168fc746426f48efce7bbc5b48e19bf1298 - url: "https://github.com/relaystr/ndk.git" - source: git + path: "../ndk/packages/ndk" + relative: true + source: path version: "0.4.0" ndk_amber: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index 4994ab16..3cd8486e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -106,11 +106,11 @@ dependencies: dependency_overrides: ndk: - #path: ../ndk/packages/ndk - git: - url: https://github.com/relaystr/ndk.git - path: packages/ndk - ref: tmp-camelus-1-6-0 + path: ../ndk/packages/ndk + #git: + # url: https://github.com/relaystr/ndk.git + # path: packages/ndk + # ref: tmp-camelus-1-6-0 #ndk_rust_verifier: # path: ../ndk/packages/rust_verifier #git: @@ -167,6 +167,7 @@ flutter: - assets/images/ - assets/lottie/ - assets/app_icons/ + - assets/animations/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware From 1dfda16252f0eb403e9bb37406b189cf974d1b5e Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:47:49 +0200 Subject: [PATCH 02/52] basic navigation --- lib/main.dart | 23 +- .../app_bottom_navigation_bar.dart | 14 + .../app_bottom_bar_provider.dart | 19 +- lib/presentation_layer/routes/home_page.dart | 1 + .../routes/nostr/nostr_drawer.dart | 2 +- .../routes/wallet/wallet_dashboard.dart | 66 +++- .../routes/wallet/wallet_navigation.dart | 323 ++++++++++++------ 7 files changed, 328 insertions(+), 120 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index ff2e7c9f..b6c524a7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -265,7 +265,28 @@ class MyApp extends ConsumerWidget { settings.arguments as StarterPackIdentifier, ), ); - case '/wallet': + + case '/wallet/dashboard': + return MaterialPageRoute( + builder: (context) => WalletNavigation( + title: "a", + ), + ); + case '/wallet/receive': + return MaterialPageRoute( + builder: (context) => WalletNavigation( + title: "a", + ), + ); + + case '/wallet/mints': + return MaterialPageRoute( + builder: (context) => WalletNavigation( + title: "a", + ), + ); + + case '/wallet/qr-scan': return MaterialPageRoute( builder: (context) => WalletNavigation( title: "a", diff --git a/lib/presentation_layer/components/app_bottom_navigation_bar/app_bottom_navigation_bar.dart b/lib/presentation_layer/components/app_bottom_navigation_bar/app_bottom_navigation_bar.dart index f6831307..91cc42fb 100644 --- a/lib/presentation_layer/components/app_bottom_navigation_bar/app_bottom_navigation_bar.dart +++ b/lib/presentation_layer/components/app_bottom_navigation_bar/app_bottom_navigation_bar.dart @@ -34,6 +34,7 @@ class AppBottomNavigationBar extends ConsumerWidget { items: [ _buildHomeItem(navigationState, ref), _buildSearchItem(navigationState), + //_buildWalletItem(navigationState), _buildNotificationsItem(navigationState), //_buildChatItem(navigationState), ], @@ -114,6 +115,19 @@ class AppBottomNavigationBar extends ConsumerWidget { label: "", ); } + + BottomNavigationBarItem _buildWalletItem(NavigationState state) { + final isSelected = state.selectedTab == NavigationTab.wallet; + + return BottomNavigationBarItem( + icon: Icon( + PhosphorIcons.wallet(), + color: isSelected ? Palette.primary : Palette.darkGray, + size: 23, + ), + label: "wallet", + ); + } } class IndicatorDot extends StatelessWidget { diff --git a/lib/presentation_layer/providers/app_bar_provider/app_bottom_bar_provider.dart b/lib/presentation_layer/providers/app_bar_provider/app_bottom_bar_provider.dart index d85c78c1..ce25483c 100644 --- a/lib/presentation_layer/providers/app_bar_provider/app_bottom_bar_provider.dart +++ b/lib/presentation_layer/providers/app_bar_provider/app_bottom_bar_provider.dart @@ -1,7 +1,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:async'; -enum NavigationTab { home, search, notifications, chat } +enum NavigationTab { + home, + search, + + notifications, + chat, + wallet, +} class NavigationState { final NavigationTab selectedTab; @@ -34,6 +41,7 @@ class NavigationEvents { final _searchTabController = StreamController.broadcast(); final _notificationsTabController = StreamController.broadcast(); final _chatTabController = StreamController.broadcast(); + final _walletTabController = StreamController.broadcast(); // Expose streams for listeners Stream get onHomeTabSelected => _homeTabController.stream; @@ -41,6 +49,7 @@ class NavigationEvents { Stream get onNotificationsTabSelected => _notificationsTabController.stream; Stream get onChatTabSelected => _chatTabController.stream; + Stream get onWalletTabSelected => _walletTabController.stream; // Methods to trigger events void triggerHomeTabEvent() { @@ -59,11 +68,16 @@ class NavigationEvents { _chatTabController.add(null); } + void triggerWalletTabEvent() { + _walletTabController.add(null); + } + void dispose() { _homeTabController.close(); _searchTabController.close(); _notificationsTabController.close(); _chatTabController.close(); + _walletTabController.close(); } } @@ -100,6 +114,9 @@ class NavigationNotifier extends StateNotifier { case NavigationTab.chat: events.triggerChatTabEvent(); break; + case NavigationTab.wallet: + events.triggerWalletTabEvent(); + break; } } diff --git a/lib/presentation_layer/routes/home_page.dart b/lib/presentation_layer/routes/home_page.dart index 8fa2bdc5..9708d4f3 100644 --- a/lib/presentation_layer/routes/home_page.dart +++ b/lib/presentation_layer/routes/home_page.dart @@ -164,6 +164,7 @@ class _HomePageState extends ConsumerState { initialTab: widget.initialTab, ), const SearchPage(), + //const WalletNavigation(title: "title"), NotificationPage( pubkey: widget.pubkey, ), diff --git a/lib/presentation_layer/routes/nostr/nostr_drawer.dart b/lib/presentation_layer/routes/nostr/nostr_drawer.dart index e08fdfed..c8bb4b48 100644 --- a/lib/presentation_layer/routes/nostr/nostr_drawer.dart +++ b/lib/presentation_layer/routes/nostr/nostr_drawer.dart @@ -271,7 +271,7 @@ class NostrDrawer extends ConsumerWidget { label: 'Payments', icon: 'assets/icons/lightning.svg', onTap: () { - Navigator.pushNamed(context, '/wallet'); + Navigator.pushNamed(context, '/wallet/dashboard'); }), _drawerItem( label: 'Blocklist', diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index fe3dfb4c..944ee851 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -1,34 +1,64 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../config/palette.dart'; +import '../../atoms/my_profile_picture.dart'; import '../../components/wallet/wallet_actions_strip.dart'; import '../../components/wallet/wallet_friends_strip.dart'; import '../../components/wallet/wallet_accounts_card.dart'; +import '../../providers/metadata_state_provider.dart'; +import '../../providers/ndk_provider.dart'; +import '../nostr/nostr_drawer.dart'; class WalletDashboard extends ConsumerWidget { const WalletDashboard({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: const [ - SizedBox(height: 20), - WalletAccountsCard(title: "hi"), - SizedBox(height: 30), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: WalletFriendsStrip(), + final myUserPubkey = ref.read(ndkProvider).accounts.getPublicKey()!; + return Scaffold( + appBar: AppBar( + backgroundColor: Palette.background, + leading: Builder( + builder: (context) { + final myMetadata = + ref.watch(metadataStateProvider(myUserPubkey)).userMetadata; + return InkWell( + borderRadius: BorderRadius.circular(100), + onTap: () => Scaffold.of(context).openDrawer(), + child: Padding( + padding: const EdgeInsets.all(9.0), + child: UserImage( + imageUrl: myMetadata?.picture, + pubkey: myUserPubkey, + ), ), - SizedBox(height: 30), - WalletActionsStrip(), - SizedBox(height: 30), - //PaymentHistoryShort(), - ]), + ); + }, + ), + ), + backgroundColor: Palette.background, + drawer: NostrDrawer(pubkey: myUserPubkey!), + body: SafeArea( + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: const [ + SizedBox(height: 20), + WalletAccountsCard(title: "hi"), + SizedBox(height: 30), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: WalletFriendsStrip(), + ), + SizedBox(height: 30), + WalletActionsStrip(), + SizedBox(height: 30), + //PaymentHistoryShort(), + ]), + ), ), ); } diff --git a/lib/presentation_layer/routes/wallet/wallet_navigation.dart b/lib/presentation_layer/routes/wallet/wallet_navigation.dart index 7b193338..baf78c57 100644 --- a/lib/presentation_layer/routes/wallet/wallet_navigation.dart +++ b/lib/presentation_layer/routes/wallet/wallet_navigation.dart @@ -1,42 +1,208 @@ import 'dart:async'; -import 'package:camelus/config/palette.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../components/app_bottom_navigation_bar/app_bottom_navigation_bar.dart'; import '../../components/wallet/sheet_send_receive.dart'; import 'wallet_dashboard.dart'; import 'wallet_mints.dart'; import 'wallet_qr_scan.dart'; import 'wallet_receive.dart'; -class WalletNavigation extends StatefulWidget { +// Navigation state class +class WalletNavigationState { + final int selectedIndex; + final int dashboardIndex; + final PageController mainPageController; + final PageController dashboardPageController; + final StreamController pageChangeController; + + const WalletNavigationState({ + required this.selectedIndex, + required this.dashboardIndex, + required this.mainPageController, + required this.dashboardPageController, + required this.pageChangeController, + }); + + WalletNavigationState copyWith({ + int? selectedIndex, + int? dashboardIndex, + PageController? mainPageController, + PageController? dashboardPageController, + StreamController? pageChangeController, + }) { + return WalletNavigationState( + selectedIndex: selectedIndex ?? this.selectedIndex, + dashboardIndex: dashboardIndex ?? this.dashboardIndex, + mainPageController: mainPageController ?? this.mainPageController, + dashboardPageController: + dashboardPageController ?? this.dashboardPageController, + pageChangeController: pageChangeController ?? this.pageChangeController, + ); + } +} + +// StateNotifier for managing navigation +class WalletNavigationNotifier extends StateNotifier { + WalletNavigationNotifier() + : super(WalletNavigationState( + selectedIndex: 0, + dashboardIndex: + 1, // Start at dashboard (index 1 in vertical PageView) + mainPageController: PageController(initialPage: 0), + dashboardPageController: PageController(initialPage: 1), + pageChangeController: StreamController.broadcast(), + )); + + // Route mappings + final Map _routeToIndex = { + '/wallet/qr-scan': 0, + '/wallet/dashboard': 0, + '/wallet/receive': 1, + '/wallet/mints': 2, + }; + + final Map _indexToRoute = { + 0: '/wallet/dashboard', + 1: '/wallet/receive', + 2: '/wallet/mints', + }; + + // Change main page (horizontal navigation) + void changeMainPage(int index, + {bool animate = true, bool updateRoute = true}) { + if (index == state.selectedIndex) return; + + state = state.copyWith(selectedIndex: index); + state.pageChangeController.add(index); + + if (animate) { + state.mainPageController.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + state.mainPageController.jumpToPage(index); + } + } + + // Change dashboard page (vertical navigation) + void changeDashboardPage(int index, {bool animate = true}) { + if (index == state.dashboardIndex) return; + + state = state.copyWith(dashboardIndex: index); + + if (animate) { + state.dashboardPageController.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + state.dashboardPageController.jumpToPage(index); + } + } + + // Navigate by route name + void navigateToRoute(String route, BuildContext context) { + final index = _routeToIndex[route]; + if (index != null) { + if (route == '/wallet/qr-scan') { + // Special case: navigate to QR scan (dashboard index 0) + changeMainPage(0, updateRoute: false); + changeDashboardPage(0); + } else { + changeMainPage(index); + if (index == 0) { + // Ensure we're on dashboard when navigating to main dashboard + changeDashboardPage(1); + } + } + Navigator.pushReplacementNamed(context, route); + } + } + + // Handle page swipe from PageView + void onMainPageSwipe(int index) { + state = state.copyWith(selectedIndex: index); + state.pageChangeController.add(index); + } + + // Handle dashboard page swipe + void onDashboardPageSwipe(int index) { + state = state.copyWith(dashboardIndex: index); + } + + // Get current route + String get currentRoute { + if (state.selectedIndex == 0 && state.dashboardIndex == 0) { + return '/wallet/qr-scan'; + } + return _indexToRoute[state.selectedIndex] ?? '/wallet/dashboard'; + } + + @override + void dispose() { + state.mainPageController.dispose(); + state.dashboardPageController.dispose(); + state.pageChangeController.close(); + super.dispose(); + } +} + +// Provider +final walletNavigationProvider = + StateNotifierProvider( + (ref) { + return WalletNavigationNotifier(); +}); + +// Convenience providers for easy access +final selectedIndexProvider = Provider((ref) { + return ref.watch(walletNavigationProvider).selectedIndex; +}); + +final dashboardIndexProvider = Provider((ref) { + return ref.watch(walletNavigationProvider).dashboardIndex; +}); + +final mainPageControllerProvider = Provider((ref) { + return ref.watch(walletNavigationProvider).mainPageController; +}); + +final dashboardPageControllerProvider = Provider((ref) { + return ref.watch(walletNavigationProvider).dashboardPageController; +}); + +final pageChangeStreamProvider = Provider>((ref) { + return ref + .watch(walletNavigationProvider) + .pageChangeController + .stream + .asBroadcastStream(); +}); + +class WalletNavigation extends ConsumerStatefulWidget { const WalletNavigation({super.key, required this.title}); final String title; @override - State createState() => _WalletNavigationState(); + ConsumerState createState() => _WalletNavigationState(); } -class _WalletNavigationState extends State +class _WalletNavigationState extends ConsumerState with SingleTickerProviderStateMixin { - int _selectedIndex = 0; late AnimationController animationController; - late final PageController dashboardPageViewController; - - late final PageController mainPageViewController; - - final StreamController _pageChangeController = - StreamController.broadcast(); + void _onItemLongPress(BuildContext myContext) { + final navigationState = ref.read(walletNavigationProvider); + final currentIndex = navigationState.selectedIndex; - void _onItemLongPress(myContext) { - final previusIndex = _selectedIndex; showModalBottomSheet( backgroundColor: Colors.black, - //anchorPoint: Offset(50, 20), - //useRootNavigator: false, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(20), @@ -49,69 +215,24 @@ class _WalletNavigationState extends State ? animationController.forward() : animationController.reverse(); }, - pageChangeStream: _pageChangeController.stream.asBroadcastStream(), + pageChangeStream: ref.read(pageChangeStreamProvider), ), - ).then( - (_) { - setState(() { - _selectedIndex = previusIndex; - }); - }, - ); + ).then((_) { + // Restore previous state + ref.read(walletNavigationProvider.notifier).changeMainPage(currentIndex); + }); } - void _onItemTapped( - int index, - myContext, - ) { - changePage(index); + void _onItemTapped(int index, BuildContext myContext) { + ref.read(walletNavigationProvider.notifier).changeMainPage(index); } - void _onPageSwipe(int index) { - changePage(index - 1); + void _onMainPageSwipe(int index) { + ref.read(walletNavigationProvider.notifier).onMainPageSwipe(index); } - void changePage(int index) { - _pageChangeController.add(index); - switch (index) { - case 0: - { - //Navigator.pushNamed(context, '/home'); - setState(() { - _selectedIndex = index; - }); - _pageChangeController.add(index); - - break; - } - - case 1: - { - //Navigator.pushNamed(context, '/receive'); - setState(() { - _selectedIndex = index; - }); - _pageChangeController.add(index); - - break; - } - case 2: - { - setState(() { - _selectedIndex = index; - }); - _pageChangeController.add(index); - break; - } - case 3: - { - setState(() { - _selectedIndex = index; - }); - _pageChangeController.add(index); - break; - } - } + void _onDashboardPageSwipe(int index) { + ref.read(walletNavigationProvider.notifier).onDashboardPageSwipe(index); } @override @@ -119,46 +240,50 @@ class _WalletNavigationState extends State super.initState(); animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200)); - - dashboardPageViewController = PageController(); - - mainPageViewController = PageController(); - animationController.forward(); + + // current route from navigator + WidgetsBinding.instance.addPostFrameCallback((_) { + final currentRoute = ModalRoute.of(context)?.settings.name; + if (currentRoute != null) { + ref + .read(walletNavigationProvider.notifier) + .navigateToRoute(currentRoute, context); + } + }); } @override void dispose() { animationController.dispose(); - dashboardPageViewController.dispose(); - mainPageViewController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Palette.background, - appBar: null, - body: PageView( - scrollDirection: Axis.horizontal, - controller: mainPageViewController, - onPageChanged: _onPageSwipe, - physics: const BouncingScrollPhysics(), + final navigationState = ref.watch(walletNavigationProvider); + final selectedIndex = ref.watch(selectedIndexProvider); + final mainPageController = ref.watch(mainPageControllerProvider); + final dashboardPageController = ref.watch(dashboardPageControllerProvider); + + return PageView( + scrollDirection: Axis.horizontal, + controller: mainPageController, + onPageChanged: _onMainPageSwipe, + physics: const BouncingScrollPhysics(), + children: [ + PageView( + scrollDirection: Axis.vertical, + controller: dashboardPageController, + onPageChanged: _onDashboardPageSwipe, children: [ - PageView( - scrollDirection: Axis.vertical, - controller: dashboardPageViewController, - children: [ - WalletQrScan(), - WalletDashboard(), - ]), - WalletReceive(), - WalletMints(), + WalletQrScan(), + WalletDashboard(), ], ), - bottomNavigationBar: AppBottomNavigationBar( - pageController: mainPageViewController, // todo: replace with actual - )); + WalletReceive(), + WalletMints(), + ], + ); } } From d25c7a67327efedfc5f7c10b003cf0c102c11423 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:51:04 +0200 Subject: [PATCH 03/52] cashu proof of concept --- .../wallet/wallet_accounts_card.dart | 76 +++++++++--------- .../routes/wallet/wallet_dashboard.dart | 77 ++++++++++++++----- .../wallet_combined_state_provider.dart | 77 +++++++++++++++++++ pubspec.lock | 33 +++++--- pubspec.yaml | 16 ++-- 5 files changed, 202 insertions(+), 77 deletions(-) create mode 100644 lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart diff --git a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart index 65521292..65d29693 100644 --- a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart +++ b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart @@ -1,42 +1,37 @@ import 'package:flutter/material.dart'; - import 'package:lottie/lottie.dart'; import 'package:intl/intl.dart'; -class WalletAccountsCard extends StatefulWidget { - const WalletAccountsCard({super.key, required this.title}); - final String title; +class WalletAccountsCard extends StatelessWidget { + const WalletAccountsCard({ + super.key, + required this.title, + required this.nfcAnimController, + required this.alias, + required this.mainCurrencySymbol, + required this.mainCurrencyValue, + required this.mainCurrencyCode, + }); - @override - State createState() => _AccountsCard(); -} - -class _AccountsCard extends State - with TickerProviderStateMixin { - late final AnimationController _nfcAnimController; - - final satFormat = NumberFormat("#,##0.##", "de_DE"); - final eurFormat = NumberFormat("#,##0.00", "de_DE"); + final String title; + final String alias; - @override - void initState() { - super.initState(); + final AnimationController nfcAnimController; - _nfcAnimController = AnimationController(vsync: this); - } + /// ₿ + final String mainCurrencySymbol; + final double mainCurrencyValue; + final String mainCurrencyCode; @override - void dispose() { - _nfcAnimController.dispose(); - super.dispose(); - } + Widget build(BuildContext context) { + final satFormat = NumberFormat("#,##0.##", "de_DE"); + final eurFormat = NumberFormat("#,##0.00", "de_DE"); - @override - build(BuildContext context) { return Column( - children: [ + children: [ Column( - children: [ + children: [ Container( width: 370, height: 210, @@ -66,15 +61,15 @@ class _AccountsCard extends State children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Text( - "alias", + alias, style: TextStyle(fontSize: 18), ), Container( margin: const EdgeInsets.fromLTRB(10.0, 10, 0, 0), - child: const Text( - "main ", + child: Text( + title, style: TextStyle( fontSize: 35, color: Colors.white, @@ -90,8 +85,8 @@ class _AccountsCard extends State Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - const Text( - "₿", + Text( + mainCurrencySymbol, style: TextStyle( fontSize: 37, fontWeight: FontWeight.bold, @@ -99,15 +94,15 @@ class _AccountsCard extends State ), ), Text( - '${satFormat.format(20.4)}', + '${satFormat.format(mainCurrencyValue)}', style: const TextStyle( fontSize: 37, fontWeight: FontWeight.bold, color: Colors.white, ), ), - const Text( - "sat", + Text( + mainCurrencyCode, style: TextStyle( fontSize: 25, fontWeight: FontWeight.bold, @@ -151,7 +146,6 @@ class _AccountsCard extends State ], ), //lottie animation - Positioned( right: -50, child: Transform( @@ -163,11 +157,11 @@ class _AccountsCard extends State 'assets/animations/nfc-mood.json', width: 150, //fit: BoxFit.cover, - controller: _nfcAnimController, + controller: nfcAnimController, onLoaded: (composition) { - _nfcAnimController.duration = composition.duration; - _nfcAnimController.repeat(); - _nfcAnimController.forward(); + nfcAnimController.duration = composition.duration; + nfcAnimController.repeat(); + nfcAnimController.forward(); }, ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index 944ee851..55de352b 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -1,3 +1,4 @@ +import 'package:camelus/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -10,12 +11,39 @@ import '../../providers/metadata_state_provider.dart'; import '../../providers/ndk_provider.dart'; import '../nostr/nostr_drawer.dart'; -class WalletDashboard extends ConsumerWidget { +class WalletDashboard extends ConsumerStatefulWidget { const WalletDashboard({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _WalletDashboardState(); +} + +class _WalletDashboardState extends ConsumerState + with TickerProviderStateMixin { + late AnimationController _nfcAnimController; + + @override + void initState() { + super.initState(); + _nfcAnimController = AnimationController(vsync: this); + + // If using Option 1, initialize the provider + // ref.read(nfcAnimationControllerProvider.notifier).initialize(this); + } + + @override + void dispose() { + _nfcAnimController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final myUserPubkey = ref.read(ndkProvider).accounts.getPublicKey()!; + + final combinedWallet = ref.watch(walletCombinedProvider); + final combinedWalletNotifier = ref.watch(walletCombinedProvider.notifier); + return Scaffold( appBar: AppBar( backgroundColor: Palette.background, @@ -38,26 +66,39 @@ class WalletDashboard extends ConsumerWidget { ), ), backgroundColor: Palette.background, - drawer: NostrDrawer(pubkey: myUserPubkey!), + drawer: NostrDrawer(pubkey: myUserPubkey), body: SafeArea( child: SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: const [ - SizedBox(height: 20), - WalletAccountsCard(title: "hi"), - SizedBox(height: 30), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: WalletFriendsStrip(), - ), - SizedBox(height: 30), - WalletActionsStrip(), - SizedBox(height: 30), - //PaymentHistoryShort(), - ]), + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 20), + WalletAccountsCard( + title: "mainW", + nfcAnimController: _nfcAnimController, + alias: "testwallet", + mainCurrencyCode: "sat", + mainCurrencySymbol: "₿", + mainCurrencyValue: combinedWallet.combinedAmount.toDouble(), + ), + const SizedBox(height: 30), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: WalletFriendsStrip(), + ), + const SizedBox(height: 30), + const WalletActionsStrip(), + const SizedBox(height: 30), + TextButton( + onPressed: () { + combinedWalletNotifier.fundWallet(); + }, + child: Text("invoke")) + //PaymentHistoryShort(), + ], + ), ), ), ); diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart new file mode 100644 index 00000000..7a0ef24f --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -0,0 +1,77 @@ +import 'package:camelus/presentation_layer/providers/db_ndk_provider.dart'; +import 'package:camelus/presentation_layer/providers/ndk_provider.dart'; +import 'package:ndk/domain_layer/usecases/cashu_wallet/cashu_wallet_account.dart'; +import 'package:ndk/ndk.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:serverpod_flutter/serverpod_flutter.dart'; + +/// class for to save state for each post if it is reposted or not +class WalletCombinedState { + final int combinedAmount; + + WalletCombinedState({ + required this.combinedAmount, + }); + + WalletCombinedState copyWith({ + int? combinedAmount, + }) { + return WalletCombinedState( + combinedAmount: combinedAmount ?? this.combinedAmount, + ); + } +} + +final walletCombinedProvider = StateNotifierProvider.autoDispose< + WalletCombinedStateNotifier, WalletCombinedState>( + (ref) { + final ndk = ref.watch(ndkProvider); + final ndkDb = ref.watch(dbNdkProvider)!; + return WalletCombinedStateNotifier(ndk, ndkDb); + }, +); + +class WalletCombinedStateNotifier extends StateNotifier { + final Ndk _ndk; + final CacheManager ndkDb; + + WalletCombinedStateNotifier( + this._ndk, + this.ndkDb, + ) : super(WalletCombinedState(combinedAmount: 0)) { + _initializeState(); + } + + Future _initializeState() async { + final sub = _ndk.wallet.balances.listen((data) { + final satValue = data['sat']; + state = state.copyWith(combinedAmount: satValue); + }); + + sub.onDone(() => sub.cancel()); + } + + fundWallet() async { + /// todo acc management in wallet usecase + /// just testing + final myAcc = CashuWalletAccount( + id: "myid", + name: "test", + type: WalletAccountType.CASHU, + unit: "sat", + cashuWallet: _ndk.cashuWallet, + mintUrl: "http://$localhost:8085", + cacheManager: ndkDb, + ); + + myAcc.pendingTransactions.listen((pending) { + print(pending); + }); + + _ndk.wallet.addAccount(myAcc); + final draftTransaction = await myAcc.initiateFund(amount: 50); + final transaction = + await myAcc.retriveFunds(draftTransaction: draftTransaction); + print(transaction); + } +} diff --git a/pubspec.lock b/pubspec.lock index 08f35a90..9d647c98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -285,6 +285,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + cbor: + dependency: transitive + description: + name: cbor + sha256: f5239dd6b6ad24df67d1449e87d7180727d6f43b87b3c9402e6398c7a2d9609b + url: "https://pub.dev" + source: hosted + version: "6.3.7" characters: dependency: transitive description: @@ -958,6 +966,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + ieee754: + dependency: transitive + description: + name: ieee754 + sha256: "7d87451c164a56c156180d34a4e93779372edd191d2c219206100b976203128c" + url: "https://pub.dev" + source: hosted + version: "1.0.3" image: dependency: "direct main" description: @@ -1198,12 +1214,10 @@ packages: ndk: dependency: "direct main" description: - path: "packages/ndk" - ref: tmp-camelus-1-7-2 - resolved-ref: "1dbd6bc0252febb08ff8a82a2239d98253a05fb4" - url: "https://github.com/relaystr/ndk.git" - source: git - version: "0.4.1" + path: "../ndk/packages/ndk" + relative: true + source: path + version: "0.5.0" ndk_amber: dependency: "direct main" description: @@ -1215,10 +1229,9 @@ packages: ndk_objectbox: dependency: "direct main" description: - name: ndk_objectbox - sha256: "8dddde6f0b93e0ae70e65a650a50836158b1640eddf6b52cbafaf093940c2766" - url: "https://pub.dev" - source: hosted + path: "../ndk/packages/objectbox" + relative: true + source: path version: "0.2.5" ndk_rust_verifier: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index cb59bfee..5956da48 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -80,7 +80,7 @@ dependencies: app_links: ^6.3.2 phosphor_flutter: ^2.1.0 ## ndk imnports - ndk: ^0.4.1 + ndk: ^0.5.0 ndk_rust_verifier: ^0.4.0 ndk_amber: ^0.3.1 ndk_objectbox: ^0.2.5 @@ -106,18 +106,18 @@ dependencies: dependency_overrides: ndk: - #path: ../ndk/packages/ndk - git: - url: https://github.com/relaystr/ndk.git - path: packages/ndk - ref: tmp-camelus-1-7-2 + path: ../ndk/packages/ndk + #git: + # url: https://github.com/relaystr/ndk.git + # path: packages/ndk + # ref: tmp-camelus-1-7-2 #ndk_rust_verifier: # path: ../ndk/packages/rust_verifier #git: # url: https://github.com/relaystr/ndk.git # path: packages/rust_verifier - #ndk_objectbox: - #path: ../ndk/packages/objectbox + ndk_objectbox: + path: ../ndk/packages/objectbox #git: # url: https://github.com/relaystr/ndk.git # path: packages/objectbox From 7fae02b26d1e04ae8d0531826de94dcae66f3aa7 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:28:35 +0200 Subject: [PATCH 04/52] amount proof of concept --- .../wallet_combined_state_provider.dart | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart index 7a0ef24f..d46fb606 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -1,6 +1,6 @@ import 'package:camelus/presentation_layer/providers/db_ndk_provider.dart'; import 'package:camelus/presentation_layer/providers/ndk_provider.dart'; -import 'package:ndk/domain_layer/usecases/cashu_wallet/cashu_wallet_account.dart'; +import 'package:ndk/entities.dart'; import 'package:ndk/ndk.dart'; import 'package:riverpod/riverpod.dart'; import 'package:serverpod_flutter/serverpod_flutter.dart'; @@ -43,35 +43,37 @@ class WalletCombinedStateNotifier extends StateNotifier { } Future _initializeState() async { - final sub = _ndk.wallet.balances.listen((data) { - final satValue = data['sat']; - state = state.copyWith(combinedAmount: satValue); + final sub = _ndk.wallets.combinedBalances.listen((data) { + try { + final satValue = data.firstWhere((d) => d.unit == "sat"); + state = state.copyWith(combinedAmount: satValue.amount); + } catch (_) {} }); sub.onDone(() => sub.cancel()); + + _ndk.wallets.addWallet(CashuWallet( + id: "http://$localhost:8085", + name: "$localhost:8085", + supportedUnits: {"sat"}, + mintUrl: "http://$localhost:8085", + type: WalletType.CASHU, + )); } fundWallet() async { /// todo acc management in wallet usecase /// just testing - final myAcc = CashuWalletAccount( - id: "myid", - name: "test", - type: WalletAccountType.CASHU, - unit: "sat", - cashuWallet: _ndk.cashuWallet, - mintUrl: "http://$localhost:8085", - cacheManager: ndkDb, - ); - myAcc.pendingTransactions.listen((pending) { - print(pending); - }); + final draftTransaction = await _ndk.cashu.initiateFund( + mintUrl: "http://$localhost:8085", + amount: 10, + unit: "sat", + method: "bolt11"); - _ndk.wallet.addAccount(myAcc); - final draftTransaction = await myAcc.initiateFund(amount: 50); - final transaction = - await myAcc.retriveFunds(draftTransaction: draftTransaction); + final transaction = await _ndk.cashu + .retriveFunds(draftTransaction: draftTransaction) + .toList(); print(transaction); } } From a5aa05fe71f1ee278936c0c7ca63cb9194c88e4c Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:15:09 +0200 Subject: [PATCH 05/52] payment history short --- .../wallet/payment_history_short.dart | 150 ++++++++++++++++++ .../routes/wallet/wallet_dashboard.dart | 14 +- .../wallet_combined_state_provider.dart | 40 ++++- 3 files changed, 193 insertions(+), 11 deletions(-) create mode 100644 lib/presentation_layer/components/wallet/payment_history_short.dart diff --git a/lib/presentation_layer/components/wallet/payment_history_short.dart b/lib/presentation_layer/components/wallet/payment_history_short.dart new file mode 100644 index 00000000..25e0e95d --- /dev/null +++ b/lib/presentation_layer/components/wallet/payment_history_short.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:timeago/timeago.dart' as timeago; + +class PaymentHistoryShort extends StatelessWidget { + final List transactions; + final int? maxItems; + final void Function(ndk_entities.WalletTransaction tx)? onTap; + final bool showDividers; + final String emptyText; + final double? height; + final ScrollPhysics? physics; + final ScrollController? controller; + + const PaymentHistoryShort({ + super.key, + required this.transactions, + this.maxItems, + this.onTap, + this.showDividers = true, + this.emptyText = 'no transactions yet', + + /// default height + this.height = 300, + this.physics, + this.controller, + }); + + @override + Widget build(BuildContext context) { + if (transactions.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Center(child: Text(emptyText)), + ); + } + + final items = List.from(transactions); + items.sort((a, b) => (_bestDate(b) ?? 0).compareTo(_bestDate(a) ?? 0)); + final visible = maxItems == null ? items : items.take(maxItems!).toList(); + + final list = ListView.separated( + controller: controller, + physics: physics, + padding: EdgeInsets.zero, + itemCount: visible.length, + separatorBuilder: (_, __) => + showDividers ? const Divider(height: 1) : const SizedBox.shrink(), + itemBuilder: (context, index) { + final tx = visible[index]; + final isIncoming = tx.changeAmount >= 0; + final color = isIncoming ? Colors.green : Colors.red; + final icon = isIncoming + ? Icons.arrow_downward_rounded + : Icons.arrow_upward_rounded; + + final dateToUse = _bestDate(tx); + final now = DateTime.now(); + final transactionDate = dateToUse != null + ? DateTime.fromMillisecondsSinceEpoch(dateToUse * 1000) + : null; + final difference = transactionDate != null + ? now.difference(transactionDate) + : Duration.zero; + + final String transactionDateText; + if (difference.inDays < 1) { + transactionDateText = + transactionDate != null ? timeago.format(transactionDate) : ''; + } else { + transactionDateText = transactionDate != null + ? DateFormat('MMM d, yyyy').format(transactionDate) + : ''; + } + final amountStr = _formatAmount(tx.changeAmount, tx.unit); + + return ListTile( + onTap: onTap == null ? null : () => onTap!(tx), + leading: CircleAvatar( + backgroundColor: color.withOpacity(0.1), + child: Icon(icon, color: color), + ), + title: Text( + '${isIncoming ? 'Incoming' : 'Outgoing'} - ${tx.walletType}', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + subtitle: Text( + [ + transactionDateText, + _removeHttpPrefix(tx.walletId), + //_enumLabel(tx.state), + ].where((e) => e.isNotEmpty).join(' • '), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + amountStr, + style: TextStyle( + fontWeight: FontWeight.w700, + color: color, + ), + ), + if (tx.completionMsg != null && tx.completionMsg!.isNotEmpty) + Text( + tx.completionMsg!, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + }, + ); + + return height != null ? SizedBox(height: height, child: list) : list; + } + + static int? _bestDate(ndk_entities.WalletTransaction tx) => + tx.transactionDate ?? tx.initiatedDate; + + static String _formatAmount(int change, String unit) { + final sign = change >= 0 ? '+' : '-'; + final abs = change.abs(); + final withSeparators = + abs.toString().replaceAll(RegExp(r'\B(?=(\d{3})+(?!\d))'), ','); + return '$sign$withSeparators $unit'; + } + + static String _enumLabel(Object? value) { + if (value == null) return ''; + final s = value.toString(); + final dot = s.indexOf('.'); + return dot >= 0 ? s.substring(dot + 1) : s; + } + + static String _removeHttpPrefix(String url) { + if (url.startsWith('http://')) { + return url.substring(7); + } else if (url.startsWith('https://')) { + return url.substring(8); + } + return url; + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index 55de352b..a0bef9b0 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../config/palette.dart'; import '../../atoms/my_profile_picture.dart'; +import '../../components/wallet/payment_history_short.dart'; import '../../components/wallet/wallet_actions_strip.dart'; import '../../components/wallet/wallet_friends_strip.dart'; import '../../components/wallet/wallet_accounts_card.dart'; @@ -92,11 +93,14 @@ class _WalletDashboardState extends ConsumerState const WalletActionsStrip(), const SizedBox(height: 30), TextButton( - onPressed: () { - combinedWalletNotifier.fundWallet(); - }, - child: Text("invoke")) - //PaymentHistoryShort(), + onPressed: () { + combinedWalletNotifier.fundWallet(); + }, + child: Text("invoke"), + ), + PaymentHistoryShort( + transactions: combinedWallet.recentTransactions, + ), ], ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart index d46fb606..032ead48 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -1,23 +1,33 @@ -import 'package:camelus/presentation_layer/providers/db_ndk_provider.dart'; -import 'package:camelus/presentation_layer/providers/ndk_provider.dart'; -import 'package:ndk/entities.dart'; +import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; + import 'package:riverpod/riverpod.dart'; import 'package:serverpod_flutter/serverpod_flutter.dart'; +import '../../../providers/db_ndk_provider.dart'; +import '../../../providers/ndk_provider.dart'; + /// class for to save state for each post if it is reposted or not class WalletCombinedState { final int combinedAmount; + final List recentTransactions; + final List pendingTransactions; WalletCombinedState({ required this.combinedAmount, + required this.recentTransactions, + required this.pendingTransactions, }); WalletCombinedState copyWith({ int? combinedAmount, + List? recentTransactions, + List? pendingTransactions, }) { return WalletCombinedState( combinedAmount: combinedAmount ?? this.combinedAmount, + recentTransactions: recentTransactions ?? this.recentTransactions, + pendingTransactions: pendingTransactions ?? this.pendingTransactions, ); } } @@ -38,7 +48,13 @@ class WalletCombinedStateNotifier extends StateNotifier { WalletCombinedStateNotifier( this._ndk, this.ndkDb, - ) : super(WalletCombinedState(combinedAmount: 0)) { + ) : super( + WalletCombinedState( + combinedAmount: 0, + recentTransactions: [], + pendingTransactions: [], + ), + ) { _initializeState(); } @@ -52,12 +68,24 @@ class WalletCombinedStateNotifier extends StateNotifier { sub.onDone(() => sub.cancel()); - _ndk.wallets.addWallet(CashuWallet( + final sub1 = _ndk.wallets.combinedRecentTransactions.listen((data) { + state = state.copyWith(recentTransactions: data); + }); + + sub1.onDone(() => sub1.cancel()); + + final sub2 = _ndk.wallets.combinedPendingTransactions.listen((data) { + state = state.copyWith(pendingTransactions: data); + }); + + sub2.onDone(() => sub2.cancel()); + + _ndk.wallets.addWallet(ndk_entities.CashuWallet( id: "http://$localhost:8085", name: "$localhost:8085", supportedUnits: {"sat"}, mintUrl: "http://$localhost:8085", - type: WalletType.CASHU, + type: ndk_entities.WalletType.CASHU, )); } From 3fdffda5384a232563517dde28212513e0acb084 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:11:15 +0200 Subject: [PATCH 06/52] wallet state --- .../wallet_combined_state_provider.dart | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart index 032ead48..d13cf9ae 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -9,25 +9,29 @@ import '../../../providers/ndk_provider.dart'; /// class for to save state for each post if it is reposted or not class WalletCombinedState { - final int combinedAmount; + final List balances; + final List wallets; final List recentTransactions; final List pendingTransactions; WalletCombinedState({ - required this.combinedAmount, + required this.balances, required this.recentTransactions, required this.pendingTransactions, + required this.wallets, }); WalletCombinedState copyWith({ - int? combinedAmount, + List? balances, List? recentTransactions, List? pendingTransactions, + List? wallets, }) { return WalletCombinedState( - combinedAmount: combinedAmount ?? this.combinedAmount, + balances: balances ?? this.balances, recentTransactions: recentTransactions ?? this.recentTransactions, pendingTransactions: pendingTransactions ?? this.pendingTransactions, + wallets: wallets ?? this.wallets, ); } } @@ -50,20 +54,17 @@ class WalletCombinedStateNotifier extends StateNotifier { this.ndkDb, ) : super( WalletCombinedState( - combinedAmount: 0, - recentTransactions: [], - pendingTransactions: [], - ), + balances: [], + recentTransactions: [], + pendingTransactions: [], + wallets: []), ) { _initializeState(); } Future _initializeState() async { final sub = _ndk.wallets.combinedBalances.listen((data) { - try { - final satValue = data.firstWhere((d) => d.unit == "sat"); - state = state.copyWith(combinedAmount: satValue.amount); - } catch (_) {} + state = state.copyWith(balances: data); }); sub.onDone(() => sub.cancel()); @@ -80,13 +81,10 @@ class WalletCombinedStateNotifier extends StateNotifier { sub2.onDone(() => sub2.cancel()); - _ndk.wallets.addWallet(ndk_entities.CashuWallet( - id: "http://$localhost:8085", - name: "$localhost:8085", - supportedUnits: {"sat"}, - mintUrl: "http://$localhost:8085", - type: ndk_entities.WalletType.CASHU, - )); + final sub3 = _ndk.wallets.walletsStream.listen((data) { + state = state.copyWith(wallets: data); + }); + sub3.onDone(() => sub3.cancel()); } fundWallet() async { From b261a625393a4154142c8a8bf027cbd41e66dc9a Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:12:01 +0200 Subject: [PATCH 07/52] generic wallet card --- .../wallet_account_card_placeholder.dart | 38 +++++++++ .../wallet/wallet_accounts_card.dart | 82 ++++--------------- .../routes/wallet/wallet_dashboard.dart | 12 +-- 3 files changed, 61 insertions(+), 71 deletions(-) create mode 100644 lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart diff --git a/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart b/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart new file mode 100644 index 00000000..1e2f122c --- /dev/null +++ b/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../../../config/palette.dart'; + +class WalletAccountCardPlaceholder extends StatelessWidget { + const WalletAccountCardPlaceholder({ + super.key, + this.onTap, + }); + + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Palette.extraDarkGray, + borderRadius: BorderRadius.circular(18.0), + border: Border.all( + width: 1, + color: Palette.gray, + ), + ), + child: Center( + child: Text( + 'add wallet +', + style: TextStyle( + fontSize: 16, + color: Colors.grey[700], + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart index 65d29693..fc2476aa 100644 --- a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart +++ b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart @@ -1,30 +1,31 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lottie/lottie.dart'; import 'package:intl/intl.dart'; -class WalletAccountsCard extends StatelessWidget { +import 'package:ndk/entities.dart' as ndk_entities; + +class WalletAccountsCard extends ConsumerWidget { const WalletAccountsCard({ super.key, + required this.walletId, required this.title, required this.nfcAnimController, required this.alias, - required this.mainCurrencySymbol, - required this.mainCurrencyValue, - required this.mainCurrencyCode, + required this.balances, }); + final String walletId; final String title; final String alias; final AnimationController nfcAnimController; /// ₿ - final String mainCurrencySymbol; - final double mainCurrencyValue; - final String mainCurrencyCode; + final List balances; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final satFormat = NumberFormat("#,##0.##", "de_DE"); final eurFormat = NumberFormat("#,##0.00", "de_DE"); @@ -82,64 +83,15 @@ class WalletAccountsCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - mainCurrencySymbol, - style: TextStyle( - fontSize: 37, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - Text( - '${satFormat.format(mainCurrencyValue)}', - style: const TextStyle( - fontSize: 37, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - Text( - mainCurrencyCode, - style: TextStyle( - fontSize: 25, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + for (final balance in balances) + Text( + "${satFormat.format(balance.amount)} ${balance.unit}", + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.normal, + color: Colors.white60, ), - ], - ), - const SizedBox(height: 0), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Text( - "€", - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.normal, - color: Colors.white60, - ), - ), - Text( - eurFormat.format(20.5), - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: Colors.white60, - ), - ), - const Text( - "eur", - style: TextStyle( - fontSize: 25, - fontWeight: FontWeight.normal, - color: Colors.white60, - ), - ), - ]), + ), ], ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index a0bef9b0..39813b9f 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -8,6 +8,7 @@ import '../../components/wallet/payment_history_short.dart'; import '../../components/wallet/wallet_actions_strip.dart'; import '../../components/wallet/wallet_friends_strip.dart'; import '../../components/wallet/wallet_accounts_card.dart'; +import '../../components/wallet/wallets_carusel.dart'; import '../../providers/metadata_state_provider.dart'; import '../../providers/ndk_provider.dart'; import '../nostr/nostr_drawer.dart'; @@ -45,6 +46,8 @@ class _WalletDashboardState extends ConsumerState final combinedWallet = ref.watch(walletCombinedProvider); final combinedWalletNotifier = ref.watch(walletCombinedProvider.notifier); + final wallets = combinedWallet.wallets; + return Scaffold( appBar: AppBar( backgroundColor: Palette.background, @@ -76,13 +79,10 @@ class _WalletDashboardState extends ConsumerState mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 20), - WalletAccountsCard( - title: "mainW", + WalletsCarousel( + wallets: wallets, + balances: combinedWallet.balances, nfcAnimController: _nfcAnimController, - alias: "testwallet", - mainCurrencyCode: "sat", - mainCurrencySymbol: "₿", - mainCurrencyValue: combinedWallet.combinedAmount.toDouble(), ), const SizedBox(height: 30), const Padding( From 101a36351c7e3d6224aba9377d1ffeb52645af15 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:38:10 +0200 Subject: [PATCH 08/52] wallets carousel --- .../components/wallet/wallets_carousel.dart | 136 ++++++++++++++++++ .../routes/wallet/wallet_dashboard.dart | 2 +- 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 lib/presentation_layer/components/wallet/wallets_carousel.dart diff --git a/lib/presentation_layer/components/wallet/wallets_carousel.dart b/lib/presentation_layer/components/wallet/wallets_carousel.dart new file mode 100644 index 00000000..83de49c1 --- /dev/null +++ b/lib/presentation_layer/components/wallet/wallets_carousel.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import 'wallet_account_card_placeholder.dart'; +import 'wallet_accounts_card.dart'; + +class WalletsCarousel extends StatefulWidget { + final List wallets; + final List balances; + final AnimationController? nfcAnimController; + + const WalletsCarousel({ + super.key, + required this.wallets, + required this.balances, + this.nfcAnimController, + }); + + @override + State createState() => _WalletsCarouselState(); +} + +class _WalletsCarouselState extends State { + late PageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = PageController( + viewportFraction: _viewportForCount(widget.wallets.length), + ); + } + + @override + void didUpdateWidget(covariant WalletsCarousel oldWidget) { + super.didUpdateWidget(oldWidget); + if ((oldWidget.wallets.length > 1) != (widget.wallets.length > 1)) { + _pageController.dispose(); + _pageController = PageController( + viewportFraction: _viewportForCount(widget.wallets.length), + ); + setState(() {}); + } + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + double _viewportForCount(int count) { + // show a peek of the next card + return count <= 1 ? 1.0 : 0.9; + } + + @override + Widget build(BuildContext context) { + final wallets = widget.wallets; + + if (wallets.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + height: 210, + child: _CardMaxWidth( + child: WalletAccountCardPlaceholder(), + ), + ), + ); + } + + final hasMultiple = wallets.length > 1; + + return SizedBox( + height: 210, + child: PageView.builder( + controller: _pageController, + physics: hasMultiple + ? const BouncingScrollPhysics() + : const NeverScrollableScrollPhysics(), + itemCount: wallets.length, + padEnds: false, + itemBuilder: (context, index) { + final w = wallets[index]; + + final String? mintUrl; + if (w is ndk_entities.CashuWallet) { + mintUrl = w.mintUrl; + } else { + mintUrl = null; + } + + final title = + w.name.isNotEmpty ? w.name : mintUrl ?? 'Wallet ${index + 1}'; + final alias = w.type.toString(); + + final myBalances = widget.balances + .where( + (b) => b.walletId == w.id, + ) + .toList(); + + return Padding( + // keep first left so next card peeks + padding: EdgeInsets.only( + left: index == 0 ? 16 : 8, + right: index == wallets.length - 1 ? 16 : 8, + ), + child: _CardMaxWidth( + child: WalletAccountsCard( + walletId: w.id, + title: title, + alias: alias, + balances: myBalances, + nfcAnimController: widget.nfcAnimController!), + ), + ); + }, + ), + ); + } +} + +/// prevents shrink layouts +class _CardMaxWidth extends StatelessWidget { + final Widget child; + const _CardMaxWidth({required this.child}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: child, + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index 39813b9f..455ac3a6 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -8,7 +8,7 @@ import '../../components/wallet/payment_history_short.dart'; import '../../components/wallet/wallet_actions_strip.dart'; import '../../components/wallet/wallet_friends_strip.dart'; import '../../components/wallet/wallet_accounts_card.dart'; -import '../../components/wallet/wallets_carusel.dart'; +import '../../components/wallet/wallets_carousel.dart'; import '../../providers/metadata_state_provider.dart'; import '../../providers/ndk_provider.dart'; import '../nostr/nostr_drawer.dart'; From 98de3abae73a78bc558ec635cb0a6cb248e4928b Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:31:25 +0200 Subject: [PATCH 09/52] windows build --- .metadata | 10 +- apipod/apipod_server/pubspec.lock | 86 ++++++----- pubspec.lock | 246 +++++++++++++++--------------- 3 files changed, 179 insertions(+), 163 deletions(-) diff --git a/.metadata b/.metadata index 90eabcff..95ec97b7 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819" + revision: "edada7c56edf4a183c1735310e123c7f923584f1" channel: "stable" project_type: app @@ -13,8 +13,8 @@ project_type: app migration: platforms: - platform: root - create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 - base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + create_revision: edada7c56edf4a183c1735310e123c7f923584f1 + base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - platform: android create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 @@ -31,8 +31,8 @@ migration: create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 - platform: windows - create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 - base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + create_revision: edada7c56edf4a183c1735310e123c7f923584f1 + base_revision: edada7c56edf4a183c1735310e123c7f923584f1 # User provided section diff --git a/apipod/apipod_server/pubspec.lock b/apipod/apipod_server/pubspec.lock index 8ff09351..fa78c101 100644 --- a/apipod/apipod_server/pubspec.lock +++ b/apipod/apipod_server/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "88.0.0" adaptive_number: dependency: transitive description: @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "8.1.1" args: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: e02d018628c870ef2d7f03e33f9ad179d89ff6ec52ca6c56bcb80bcef979867f + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" url: "https://pub.dev" source: hosted - version: "1.6.2" + version: "1.6.5" async: dependency: transitive description: @@ -105,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -133,10 +141,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.15.0" crypto: dependency: transitive description: @@ -237,10 +245,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" frontend_server_client: dependency: transitive description: @@ -261,10 +269,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.3+1" googleapis: dependency: transitive description: @@ -301,10 +309,10 @@ packages: dependency: "direct main" description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" http_multi_server: dependency: transitive description: @@ -373,10 +381,10 @@ packages: dependency: transitive description: name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.1" logging: dependency: transitive description: @@ -397,10 +405,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -421,10 +429,10 @@ packages: dependency: "direct main" description: name: ndk - sha256: "2f64d055e2a656ab523e3d804280e81155a14c029d16d42b804de41ac7f815c2" + sha256: c084ec0d4f630a99b4a80fe092fb4f339db36c6f53c180431a57a69ea3b14188 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" node_preamble: dependency: transitive description: @@ -485,10 +493,10 @@ packages: dependency: transitive description: name: postgres - sha256: "7a7f9805d33e41cb14fa22535959f3af51843a792015cdbd8f2fa78bcd1b501b" + sha256: "9aaa6f4872956adef653535a4e2133b167465c1a68c22b9bd0744ef1244e9393" url: "https://pub.dev" source: hosted - version: "3.5.4" + version: "3.5.6" pub_semver: dependency: transitive description: @@ -693,10 +701,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" system_resources: dependency: transitive description: @@ -717,26 +725,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.12" typed_data: dependency: transitive description: @@ -749,10 +757,10 @@ packages: dependency: transitive description: name: unorm_dart - sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" + sha256: "8e3870a1caa60bde8352f9597dd3535d8068613269444f8e35ea8925ec84c1f5" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1+1" uuid: dependency: transitive description: @@ -765,18 +773,18 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: transitive description: @@ -789,10 +797,10 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: "direct main" description: @@ -842,4 +850,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.8.0 <4.0.0" diff --git a/pubspec.lock b/pubspec.lock index 9d647c98..d379ea39 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "7fd72d77a7487c26faab1d274af23fb008763ddc10800261abbfb2c067f183d5" + sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 url: "https://pub.dev" source: hosted - version: "1.3.53" + version: "1.3.59" _macros: dependency: transitive description: dart @@ -89,10 +89,10 @@ packages: dependency: transitive description: name: archive - sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742" + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.7" args: dependency: transitive description: @@ -113,10 +113,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "068190d6c99c436287936ba5855af2e1fa78d8083ae65b4db6a281780da727ae" + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" url: "https://pub.dev" source: hosted - version: "1.6.0" + version: "1.6.5" async: dependency: transitive description: @@ -169,10 +169,10 @@ packages: dependency: "direct main" description: name: bip39_mnemonic - sha256: e36c1f1fecd938d433b8f7a1e74d5bc721e16816b59dbac5b31307f079d3b3bc + sha256: e280b785d40dda3f70186d5a51126c5d3ae2c3b9a1bafdbd20a94b50c8253fb3 url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" boolean_selector: dependency: transitive description: @@ -193,10 +193,10 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.4" build_cli_annotations: dependency: transitive description: @@ -225,26 +225,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.5.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.1.2" built_collection: dependency: transitive description: @@ -257,10 +257,10 @@ packages: dependency: transitive description: name: built_value - sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb url: "https://pub.dev" source: hosted - version: "8.9.5" + version: "8.11.1" cached_network_image: dependency: "direct main" description: @@ -305,10 +305,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" ci: dependency: transitive description: @@ -317,6 +317,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" cli_util: dependency: transitive description: @@ -353,10 +361,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" connectivity_plus_platform_interface: dependency: transitive description: @@ -377,10 +385,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.15.0" crop_your_image: dependency: "direct main" description: @@ -497,10 +505,10 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "7.0.3" elliptic: dependency: transitive description: @@ -553,10 +561,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "8d938fd5c11dc81bf1acd4f7f0486c683fe9e79a0b13419e27730f9ce4d8a25b" + sha256: "09b474c0c8117484b80cbebc043801ff91e05cfbd2874d512825c899e1754694" url: "https://pub.dev" source: hosted - version: "9.2.1" + version: "9.2.3" file_selector_linux: dependency: transitive description: @@ -569,10 +577,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" url: "https://pub.dev" source: hosted - version: "0.9.4+2" + version: "0.9.4+3" file_selector_platform_interface: dependency: transitive description: @@ -593,50 +601,50 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: f4d8f49574a4e396f34567f3eec4d38ab9c3910818dec22ca42b2a467c685d8b + sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" url: "https://pub.dev" source: hosted - version: "3.12.1" + version: "3.15.2" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: faa5a76f6380a9b90b53bc3bdcb85bc7926a382e0709b9b5edac9f7746651493 + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" url: "https://pub.dev" source: hosted - version: "2.21.1" + version: "2.24.1" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "5fc345c6341f9dc69fd0ffcbf508c784fd6d1b9e9f249587f30434dd8b6aa281" + sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc" url: "https://pub.dev" source: hosted - version: "15.2.4" + version: "15.2.10" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: a935924cf40925985c8049df4968b1dde5c704f570f3ce380b31d3de6990dd94 + sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754" url: "https://pub.dev" source: hosted - version: "4.6.4" + version: "4.6.10" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: fafebf6a1921931334f3f10edb5037a5712288efdd022881e2d093e5654a2fd4 + sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390" url: "https://pub.dev" source: hosted - version: "3.10.4" + version: "3.10.10" fixnum: dependency: transitive description: @@ -699,10 +707,10 @@ packages: dependency: "direct main" description: name: flutter_launcher_icons - sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.14.3" + version: "0.14.4" flutter_link_previewer: dependency: "direct main" description: @@ -739,10 +747,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: b94a50aabbe56ef254f95f3be75640f99120429f0a153b2dc30143cffc9bfdf3 + sha256: "20ca0a9c82ce0c855ac62a2e580ab867f3fbea82680a90647f7953832d0850ae" url: "https://pub.dev" source: hosted - version: "19.2.1" + version: "19.4.0" flutter_local_notifications_linux: dependency: transitive description: @@ -755,18 +763,18 @@ packages: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c" + sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.1.0" flutter_local_notifications_windows: dependency: transitive description: name: flutter_local_notifications_windows - sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e + sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" flutter_mentions: dependency: "direct main" description: @@ -779,10 +787,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab" url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.0.29" flutter_portal: dependency: transitive description: @@ -819,10 +827,10 @@ packages: dependency: transitive description: name: flutter_secure_storage_linux - sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705 + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" flutter_secure_storage_macos: dependency: transitive description: @@ -859,10 +867,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -946,10 +954,10 @@ packages: dependency: "direct main" description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" http_multi_server: dependency: transitive description: @@ -978,10 +986,10 @@ packages: dependency: "direct main" description: name: image - sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.5.4" image_picker: dependency: "direct main" description: @@ -994,10 +1002,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9" + sha256: b08e9a04d0f8d91f4a6e767a745b9871bfbc585410205c311d0492de20a7ccd6 url: "https://pub.dev" source: hosted - version: "0.8.12+22" + version: "0.8.12+25" image_picker_for_web: dependency: transitive description: @@ -1135,10 +1143,10 @@ packages: dependency: transitive description: name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.1" logging: dependency: transitive description: @@ -1222,10 +1230,10 @@ packages: dependency: "direct main" description: name: ndk_amber - sha256: "19ae227ae41e93147e54f099aeab03a94f7a260d5d31098856c05430d30e681e" + sha256: "757dce858237ffc6b1b2e1362ec9e633d5218a6c4fea450d7f5b5d6783543df6" url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.3.2" ndk_objectbox: dependency: "direct main" description: @@ -1301,18 +1309,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" url: "https://pub.dev" source: hosted - version: "8.3.0" + version: "8.3.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" path: dependency: "direct main" description: @@ -1341,10 +1349,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -1437,10 +1445,10 @@ packages: dependency: transitive description: name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.3" process: dependency: transitive description: @@ -1557,34 +1565,34 @@ packages: dependency: "direct main" description: name: share_plus - sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0 + sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 url: "https://pub.dev" source: hosted - version: "11.0.0" + version: "11.1.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef" + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.11" shared_preferences_foundation: dependency: transitive description: @@ -1730,10 +1738,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" url: "https://pub.dev" source: hosted - version: "2.5.5" + version: "2.5.6" sqflite_darwin: dependency: transitive description: @@ -1802,10 +1810,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" term_glyph: dependency: transitive description: @@ -1842,10 +1850,10 @@ packages: dependency: transitive description: name: timeago - sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de" + sha256: b05159406a97e1cbb2b9ee4faa9fb096fe0e2dfcd8b08fcd2a00553450d3422e url: "https://pub.dev" source: hosted - version: "3.7.0" + version: "3.7.1" timeago_flutter: dependency: "direct main" description: @@ -1858,10 +1866,10 @@ packages: dependency: transitive description: name: timezone - sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.1" timing: dependency: transitive description: @@ -1882,34 +1890,34 @@ packages: dependency: transitive description: name: unorm_dart - sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" + sha256: "8e3870a1caa60bde8352f9597dd3535d8068613269444f8e35ea8925ec84c1f5" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1+1" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" url: "https://pub.dev" source: hosted - version: "6.3.15" + version: "6.3.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -1938,10 +1946,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1962,10 +1970,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -1978,10 +1986,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.17" vector_math: dependency: transitive description: @@ -2002,34 +2010,34 @@ packages: dependency: transitive description: name: video_player_android - sha256: "4a5135754a62dbc827a64a42ef1f8ed72c962e191c97e2d48744225c2b9ebb73" + sha256: d26f8791c8f670825cc227e2cad4319d2ac02b71b2ad5c2b67786bb873ac43b1 url: "https://pub.dev" source: hosted - version: "2.8.7" + version: "2.8.11" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f" + sha256: f52261d11f97bf14c43e8ed5714f71d8ce4538552b8cc87f45e5d87d3c205e41 url: "https://pub.dev" source: hosted - version: "2.7.1" + version: "2.8.3" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 + sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.4.0" video_player_web: dependency: transitive description: name: video_player_web - sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.0" visibility_detector: dependency: "direct main" description: @@ -2050,10 +2058,10 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: transitive description: @@ -2066,10 +2074,10 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: "direct main" description: @@ -2106,10 +2114,10 @@ packages: dependency: transitive description: name: win32 - sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "5.14.0" win32_registry: dependency: transitive description: @@ -2151,5 +2159,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.32.0" From 5c91b4a782deeeee4e4b7dd570e03c518844241f Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:08:16 +0200 Subject: [PATCH 10/52] wallet state cleanup --- .../wallet_combined_state_provider.dart | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart index d13cf9ae..c3d09c13 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; @@ -41,6 +43,7 @@ final walletCombinedProvider = StateNotifierProvider.autoDispose< (ref) { final ndk = ref.watch(ndkProvider); final ndkDb = ref.watch(dbNdkProvider)!; + return WalletCombinedStateNotifier(ndk, ndkDb); }, ); @@ -49,6 +52,8 @@ class WalletCombinedStateNotifier extends StateNotifier { final Ndk _ndk; final CacheManager ndkDb; + final _subscriptions = []; + WalletCombinedStateNotifier( this._ndk, this.ndkDb, @@ -62,29 +67,29 @@ class WalletCombinedStateNotifier extends StateNotifier { _initializeState(); } - Future _initializeState() async { - final sub = _ndk.wallets.combinedBalances.listen((data) { - state = state.copyWith(balances: data); - }); - - sub.onDone(() => sub.cancel()); - - final sub1 = _ndk.wallets.combinedRecentTransactions.listen((data) { - state = state.copyWith(recentTransactions: data); - }); - - sub1.onDone(() => sub1.cancel()); - - final sub2 = _ndk.wallets.combinedPendingTransactions.listen((data) { - state = state.copyWith(pendingTransactions: data); - }); - - sub2.onDone(() => sub2.cancel()); + @override + void dispose() { + for (final subscription in _subscriptions) { + subscription.cancel(); + } + super.dispose(); + } - final sub3 = _ndk.wallets.walletsStream.listen((data) { - state = state.copyWith(wallets: data); - }); - sub3.onDone(() => sub3.cancel()); + Future _initializeState() async { + _subscriptions.addAll([ + _ndk.wallets.combinedBalances.listen((data) { + state = state.copyWith(balances: data); + }), + _ndk.wallets.combinedRecentTransactions.listen((data) { + state = state.copyWith(recentTransactions: data); + }), + _ndk.wallets.combinedPendingTransactions.listen((data) { + state = state.copyWith(pendingTransactions: data); + }), + _ndk.wallets.walletsStream.listen((data) { + state = state.copyWith(wallets: data); + }), + ]); } fundWallet() async { From b1e15b727df95e3e203e1a7e98ed68f2a772a5e7 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:08:47 +0200 Subject: [PATCH 11/52] slivers in payment history --- .../wallet/payment_history_short.dart | 189 +++++++++++------- .../routes/wallet/wallet_dashboard.dart | 2 + 2 files changed, 117 insertions(+), 74 deletions(-) diff --git a/lib/presentation_layer/components/wallet/payment_history_short.dart b/lib/presentation_layer/components/wallet/payment_history_short.dart index 25e0e95d..9b0f8d7c 100644 --- a/lib/presentation_layer/components/wallet/payment_history_short.dart +++ b/lib/presentation_layer/components/wallet/payment_history_short.dart @@ -3,8 +3,11 @@ import 'package:intl/intl.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:timeago/timeago.dart' as timeago; +import '../../../config/palette.dart'; + class PaymentHistoryShort extends StatelessWidget { final List transactions; + final List pendingTransactions; final int? maxItems; final void Function(ndk_entities.WalletTransaction tx)? onTap; final bool showDividers; @@ -16,6 +19,7 @@ class PaymentHistoryShort extends StatelessWidget { const PaymentHistoryShort({ super.key, required this.transactions, + required this.pendingTransactions, this.maxItems, this.onTap, this.showDividers = true, @@ -29,96 +33,133 @@ class PaymentHistoryShort extends StatelessWidget { @override Widget build(BuildContext context) { - if (transactions.isEmpty) { + if (transactions.isEmpty && pendingTransactions.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 24.0), child: Center(child: Text(emptyText)), ); } - final items = List.from(transactions); - items.sort((a, b) => (_bestDate(b) ?? 0).compareTo(_bestDate(a) ?? 0)); - final visible = maxItems == null ? items : items.take(maxItems!).toList(); + final sortedPending = + List.from(pendingTransactions); + sortedPending + .sort((a, b) => (_bestDate(b) ?? 0).compareTo(_bestDate(a) ?? 0)); + + final sortedTransactions = + List.from(transactions); + sortedTransactions + .sort((a, b) => (_bestDate(b) ?? 0).compareTo(_bestDate(a) ?? 0)); + + final allTransactions = [...sortedPending, ...sortedTransactions]; + + final visible = maxItems == null + ? allTransactions + : allTransactions.take(maxItems!).toList(); - final list = ListView.separated( + final scrollView = CustomScrollView( controller: controller, physics: physics, - padding: EdgeInsets.zero, - itemCount: visible.length, - separatorBuilder: (_, __) => - showDividers ? const Divider(height: 1) : const SizedBox.shrink(), - itemBuilder: (context, index) { - final tx = visible[index]; - final isIncoming = tx.changeAmount >= 0; - final color = isIncoming ? Colors.green : Colors.red; - final icon = isIncoming - ? Icons.arrow_downward_rounded - : Icons.arrow_upward_rounded; - - final dateToUse = _bestDate(tx); - final now = DateTime.now(); - final transactionDate = dateToUse != null - ? DateTime.fromMillisecondsSinceEpoch(dateToUse * 1000) - : null; - final difference = transactionDate != null - ? now.difference(transactionDate) - : Duration.zero; - - final String transactionDateText; - if (difference.inDays < 1) { - transactionDateText = - transactionDate != null ? timeago.format(transactionDate) : ''; - } else { - transactionDateText = transactionDate != null - ? DateFormat('MMM d, yyyy').format(transactionDate) - : ''; - } - final amountStr = _formatAmount(tx.changeAmount, tx.unit); - - return ListTile( - onTap: onTap == null ? null : () => onTap!(tx), - leading: CircleAvatar( - backgroundColor: color.withOpacity(0.1), - child: Icon(icon, color: color), - ), - title: Text( - '${isIncoming ? 'Incoming' : 'Outgoing'} - ${tx.walletType}', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - subtitle: Text( - [ - transactionDateText, - _removeHttpPrefix(tx.walletId), - //_enumLabel(tx.state), - ].where((e) => e.isNotEmpty).join(' • '), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - amountStr, - style: TextStyle( - fontWeight: FontWeight.w700, - color: color, + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final tx = visible[index]; + final isPending = pendingTransactions.contains(tx); + final isIncoming = tx.changeAmount >= 0; + final color = isIncoming ? Colors.green : Colors.red; + final icon = isIncoming + ? Icons.arrow_downward_rounded + : Icons.arrow_upward_rounded; + + final dateToUse = _bestDate(tx); + final now = DateTime.now(); + final transactionDate = dateToUse != null + ? DateTime.fromMillisecondsSinceEpoch(dateToUse * 1000) + : null; + final difference = transactionDate != null + ? now.difference(transactionDate) + : Duration.zero; + + final String transactionDateText; + if (difference.inDays < 1) { + transactionDateText = transactionDate != null + ? timeago.format(transactionDate) + : ''; + } else { + transactionDateText = transactionDate != null + ? DateFormat('MMM d, yyyy').format(transactionDate) + : ''; + } + + final amountStr = _formatAmount(tx.changeAmount, tx.unit); + + Widget listTile = ListTile( + onTap: onTap == null ? null : () => onTap!(tx), + leading: CircleAvatar( + backgroundColor: isPending + ? Colors.blue.withValues(alpha: 0.1) + : color.withValues(alpha: 0.1), + child: Icon( + isPending ? Icons.pending : icon, + color: isPending ? Colors.blue : color, + ), + ), + title: Text( + '${isPending ? 'Pending' : (isIncoming ? 'Incoming' : 'Outgoing')} - ${tx.walletType}', + style: const TextStyle(fontWeight: FontWeight.w600), ), - ), - if (tx.completionMsg != null && tx.completionMsg!.isNotEmpty) - Text( - tx.completionMsg!, - style: Theme.of(context).textTheme.bodySmall, + subtitle: Text( + [ + transactionDateText, + _removeHttpPrefix(tx.walletId), + //_enumLabel(tx.state), + ].where((e) => e.isNotEmpty).join(' • '), maxLines: 1, overflow: TextOverflow.ellipsis, ), - ], + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + amountStr, + style: TextStyle( + fontWeight: FontWeight.w700, + color: isPending ? Colors.blue : color, + ), + ), + if (tx.completionMsg != null && + tx.completionMsg!.isNotEmpty) + Text( + tx.completionMsg!, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + + if (showDividers && index < visible.length - 1) { + return Column( + children: [ + listTile, + const Divider(height: 1, color: Palette.darkGray), + ], + ); + } + + return listTile; + }, + childCount: visible.length, ), - ); - }, + ), + ], ); - return height != null ? SizedBox(height: height, child: list) : list; + return height != null + ? SizedBox(height: height, child: scrollView) + : scrollView; } static int? _bestDate(ndk_entities.WalletTransaction tx) => diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index 455ac3a6..38f5989c 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -100,6 +100,8 @@ class _WalletDashboardState extends ConsumerState ), PaymentHistoryShort( transactions: combinedWallet.recentTransactions, + pendingTransactions: combinedWallet.pendingTransactions, + showDividers: false, ), ], ), From b1f7e97c823df3c39e24748521c70a2020d642a9 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:20:34 +0200 Subject: [PATCH 12/52] wallet action strip params --- .../wallet/wallet_actions_strip.dart | 43 +++++++++---------- .../routes/wallet/wallet_dashboard.dart | 7 ++- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/presentation_layer/components/wallet/wallet_actions_strip.dart b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart index affa3863..5a3ec5d0 100644 --- a/lib/presentation_layer/components/wallet/wallet_actions_strip.dart +++ b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart @@ -1,33 +1,30 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../config/palette.dart'; class WalletActionsStrip extends StatelessWidget { - const WalletActionsStrip({super.key}); + final void Function() onScan; + final void Function() onReceive; + final void Function() onPay; + final void Function() onHistory; + + const WalletActionsStrip({ + super.key, + required this.onScan, + required this.onReceive, + required this.onPay, + required this.onHistory, + }); final myIconColor = Colors.white70; final myTextColor = Colors.white60; - _onScan() { - log("onScan"); - } - - _onReceive() { - log("onReceive"); - } - - _onPay(context) { - log("onPay"); - } - @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12), - child: Container( + child: SizedBox( width: MediaQuery.of(context).size.width, //height: 120, //color: Colors.white12, @@ -38,7 +35,7 @@ class WalletActionsStrip extends StatelessWidget { padding: const EdgeInsets.all(2), child: _actionButton( iconData: PhosphorIcons.barcode(), - onTab: () => _onScan(), + onTab: () => onScan(), text: "scan", ), ), @@ -46,7 +43,7 @@ class WalletActionsStrip extends StatelessWidget { padding: const EdgeInsets.all(2), child: _actionButton( iconData: PhosphorIcons.wallet(), - onTab: () => _onPay(context), + onTab: () => onPay(), text: "pay", ), ), @@ -54,16 +51,16 @@ class WalletActionsStrip extends StatelessWidget { padding: const EdgeInsets.all(2), child: _actionButton( iconData: PhosphorIcons.piggyBank(), - onTab: () => _onReceive(), + onTab: () => onReceive(), text: "receive"), ), Container( - padding: const EdgeInsets.all(2), - child: _actionButton( + padding: const EdgeInsets.all(2), + child: _actionButton( iconData: PhosphorIcons.receipt(), text: "history", - onTab: () {}), - ), + onTab: () => onHistory(), + )), ], ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index 38f5989c..adcd65a3 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -90,7 +90,12 @@ class _WalletDashboardState extends ConsumerState child: WalletFriendsStrip(), ), const SizedBox(height: 30), - const WalletActionsStrip(), + WalletActionsStrip( + onScan: () {}, + onReceive: () {}, + onPay: () {}, + onHistory: () {}, + ), const SizedBox(height: 30), TextButton( onPressed: () { From 63f394c98fe88ed6fb63b39f138c8203c076f877 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:33:18 +0200 Subject: [PATCH 13/52] number formatting --- lib/helpers/wallet_number_formatting.dart | 32 +++++++++++++++++++ .../wallet/wallet_accounts_card.dart | 11 ++++--- 2 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 lib/helpers/wallet_number_formatting.dart diff --git a/lib/helpers/wallet_number_formatting.dart b/lib/helpers/wallet_number_formatting.dart new file mode 100644 index 00000000..1dab41ba --- /dev/null +++ b/lib/helpers/wallet_number_formatting.dart @@ -0,0 +1,32 @@ +import 'package:intl/intl.dart'; + +class WalletNumberFormatting { + final String langCode; + final NumberFormat satFormat; + final NumberFormat fiatFormat; + + WalletNumberFormatting({ + this.langCode = "en_US", + }) : satFormat = NumberFormat("#,##0.##", langCode), + fiatFormat = NumberFormat("#,##0.00", langCode); + + String formatSat(int amount) { + return satFormat.format(amount); + } + + String formatFiat(int amountInCents) { + final amount = amountInCents / 100; + return fiatFormat.format(amount); + } + + String formatAmount({ + required int amount, + required String unit, + }) { + if (unit == "sat") { + return formatSat(amount); + } else { + return formatFiat(amount); + } + } +} diff --git a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart index fc2476aa..bc0f2b44 100644 --- a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart +++ b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart @@ -5,8 +5,10 @@ import 'package:intl/intl.dart'; import 'package:ndk/entities.dart' as ndk_entities; +import '../../../helpers/wallet_number_formatting.dart'; + class WalletAccountsCard extends ConsumerWidget { - const WalletAccountsCard({ + WalletAccountsCard({ super.key, required this.walletId, required this.title, @@ -24,11 +26,10 @@ class WalletAccountsCard extends ConsumerWidget { /// ₿ final List balances; + final WalletNumberFormatting _numberFormatter = WalletNumberFormatting(); + @override Widget build(BuildContext context, WidgetRef ref) { - final satFormat = NumberFormat("#,##0.##", "de_DE"); - final eurFormat = NumberFormat("#,##0.00", "de_DE"); - return Column( children: [ Column( @@ -85,7 +86,7 @@ class WalletAccountsCard extends ConsumerWidget { children: [ for (final balance in balances) Text( - "${satFormat.format(balance.amount)} ${balance.unit}", + "${_numberFormatter.formatAmount(amount: balance.amount, unit: balance.unit)} ${balance.unit}", style: const TextStyle( fontSize: 20, fontWeight: FontWeight.normal, From 92ebed4562b1be19039a7907205af317710bf6f1 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:39:09 +0200 Subject: [PATCH 14/52] refactor: static amount formatter --- lib/helpers/wallet_number_formatting.dart | 22 ++++++++----------- .../wallet/payment_history_short.dart | 12 +++------- .../wallet/wallet_accounts_card.dart | 6 ++--- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/lib/helpers/wallet_number_formatting.dart b/lib/helpers/wallet_number_formatting.dart index 1dab41ba..1dbfa042 100644 --- a/lib/helpers/wallet_number_formatting.dart +++ b/lib/helpers/wallet_number_formatting.dart @@ -1,32 +1,28 @@ import 'package:intl/intl.dart'; class WalletNumberFormatting { - final String langCode; - final NumberFormat satFormat; - final NumberFormat fiatFormat; + WalletNumberFormatting._(); - WalletNumberFormatting({ - this.langCode = "en_US", - }) : satFormat = NumberFormat("#,##0.##", langCode), - fiatFormat = NumberFormat("#,##0.00", langCode); - - String formatSat(int amount) { + static String formatSat(int amount, {String langCode = "en_US"}) { + final satFormat = NumberFormat("#,##0.##", langCode); return satFormat.format(amount); } - String formatFiat(int amountInCents) { + static String formatFiat(int amountInCents, {String langCode = "en_US"}) { + final fiatFormat = NumberFormat("#,##0.00", langCode); final amount = amountInCents / 100; return fiatFormat.format(amount); } - String formatAmount({ + static String formatAmount({ required int amount, required String unit, + String langCode = "en_US", }) { if (unit == "sat") { - return formatSat(amount); + return formatSat(amount, langCode: langCode); } else { - return formatFiat(amount); + return formatFiat(amount, langCode: langCode); } } } diff --git a/lib/presentation_layer/components/wallet/payment_history_short.dart b/lib/presentation_layer/components/wallet/payment_history_short.dart index 9b0f8d7c..03a0f58c 100644 --- a/lib/presentation_layer/components/wallet/payment_history_short.dart +++ b/lib/presentation_layer/components/wallet/payment_history_short.dart @@ -4,6 +4,7 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:timeago/timeago.dart' as timeago; import '../../../config/palette.dart'; +import '../../../helpers/wallet_number_formatting.dart'; class PaymentHistoryShort extends StatelessWidget { final List transactions; @@ -91,7 +92,8 @@ class PaymentHistoryShort extends StatelessWidget { : ''; } - final amountStr = _formatAmount(tx.changeAmount, tx.unit); + final amountStr = WalletNumberFormatting.formatAmount( + amount: tx.changeAmount, unit: tx.unit); Widget listTile = ListTile( onTap: onTap == null ? null : () => onTap!(tx), @@ -165,14 +167,6 @@ class PaymentHistoryShort extends StatelessWidget { static int? _bestDate(ndk_entities.WalletTransaction tx) => tx.transactionDate ?? tx.initiatedDate; - static String _formatAmount(int change, String unit) { - final sign = change >= 0 ? '+' : '-'; - final abs = change.abs(); - final withSeparators = - abs.toString().replaceAll(RegExp(r'\B(?=(\d{3})+(?!\d))'), ','); - return '$sign$withSeparators $unit'; - } - static String _enumLabel(Object? value) { if (value == null) return ''; final s = value.toString(); diff --git a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart index bc0f2b44..220dddf7 100644 --- a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart +++ b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart @@ -8,7 +8,7 @@ import 'package:ndk/entities.dart' as ndk_entities; import '../../../helpers/wallet_number_formatting.dart'; class WalletAccountsCard extends ConsumerWidget { - WalletAccountsCard({ + const WalletAccountsCard({ super.key, required this.walletId, required this.title, @@ -26,8 +26,6 @@ class WalletAccountsCard extends ConsumerWidget { /// ₿ final List balances; - final WalletNumberFormatting _numberFormatter = WalletNumberFormatting(); - @override Widget build(BuildContext context, WidgetRef ref) { return Column( @@ -86,7 +84,7 @@ class WalletAccountsCard extends ConsumerWidget { children: [ for (final balance in balances) Text( - "${_numberFormatter.formatAmount(amount: balance.amount, unit: balance.unit)} ${balance.unit}", + "${WalletNumberFormatting.formatAmount(amount: balance.amount, unit: balance.unit)} ${balance.unit}", style: const TextStyle( fontSize: 20, fontWeight: FontWeight.normal, From 4dd348170a19f017a97aad4072d37429a8f300dc Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:37:01 +0200 Subject: [PATCH 15/52] wallet pay reciever page --- .../entities/generated_private_key.dart | 4 +- lib/helpers/bip340.dart | 4 +- lib/helpers/helpers.dart | 7 +- lib/helpers/nevent_helper.dart | 2 +- lib/helpers/nprofile_helper.dart | 3 +- lib/helpers/wallet_number_formatting.dart | 14 +- lib/main.dart | 5 + lib/presentation_layer/atoms/nip_05_text.dart | 2 +- .../components/note_card/in_reply_to.dart | 2 +- .../components/note_card/name_row.dart | 2 +- .../note_card/note_card_repost.dart | 2 +- .../starter_packs/open_starter_pack.dart | 6 +- .../starter_packs/starter_pack_card.dart | 2 +- .../components/write_post.dart | 2 +- .../nostr/onboarding/onboarding_login.dart | 6 +- .../routes/nostr/profile/profile_page_2.dart | 4 +- .../routes/wallet/wallet_dashboard.dart | 20 +- .../wallet_pay_reciever.dart | 311 ++++++++++++++++++ .../wallet_pay_reciever_state_provider.dart | 167 ++++++++++ .../wallet_combined_state_provider.dart | 21 +- 20 files changed, 553 insertions(+), 33 deletions(-) create mode 100644 lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart diff --git a/lib/domain_layer/entities/generated_private_key.dart b/lib/domain_layer/entities/generated_private_key.dart index 7689b187..80160ccf 100644 --- a/lib/domain_layer/entities/generated_private_key.dart +++ b/lib/domain_layer/entities/generated_private_key.dart @@ -6,8 +6,8 @@ class GeneratedPrivateKey { final String privateKey; final String publicKey; - String get privKeyHr => Helpers().encodeBech32(privateKey, 'nsec'); - String get publicKeyHr => Helpers().encodeBech32(publicKey, 'npub'); + String get privKeyHr => Helpers.encodeBech32(privateKey, 'nsec'); + String get publicKeyHr => Helpers.encodeBech32(publicKey, 'npub'); GeneratedPrivateKey({ required this.mnemonicSentence, diff --git a/lib/helpers/bip340.dart b/lib/helpers/bip340.dart index d6a59491..8084f66d 100644 --- a/lib/helpers/bip340.dart +++ b/lib/helpers/bip340.dart @@ -34,8 +34,8 @@ class Bip340 { final privKey = _helpers.getSecureRandomHex(32); final pubKey = getPublicKey(privKey); - final privKeyHr = _helpers.encodeBech32(privKey, 'nsec'); - final pubKeyHr = _helpers.encodeBech32(pubKey, 'npub'); + final privKeyHr = Helpers.encodeBech32(privKey, 'nsec'); + final pubKeyHr = Helpers.encodeBech32(pubKey, 'npub'); return KeyPair( privateKey: privKey, diff --git a/lib/helpers/helpers.dart b/lib/helpers/helpers.dart index 4318cc36..64e88fb8 100644 --- a/lib/helpers/helpers.dart +++ b/lib/helpers/helpers.dart @@ -36,7 +36,7 @@ class Helpers { } /// Encode a hex string + human readable part as a bech32 string - String encodeBech32(String hex, String hrp) { + static String encodeBech32(String hex, String hrp) { final bytes = HEX.decode(hex); final fiveBitWords = _convertBits(bytes, 8, 5, true); return bech32.encode(Bech32(hrp, fiveBitWords), hex.length + hrp.length); @@ -91,7 +91,8 @@ class Helpers { /// [pad] - whether to pad the output if there are not enough bits /// If pad is true, and there are remaining bits after the conversion, then the remaining bits are left-shifted and added to the result /// [return] - the converted data - List _convertBits(List data, int fromBits, int toBits, bool pad) { + static List _convertBits( + List data, int fromBits, int toBits, bool pad) { int acc = 0; int bits = 0; List result = []; @@ -117,7 +118,7 @@ class Helpers { return result; } - String shortHr(String pubkey, {bool cutPubkey = true}) { + static String shortHr(String pubkey, {bool cutPubkey = true}) { final npubHr = encodeBech32(pubkey, "npub"); if (!cutPubkey) { return npubHr; diff --git a/lib/helpers/nevent_helper.dart b/lib/helpers/nevent_helper.dart index dbbd58d3..8bca1d83 100644 --- a/lib/helpers/nevent_helper.dart +++ b/lib/helpers/nevent_helper.dart @@ -15,7 +15,7 @@ class NeventHelper { final List tlvList = _generateTlvList(eventId, authorPubkey, relays); final String dataString = HEX.encode(TlvUtils.encode(tlvList)); - return _helper.encodeBech32(dataString, 'nevent'); + return Helpers.encodeBech32(dataString, 'nevent'); } /// Generates a list of TLV objects diff --git a/lib/helpers/nprofile_helper.dart b/lib/helpers/nprofile_helper.dart index b6ca2021..2d1b6ca9 100644 --- a/lib/helpers/nprofile_helper.dart +++ b/lib/helpers/nprofile_helper.dart @@ -56,7 +56,6 @@ class NprofileHelper { /// expects a map with pubkey and relays and [returns] a bech32 encoded nprofile String mapToBech32(Map map) { - final Helpers helper = Helpers(); final String pubkey = map["pubkey"]; final List relays = List.from(map['relays']); @@ -64,7 +63,7 @@ class NprofileHelper { final Uint8List bytes = TlvUtils.encode(tlvList); final String dataString = HEX.encode(bytes); - return helper.encodeBech32(dataString, "nprofile"); + return Helpers.encodeBech32(dataString, "nprofile"); } Map _parseTlvList(List tlvList) { diff --git a/lib/helpers/wallet_number_formatting.dart b/lib/helpers/wallet_number_formatting.dart index 1dbfa042..d3101c0c 100644 --- a/lib/helpers/wallet_number_formatting.dart +++ b/lib/helpers/wallet_number_formatting.dart @@ -3,12 +3,20 @@ import 'package:intl/intl.dart'; class WalletNumberFormatting { WalletNumberFormatting._(); - static String formatSat(int amount, {String langCode = "en_US"}) { + static const enUSLangCode = "en_US"; + + static String formatSat( + int amount, { + String langCode = enUSLangCode, + }) { final satFormat = NumberFormat("#,##0.##", langCode); return satFormat.format(amount); } - static String formatFiat(int amountInCents, {String langCode = "en_US"}) { + static String formatFiat( + int amountInCents, { + String langCode = enUSLangCode, + }) { final fiatFormat = NumberFormat("#,##0.00", langCode); final amount = amountInCents / 100; return fiatFormat.format(amount); @@ -17,7 +25,7 @@ class WalletNumberFormatting { static String formatAmount({ required int amount, required String unit, - String langCode = "en_US", + String langCode = enUSLangCode, }) { if (unit == "sat") { return formatSat(amount, langCode: langCode); diff --git a/lib/main.dart b/lib/main.dart index b6c524a7..6196a32c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,6 +38,7 @@ import 'presentation_layer/routes/nostr/settings/locale/locale_settings.dart'; import 'presentation_layer/routes/nostr/settings/moderation/moderation_settings.dart'; import 'presentation_layer/routes/nostr/settings/settings_page.dart'; import 'presentation_layer/routes/wallet/wallet_navigation.dart'; +import 'presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart'; import 'theme.dart' as theme; const devDeviceFrame = true; @@ -292,6 +293,10 @@ class MyApp extends ConsumerWidget { title: "a", ), ); + case '/wallet/pay': + return MaterialPageRoute( + builder: (context) => WalletPaymentPage(), + ); } assert(false, 'Need to implement ${settings.name}'); return null; diff --git a/lib/presentation_layer/atoms/nip_05_text.dart b/lib/presentation_layer/atoms/nip_05_text.dart index 9a5a2fd1..776f8980 100644 --- a/lib/presentation_layer/atoms/nip_05_text.dart +++ b/lib/presentation_layer/atoms/nip_05_text.dart @@ -8,7 +8,7 @@ class Nip05Text extends StatelessWidget { final bool cutPubkey; String shortHr(String pubkey) { - final npubHr = Helpers().encodeBech32(pubkey, "npub"); + final npubHr = Helpers.encodeBech32(pubkey, "npub"); if (!cutPubkey) { return npubHr; } diff --git a/lib/presentation_layer/components/note_card/in_reply_to.dart b/lib/presentation_layer/components/note_card/in_reply_to.dart index 82d0e298..32f6fe83 100644 --- a/lib/presentation_layer/components/note_card/in_reply_to.dart +++ b/lib/presentation_layer/components/note_card/in_reply_to.dart @@ -17,7 +17,7 @@ class InReplyTo extends ConsumerWidget { final NostrNote myNote; String _formatPubkey(String pubkey) { - final pubkeyBech = Helpers().encodeBech32(pubkey, "npub"); + final pubkeyBech = Helpers.encodeBech32(pubkey, "npub"); final pubkeyHr = "${pubkeyBech.substring(0, 4)}:${pubkeyBech.substring(pubkeyBech.length - 5)}"; return pubkeyHr; diff --git a/lib/presentation_layer/components/note_card/name_row.dart b/lib/presentation_layer/components/note_card/name_row.dart index a385cee9..abc68d7b 100644 --- a/lib/presentation_layer/components/note_card/name_row.dart +++ b/lib/presentation_layer/components/note_card/name_row.dart @@ -49,7 +49,7 @@ class _NoteCardNameRowState extends ConsumerState { } void _initSequence() { - var npubHr = Helpers().encodeBech32(widget.pubkey, "npub"); + var npubHr = Helpers.encodeBech32(widget.pubkey, "npub"); npubHrShort = "${npubHr.substring(0, 4)}...${npubHr.substring(npubHr.length - 4)}"; } diff --git a/lib/presentation_layer/components/note_card/note_card_repost.dart b/lib/presentation_layer/components/note_card/note_card_repost.dart index f5b68ba7..c4ec9fd0 100644 --- a/lib/presentation_layer/components/note_card/note_card_repost.dart +++ b/lib/presentation_layer/components/note_card/note_card_repost.dart @@ -69,7 +69,7 @@ class NoteCardRepost extends ConsumerWidget { children: [ TextSpan( text: repostedByMetadata?.name ?? - Helpers().shortHr(repostEvent.pubkey), + Helpers.shortHr(repostEvent.pubkey), style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, diff --git a/lib/presentation_layer/components/starter_packs/open_starter_pack.dart b/lib/presentation_layer/components/starter_packs/open_starter_pack.dart index 1e2ca6b5..620c423e 100644 --- a/lib/presentation_layer/components/starter_packs/open_starter_pack.dart +++ b/lib/presentation_layer/components/starter_packs/open_starter_pack.dart @@ -349,7 +349,7 @@ class _OpenStarterPackState extends ConsumerState { ), const SizedBox(height: 4), Text( - "Starter pack by ${isOwnStarterPack ? "you" : ref.watch(metadataStateProvider(widget.starterPackIdentifier.pubkey)).userMetadata?.name ?? Helpers().shortHr(widget.starterPackIdentifier.pubkey)}", + "Starter pack by ${isOwnStarterPack ? "you" : ref.watch(metadataStateProvider(widget.starterPackIdentifier.pubkey)).userMetadata?.name ?? Helpers.shortHr(widget.starterPackIdentifier.pubkey)}", style: TextStyle( color: Palette.gray, fontSize: 14, @@ -444,8 +444,8 @@ class _OpenStarterPackState extends ConsumerState { return PersonCard( pubkey: displayPubkey, - name: displayMetadata?.name ?? - Helpers().shortHr(displayPubkey), + name: + displayMetadata?.name ?? Helpers.shortHr(displayPubkey), pictureUrl: displayMetadata?.picture ?? "", about: displayMetadata?.about ?? "", isFollowing: myContactListState.contactList.contacts diff --git a/lib/presentation_layer/components/starter_packs/starter_pack_card.dart b/lib/presentation_layer/components/starter_packs/starter_pack_card.dart index a6c91de9..3e2275a2 100644 --- a/lib/presentation_layer/components/starter_packs/starter_pack_card.dart +++ b/lib/presentation_layer/components/starter_packs/starter_pack_card.dart @@ -168,7 +168,7 @@ class _StarterPackCardState extends ConsumerState { } _pubkeyToHrBech32Short(pubkey) { - final bech = Helpers().encodeBech32(pubkey, "npub"); + final bech = Helpers.encodeBech32(pubkey, "npub"); final bechShort = NprofileHelper().bech32toHr(bech, cutLength: 11); return bechShort; diff --git a/lib/presentation_layer/components/write_post.dart b/lib/presentation_layer/components/write_post.dart index b1bafd81..97e1ce46 100644 --- a/lib/presentation_layer/components/write_post.dart +++ b/lib/presentation_layer/components/write_post.dart @@ -515,7 +515,7 @@ class _TopBar extends ConsumerWidget { }); getPubkeyHrShort(String pubkey) { - final pubkeyHr = Helpers().encodeBech32(pubkey, "npub"); + final pubkeyHr = Helpers.encodeBech32(pubkey, "npub"); final pubkeyHrShort = "${pubkeyHr.substring(0, 5)}...${pubkeyHr.substring(pubkeyHr.length - 5)}"; return pubkeyHrShort; diff --git a/lib/presentation_layer/routes/nostr/onboarding/onboarding_login.dart b/lib/presentation_layer/routes/nostr/onboarding/onboarding_login.dart index 7e49e395..c0e5d79c 100644 --- a/lib/presentation_layer/routes/nostr/onboarding/onboarding_login.dart +++ b/lib/presentation_layer/routes/nostr/onboarding/onboarding_login.dart @@ -63,7 +63,7 @@ class _OnboardingLoginPageState extends ConsumerState { var privkey = Helpers().decodeBech32(nsec)[0]; var pubkey = Bip340().getPublicKey(privkey); var privKeyHr = nsec; - var publicKeyHr = Helpers().encodeBech32(pubkey, 'npub'); + var publicKeyHr = Helpers.encodeBech32(pubkey, 'npub'); setState(() { myKeys = KeyPair( @@ -94,8 +94,8 @@ class _OnboardingLoginPageState extends ConsumerState { final privkeyHex = HEX.encode(child.privateKey!); var pubkey = Bip340().getPublicKey(privkeyHex); - var privKeyHr = Helpers().encodeBech32(privkeyHex, 'nsec'); - var publicKeyHr = Helpers().encodeBech32(pubkey, 'npub'); + var privKeyHr = Helpers.encodeBech32(privkeyHex, 'nsec'); + var publicKeyHr = Helpers.encodeBech32(pubkey, 'npub'); setState(() { myKeys = KeyPair( diff --git a/lib/presentation_layer/routes/nostr/profile/profile_page_2.dart b/lib/presentation_layer/routes/nostr/profile/profile_page_2.dart index 5f12671c..4eec2579 100644 --- a/lib/presentation_layer/routes/nostr/profile/profile_page_2.dart +++ b/lib/presentation_layer/routes/nostr/profile/profile_page_2.dart @@ -281,7 +281,7 @@ class _BuildProfileHeader extends ConsumerWidget { onTap: () { /// copy to clipboard _copyToClipboard( - Helpers().encodeBech32(userMetadata.pubkey, "npub"), + Helpers.encodeBech32(userMetadata.pubkey, "npub"), ); }, child: Nip05Text( @@ -410,7 +410,7 @@ class _FollowButtonState extends ConsumerState<_FollowButton> { } _pubkeyToHrBech32Short(pubkey) { - final bech = Helpers().encodeBech32(pubkey, "npub"); + final bech = Helpers.encodeBech32(pubkey, "npub"); final bechShort = NprofileHelper().bech32toHr(bech, cutLength: 11); return bechShort; diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index adcd65a3..ab901a42 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -91,9 +91,15 @@ class _WalletDashboardState extends ConsumerState ), const SizedBox(height: 30), WalletActionsStrip( - onScan: () {}, - onReceive: () {}, - onPay: () {}, + onScan: () { + Navigator.pushNamed(context, '/wallet/scan'); + }, + onReceive: () { + Navigator.pushNamed(context, '/wallet/receive'); + }, + onPay: () { + Navigator.pushNamed(context, '/wallet/pay'); + }, onHistory: () {}, ), const SizedBox(height: 30), @@ -101,7 +107,13 @@ class _WalletDashboardState extends ConsumerState onPressed: () { combinedWalletNotifier.fundWallet(); }, - child: Text("invoke"), + child: Text("fund"), + ), + TextButton( + onPressed: () { + combinedWalletNotifier.spend(); + }, + child: Text("spend"), ), PaymentHistoryShort( transactions: combinedWallet.recentTransactions, diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart new file mode 100644 index 00000000..122b8084 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart @@ -0,0 +1,311 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart' as ndk_entities; + +import '../../../../../config/palette.dart'; +import '../../../../../domain_layer/entities/user_metadata.dart'; + +import '../../../../../helpers/helpers.dart'; +import '../../../../../helpers/wallet_number_formatting.dart'; +import '../../../../atoms/long_button.dart'; +import '../../../../atoms/my_profile_picture.dart'; +import 'wallet_pay_reciever_state_provider.dart'; + +class WalletPaymentPage extends ConsumerWidget { + final String? walletId; + + const WalletPaymentPage({super.key, this.walletId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(walletPayRecieverProvider(walletId)); + final notifier = ref.read(walletPayRecieverProvider(walletId).notifier); + + return Scaffold( + backgroundColor: Palette.background, + appBar: AppBar( + backgroundColor: Palette.background, + title: const Text('Pay to'), + leading: Container(), + leadingWidth: 0, + actions: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(64), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: TextField( + onChanged: notifier.setSearchQuery, + decoration: InputDecoration( + isDense: true, + hintText: ' Search by name', + hintStyle: + const TextStyle(color: Palette.white, letterSpacing: 1.1), + filled: true, + fillColor: Palette.background, + enabledBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(50.0)), + borderSide: BorderSide(color: Palette.extraDarkGray), + ), + focusedBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(25.0)), + borderSide: BorderSide(color: Palette.background), + ), + ), + ), + ), + ), + ), + body: SafeArea( + child: state.isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + // scrollable results + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), + children: [ + if (state.searchQuery.trim().isEmpty) + _Island( + title: "Anonymous Token", + child: longButton( + name: "create token", + onPressed: () {}, + ), + ), + if (state.searchQuery.trim().isEmpty) + _Island( + title: 'Recent contacts', + child: _ContactsList( + contacts: state.recentContacts, + onTap: notifier.selectContact, + ), + ), + if (state.searchQuery.trim().isNotEmpty) + _Island( + title: 'Matching contacts', + child: _ContactsList( + contacts: state.filteredContacts, + onTap: notifier.selectContact, + emptyPlaceholder: const Padding( + padding: EdgeInsets.all(12.0), + child: Text('No matching contacts'), + ), + ), + ), + _Island( + title: 'Suggestions', + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _SuggestionChip( + label: 'action 1', + icon: Icons.account_balance_wallet_outlined, + onTap: () {}, + ), + _SuggestionChip( + label: 'action 2', + icon: Icons.group_outlined, + onTap: () {}, + ), + _SuggestionChip( + label: 'action 3', + icon: Icons.receipt_long_outlined, + onTap: () {}, + ), + ], + ), + ), + _Island( + title: 'All wallets', + child: _WalletsList( + wallets: state.filteredWallets, + balances: state.balances, + selectedWalletId: state.selectedWalletId, + onTap: notifier.onWalletTab, + ), + ), + ], + ), + ), + ], + ), + ), + + /// next button + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), + child: SizedBox( + width: double.infinity, + height: 45, + child: longButton(name: "next", onPressed: () {}, inverted: true), + ), + ), + ), + ); + } +} + +class _Island extends StatelessWidget { + final String title; + final Widget child; + const _Island({required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + child, + ], + ), + ), + ); + } +} + +class _ContactsList extends StatelessWidget { + final List contacts; + final void Function(UserMetadata) onTap; + final Widget? emptyPlaceholder; + + const _ContactsList({ + required this.contacts, + required this.onTap, + this.emptyPlaceholder, + }); + + @override + Widget build(BuildContext context) { + if (contacts.isEmpty) { + return emptyPlaceholder ?? + const Padding( + padding: EdgeInsets.all(12.0), + child: Text('No recent contacts'), + ); + } + return ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: contacts.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, i) { + final c = contacts[i]; + return ListTile( + leading: SizedBox( + height: 50, + width: 50, + child: UserImage(imageUrl: c.picture, pubkey: c.pubkey), + ), + title: Text(c.name ?? ''), + subtitle: Text( + c.nip05 ?? + Helpers.shortHr( + c.pubkey, + ), + style: TextStyle(color: Palette.gray), + ), + onTap: () => onTap(c), + ); + }, + ); + } +} + +class _WalletsList extends StatelessWidget { + final List wallets; + final List balances; + final String? selectedWalletId; + final void Function(ndk_entities.Wallet) onTap; + + const _WalletsList({ + required this.wallets, + required this.balances, + required this.selectedWalletId, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + if (wallets.isEmpty) { + return const Padding( + padding: EdgeInsets.all(12.0), + child: Text('No wallets found'), + ); + } + return ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: wallets.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, i) { + final w = wallets[i]; + final wBallances = balances.where((b) => b.walletId == w.id); + final selected = w.id == selectedWalletId; + + /// dont show selected wallet + if (selected) { + return Container(); + } + + return ListTile( + title: Text(w.name), + subtitle: Text(w.type.toString()), + + /// show all balances for the wallet + trailing: Column(children: [ + for (final b in wBallances) + Text( + "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", + style: TextStyle( + color: Palette.white, + ), + ), + ]), + + onTap: () => onTap(w), + ); + }, + ); + } +} + +class _SuggestionChip extends StatelessWidget { + final String label; + final IconData icon; + final VoidCallback onTap; + const _SuggestionChip({ + required this.label, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ActionChip( + avatar: Icon(icon, size: 18), + label: Text(label), + onPressed: onTap, + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart new file mode 100644 index 00000000..e0da5fc0 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart @@ -0,0 +1,167 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:ndk/ndk.dart'; + +import '../../../../../domain_layer/entities/user_metadata.dart'; +import '../../../../../domain_layer/usecases/get_user_metadata.dart'; + +import '../../../../providers/following_contact_state_provider.dart'; +import '../../../../providers/metadata_provider.dart'; +import '../../../../providers/ndk_provider.dart'; + +class WalletPayRecieverState { + final List wallets; + final List balances; + final List allContacts; + final List recentContacts; + final String? selectedWalletId; + final String searchQuery; + final bool isLoading; + + WalletPayRecieverState({ + required this.wallets, + required this.balances, + required this.allContacts, + required this.recentContacts, + required this.selectedWalletId, + required this.searchQuery, + required this.isLoading, + }); + + WalletPayRecieverState copyWith({ + List? wallets, + List? balances, + List? allContacts, + List? recentContacts, + String? selectedWalletId, + String? searchQuery, + bool? isLoading, + }) { + return WalletPayRecieverState( + wallets: wallets ?? this.wallets, + balances: balances ?? this.balances, + allContacts: allContacts ?? this.allContacts, + recentContacts: recentContacts ?? this.recentContacts, + selectedWalletId: selectedWalletId ?? this.selectedWalletId, + searchQuery: searchQuery ?? this.searchQuery, + isLoading: isLoading ?? this.isLoading, + ); + } + + List get filteredContacts { + final q = searchQuery.trim().toLowerCase(); + if (q.isEmpty) return const []; + + return allContacts + .where((c) => + (c.name != null && c.name!.toLowerCase().contains(q)) || + (c.pubkey.toLowerCase().contains(q))) + .toList(); + } + + List get filteredWallets { + final q = searchQuery.trim().toLowerCase(); + if (q.isEmpty) return wallets; + return wallets.where((w) => w.name.toLowerCase().contains(q)).toList(); + } +} + +class WalletPaymentNotifier extends StateNotifier { + final Ndk _ndk; + + final GetUserMetadata _getUserMetadata; + + final List _contactPubkeys; + + WalletPaymentNotifier({ + String? initialWalletId, + required Ndk ndk, + required List contactPubkeys, + required GetUserMetadata getUserMetadata, + }) : _ndk = ndk, + _contactPubkeys = contactPubkeys, + _getUserMetadata = getUserMetadata, + super( + WalletPayRecieverState( + wallets: const [], + balances: const [], + allContacts: const [], + recentContacts: const [], + selectedWalletId: initialWalletId, + searchQuery: '', + isLoading: true, + ), + ) { + _loadInitial(); + } + + Future _loadInitial() async { + final balances = await _ndk.wallets.combinedBalances.first; + + final wallets = await _ndk.wallets.walletsStream.first; + + List contactMetadata = []; + final List futures = []; + for (final cp in _contactPubkeys) { + futures.add( + _getUserMetadata.getMetadataByPubkey(cp).last.then((metadata) { + contactMetadata.add(metadata); + }), + ); + } + //await Future.wait(futures); + + final contacts = contactMetadata; + + final recent = contactMetadata; + + String? selected = state.selectedWalletId; + // If the provided walletId isn't in the list, fall back to first. + if (selected == null || !wallets.any((w) => w.id == selected)) { + selected = wallets.isNotEmpty ? wallets.first.id : null; + } + + state = state.copyWith( + wallets: wallets, + balances: balances, + allContacts: contacts, + recentContacts: recent, + selectedWalletId: selected, + isLoading: false, + ); + } + + void setSelectedWallet(String? walletId) { + state = state.copyWith(selectedWalletId: walletId); + } + + void setSearchQuery(String q) { + state = state.copyWith(searchQuery: q); + } + + void selectContact(UserMetadata c) { + throw UnimplementedError(); + } + + void onWalletTab(ndk_entities.Wallet w) { + throw UnimplementedError(); + } +} + +final walletPayRecieverProvider = StateNotifierProvider.family< + WalletPaymentNotifier, + WalletPayRecieverState, + String?>((ref, initialWalletId) { + final ndk = ref.watch(ndkProvider); + + final contacts = ref.watch(contactListSelfStateProvider); + + final getUserMetadata = ref.watch(metadataProvider); + + return WalletPaymentNotifier( + initialWalletId: initialWalletId, + ndk: ndk, + contactPubkeys: contacts.contactList.contacts, + getUserMetadata: getUserMetadata, + ); +}); diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart index c3d09c13..b3b524d9 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -92,12 +92,12 @@ class WalletCombinedStateNotifier extends StateNotifier { ]); } - fundWallet() async { + void fundWallet() async { /// todo acc management in wallet usecase /// just testing final draftTransaction = await _ndk.cashu.initiateFund( - mintUrl: "http://$localhost:8085", + mintUrl: "http://$localhost:8086", amount: 10, unit: "sat", method: "bolt11"); @@ -107,4 +107,21 @@ class WalletCombinedStateNotifier extends StateNotifier { .toList(); print(transaction); } + + void spend() async { + final mintUrl = "http://$localhost:8086"; + final unit = "eur"; + + final spend = await _ndk.cashu.initiateSpend( + mintUrl: mintUrl, + amount: 1, + unit: unit, + ); + + print(spend); + + final token = + _ndk.cashu.proofsToToken(proofs: spend, mintUrl: mintUrl, unit: unit); + print(token); + } } From 2c7dc5698e0b2a66dd1fe4b7db273f8381fd4adf Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Thu, 14 Aug 2025 18:34:32 +0200 Subject: [PATCH 16/52] pay flow, select amount --- lib/main.dart | 3 +- .../atoms/wallet/wallet_card.dart | 83 +++++++ .../wallet/wallets_select_bottom_sheet.dart | 96 ++++++++ .../wallet_pay_select_amount.dart | 215 ++++++++++++++++++ ...llet_pay_select_amount_state_provider.dart | 53 +++++ .../wallet_pay_reciever.dart | 62 ++++- .../wallet_pay_reciever_state_provider.dart | 20 +- .../wallet_pay/wallet_pay_state_provider.dart | 155 +++++++++++++ .../wallet_pay_summary.dart | 15 ++ .../routes/wallet/wallet_pay_page.dart | 71 ++++++ 10 files changed, 749 insertions(+), 24 deletions(-) create mode 100644 lib/presentation_layer/atoms/wallet/wallet_card.dart create mode 100644 lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount_state_provider.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_pay_page.dart diff --git a/lib/main.dart b/lib/main.dart index 6196a32c..84e32b46 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,6 +39,7 @@ import 'presentation_layer/routes/nostr/settings/moderation/moderation_settings. import 'presentation_layer/routes/nostr/settings/settings_page.dart'; import 'presentation_layer/routes/wallet/wallet_navigation.dart'; import 'presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart'; +import 'presentation_layer/routes/wallet/wallet_pay_page.dart'; import 'theme.dart' as theme; const devDeviceFrame = true; @@ -295,7 +296,7 @@ class MyApp extends ConsumerWidget { ); case '/wallet/pay': return MaterialPageRoute( - builder: (context) => WalletPaymentPage(), + builder: (context) => WalletPayPage(), ); } assert(false, 'Need to implement ${settings.name}'); diff --git a/lib/presentation_layer/atoms/wallet/wallet_card.dart b/lib/presentation_layer/atoms/wallet/wallet_card.dart new file mode 100644 index 00000000..c1225b1e --- /dev/null +++ b/lib/presentation_layer/atoms/wallet/wallet_card.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:ndk/entities.dart' as ndk_entities; + +import '../../../config/palette.dart'; +import '../../../helpers/wallet_number_formatting.dart'; + +class WalletCard extends StatelessWidget { + final ndk_entities.Wallet wallet; + final List balances; + final Function(String) onTap; + final bool isSelected; + final bool isDisabled; + + final Widget? tralling; + + const WalletCard({ + super.key, + required this.wallet, + required this.balances, + required this.onTap, + this.isSelected = false, + this.isDisabled = false, + this.tralling, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: isDisabled ? null : () => onTap(wallet.id), + child: Card( + color: Palette.extraDarkGray, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + wallet.name, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isDisabled ? Palette.gray : Palette.white, + ), + ), + Text( + wallet.id, + style: TextStyle( + fontSize: 14, + color: isDisabled ? Palette.gray : Palette.lightGray, + ), + ), + ], + ), + ], + ), + const SizedBox(width: 12), + Spacer(flex: 1), + Column(children: [ + for (final b in balances) + Text( + "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", + style: TextStyle( + color: Palette.white, + ), + ), + ]), + if (tralling != null) ...[ + Spacer(flex: 2), + tralling!, + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart new file mode 100644 index 00000000..81263249 --- /dev/null +++ b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import '../../../config/palette.dart'; +import '../../atoms/wallet/wallet_card.dart'; + +Future showWalletsSelectBottomSheet({ + required BuildContext context, + required List wallets, + required List balances, + String? selectedId, + double maxHeightFactor = 0.7, // cap as a fraction of screen height +}) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Palette.background, + builder: (ctx) { + final size = MediaQuery.of(ctx).size; + + const double handleHeight = 8 + 4 + 8; + final double maxScrollableHeight = + (size.height * maxHeightFactor) - handleHeight; + + return SafeArea( + top: false, + child: Container( + // upper bound + constraints: BoxConstraints(maxHeight: size.height * maxHeightFactor), + decoration: BoxDecoration( + color: Palette.background, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: const [ + BoxShadow(blurRadius: 16, color: Palette.background) + ], + ), + child: Material( + type: MaterialType.transparency, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 8), + Flexible( + fit: FlexFit.loose, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + maxScrollableHeight.clamp(120.0, double.infinity), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 16), + child: Wrap( + spacing: 12, + runSpacing: 12, + children: wallets.map((wallet) { + final wBallances = balances + .where((b) => b.walletId == wallet.id) + .toList(); + final isSelected = wallet.id == selectedId; + return WalletCard( + wallet: wallet, + balances: wBallances, + isSelected: isSelected, + onTap: (id) => Navigator.of(ctx).pop(id), + tralling: Radio( + activeColor: Palette.primary, + value: wallet.id, + groupValue: isSelected ? wallet.id : null, + onChanged: (value) { + if (value != null) { + Navigator.of(ctx).pop(value); + } + }, + ), + ); + }).toList(), + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart new file mode 100644 index 00000000..d82b2702 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart @@ -0,0 +1,215 @@ +import 'package:camelus/presentation_layer/atoms/wallet/wallet_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../../../../../config/palette.dart'; +import '../../../../atoms/long_button.dart'; +import '../../../../components/wallet/wallets_select_bottom_sheet.dart'; +import '../wallet_pay_state_provider.dart'; +import 'wallet_pay_select_amount_state_provider.dart'; + +class WalletPaySelectAmount extends ConsumerStatefulWidget { + final Function doneCallback; + final Function backCallback; + + const WalletPaySelectAmount({ + super.key, + required this.doneCallback, + required this.backCallback, + required this.currencies, + this.title, + }); + + final List currencies; + + final String? title; + + @override + ConsumerState createState() => + _WalletPaySelectAmountState(); +} + +class _WalletPaySelectAmountState extends ConsumerState { + late final TextEditingController _amountController; + late final TextEditingController _memoController; + final FocusNode _amountFocus = FocusNode(); + final FocusNode _memoFocus = FocusNode(); + + @override + void initState() { + super.initState(); + final state = ref.read(walletSelectAmountStateProvider); + _amountController = TextEditingController(text: state.amount.toString()); + _memoController = TextEditingController(text: state.memo); + + _amountController.addListener(() { + //todo: input validation - double.parse sat vs fiat + ref + .read(walletSelectAmountStateProvider.notifier) + .setAmount(int.parse(_amountController.text)); + }); + _memoController.addListener(() { + ref + .read(walletSelectAmountStateProvider.notifier) + .setMemo(_memoController.text); + }); + } + + @override + void dispose() { + _amountController.dispose(); + _memoController.dispose(); + _amountFocus.dispose(); + _memoFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final selectAmountState = ref.watch(walletSelectAmountStateProvider); + final notifier = ref.read(walletSelectAmountStateProvider.notifier); + + final payState = ref.watch(walletPayStateProvider); + final payNotifier = ref.read(walletPayStateProvider.notifier); + + return Scaffold( + appBar: widget.title != null + ? AppBar( + backgroundColor: Palette.background, + title: Text(widget.title!), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + widget.backCallback(); + }, + ), + ) + : null, + backgroundColor: Palette.background, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), + child: Column( + children: [ + Container( + child: WalletCard( + wallet: payState.availableWallets.firstWhere( + (w) => w.id == payState.payFromWalletId, + orElse: () => ndk_entities.CashuWallet( + id: '', + name: 'Select Wallet', + type: ndk_entities.WalletType.CASHU, + supportedUnits: {}, + mintUrl: '', + ), + ), + balances: payState.availableBalances + .where((b) => b.walletId == payState.payFromWalletId) + .toList(), + onTap: (_) {}, + tralling: IconButton( + icon: Icon( + PhosphorIcons.notePencil(), + size: 25, + ), + color: Palette.primary, + onPressed: () async { + final selectedId = await showWalletsSelectBottomSheet( + context: context, + selectedId: payState.payFromWalletId, + wallets: payState.availableWallets, + balances: payState.availableBalances, + ); + if (selectedId != null) { + payNotifier.updatePayFromWalletId(selectedId); + } + }, + ), + ), + ), + + Spacer(flex: 1), + TextField( + controller: _amountController, + focusNode: _amountFocus, + autofocus: true, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')), + ], + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 40, + fontWeight: FontWeight.w600, + ), + decoration: const InputDecoration( + border: InputBorder.none, + hintText: '0.00', + ), + ), + + const SizedBox(height: 12), + Spacer(flex: 1), + + /// currency selector + Align( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: DropdownButtonFormField( + value: selectAmountState.currency, + isExpanded: true, + decoration: const InputDecoration( + labelText: 'Currency', + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + items: widget.currencies + .map( + (c) => DropdownMenuItem( + value: c, + child: Text(c), + ), + ) + .toList(), + onChanged: (value) => notifier.setCurrency(value), + ), + ), + ), + Spacer(flex: 5), + + /// memo input + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: TextField( + controller: _memoController, + focusNode: _memoFocus, + keyboardType: TextInputType.text, + maxLines: 3, + minLines: 1, + decoration: const InputDecoration( + labelText: 'Memo', + hintText: 'Add a memo (optional)', + border: OutlineInputBorder(), + ), + ), + ), + ], + ), + ), + ), + bottomNavigationBar: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: longButton(name: "next", onPressed: () {}, inverted: true), + ), + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount_state_provider.dart new file mode 100644 index 00000000..d567b3cf --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount_state_provider.dart @@ -0,0 +1,53 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class WalletSelectAmountState { + final int amount; + final String? currency; + final String memo; + + const WalletSelectAmountState({ + this.amount = 0, + this.currency, + this.memo = '', + }); + + WalletSelectAmountState copyWith({ + int? amount, + String? currency, + String? memo, + }) { + return WalletSelectAmountState( + amount: amount ?? this.amount, + currency: currency ?? this.currency, + memo: memo ?? this.memo, + ); + } + + bool get isValid => amount > 0 && currency != null; +} + +class WalletSelectAmountNotifier + extends StateNotifier { + WalletSelectAmountNotifier() : super(const WalletSelectAmountState()); + + void setAmount(int value) { + state = state.copyWith(amount: value); + } + + void setCurrency(String? value) { + state = state.copyWith(currency: value); + } + + void setMemo(String value) { + state = state.copyWith(memo: value); + } + + void reset() { + state = const WalletSelectAmountState(); + } +} + +final walletSelectAmountStateProvider = StateNotifierProvider.autoDispose< + WalletSelectAmountNotifier, WalletSelectAmountState>( + (ref) => WalletSelectAmountNotifier(), +); diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart index 122b8084..8e788fec 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart @@ -9,18 +9,51 @@ import '../../../../../helpers/helpers.dart'; import '../../../../../helpers/wallet_number_formatting.dart'; import '../../../../atoms/long_button.dart'; import '../../../../atoms/my_profile_picture.dart'; +import '../wallet_pay_select_amount/wallet_pay_select_amount.dart'; + +import '../wallet_pay_state_provider.dart'; import 'wallet_pay_reciever_state_provider.dart'; -class WalletPaymentPage extends ConsumerWidget { +class WalletSelectReciever extends ConsumerWidget { final String? walletId; + final Function doneCallback; + + const WalletSelectReciever({ + super.key, + this.walletId, + required this.doneCallback, + }); + + _onTokenSelected(WalletPayNotifier notifier) { + notifier.updateRecieverType(PaymentRecieverType.token); + doneCallback(); + } + + _onContactSelected({ + required WalletPayNotifier notifier, + required String pubkey, + }) { + notifier.updateRecieverType(PaymentRecieverType.contact); + notifier.updatePayToPubkey(pubkey); + doneCallback(); + } - const WalletPaymentPage({super.key, this.walletId}); + _onWalletSelected({ + required WalletPayNotifier notifier, + required String walletId, + }) { + notifier.updateRecieverType(PaymentRecieverType.wallet); + notifier.updatePayToWalletId(walletId); + doneCallback(); + } @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(walletPayRecieverProvider(walletId)); final notifier = ref.read(walletPayRecieverProvider(walletId).notifier); + final paymentStateNotifier = ref.watch(walletPayStateProvider.notifier); + return Scaffold( backgroundColor: Palette.background, appBar: AppBar( @@ -76,7 +109,8 @@ class WalletPaymentPage extends ConsumerWidget { title: "Anonymous Token", child: longButton( name: "create token", - onPressed: () {}, + onPressed: () => + _onTokenSelected(paymentStateNotifier), ), ), if (state.searchQuery.trim().isEmpty) @@ -84,7 +118,10 @@ class WalletPaymentPage extends ConsumerWidget { title: 'Recent contacts', child: _ContactsList( contacts: state.recentContacts, - onTap: notifier.selectContact, + onTap: (c) => _onContactSelected( + notifier: paymentStateNotifier, + pubkey: c.pubkey, + ), ), ), if (state.searchQuery.trim().isNotEmpty) @@ -92,7 +129,10 @@ class WalletPaymentPage extends ConsumerWidget { title: 'Matching contacts', child: _ContactsList( contacts: state.filteredContacts, - onTap: notifier.selectContact, + onTap: (c) => _onContactSelected( + notifier: paymentStateNotifier, + pubkey: c.pubkey, + ), emptyPlaceholder: const Padding( padding: EdgeInsets.all(12.0), child: Text('No matching contacts'), @@ -129,7 +169,10 @@ class WalletPaymentPage extends ConsumerWidget { wallets: state.filteredWallets, balances: state.balances, selectedWalletId: state.selectedWalletId, - onTap: notifier.onWalletTab, + onTap: (wallet) => _onWalletSelected( + notifier: paymentStateNotifier, + walletId: wallet.id, + ), ), ), ], @@ -146,7 +189,12 @@ class WalletPaymentPage extends ConsumerWidget { child: SizedBox( width: double.infinity, height: 45, - child: longButton(name: "next", onPressed: () {}, inverted: true), + child: longButton( + name: "next", + onPressed: () { + _onTokenSelected(paymentStateNotifier); + }, + inverted: true), ), ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart index e0da5fc0..91962eb4 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart @@ -66,14 +66,14 @@ class WalletPayRecieverState { } } -class WalletPaymentNotifier extends StateNotifier { +class WalletPayToNotifier extends StateNotifier { final Ndk _ndk; final GetUserMetadata _getUserMetadata; final List _contactPubkeys; - WalletPaymentNotifier({ + WalletPayToNotifier({ String? initialWalletId, required Ndk ndk, required List contactPubkeys, @@ -131,25 +131,13 @@ class WalletPaymentNotifier extends StateNotifier { ); } - void setSelectedWallet(String? walletId) { - state = state.copyWith(selectedWalletId: walletId); - } - void setSearchQuery(String q) { state = state.copyWith(searchQuery: q); } - - void selectContact(UserMetadata c) { - throw UnimplementedError(); - } - - void onWalletTab(ndk_entities.Wallet w) { - throw UnimplementedError(); - } } final walletPayRecieverProvider = StateNotifierProvider.family< - WalletPaymentNotifier, + WalletPayToNotifier, WalletPayRecieverState, String?>((ref, initialWalletId) { final ndk = ref.watch(ndkProvider); @@ -158,7 +146,7 @@ final walletPayRecieverProvider = StateNotifierProvider.family< final getUserMetadata = ref.watch(metadataProvider); - return WalletPaymentNotifier( + return WalletPayToNotifier( initialWalletId: initialWalletId, ndk: ndk, contactPubkeys: contacts.contactList.contacts, diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart new file mode 100644 index 00000000..6c5eb990 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart @@ -0,0 +1,155 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:ndk/ndk.dart'; + +import '../../../providers/ndk_provider.dart'; + +class WalletPayState { + final List availableWallets; + final List availableBalances; + + final int? amount; + final String? unit; + + final String? memo; + + final PaymentRecieverType? recieverType; + final String? payToPubkey; + final String? payToWalletId; + final String? payFromWalletId; + + WalletPayState({ + required this.availableWallets, + required this.availableBalances, + required this.payFromWalletId, + required this.amount, + required this.unit, + required this.memo, + required this.recieverType, + required this.payToPubkey, + required this.payToWalletId, + }); + + WalletPayState copyWith({ + List? availableWallets, + List? availableBalances, + String? payFromWalletId, + int? amount, + String? unit, + String? memo, + PaymentRecieverType? recieverType, + String? payToPubkey, + String? payToWalletId, + }) { + return WalletPayState( + availableWallets: availableWallets ?? this.availableWallets, + availableBalances: availableBalances ?? this.availableBalances, + payFromWalletId: payFromWalletId ?? this.payFromWalletId, + amount: amount ?? this.amount, + unit: unit ?? this.unit, + memo: memo ?? this.memo, + recieverType: recieverType ?? this.recieverType, + payToPubkey: payToPubkey ?? this.payToPubkey, + payToWalletId: payToWalletId ?? this.payToWalletId, + ); + } +} + +enum PaymentRecieverType { + token, + contact, + wallet, +} + +class WalletPayNotifier extends StateNotifier { + final Ndk _ndk; + + WalletPayNotifier({required Ndk ndk}) + : _ndk = ndk, + super( + WalletPayState( + availableWallets: [], + availableBalances: [], + payFromWalletId: null, + amount: null, + unit: null, + memo: null, + recieverType: null, + payToPubkey: null, + payToWalletId: null, + ), + ) { + _loadInitial(); + } + + Future _loadInitial() async { + final balances = await _ndk.wallets.combinedBalances.first; + + final wallets = await _ndk.wallets.walletsStream.first; + + state = state.copyWith( + availableWallets: wallets, + availableBalances: balances, + ); + } + + bool get valid { + return state.payFromWalletId != null && + state.amount != null && + state.unit != null && + state.recieverType != null && + (state.recieverType == PaymentRecieverType.contact + ? state.payToPubkey != null + : state.recieverType == PaymentRecieverType.wallet + ? state.payToWalletId != null + : true); + } + + void updatePayFromWalletId(String walletId) { + state = state.copyWith(payFromWalletId: walletId); + } + + void updateAmount(int amount) { + state = state.copyWith(amount: amount); + } + + void updateUnit(String unit) { + state = state.copyWith(unit: unit); + } + + void updateMemo(String? memo) { + state = state.copyWith(memo: memo); + } + + void updateRecieverType(PaymentRecieverType type) { + state = state.copyWith(recieverType: type); + } + + void updatePayToPubkey(String? pubkey) { + state = state.copyWith(payToPubkey: pubkey); + } + + void updatePayToWalletId(String? walletId) { + state = state.copyWith(payToWalletId: walletId); + } + + void reset() { + state = WalletPayState( + availableBalances: state.availableBalances, + availableWallets: state.availableWallets, + payFromWalletId: null, + amount: null, + unit: null, + memo: null, + recieverType: null, + payToPubkey: null, + payToWalletId: null, + ); + } +} + +final walletPayStateProvider = + StateNotifierProvider((ref) { + final ndk = ref.watch(ndkProvider); + return WalletPayNotifier(ndk: ndk); +}); diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart new file mode 100644 index 00000000..0f1399fa --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class WalletPaySummary extends ConsumerWidget { + const WalletPaySummary({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Center( + child: Text('todo: pay summary'), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay_page.dart b/lib/presentation_layer/routes/wallet/wallet_pay_page.dart new file mode 100644 index 00000000..90dfaa9f --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_pay_page.dart @@ -0,0 +1,71 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart'; +import 'wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart'; +import 'wallet_pay/wallet_pay_summary/wallet_pay_summary.dart'; + +class WalletPayPage extends ConsumerStatefulWidget { + const WalletPayPage({ + super.key, + }); + + @override + ConsumerState createState() => _WalletPayPageState(); +} + +class _WalletPayPageState extends ConsumerState + with TickerProviderStateMixin { + final PageController _horizontalPageController = PageController( + initialPage: 0, + keepPage: true, + ); + + bool horizontalScrollLock = false; + + _navigateToNextPage() { + if (_horizontalPageController.page != null) { + _horizontalPageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + _navigateToPreviousPage() { + if (_horizontalPageController.page != null) { + _horizontalPageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + @override + Widget build(BuildContext context) { + return PageView( + controller: _horizontalPageController, + physics: horizontalScrollLock + ? const NeverScrollableScrollPhysics() + : const AlwaysScrollableScrollPhysics(), + children: [ + WalletSelectReciever( + doneCallback: () { + _navigateToNextPage(); + }, + ), + WalletPaySelectAmount( + backCallback: () { + _navigateToPreviousPage(); + }, + doneCallback: () { + _navigateToNextPage(); + }, + currencies: ['todo', 'sat', 'eur', 'usd'], + title: 'Select Amount', + ), + WalletPaySummary(), + ], + ); + } +} From 71b49f904e885cc6daa59d019535c6ea5431f88f Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sat, 16 Aug 2025 11:49:55 +0200 Subject: [PATCH 17/52] pay select amount --- .../atoms/currency_picker_bar.dart | 346 ++++++++++++++++++ .../wallet_pay_select_amount.dart | 151 ++++++-- ...llet_pay_select_amount_state_provider.dart | 53 --- .../wallet_pay/wallet_pay_state_provider.dart | 24 +- .../routes/wallet/wallet_pay_page.dart | 3 +- 5 files changed, 482 insertions(+), 95 deletions(-) create mode 100644 lib/presentation_layer/atoms/currency_picker_bar.dart delete mode 100644 lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount_state_provider.dart diff --git a/lib/presentation_layer/atoms/currency_picker_bar.dart b/lib/presentation_layer/atoms/currency_picker_bar.dart new file mode 100644 index 00000000..c08f5a1e --- /dev/null +++ b/lib/presentation_layer/atoms/currency_picker_bar.dart @@ -0,0 +1,346 @@ +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../config/palette.dart'; + +class CurrencyPickerBar extends StatefulWidget { + const CurrencyPickerBar({ + super.key, + required this.currencies, + this.initialIndex = 0, + this.onChanged, + this.height = 84, + this.lensRadius = 28, + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + this.trackColor, + this.activeColor, + this.inactiveColor, + this.showHaptics = true, + this.labelFontSize = 18, + this.snapDuration = const Duration(milliseconds: 180), + this.isDark = true, + }); + + final List currencies; + final int initialIndex; + final ValueChanged? onChanged; + final double height; + final double lensRadius; + final EdgeInsets padding; + final Color? trackColor; + final Color? activeColor; + final Color? inactiveColor; + final bool showHaptics; + final bool isDark; + + final double labelFontSize; + + final Duration snapDuration; + + @override + State createState() => _CurrencyPickerBarState(); +} + +class _CurrencyPickerBarState extends State + with SingleTickerProviderStateMixin { + late int _selectedIndex; + late double _lensX; + late AnimationController _snapController; + Animation? _snapAnim; + + @override + void initState() { + super.initState(); + _selectedIndex = widget.currencies.isEmpty + ? 0 + : widget.initialIndex + .clamp(0, math.max(0, widget.currencies.length - 1)); + _lensX = 0; // will be set on first layout + _snapController = + AnimationController(vsync: this, duration: widget.snapDuration); + } + + @override + void didUpdateWidget(covariant CurrencyPickerBar oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.snapDuration != widget.snapDuration) { + _snapController.duration = widget.snapDuration; + } + // if (oldWidget.currencies != widget.currencies) { + // widget.onChanged?.call(_selectedIndex); + // } + } + + @override + void dispose() { + _snapController.dispose(); + super.dispose(); + } + + double _leftBound(double width) => widget.padding.left + widget.lensRadius; + double _rightBound(double width) => + width - widget.padding.right - widget.lensRadius; + + // position for index i across the track + double _xForIndex(int i, double width) { + if (widget.currencies.length <= 1) { + return (_leftBound(width) + _rightBound(width)) / 2; + } + final span = _rightBound(width) - _leftBound(width); + final t = i / (widget.currencies.length - 1); + return _leftBound(width) + span * t; + } + + int _nearestIndexForX(double x, double width) { + if (widget.currencies.isEmpty) return 0; + int closest = 0; + double best = double.infinity; + for (int i = 0; i < widget.currencies.length; i++) { + final xi = _xForIndex(i, width); + final d = (x - xi).abs(); + if (d < best) { + best = d; + closest = i; + } + } + return closest; + } + + void _animateLensToIndex(int index, double width) { + final targetX = _xForIndex(index, width); + _snapController.stop(); + final start = _lensX; + _snapAnim = Tween(begin: start, end: targetX) + .chain(CurveTween(curve: Curves.easeOutCubic)) + .animate(_snapController); + _snapAnim!.addListener(() { + setState(() { + _lensX = _snapAnim!.value; + }); + }); + _snapController.forward(from: 0); + } + + void _setSelectedIndex(int index, {bool fromUser = true}) { + if (index == _selectedIndex) return; + setState(() { + _selectedIndex = index; + }); + if (widget.showHaptics && fromUser) { + HapticFeedback.selectionClick(); + } + widget.onChanged?.call(_selectedIndex); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final trackColor = widget.trackColor ?? + (widget.isDark + ? Palette.extraDarkGray.withValues(alpha: 0.22) + : Palette.lightGray.withValues(alpha: 0.85)); + + final inactiveColor = widget.inactiveColor ?? + (widget.isDark + ? Palette.primary.withValues(alpha: 0.88) + : Palette.primary.withValues(alpha: 0.9)); + + final activeColor = widget.activeColor ?? theme.colorScheme.primary; + + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final height = widget.height; + + // initialize lens position on first layout + if (_lensX == 0 && width > 0) { + _lensX = _xForIndex(_selectedIndex, width); + } + + void handleTapOrPanTo(Offset localPos) { + final clamped = + localPos.dx.clamp(_leftBound(width), _rightBound(width)); + setState(() => _lensX = clamped.toDouble()); + final nearest = _nearestIndexForX(_lensX, width); + _setSelectedIndex(nearest); + } + + Widget buildTrackContent() { + final labels = []; + for (int i = 0; i < widget.currencies.length; i++) { + final x = _xForIndex(i, width); + final t = (i == _selectedIndex) ? 1.0 : 0.0; + final color = + Color.lerp(inactiveColor, activeColor, t) ?? activeColor; + + labels.add( + Positioned( + left: x - 36, // centered width=72 + width: 72, + top: 0, + bottom: 0, + child: Center( + child: Text( + widget.currencies[i], + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: theme.textTheme.titleMedium?.copyWith( + fontSize: widget.labelFontSize, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + color: color, + ), + ), + ), + ), + ); + } + + return Stack( + children: [ + // track background + Positioned.fill( + child: Container( + margin: widget.padding, + decoration: BoxDecoration( + color: trackColor, + borderRadius: BorderRadius.circular(999), + boxShadow: [ + if (!widget.isDark) + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + ), + ), + + ...labels, + ], + ); + } + + final lensRadius = widget.lensRadius; + final lensDiameter = lensRadius * 2; + final lensCenterY = height / 2; + final hasItems = widget.currencies.isNotEmpty; + final canDecrease = hasItems && _selectedIndex > 0; + final canIncrease = + hasItems && _selectedIndex < widget.currencies.length - 1; + + return Semantics( + label: 'Currency picker', + value: hasItems ? widget.currencies[_selectedIndex] : '', + increasedValue: + canIncrease ? widget.currencies[_selectedIndex + 1] : null, + decreasedValue: + canDecrease ? widget.currencies[_selectedIndex - 1] : null, + onIncrease: canIncrease + ? () { + _setSelectedIndex(_selectedIndex + 1); + _animateLensToIndex(_selectedIndex, width); + } + : null, + onDecrease: canDecrease + ? () { + _setSelectedIndex(_selectedIndex - 1); + _animateLensToIndex(_selectedIndex, width); + } + : null, + child: SizedBox( + height: height, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTapDown: (d) { + handleTapOrPanTo(d.localPosition); + _animateLensToIndex(_selectedIndex, width); + }, + onHorizontalDragUpdate: (d) { + final moved = (_lensX + d.delta.dx) + .clamp(_leftBound(width), _rightBound(width)); + setState(() => _lensX = moved.toDouble()); + final nearest = _nearestIndexForX(_lensX, width); + _setSelectedIndex(nearest); + }, + onHorizontalDragEnd: (d) { + _animateLensToIndex(_selectedIndex, width); + }, + child: Stack( + clipBehavior: Clip.none, + children: [ + // background track + Positioned.fill(child: buildTrackContent()), + + // lens + Positioned( + left: _lensX - lensRadius, + top: lensCenterY - lensRadius, + width: lensDiameter, + height: lensDiameter, + child: _LensRing( + isDark: widget.isDark, + borderWidth: 3, + shadowColor: widget.isDark + ? Palette.black.withValues(alpha: 0.5) + : Palette.black.withValues(alpha: 0.15), + highlightColor: widget.isDark + ? Colors.white.withValues(alpha: 0.08) + : Colors.white.withValues(alpha: 0.18), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class _LensRing extends StatelessWidget { + const _LensRing({ + required this.isDark, + this.borderWidth = 3, + this.shadowColor = Palette.gray, + this.highlightColor = Palette.gray, + }); + + final bool isDark; + final double borderWidth; + final Color shadowColor; + final Color highlightColor; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: borderWidth, + color: isDark ? Palette.darkGray : Palette.background, + ), + boxShadow: [ + BoxShadow( + color: Colors.transparent, + blurRadius: 10, + offset: const Offset(0, 4), + ), + BoxShadow( + color: highlightColor, + blurRadius: 8, + spreadRadius: -2, + offset: const Offset(0, -1), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart index d82b2702..93ee91e0 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart @@ -6,10 +6,10 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../../../config/palette.dart'; +import '../../../../atoms/currency_picker_bar.dart'; import '../../../../atoms/long_button.dart'; import '../../../../components/wallet/wallets_select_bottom_sheet.dart'; import '../wallet_pay_state_provider.dart'; -import 'wallet_pay_select_amount_state_provider.dart'; class WalletPaySelectAmount extends ConsumerStatefulWidget { final Function doneCallback; @@ -19,12 +19,9 @@ class WalletPaySelectAmount extends ConsumerStatefulWidget { super.key, required this.doneCallback, required this.backCallback, - required this.currencies, this.title, }); - final List currencies; - final String? title; @override @@ -41,20 +38,31 @@ class _WalletPaySelectAmountState extends ConsumerState { @override void initState() { super.initState(); - final state = ref.read(walletSelectAmountStateProvider); - _amountController = TextEditingController(text: state.amount.toString()); + final state = ref.read(walletPayStateProvider); + _amountController = TextEditingController(text: state.amount?.toString()); _memoController = TextEditingController(text: state.memo); _amountController.addListener(() { - //todo: input validation - double.parse sat vs fiat - ref - .read(walletSelectAmountStateProvider.notifier) - .setAmount(int.parse(_amountController.text)); + final stateR = ref.read(walletPayStateProvider); + if (_amountController.text.isEmpty) { + return; + } + + final int? parsedAmount; + if (stateR.unit == 'sat') { + parsedAmount = int.tryParse(_amountController.text); + } else { + // Handle decimal input for fiat currencies + final sanitizedInput = _amountController.text.replaceAll(',', '.'); + parsedAmount = ((double.tryParse(sanitizedInput) ?? 0) * 100).toInt(); + } + + ref.read(walletPayStateProvider.notifier).updateAmount(parsedAmount ?? 0); }); _memoController.addListener(() { ref - .read(walletSelectAmountStateProvider.notifier) - .setMemo(_memoController.text); + .read(walletPayStateProvider.notifier) + .updateMemo(_memoController.text); }); } @@ -67,10 +75,26 @@ class _WalletPaySelectAmountState extends ConsumerState { super.dispose(); } + _onSwitchCurrency() { + final state = ref.read(walletPayStateProvider); + final stateNotifier = ref.read(walletPayStateProvider.notifier); + if (state.unit == 'sat') { + // remove last two digits from state.amount + final newAmount = + state.amount != null ? (state.amount! / 100).toStringAsFixed(0) : '0'; + _amountController.text = newAmount; + stateNotifier.updateAmount((state.amount! / 100).toInt()); + } else { + // Convert amount to fiat format + final amountInFiat = (state.amount ?? 0) / 100; + _amountController.text = amountInFiat.toStringAsFixed(2); + } + } + @override Widget build(BuildContext context) { - final selectAmountState = ref.watch(walletSelectAmountStateProvider); - final notifier = ref.read(walletSelectAmountStateProvider.notifier); + final state = ref.watch(walletPayStateProvider); + final notifier = ref.read(walletPayStateProvider.notifier); final payState = ref.watch(walletPayStateProvider); final payNotifier = ref.read(walletPayStateProvider.notifier); @@ -136,19 +160,24 @@ class _WalletPaySelectAmountState extends ConsumerState { controller: _amountController, focusNode: _amountFocus, autofocus: true, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')), - ], + keyboardType: TextInputType.numberWithOptions( + decimal: state.unit != 'sat'), + inputFormatters: state.unit == 'sat' + ? [ + FilteringTextInputFormatter.digitsOnly, + ] + : [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9\.,]')), + DecimalTextInputFormatter(decimalRange: 2), + ], textAlign: TextAlign.center, style: const TextStyle( fontSize: 40, fontWeight: FontWeight.w600, ), - decoration: const InputDecoration( + decoration: InputDecoration( border: InputBorder.none, - hintText: '0.00', + hintText: state.unit != 'sat' ? '0.00' : '0', ), ), @@ -160,27 +189,28 @@ class _WalletPaySelectAmountState extends ConsumerState { alignment: Alignment.center, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 320), - child: DropdownButtonFormField( - value: selectAmountState.currency, - isExpanded: true, - decoration: const InputDecoration( - labelText: 'Currency', - border: OutlineInputBorder(), - contentPadding: - EdgeInsets.symmetric(horizontal: 12, vertical: 10), - ), - items: widget.currencies - .map( - (c) => DropdownMenuItem( - value: c, - child: Text(c), - ), - ) - .toList(), - onChanged: (value) => notifier.setCurrency(value), + child: CurrencyPickerBar( + currencies: state.supportedUnitsByWallet != null + ? state.supportedUnitsByWallet!.toList() + : [], + initialIndex: state.unit != null + ? state.supportedUnitsByWallet! + .toList() + .indexOf(state.unit!) + : 0, + onChanged: (i) => { + payNotifier.updateUnit( + state.supportedUnitsByWallet!.elementAt(i), + ), + _onSwitchCurrency(), + }, + showHaptics: true, + trackColor: Palette.extraDarkGray, + activeColor: Palette.primary, ), ), ), + Spacer(flex: 5), /// memo input @@ -199,6 +229,7 @@ class _WalletPaySelectAmountState extends ConsumerState { ), ), ), + Spacer(flex: 1), ], ), ), @@ -213,3 +244,45 @@ class _WalletPaySelectAmountState extends ConsumerState { ); } } + +class DecimalTextInputFormatter extends TextInputFormatter { + DecimalTextInputFormatter({required this.decimalRange}) + : assert(decimalRange > 0); + + final int decimalRange; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + String newText = newValue.text; + + newText = newText.replaceAll(',', '.'); + + if (newText.isEmpty) { + return newValue.copyWith(text: ''); + } + + if (newText == '.') { + return newValue.copyWith(text: '0.'); + } + + if (!RegExp(r'^\d*\.?\d*').hasMatch(newText)) { + return oldValue; + } + + if (newText.contains('.')) { + List parts = newText.split('.'); + if (parts.length > 2) { + return oldValue; + } + if (parts[1].length > decimalRange) { + // too many decimal places + return oldValue; + } + } + + return newValue.copyWith(text: newText); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount_state_provider.dart deleted file mode 100644 index d567b3cf..00000000 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount_state_provider.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class WalletSelectAmountState { - final int amount; - final String? currency; - final String memo; - - const WalletSelectAmountState({ - this.amount = 0, - this.currency, - this.memo = '', - }); - - WalletSelectAmountState copyWith({ - int? amount, - String? currency, - String? memo, - }) { - return WalletSelectAmountState( - amount: amount ?? this.amount, - currency: currency ?? this.currency, - memo: memo ?? this.memo, - ); - } - - bool get isValid => amount > 0 && currency != null; -} - -class WalletSelectAmountNotifier - extends StateNotifier { - WalletSelectAmountNotifier() : super(const WalletSelectAmountState()); - - void setAmount(int value) { - state = state.copyWith(amount: value); - } - - void setCurrency(String? value) { - state = state.copyWith(currency: value); - } - - void setMemo(String value) { - state = state.copyWith(memo: value); - } - - void reset() { - state = const WalletSelectAmountState(); - } -} - -final walletSelectAmountStateProvider = StateNotifierProvider.autoDispose< - WalletSelectAmountNotifier, WalletSelectAmountState>( - (ref) => WalletSelectAmountNotifier(), -); diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart index 6c5eb990..73f9cf49 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart @@ -17,6 +17,7 @@ class WalletPayState { final String? payToPubkey; final String? payToWalletId; final String? payFromWalletId; + final Set? supportedUnitsByWallet; WalletPayState({ required this.availableWallets, @@ -28,8 +29,18 @@ class WalletPayState { required this.recieverType, required this.payToPubkey, required this.payToWalletId, + this.supportedUnitsByWallet, }); + ndk_entities.Wallet? get payFromWallet { + if (payFromWalletId == null) return null; + final filter = availableWallets.where( + (wallet) => wallet.id == payFromWalletId, + ); + if (filter.isEmpty) return null; + return filter.first; + } + WalletPayState copyWith({ List? availableWallets, List? availableBalances, @@ -40,6 +51,7 @@ class WalletPayState { PaymentRecieverType? recieverType, String? payToPubkey, String? payToWalletId, + Set? supportedUnitsByWallet, }) { return WalletPayState( availableWallets: availableWallets ?? this.availableWallets, @@ -51,6 +63,8 @@ class WalletPayState { recieverType: recieverType ?? this.recieverType, payToPubkey: payToPubkey ?? this.payToPubkey, payToWalletId: payToWalletId ?? this.payToWalletId, + supportedUnitsByWallet: + supportedUnitsByWallet ?? this.supportedUnitsByWallet, ); } } @@ -106,15 +120,23 @@ class WalletPayNotifier extends StateNotifier { } void updatePayFromWalletId(String walletId) { - state = state.copyWith(payFromWalletId: walletId); + state = state.copyWith( + payFromWalletId: walletId, + ); + state = state.copyWith( + supportedUnitsByWallet: state.payFromWallet?.supportedUnits, + unit: "sat", // state.payFromWallet?.supportedUnits.first, + ); } void updateAmount(int amount) { state = state.copyWith(amount: amount); + print('Updated amount: $amount'); } void updateUnit(String unit) { state = state.copyWith(unit: unit); + print('Updated unit: $unit'); } void updateMemo(String? memo) { diff --git a/lib/presentation_layer/routes/wallet/wallet_pay_page.dart b/lib/presentation_layer/routes/wallet/wallet_pay_page.dart index 90dfaa9f..7a403aad 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay_page.dart @@ -61,8 +61,7 @@ class _WalletPayPageState extends ConsumerState doneCallback: () { _navigateToNextPage(); }, - currencies: ['todo', 'sat', 'eur', 'usd'], - title: 'Select Amount', + title: 'Enter amount', ), WalletPaySummary(), ], From 4a9f54a508a74e1c7ba545d79322a202e81a4137 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sat, 16 Aug 2025 12:46:53 +0200 Subject: [PATCH 18/52] pay summary page --- .../atoms/wallet/wallet_card.dart | 36 ++- .../wallet_pay_select_amount.dart | 2 +- .../wallet_pay/wallet_pay_state_provider.dart | 20 +- .../wallet_pay_summary.dart | 271 +++++++++++++++++- .../routes/wallet/wallet_pay_page.dart | 6 +- 5 files changed, 317 insertions(+), 18 deletions(-) diff --git a/lib/presentation_layer/atoms/wallet/wallet_card.dart b/lib/presentation_layer/atoms/wallet/wallet_card.dart index c1225b1e..2e13691a 100644 --- a/lib/presentation_layer/atoms/wallet/wallet_card.dart +++ b/lib/presentation_layer/atoms/wallet/wallet_card.dart @@ -13,6 +13,10 @@ class WalletCard extends StatelessWidget { final Widget? tralling; + final bool showBalances; + + final Color backgroundColor; + const WalletCard({ super.key, required this.wallet, @@ -20,20 +24,31 @@ class WalletCard extends StatelessWidget { required this.onTap, this.isSelected = false, this.isDisabled = false, + this.backgroundColor = Palette.extraDarkGray, this.tralling, + this.showBalances = true, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: isDisabled ? null : () => onTap(wallet.id), - child: Card( - color: Palette.extraDarkGray, + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(12), + ), child: Padding( padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ + CircleAvatar( + backgroundColor: Palette.primary.withValues(alpha: 0.8), + child: Text(wallet.name.substring(0, 2).toUpperCase(), + style: TextStyle(color: Colors.white, fontSize: 12)), + ), + const SizedBox(width: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -61,15 +76,16 @@ class WalletCard extends StatelessWidget { ), const SizedBox(width: 12), Spacer(flex: 1), - Column(children: [ - for (final b in balances) - Text( - "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", - style: TextStyle( - color: Palette.white, + if (showBalances) + Column(children: [ + for (final b in balances) + Text( + "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", + style: TextStyle( + color: Palette.white, + ), ), - ), - ]), + ]), if (tralling != null) ...[ Spacer(flex: 2), tralling!, diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart index 93ee91e0..edfc3935 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart @@ -173,7 +173,7 @@ class _WalletPaySelectAmountState extends ConsumerState { textAlign: TextAlign.center, style: const TextStyle( fontSize: 40, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.bold, ), decoration: InputDecoration( border: InputBorder.none, diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart index 73f9cf49..0740c69f 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart @@ -70,9 +70,23 @@ class WalletPayState { } enum PaymentRecieverType { - token, - contact, - wallet, + token('token'), + contact('contact'), + wallet('wallet'); + + final String value; + + const PaymentRecieverType(this.value); + + factory PaymentRecieverType.fromValue(String value) { + return PaymentRecieverType.values.firstWhere( + (kind) => kind.value == value, + orElse: () => throw ArgumentError('Invalid event kind value: $value'), + ); + } + + @override + String toString() => value; } class WalletPayNotifier extends StateNotifier { diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart index 0f1399fa..e6af27b3 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -1,15 +1,280 @@ -import 'package:flutter/widgets.dart'; +import 'package:camelus/presentation_layer/atoms/long_button.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import 'package:ndk/entities.dart' as ndk_entities; + +import '../../../../../config/palette.dart'; +import '../../../../../helpers/wallet_number_formatting.dart'; +import '../../../../atoms/wallet/wallet_card.dart'; +import '../../../../components/wallet/wallets_select_bottom_sheet.dart'; +import '../wallet_pay_state_provider.dart'; class WalletPaySummary extends ConsumerWidget { + final Function backCallback; const WalletPaySummary({ super.key, + required this.backCallback, }); @override Widget build(BuildContext context, WidgetRef ref) { - return Center( - child: Text('todo: pay summary'), + final state = ref.watch(walletPayStateProvider); + final payNotifier = ref.read(walletPayStateProvider.notifier); + + return Scaffold( + backgroundColor: Colors.black, + body: Column( + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + color: Palette.primary.withValues(alpha: 0.9), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + /// header back, close + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + Icons.arrow_back, + color: Colors.white, + size: 24, + ), + onPressed: () => backCallback(), + ), + Text( + 'summary', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + IconButton( + icon: Icon( + Icons.close, + color: Palette.lightGray, + size: 24, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + SizedBox(height: 40), + + /// amount section + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: 30), + Text( + state.amount != null && state.unit != null + ? WalletNumberFormatting.formatAmount( + amount: state.amount!, unit: state.unit!) + : '0', + style: TextStyle( + color: Colors.white, + fontSize: 48, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 4), + IconButton( + icon: Icon( + PhosphorIcons.notePencil(), + size: 24, + color: Palette.extraLightGray, + ), + onPressed: () => backCallback(), + ), + ], + ), + Text( + state.unit ?? '', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 20), + ], + ), + ), + ), + ), + + /// main + Expanded( + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Color(0xFF2A2A2A), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + WalletCard( + showBalances: false, + backgroundColor: Colors.transparent, + wallet: state.availableWallets.firstWhere( + (w) => w.id == state.payFromWalletId, + orElse: () => ndk_entities.CashuWallet( + id: '', + name: 'Select Wallet', + type: ndk_entities.WalletType.CASHU, + supportedUnits: {}, + mintUrl: '', + ), + ), + balances: state.availableBalances + .where((b) => b.walletId == state.payFromWalletId) + .toList(), + onTap: (_) {}, + tralling: IconButton( + icon: Icon( + PhosphorIcons.notePencil(), + size: 25, + ), + color: Palette.primary, + onPressed: () async { + final selectedId = + await showWalletsSelectBottomSheet( + context: context, + selectedId: state.payFromWalletId, + wallets: state.availableWallets, + balances: state.availableBalances, + ); + if (selectedId != null) { + payNotifier.updatePayFromWalletId(selectedId); + } + }, + ), + ), + + Divider(color: Palette.darkGray, height: 1), + + /// receiver + Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + CircleAvatar( + backgroundColor: Color(0xFF1976D2), + child: Text('TK', + style: TextStyle( + color: Colors.white, fontSize: 12)), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('receiver', + style: TextStyle( + color: Colors.grey, fontSize: 12)), + Text('TOKEN', + style: TextStyle( + color: Colors.white, fontSize: 16)), + Text('', + style: TextStyle( + color: Colors.grey, fontSize: 14)), + ], + ), + ), + Icon( + PhosphorIcons.notePencil(), + size: 24, + color: Palette.lightGray, + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 16), + + /// details + _buildDetailRow( + label: 'transaction type', + value: state.recieverType.toString(), + isEditable: false, + ), + + _buildDetailRow( + label: 'Memo', + value: state.memo ?? '', + onEdit: () { + backCallback(); + }, + ), + + SizedBox(height: 24), + + Spacer(), + ], + ), + ), + ), + ], + ), + bottomNavigationBar: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: longButton(name: "send", onPressed: () {}, inverted: true), + ), + ), + ); + } + + Widget _buildDetailRow({ + required String label, + required String value, + bool isEditable = true, + Function()? onEdit, + }) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: TextStyle(color: Colors.grey, fontSize: 12)), + Text(value, style: TextStyle(color: Colors.white, fontSize: 16)), + ], + ), + if (isEditable && onEdit != null) + IconButton( + icon: Icon( + PhosphorIcons.notePencil(), + size: 25, + ), + color: Palette.primary, + onPressed: isEditable ? onEdit : null, + ), + ], + ), ); } } diff --git a/lib/presentation_layer/routes/wallet/wallet_pay_page.dart b/lib/presentation_layer/routes/wallet/wallet_pay_page.dart index 7a403aad..0ac5d8e6 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay_page.dart @@ -63,7 +63,11 @@ class _WalletPayPageState extends ConsumerState }, title: 'Enter amount', ), - WalletPaySummary(), + WalletPaySummary( + backCallback: () { + _navigateToPreviousPage(); + }, + ), ], ); } From 9b9d8a523ca2aeb6f402702998d120830f92a049 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sat, 16 Aug 2025 12:58:42 +0200 Subject: [PATCH 19/52] wallet summary reciever details --- .../wallet_pay_summary.dart | 156 ++++++++++++++---- 1 file changed, 121 insertions(+), 35 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart index e6af27b3..3448c5d5 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -171,41 +171,16 @@ class WalletPaySummary extends ConsumerWidget { Divider(color: Palette.darkGray, height: 1), /// receiver - Padding( - padding: EdgeInsets.all(16), - child: Row( - children: [ - CircleAvatar( - backgroundColor: Color(0xFF1976D2), - child: Text('TK', - style: TextStyle( - color: Colors.white, fontSize: 12)), - ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('receiver', - style: TextStyle( - color: Colors.grey, fontSize: 12)), - Text('TOKEN', - style: TextStyle( - color: Colors.white, fontSize: 16)), - Text('', - style: TextStyle( - color: Colors.grey, fontSize: 14)), - ], - ), - ), - Icon( - PhosphorIcons.notePencil(), - size: 24, - color: Palette.lightGray, - ), - ], - ), - ), + if (state.recieverType == + PaymentRecieverType.contact) ...[ + ContactReciever(), + ] else if (state.recieverType == + PaymentRecieverType.token) ...[ + TokenReciever(), + ] else if (state.recieverType == + PaymentRecieverType.wallet) ...[ + WalletReciever(), + ], ], ), ), @@ -278,3 +253,114 @@ class WalletPaySummary extends ConsumerWidget { ); } } + +class ContactReciever extends StatelessWidget { + const ContactReciever({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + CircleAvatar( + backgroundColor: Palette.primary.withValues(alpha: 0.8), + child: + Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('receiver', + style: TextStyle(color: Colors.grey, fontSize: 12)), + Text('NOT IMPLEMENTED', + style: TextStyle(color: Colors.white, fontSize: 16)), + Text('send to pubkey not implemented', + style: TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ); + } +} + +class TokenReciever extends StatelessWidget { + const TokenReciever({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + CircleAvatar( + backgroundColor: Palette.primary.withValues(alpha: 0.8), + child: + Text('TK', style: TextStyle(color: Colors.white, fontSize: 12)), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('receiver', + style: TextStyle(color: Palette.gray, fontSize: 12)), + Text('Token', + style: TextStyle( + color: Palette.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + Text('send as a token or qr code', + style: TextStyle(color: Palette.lightGray, fontSize: 14)), + ], + ), + ), + ], + ), + ); + } +} + +class WalletReciever extends StatelessWidget { + const WalletReciever({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + CircleAvatar( + backgroundColor: Palette.primary.withValues(alpha: 0.8), + child: + Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('receiver', + style: TextStyle(color: Colors.grey, fontSize: 12)), + Text('NOT IMPLEMENTED', + style: TextStyle(color: Colors.white, fontSize: 16)), + Text('send to wallet not implemented', + style: TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ); + } +} From 8f9f6dce77cc9338bdd13e716cfbfaf50e410489 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:21:26 +0200 Subject: [PATCH 20/52] qr code token display --- .../components/wallet/animated_qr.dart | 187 ++++++++++++ .../wallet_pay_done/wallet_pay_done.dart | 266 ++++++++++++++++++ .../wallet_pay/wallet_pay_state_provider.dart | 72 +++++ .../wallet_pay_summary.dart | 64 ++++- pubspec.lock | 23 ++ pubspec.yaml | 4 + 6 files changed, 615 insertions(+), 1 deletion(-) create mode 100644 lib/presentation_layer/components/wallet/animated_qr.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart diff --git a/lib/presentation_layer/components/wallet/animated_qr.dart b/lib/presentation_layer/components/wallet/animated_qr.dart new file mode 100644 index 00000000..dd8418ac --- /dev/null +++ b/lib/presentation_layer/components/wallet/animated_qr.dart @@ -0,0 +1,187 @@ +import 'dart:typed_data'; +import 'dart:convert' show base64Decode, base64Url, utf8; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:bc_ur_dart/bc_ur_dart.dart'; +import 'package:pretty_qr_code/pretty_qr_code.dart'; +import 'dart:async'; + +class AnimatedQr extends ConsumerStatefulWidget { + final String? qrCodeData; + + const AnimatedQr({super.key, this.qrCodeData}); + + @override + ConsumerState createState() => AnimatedQrState(); +} + +class AnimatedQrState extends ConsumerState { + Timer? _timer; + int _currentIndex = 0; + List _urParts = []; + UR? _encoder; + + bool showAnimatedQr = true; + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _initializeUR(); + } + + @override + void didUpdateWidget(AnimatedQr oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.qrCodeData != widget.qrCodeData) { + _timer?.cancel(); + _initializeUR(); + } + } + + void _initializeUR() { + if (widget.qrCodeData != null && widget.qrCodeData!.isNotEmpty) { + try { + final tokenString = widget.qrCodeData!; + + Uint8List bytes; + if (tokenString.startsWith('cashuB')) { + /// remove cashuB prefix + final tokenData = tokenString.substring(6); + + /// fix: add padding for base64url decoding + String paddedTokenData = tokenData; + while (paddedTokenData.length % 4 != 0) { + paddedTokenData += '='; + } + + bytes = base64Url.decode(paddedTokenData); + } else { + bytes = utf8.encode(tokenString); + } + + const int maxLength = 200; + + _encoder = UR( + payload: bytes, + maxLength: maxLength, + type: "bytes", + ); + + _urParts.clear(); + + final minParts = (bytes.length / maxLength).ceil(); + final targetParts = (minParts * 1.5).ceil(); // 50% overhead + + for (int i = 0; i < targetParts; i++) { + final part = _encoder!.next(); + _urParts.add(part); + } + + if (_urParts.isNotEmpty) { + _startAnimation(); + } + } catch (e) { + // fallback to single QR code if UR encoding fails + _urParts = [widget.qrCodeData!]; + } + } + } + + void _startAnimation() { + if (_urParts.length > 1) { + _timer = Timer.periodic(const Duration(milliseconds: 500), (timer) { + if (mounted) { + setState(() { + _currentIndex = (_currentIndex + 1) % _urParts.length; + }); + } + }); + } + } + + @override + Widget build(BuildContext context) { + if (_urParts.isEmpty) { + return Container( + width: 200, + height: 200, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Text( + 'No QR Data', + style: TextStyle(color: Colors.grey), + ), + ), + ); + } + + return Container( + child: Column( + children: [ + if (showAnimatedQr && _urParts.isNotEmpty) + PrettyQrView.data( + data: _urParts[_currentIndex], + decoration: const PrettyQrDecoration( + shape: PrettyQrSmoothSymbol( + color: Colors.black, + ), + background: Colors.white, + ), + ), + const SizedBox(height: 8), + if (!showAnimatedQr) + PrettyQrView.data( + data: widget.qrCodeData ?? '', + decoration: const PrettyQrDecoration( + shape: PrettyQrSmoothSymbol( + color: Colors.black, + ), + background: Colors.white, + ), + ), + Row( + children: [ + if (showAnimatedQr && _urParts.length > 1) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.black, width: 1), + ), + child: Text( + '${_currentIndex + 1}/${_urParts.length}', + style: const TextStyle( + color: Colors.black, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + const Spacer(), + Switch( + activeColor: Colors.black, + value: showAnimatedQr, + onChanged: (value) { + setState(() { + showAnimatedQr = value; + }); + }), + ], + ) + ], + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart new file mode 100644 index 00000000..a9cf036a --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart @@ -0,0 +1,266 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart' as ndk_entities; + +import '../../../../../config/palette.dart'; +import '../../../../../helpers/wallet_number_formatting.dart'; +import '../../../../atoms/long_button.dart'; +import '../../../../components/wallet/animated_qr.dart'; +import '../wallet_pay_state_provider.dart'; + +class WalletPayDone extends ConsumerWidget { + const WalletPayDone({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final walletPayState = ref.watch(walletPayStateProvider); + + return Scaffold( + backgroundColor: Palette.background, + appBar: null, + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: _buildCurrentStep(walletPayState), + ), + ), + bottomNavigationBar: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: longButton( + name: "close", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ), + ); + } + + Widget _buildCurrentStep(WalletPayState state) { + if (state.isProcessing) { + return const ProcessingStep(); + } + + if (state.isError) { + return ErrorStep(errorMessage: state.errorMessage); + } + + if (state.isSuccess && state.outputToken != null) { + return SuccessStep( + outputToken: state.outputToken!, + amount: state.amount!, + unit: state.unit!, + ); + } + + return Container(); + } +} + +// Processing step widget +class ProcessingStep extends StatelessWidget { + const ProcessingStep({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + strokeWidth: 3, + ), + SizedBox(height: 24), + Text( + 'Creating Token...', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } +} + +class ErrorStep extends StatelessWidget { + final String? errorMessage; + + const ErrorStep({super.key, this.errorMessage}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + color: Palette.error, + size: 80, + ), + const SizedBox(height: 24), + const Text( + 'Error', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Palette.error, + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.shade200), + ), + child: Text( + errorMessage ?? 'An unknown error occurred', + style: const TextStyle( + fontSize: 16, + color: Palette.error, + ), + textAlign: TextAlign.center, + ), + ), + ], + ); + } +} + +// Success step widget +class SuccessStep extends StatelessWidget { + final ndk_entities.CashuToken? outputToken; + final int amount; + final String unit; + + const SuccessStep({ + super.key, + required this.outputToken, + required this.amount, + required this.unit, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + WalletNumberFormatting.formatAmount( + amount: amount, + unit: unit, + ), + style: TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + color: Palette.white, + ), + ), + const SizedBox(width: 16), + Text( + unit, + style: TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + color: Palette.extraLightGray, + ), + ), + ], + ), + const SizedBox(height: 24), + const Text( + 'Pending Ecash', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Palette.white, + ), + ), + const SizedBox(height: 32), + + Container( + width: 240, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Palette.white, + borderRadius: BorderRadius.circular(8), + ), + child: AnimatedQr( + qrCodeData: outputToken!.toV4TokenString(), + ), + ), + const SizedBox(height: 32), + + const SizedBox(height: 24), + + /// copy button + SizedBox( + width: 250, + child: CopyTokenButton( + token: outputToken!.toV4TokenString(), + ), + ), + ], + ); + } +} + +class CopyTokenButton extends StatefulWidget { + final String token; + + const CopyTokenButton({super.key, required this.token}); + + @override + State createState() => _CopyTokenButtonState(); +} + +class _CopyTokenButtonState extends State { + bool _copied = false; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _copied ? null : _copyToClipboard, + icon: Icon(_copied ? Icons.check : Icons.copy), + label: Text(_copied ? 'Copied to Clipboard!' : 'Copy Token'), + style: ElevatedButton.styleFrom( + backgroundColor: + _copied ? Palette.white : Palette.primary.withValues(alpha: 0.9), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ); + } + + Future _copyToClipboard() async { + await Clipboard.setData(ClipboardData(text: widget.token)); + setState(() { + _copied = true; + }); + + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + setState(() { + _copied = false; + }); + } + }); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart index 0740c69f..605e331f 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart @@ -19,6 +19,12 @@ class WalletPayState { final String? payFromWalletId; final Set? supportedUnitsByWallet; + final bool isProcessing; + final bool isError; + final bool isSuccess; + final String? errorMessage; + final ndk_entities.CashuToken? outputToken; + WalletPayState({ required this.availableWallets, required this.availableBalances, @@ -30,6 +36,11 @@ class WalletPayState { required this.payToPubkey, required this.payToWalletId, this.supportedUnitsByWallet, + this.isProcessing = false, + this.isError = false, + this.isSuccess = false, + this.errorMessage, + this.outputToken, }); ndk_entities.Wallet? get payFromWallet { @@ -52,6 +63,11 @@ class WalletPayState { String? payToPubkey, String? payToWalletId, Set? supportedUnitsByWallet, + bool? isProcessing, + bool? isError, + bool? isSuccess, + String? errorMessage, + ndk_entities.CashuToken? outputToken, }) { return WalletPayState( availableWallets: availableWallets ?? this.availableWallets, @@ -65,6 +81,11 @@ class WalletPayState { payToWalletId: payToWalletId ?? this.payToWalletId, supportedUnitsByWallet: supportedUnitsByWallet ?? this.supportedUnitsByWallet, + isProcessing: isProcessing ?? this.isProcessing, + isError: isError ?? this.isError, + isSuccess: isSuccess ?? this.isSuccess, + errorMessage: errorMessage ?? this.errorMessage, + outputToken: outputToken ?? this.outputToken, ); } } @@ -169,6 +190,51 @@ class WalletPayNotifier extends StateNotifier { state = state.copyWith(payToWalletId: walletId); } + void setProcessing({bool isProcessing = true}) { + state = state.copyWith(isProcessing: isProcessing); + } + + void setError({bool isError = true, String? errorMessage}) { + state = state.copyWith( + isError: isError, + errorMessage: errorMessage, + isProcessing: false, + ); + } + + void setSuccessToken( + {bool isSuccess = true, required ndk_entities.CashuToken outputToken}) { + state = state.copyWith( + isSuccess: isSuccess, + outputToken: outputToken, + isProcessing: false, + ); + } + + void createToken() async { + try { + final proofs = await _ndk.cashu.initiateSpend( + mintUrl: state.payFromWallet!.id, + amount: state.amount!, + unit: state.unit!, + ); + + final token = _ndk.cashu.proofsToToken( + proofs: proofs, + mintUrl: state.payFromWallet!.id, + unit: state.unit!, + memo: state.memo ?? '', + ); + setSuccessToken(outputToken: token); + } catch (e) { + setError( + errorMessage: e.toString(), + ); + + return; + } + } + void reset() { state = WalletPayState( availableBalances: state.availableBalances, @@ -180,6 +246,12 @@ class WalletPayNotifier extends StateNotifier { recieverType: null, payToPubkey: null, payToWalletId: null, + supportedUnitsByWallet: null, + isProcessing: false, + isError: false, + isSuccess: false, + errorMessage: null, + outputToken: null, ); } } diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart index 3448c5d5..3cd42552 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -9,6 +9,7 @@ import '../../../../../config/palette.dart'; import '../../../../../helpers/wallet_number_formatting.dart'; import '../../../../atoms/wallet/wallet_card.dart'; import '../../../../components/wallet/wallets_select_bottom_sheet.dart'; +import '../wallet_pay_done/wallet_pay_done.dart'; import '../wallet_pay_state_provider.dart'; class WalletPaySummary extends ConsumerWidget { @@ -18,11 +19,71 @@ class WalletPaySummary extends ConsumerWidget { required this.backCallback, }); + showSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Palette.warn, + content: Text( + message, + style: TextStyle(color: Colors.white), + ), + ), + ); + } + @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(walletPayStateProvider); final payNotifier = ref.read(walletPayStateProvider.notifier); + bool isValid() { + if (state.payFromWalletId == null || state.payFromWalletId!.isEmpty) { + showSnackBar(context, 'Please select a wallet'); + return false; + } + if (state.amount == null || state.amount! <= 0) { + showSnackBar(context, 'Please enter a valid amount'); + return false; + } + if (state.unit == null || state.unit!.isEmpty) { + showSnackBar(context, 'Please select a unit'); + return false; + } + //! only token support for now + if (state.recieverType != PaymentRecieverType.token) { + showSnackBar(context, 'Only token payments are supported for now'); + return false; + } + + final availableBalanceForUnit = state.availableBalances.where( + (balance) => + balance.walletId == state.payFromWalletId && + balance.unit == state.unit, + ); + if (availableBalanceForUnit.isEmpty || + availableBalanceForUnit.first.amount < state.amount!) { + showSnackBar(context, + 'Insufficient balance in the selected wallet for the specified unit'); + return false; + } + + return true; + } + + void onSend() async { + final stateNotifier = ref.read(walletPayStateProvider.notifier); + if (!isValid()) return; + + stateNotifier.createToken(); + + /// navigate to done page + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => WalletPayDone(), + ), + ); + } + return Scaffold( backgroundColor: Colors.black, body: Column( @@ -215,7 +276,8 @@ class WalletPaySummary extends ConsumerWidget { top: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: longButton(name: "send", onPressed: () {}, inverted: true), + child: longButton( + name: "send", onPressed: () => onSend(), inverted: true), ), ), ); diff --git a/pubspec.lock b/pubspec.lock index d379ea39..9242792d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -141,6 +141,13 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + bc_ur_dart: + dependency: "direct main" + description: + path: "../fx-wallet-packages/packages/bc_ur_dart" + relative: true + source: path + version: "0.1.19" bech32: dependency: "direct main" description: @@ -1449,6 +1456,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + pretty_qr_code: + dependency: "direct main" + description: + name: pretty_qr_code + sha256: "2291db3f68d70a3dcd46c6bd599f30991ae4c02f27f36215fbb3f4865a609259" + url: "https://pub.dev" + source: hosted + version: "3.5.0" process: dependency: transitive description: @@ -2142,6 +2157,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + xrandom: + dependency: transitive + description: + name: xrandom + sha256: d843a7f4004d91b5a0341a28a781fddaf92a5b377f776a06a08e7a9320e95ad3 + url: "https://pub.dev" + source: hosted + version: "0.7.2" xxh3: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5956da48..7ed057d5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,6 +100,8 @@ dependencies: flutter_chat_types: ^3.6.2 visibility_detector: ^0.4.0+2 video_player: ^2.10.0 + pretty_qr_code: ^3.5.0 + bc_ur_dart: ^0.1.19 @@ -129,6 +131,8 @@ dependency_overrides: # url: https://github.com/relaystr/ndk.git #path: packages/amber # ref: nip-59-gift-wrap + bc_ur_dart: + path: ../fx-wallet-packages/packages/bc_ur_dart dev_dependencies: From 102fce095d84ecbc048d1a3db0fe7a5be1ca15de Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sun, 17 Aug 2025 11:49:01 +0200 Subject: [PATCH 21/52] fix: parsing issue on invalid data --- lib/helpers/nprofile_helper.dart | 2 +- .../components/note_card/nostr_parser.dart | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/helpers/nprofile_helper.dart b/lib/helpers/nprofile_helper.dart index 2d1b6ca9..fc1e9181 100644 --- a/lib/helpers/nprofile_helper.dart +++ b/lib/helpers/nprofile_helper.dart @@ -48,7 +48,7 @@ class NprofileHelper { final Map resultMap = _parseTlvList(tlvList); if (resultMap["pubkey"].length != 64) { - throw Exception("Invalid pubkey length"); + throw Exception("Invalid pubkey length $resultMap"); } return resultMap; diff --git a/lib/presentation_layer/components/note_card/nostr_parser.dart b/lib/presentation_layer/components/note_card/nostr_parser.dart index e6acb20c..55fd3248 100644 --- a/lib/presentation_layer/components/note_card/nostr_parser.dart +++ b/lib/presentation_layer/components/note_card/nostr_parser.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/foundation.dart'; import '../../../domain_layer/entities/nostr_note.dart'; @@ -93,11 +95,15 @@ class NostrParser { // Parse different types if (matchText.startsWith(RegExp(r'nostr:(nprofile|npub)[a-zA-Z0-9]+'))) { - segments.add(ContentSegment( - content: matchText, - type: ContentType.mention, - metadata: _extractUserIdFromNostr(matchText), - )); + try { + segments.add(ContentSegment( + content: matchText, + type: ContentType.mention, + metadata: _extractUserIdFromNostr(matchText), + )); + } catch (e) { + log('Error parsing Nostr reference: $matchText', error: e); + } } else if (matchText.startsWith('nostr:note1')) { segments.add(ContentSegment( content: 'Note reference', From 8d276003e54959d30fb0eb5ea4d80a3d833fd61c Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sun, 17 Aug 2025 11:49:30 +0200 Subject: [PATCH 22/52] pay flow clear state on cancel --- .../wallet_pay_select_amount.dart | 29 ++++++++++++++++++- .../wallet_pay_reciever.dart | 5 +++- .../wallet_pay_summary.dart | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart index edfc3935..d2a01aed 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart @@ -91,6 +91,18 @@ class _WalletPaySelectAmountState extends ConsumerState { } } + showSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Palette.warn, + content: Text( + message, + style: TextStyle(color: Colors.white), + ), + ), + ); + } + @override Widget build(BuildContext context) { final state = ref.watch(walletPayStateProvider); @@ -238,7 +250,22 @@ class _WalletPaySelectAmountState extends ConsumerState { top: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: longButton(name: "next", onPressed: () {}, inverted: true), + child: longButton( + name: "next", + onPressed: () { + if (state.payFromWalletId == null || + state.payFromWalletId!.isEmpty) { + showSnackBar(context, 'Please select a wallet to pay from.'); + return; + } + if (state.amount == null || state.amount! <= 0) { + _amountFocus.requestFocus(); + showSnackBar(context, 'Please enter a valid amount.'); + return; + } + widget.doneCallback(); + }, + inverted: true), ), ), ); diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart index 8e788fec..14d410b3 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart @@ -64,7 +64,10 @@ class WalletSelectReciever extends ConsumerWidget { actions: [ IconButton( icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), + onPressed: () { + paymentStateNotifier.reset(); + Navigator.pop(context); + }, ), ], bottom: PreferredSize( diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart index 3cd42552..74516d68 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -129,6 +129,7 @@ class WalletPaySummary extends ConsumerWidget { size: 24, ), onPressed: () { + payNotifier.reset(); Navigator.of(context).pop(); }, ), From 526b6a931fadca1cf3b149e027e2f4b21287d729 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:45:40 +0200 Subject: [PATCH 23/52] disable thread parsing --- .../components/note_card/nostr_parser.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/presentation_layer/components/note_card/nostr_parser.dart b/lib/presentation_layer/components/note_card/nostr_parser.dart index 55fd3248..c14b0ee0 100644 --- a/lib/presentation_layer/components/note_card/nostr_parser.dart +++ b/lib/presentation_layer/components/note_card/nostr_parser.dart @@ -8,9 +8,15 @@ import '../../../helpers/helpers.dart'; import '../../../helpers/nprofile_helper.dart'; class NostrParser { + static const bool useThread = false; + /// parses the event in a seperate thread static Future parseEvent(NostrNote event) async { - return compute((e) => _parse(e), event); + if (useThread) { + return compute((e) => _parse(e), event); + } + + return _parse(event); } /// parses the event in current thread @@ -24,7 +30,11 @@ class NostrParser { /// parses multiple event in seperate thread static Future> parseEvents(List events) async { - return compute((e) => _parseEvents(e), events); + if (useThread) { + return compute((e) => _parseEvents(e), events); + } + + return _parseEvents(events); } static List _parseEvents(List events) { From 8cc00415f35537a3c69acaf403005d642e63c724 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:06:09 +0200 Subject: [PATCH 24/52] qr scan setup --- .../routes/wallet/wallet_qr_scan.dart | 197 +++++++++++++++--- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 8 + pubspec.yaml | 1 + 4 files changed, 183 insertions(+), 25 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart index 8de78650..3c5e6fde 100644 --- a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../../../config/palette.dart'; +import '../../atoms/spinner_center.dart'; class WalletQrScan extends StatefulWidget { - const WalletQrScan({Key? key}) : super(key: key); + const WalletQrScan({super.key}); @override State createState() => _QrScan(); } class _QrScan extends State { - String qrcode = 'Unknown'; + Barcode? _barcode; + MobileScannerController controller = MobileScannerController(); + String? _errorMessage; @override void initState() { @@ -17,42 +25,181 @@ class _QrScan extends State { @override void dispose() { + controller.dispose(); super.dispose(); } + void _handleBarcode(BarcodeCapture barcodes) { + if (mounted) { + setState(() { + _barcode = barcodes.barcodes.firstOrNull; + }); + } + if (_barcode != null) { + _processValue(_barcode!.rawValue!); + } + } + + Future _handleReadClipboard() async { + try { + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + return data?.text; + } catch (e) { + setState(() { + _errorMessage = 'Failed to read clipboard: $e'; + }); + return null; + } + } + + void _processValue(String barcode) { + print('Scanned barcode: $barcode'); + } + @override Widget build(BuildContext context) { - return SafeArea( - child: Stack( + return Scaffold( + backgroundColor: Colors.black, + body: Stack( children: [ - Center( - child: Text("scanner here"), + MobileScanner( + controller: controller, + onDetect: _handleBarcode, + errorBuilder: (p0, p1) { + return Center( + child: Text( + 'Error: $p1', + style: const TextStyle(color: Palette.error, fontSize: 16), + ), + ); + }, + placeholderBuilder: (context) { + return SpinnerCenter(); + }, ), - //fix outer border radius - Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - decoration: BoxDecoration( - border: Border.all( - color: Colors.black, - width: 15, + + // Container( + // width: MediaQuery.of(context).size.width, + // height: MediaQuery.of(context).size.height, + // decoration: BoxDecoration( + // border: Border.lerp( + // Border.all(color: Colors.black, width: 10), + // Border.all(color: Colors.transparent, width: 1), + // 0.5, + // ), + // borderRadius: BorderRadius.circular(40), + // ), + // ), + + /// scanning indicator + Center( + child: Container( + width: 250, + height: 250, + decoration: BoxDecoration( + border: Border.all( + color: Palette.white, + width: 2, + ), + borderRadius: BorderRadius.circular(20), + ), + child: const Center( + child: Text( + 'Position QR code here', + style: TextStyle( + color: Colors.white70, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), ), ), ), -//rounded corners - GestureDetector( - onTap: () { - print("unimplemented"); - }, + + /// bottom area + Align( + alignment: Alignment.bottomCenter, child: Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, + alignment: Alignment.bottomCenter, + height: 120, + width: double.infinity, decoration: BoxDecoration( - border: Border.all( - color: Colors.black, - width: 20, + color: Palette.black.withValues(alpha: 0.8), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton.icon( + onPressed: () { + _handleReadClipboard().then((value) { + if (value != null) { + _processValue(value); + } + }); + }, + icon: Icon( + PhosphorIcons.clipboardText(), + size: 18, + ), + label: const Text('paste from clipboard'), + style: ElevatedButton.styleFrom( + backgroundColor: Palette.primary, + foregroundColor: Palette.white, + ), + ), + ], + ), + ], ), - borderRadius: BorderRadius.circular(40), + ), + ), + ), + + /// top controls + Positioned( + top: 20, + left: 20, + right: 20, + child: SafeArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _errorMessage != null + ? Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(50), + ), + child: Text( + _errorMessage ?? "", + style: const TextStyle( + color: Palette.warn, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + ) + : const SizedBox.shrink(), + IconButton( + onPressed: () => controller.toggleTorch(), + icon: const Icon(Icons.flash_on, color: Colors.white), + style: IconButton.styleFrom( + backgroundColor: Colors.black54, + ), + ), + ], ), ), ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 03ebc16d..2d5dc12d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,6 +14,7 @@ import firebase_core import firebase_messaging import flutter_local_notifications import flutter_secure_storage_macos +import mobile_scanner import objectbox_flutter_libs import package_info_plus import path_provider_foundation @@ -33,6 +34,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 9242792d..7da3b649 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1218,6 +1218,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" + url: "https://pub.dev" + source: hosted + version: "7.0.1" mockito: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ed057d5..5f86a347 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -102,6 +102,7 @@ dependencies: video_player: ^2.10.0 pretty_qr_code: ^3.5.0 bc_ur_dart: ^0.1.19 + mobile_scanner: ^7.0.1 From 232855b7f017069b36e459a76eebb9e911cd7ab0 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:19:44 +0200 Subject: [PATCH 25/52] qr scan navigation logic --- .../qr_value_processing_state_provider.dart | 118 ++++++++++++++++++ .../routes/wallet/wallet_qr_scan.dart | 55 ++++++-- 2 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart new file mode 100644 index 00000000..684b8ce5 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart @@ -0,0 +1,118 @@ +import 'package:riverpod/riverpod.dart'; + +class QRScannerState { + final bool isProcessing; + final String? error; + final QRNavigationTarget? navigationTarget; + final Map? navigationData; + + QRScannerState({ + required this.isProcessing, + required this.error, + required this.navigationTarget, + required this.navigationData, + }); + + QRScannerState copyWith({ + bool? isProcessing, + String? error, + QRNavigationTarget? navigationTarget, + Map? navigationData, + }) { + return QRScannerState( + isProcessing: isProcessing ?? this.isProcessing, + error: error ?? this.error, + navigationTarget: navigationTarget ?? this.navigationTarget, + navigationData: navigationData ?? this.navigationData, + ); + } +} + +enum QRNavigationTarget { + rcvPage, +} + +class QRScanTypeResult { + final String value; + final QRScanTypes type; + + QRScanTypeResult({ + required this.value, + required this.type, + }); +} + +enum QRScanTypes { + cashuToken, + lightningInvoice, + unknown, +} + +class QRScannerNotifier extends StateNotifier { + QRScannerNotifier() + : super( + QRScannerState( + isProcessing: false, + error: null, + navigationTarget: null, + navigationData: null, + ), + ); + + void setError(String error) { + state = state.copyWith(error: error); + } + + Future processQRCode(String qrData) async { + state = state.copyWith(isProcessing: true, error: null); + + final result = await _analyzeQRData(qrData); + + if (result.type == QRScanTypes.cashuToken) { + state = state.copyWith( + isProcessing: false, + navigationTarget: QRNavigationTarget.rcvPage, + navigationData: {'cashuTokenString': result.value}, + ); + } else if (result.type == QRScanTypes.lightningInvoice) { + state = state.copyWith( + isProcessing: false, + navigationTarget: QRNavigationTarget.rcvPage, + navigationData: {'lightningInvoice': result.value}, + ); + } else { + state = state.copyWith( + isProcessing: false, + error: 'Unsupported QR code type', + ); + } + } + + void clearNavigation() { + state = state.copyWith(navigationTarget: null, navigationData: null); + } + + Future _analyzeQRData(String qrData) async { + // Analyze the QR data and return the appropriate type + if (qrData.startsWith('cashuB')) { + return QRScanTypeResult( + value: qrData, + type: QRScanTypes.cashuToken, + ); + } else if (qrData.startsWith('lightning:')) { + return QRScanTypeResult( + value: qrData, + type: QRScanTypes.lightningInvoice, + ); + } + return QRScanTypeResult( + value: qrData, + type: QRScanTypes.unknown, + ); + } +} + +final qrScannerProvider = + StateNotifierProvider.autoDispose( + (ref) => QRScannerNotifier(), +); diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart index 3c5e6fde..179f8ef6 100644 --- a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -1,22 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../config/palette.dart'; import '../../atoms/spinner_center.dart'; +import 'wallet_providers/qr_value_processing_state_provider.dart'; -class WalletQrScan extends StatefulWidget { +class WalletQrScan extends ConsumerStatefulWidget { const WalletQrScan({super.key}); @override - State createState() => _QrScan(); + ConsumerState createState() => _QrScan(); } -class _QrScan extends State { +class _QrScan extends ConsumerState { Barcode? _barcode; MobileScannerController controller = MobileScannerController(); - String? _errorMessage; @override void initState() { @@ -45,19 +46,42 @@ class _QrScan extends State { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); return data?.text; } catch (e) { - setState(() { - _errorMessage = 'Failed to read clipboard: $e'; - }); + ref + .read(qrScannerProvider.notifier) + .setError('Failed to read clipboard: $e'); + return null; } } void _processValue(String barcode) { - print('Scanned barcode: $barcode'); + ref.read(qrScannerProvider.notifier).processQRCode(barcode); } @override Widget build(BuildContext context) { + final qrScannerState = ref.watch(qrScannerProvider); + + // apperently this is save to do with riverpod + ref.listen(qrScannerProvider, (previous, next) { + // only navigate if its new + if (previous?.navigationTarget != next.navigationTarget && + next.navigationTarget != null && + next.navigationData != null) { + ref.read(qrScannerProvider.notifier).clearNavigation(); + + switch (next.navigationTarget!) { + case QRNavigationTarget.rcvPage: + Navigator.pushNamed( + context, + '/wallet/receive', + arguments: next.navigationData, + ); + break; + } + } + }); + return Scaffold( backgroundColor: Colors.black, body: Stack( @@ -164,6 +188,17 @@ class _QrScan extends State { ), ), ), + if (qrScannerState.isProcessing) + Positioned( + bottom: 115, + left: 20, + right: 20, + child: LinearProgressIndicator( + borderRadius: BorderRadius.circular(20), + backgroundColor: Palette.extraDarkGray, + color: Palette.white, + ), + ), /// top controls Positioned( @@ -174,7 +209,7 @@ class _QrScan extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _errorMessage != null + qrScannerState.error != null ? Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), @@ -183,7 +218,7 @@ class _QrScan extends State { borderRadius: BorderRadius.circular(50), ), child: Text( - _errorMessage ?? "", + qrScannerState.error ?? "", style: const TextStyle( color: Palette.warn, fontSize: 16, From 50e61b6ed5a14a6bc5e3adfaab5782d0bcc64335 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:45:55 +0200 Subject: [PATCH 26/52] feat: rcv page --- lib/config/palette.dart | 1 + lib/main.dart | 6 +- .../qr_value_processing_state_provider.dart | 12 +- .../routes/wallet/wallet_qr_scan.dart | 12 +- .../wallet/wallet_rcv/wallet_rcv_page.dart | 231 ++++++++++++++++++ .../wallet_rcv/wallet_rcv_state_provider.dart | 147 +++++++++++ 6 files changed, 403 insertions(+), 6 deletions(-) create mode 100644 lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_state_provider.dart diff --git a/lib/config/palette.dart b/lib/config/palette.dart index 3bf71a52..039318c5 100644 --- a/lib/config/palette.dart +++ b/lib/config/palette.dart @@ -15,6 +15,7 @@ class Palette { static const Color black = Color(0xFF000000); static const Color warn = Color.fromARGB(255, 252, 127, 3); static const Color error = Color.fromARGB(255, 254, 29, 29); + static const Color success = Color.fromARGB(255, 22, 163, 74); static const Color likeActive = Color.fromARGB(255, 230, 40, 85); static const Color repostActive = Color.fromARGB(255, 22, 163, 74); } diff --git a/lib/main.dart b/lib/main.dart index 84e32b46..dd02cc70 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,8 +38,8 @@ import 'presentation_layer/routes/nostr/settings/locale/locale_settings.dart'; import 'presentation_layer/routes/nostr/settings/moderation/moderation_settings.dart'; import 'presentation_layer/routes/nostr/settings/settings_page.dart'; import 'presentation_layer/routes/wallet/wallet_navigation.dart'; -import 'presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart'; import 'presentation_layer/routes/wallet/wallet_pay_page.dart'; +import 'presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart'; import 'theme.dart' as theme; const devDeviceFrame = true; @@ -276,9 +276,7 @@ class MyApp extends ConsumerWidget { ); case '/wallet/receive': return MaterialPageRoute( - builder: (context) => WalletNavigation( - title: "a", - ), + builder: (context) => WalletReceivePage(), ); case '/wallet/mints': diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart index 684b8ce5..49bd34cd 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart @@ -30,6 +30,7 @@ class QRScannerState { enum QRNavigationTarget { rcvPage, + sendPage, } class QRScanTypeResult { @@ -77,7 +78,7 @@ class QRScannerNotifier extends StateNotifier { } else if (result.type == QRScanTypes.lightningInvoice) { state = state.copyWith( isProcessing: false, - navigationTarget: QRNavigationTarget.rcvPage, + navigationTarget: QRNavigationTarget.sendPage, navigationData: {'lightningInvoice': result.value}, ); } else { @@ -92,6 +93,15 @@ class QRScannerNotifier extends StateNotifier { state = state.copyWith(navigationTarget: null, navigationData: null); } + void reset() { + state = state.copyWith( + isProcessing: false, + error: null, + navigationTarget: null, + navigationData: null, + ); + } + Future _analyzeQRData(String qrData) async { // Analyze the QR data and return the appropriate type if (qrData.startsWith('cashuB')) { diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart index 179f8ef6..6e2a32a3 100644 --- a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -7,6 +7,7 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../config/palette.dart'; import '../../atoms/spinner_center.dart'; import 'wallet_providers/qr_value_processing_state_provider.dart'; +import 'wallet_rcv/wallet_rcv_state_provider.dart'; class WalletQrScan extends ConsumerStatefulWidget { const WalletQrScan({super.key}); @@ -27,6 +28,7 @@ class _QrScan extends ConsumerState { @override void dispose() { controller.dispose(); + super.dispose(); } @@ -70,14 +72,22 @@ class _QrScan extends ConsumerState { next.navigationData != null) { ref.read(qrScannerProvider.notifier).clearNavigation(); + final rcvProvider = ref.read(walletReceiveProvider.notifier); + switch (next.navigationTarget!) { case QRNavigationTarget.rcvPage: + final ecashTokenString = + next.navigationData!['cashuTokenString'] as String; + rcvProvider.receiveEcash(tokenString: ecashTokenString); Navigator.pushNamed( context, '/wallet/receive', - arguments: next.navigationData, ); break; + case QRNavigationTarget.sendPage: + throw UnimplementedError( + 'Send page navigation is not implemented yet'); + break; } } }); diff --git a/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart b/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart new file mode 100644 index 00000000..97a81c4c --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../../../../config/palette.dart'; +import '../../../../helpers/wallet_number_formatting.dart'; +import '../../../atoms/long_button.dart'; +import 'wallet_rcv_state_provider.dart'; + +class WalletReceivePage extends ConsumerWidget { + const WalletReceivePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final walletState = ref.watch(walletReceiveProvider); + + return Scaffold( + backgroundColor: Palette.background, + appBar: null, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Spacer(flex: 1), + // Status Card + _buildStatusCard(walletState), + + Spacer(flex: 7), + + if (walletState.amount != null && walletState.unit != null) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '+', + style: const TextStyle( + color: Palette.lightGray, + fontSize: 28, + ), + ), + const SizedBox(width: 8), + Text( + WalletNumberFormatting.formatAmount( + amount: walletState.amount!, unit: walletState.unit!), + style: const TextStyle( + color: Palette.white, + fontSize: 48, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Text( + walletState.unit ?? '', + style: const TextStyle( + fontSize: 16, color: Palette.lightGray), + ), + ], + ), + ], + Spacer(flex: 1), + if (walletState.memo != null) ...[ + const SizedBox(height: 16), + Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width - 32, + // max height for memo + maxHeight: 120, + ), + child: SingleChildScrollView( + child: Text( + walletState.memo!, + style: const TextStyle( + color: Palette.white, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + + if (walletState.errorMessage != null) ...[ + Spacer(flex: 1), + _buildErrorCard(walletState.errorMessage!), + ], + Spacer(flex: 10), + ], + ), + ), + ), + bottomNavigationBar: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: longButton( + name: "close", + onPressed: () => {Navigator.of(context).pop()}, + inverted: true), + ), + ), + ); + } + + Widget _buildStatusCard(WalletRcvState state) { + IconData icon; + Color color; + String title; + String subtitle; + + if (state.isPending) { + icon = PhosphorIcons.hourglass(); + color = Palette.primary; + title = 'Processing'; + subtitle = 'Receiving...'; + } else if (state.isError) { + icon = PhosphorIcons.warningCircle(); + color = Palette.error; + title = 'Failed'; + subtitle = 'transaction failed'; + } else if (state.isSuccess) { + icon = PhosphorIcons.checkCircle(); + color = Palette.success; + title = 'Success'; + subtitle = 'received successfully'; + } else { + icon = PhosphorIcons.info(); + color = Palette.gray; + title = 'Ready'; + subtitle = 'waiting for transaction'; + } + + return Card( + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(50), + ), + child: Icon( + icon, + color: color, + size: 32, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + if (state.isPending) + const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ], + ), + ), + ); + } + + Widget _buildErrorCard(String errorMessage) { + return Card( + elevation: 2, + color: Colors.red[50], + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.error_outline, + color: Colors.red[700], + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Error', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.red[700], + ), + ), + const SizedBox(height: 4), + Text( + errorMessage, + style: TextStyle( + fontSize: 14, + color: Colors.red[600], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_state_provider.dart new file mode 100644 index 00000000..ed29d455 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_state_provider.dart @@ -0,0 +1,147 @@ +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:ndk/ndk.dart'; +import 'package:riverpod/riverpod.dart'; + +import '../../../providers/ndk_provider.dart'; + +enum WalletRcvType { + eCash, +} + +class WalletRcvState { + final bool isPending; + final bool isCompleted; + final bool isError; + final bool isSuccess; + final String? errorMessage; + + final String? eCashTokenString; + final WalletRcvType? type; + + final String? memo; + final int? amount; + final String? unit; + + WalletRcvState({ + required this.isPending, + required this.isCompleted, + required this.isError, + required this.isSuccess, + this.errorMessage, + this.memo, + this.eCashTokenString, + this.type, + this.amount, + this.unit, + }); + + WalletRcvState copyWith({ + bool? isPending, + bool? isCompleted, + bool? isError, + bool? isSuccess, + String? errorMessage, + String? memo, + String? eCashTokenString, + WalletRcvType? type, + int? amount, + String? unit, + }) { + return WalletRcvState( + isPending: isPending ?? this.isPending, + isCompleted: isCompleted ?? this.isCompleted, + isError: isError ?? this.isError, + isSuccess: isSuccess ?? this.isSuccess, + errorMessage: errorMessage ?? this.errorMessage, + memo: memo ?? this.memo, + eCashTokenString: eCashTokenString ?? this.eCashTokenString, + type: type ?? this.type, + amount: amount ?? this.amount, + unit: unit ?? this.unit, + ); + } +} + +class WalletRcvNotifier extends StateNotifier { + final Ndk _ndk; + WalletRcvNotifier({required Ndk ndk}) + : _ndk = ndk, + super( + WalletRcvState( + isPending: false, + isCompleted: false, + isError: false, + isSuccess: false, + ), + ); + + void receiveEcash({ + required String tokenString, + }) async { + reset(); + + state = state.copyWith(isPending: true); + try { + final rcvResultStream = _ndk.cashu.receive(tokenString); + + await for (final rcvResult in rcvResultStream) { + if (rcvResult.state == ndk_entities.WalletTransactionState.pending) { + state = state.copyWith( + isPending: true, + eCashTokenString: tokenString, + type: WalletRcvType.eCash, + amount: rcvResult.changeAmount, + unit: rcvResult.unit, + memo: rcvResult.note, + ); + } else if (rcvResult.state == + ndk_entities.WalletTransactionState.completed) { + state = state.copyWith( + isSuccess: true, + isCompleted: true, + isPending: false, + ); + } else if (rcvResult.state == + ndk_entities.WalletTransactionState.failed) { + state = state.copyWith( + isError: true, + isCompleted: true, + isPending: false, + errorMessage: 'Failed to receive eCash: ${rcvResult.completionMsg}', + ); + } + } + } catch (e) { + state = state.copyWith( + isPending: false, + isError: true, + isCompleted: true, + errorMessage: 'Failed to receive eCash: $e', + ); + return; + } + } + + void reset() { + state = WalletRcvState( + isPending: false, + isCompleted: false, + isError: false, + isSuccess: false, + errorMessage: null, + eCashTokenString: null, + type: null, + memo: null, + amount: null, + unit: null, + ); + } +} + +final walletReceiveProvider = + StateNotifierProvider( + (ref) { + final ndk = ref.watch(ndkProvider); + return WalletRcvNotifier(ndk: ndk); + }, +); From 956ad883db8b3f1c58996e789efad234107186a1 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:24:26 +0200 Subject: [PATCH 27/52] pay summary reporting --- .../wallet_pay_done/wallet_pay_done.dart | 86 ++++++++++++++++--- .../wallet_pay/wallet_pay_state_provider.dart | 26 +++--- .../wallet_pay_summary.dart | 2 +- .../wallet_combined_state_provider.dart | 6 +- 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart index a9cf036a..d2997948 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart @@ -4,11 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/entities.dart' as ndk_entities; +import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../../../config/palette.dart'; import '../../../../../helpers/wallet_number_formatting.dart'; import '../../../../atoms/long_button.dart'; import '../../../../components/wallet/animated_qr.dart'; +import '../../wallet_providers/wallet_combined_state_provider.dart'; import '../wallet_pay_state_provider.dart'; class WalletPayDone extends ConsumerWidget { @@ -32,11 +34,11 @@ class WalletPayDone extends ConsumerWidget { child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: longButton( - name: "close", - onPressed: () { - Navigator.of(context).pop(); - }, - ), + name: "close", + onPressed: () { + Navigator.of(context).pop(); + }, + inverted: true), ), ), ); @@ -179,14 +181,8 @@ class SuccessStep extends StatelessWidget { ], ), const SizedBox(height: 24), - const Text( - 'Pending Ecash', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Palette.white, - ), - ), + + TransactionState(), const SizedBox(height: 32), Container( @@ -264,3 +260,67 @@ class _CopyTokenButtonState extends State { }); } } + +class TransactionState extends ConsumerWidget { + const TransactionState({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final walletPayState = ref.watch(walletPayStateProvider); + final walletCombined = ref.watch(walletCombinedProvider); + + final myTransactionList = walletCombined.recentTransactions.where( + (transaction) => transaction.id == walletPayState.transactionId, + ); + + final myPendingTransactionList = walletCombined.pendingTransactions.where( + (transaction) => transaction.id == walletPayState.transactionId, + ); + + if (myTransactionList.isEmpty && myPendingTransactionList.isEmpty) { + return const SizedBox.shrink(); + } + final myTransaction = myTransactionList.isNotEmpty + ? myTransactionList.first + : myPendingTransactionList.first; + + IconData icon; + Color color; + String title; + + if (myTransaction.state == ndk_entities.WalletTransactionState.pending) { + icon = PhosphorIcons.hourglass(); + color = Palette.lightGray; + title = 'pending ecash'; + } else if (myTransaction.state == + ndk_entities.WalletTransactionState.failed) { + icon = PhosphorIcons.warningCircle(); + color = Palette.error; + title = 'failed'; + } else { + icon = PhosphorIcons.checkCircle(); + color = Palette.success; + title = 'ecash claimed'; + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Icon(icon, color: color, size: 23), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart index 605e331f..bc56a483 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart @@ -24,6 +24,7 @@ class WalletPayState { final bool isSuccess; final String? errorMessage; final ndk_entities.CashuToken? outputToken; + final String? transactionId; WalletPayState({ required this.availableWallets, @@ -41,6 +42,7 @@ class WalletPayState { this.isSuccess = false, this.errorMessage, this.outputToken, + this.transactionId, }); ndk_entities.Wallet? get payFromWallet { @@ -68,6 +70,7 @@ class WalletPayState { bool? isSuccess, String? errorMessage, ndk_entities.CashuToken? outputToken, + String? transactionId, }) { return WalletPayState( availableWallets: availableWallets ?? this.availableWallets, @@ -86,6 +89,7 @@ class WalletPayState { isSuccess: isSuccess ?? this.isSuccess, errorMessage: errorMessage ?? this.errorMessage, outputToken: outputToken ?? this.outputToken, + transactionId: transactionId ?? this.transactionId, ); } } @@ -202,30 +206,32 @@ class WalletPayNotifier extends StateNotifier { ); } - void setSuccessToken( - {bool isSuccess = true, required ndk_entities.CashuToken outputToken}) { + void setSuccessToken({ + bool isSuccess = true, + required ndk_entities.CashuToken outputToken, + String? transactionId, + }) { state = state.copyWith( isSuccess: isSuccess, outputToken: outputToken, isProcessing: false, + transactionId: transactionId, ); } - void createToken() async { + void createToken({String? memo}) async { try { - final proofs = await _ndk.cashu.initiateSpend( + final result = await _ndk.cashu.initiateSpend( mintUrl: state.payFromWallet!.id, amount: state.amount!, unit: state.unit!, + memo: memo, ); - final token = _ndk.cashu.proofsToToken( - proofs: proofs, - mintUrl: state.payFromWallet!.id, - unit: state.unit!, - memo: state.memo ?? '', + setSuccessToken( + outputToken: result.token, + transactionId: result.transaction.id, ); - setSuccessToken(outputToken: token); } catch (e) { setError( errorMessage: e.toString(), diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart index 74516d68..29bc1a90 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -74,7 +74,7 @@ class WalletPaySummary extends ConsumerWidget { final stateNotifier = ref.read(walletPayStateProvider.notifier); if (!isValid()) return; - stateNotifier.createToken(); + stateNotifier.createToken(memo: state.memo); /// navigate to done page Navigator.of(context).pushReplacement( diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart index b3b524d9..55307895 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -118,10 +118,6 @@ class WalletCombinedStateNotifier extends StateNotifier { unit: unit, ); - print(spend); - - final token = - _ndk.cashu.proofsToToken(proofs: spend, mintUrl: mintUrl, unit: unit); - print(token); + print(spend.token.toV4TokenString()); } } From ebb1f52417e5be99cb1fe705fb01bd4824f33153 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:37:48 +0200 Subject: [PATCH 28/52] fix: pay state dependency --- .../wallet_pay/wallet_pay_state_provider.dart | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart index bc56a483..6364b739 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart @@ -3,6 +3,7 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; import '../../../providers/ndk_provider.dart'; +import '../wallet_providers/wallet_combined_state_provider.dart'; class WalletPayState { final List availableWallets; @@ -116,8 +117,9 @@ enum PaymentRecieverType { class WalletPayNotifier extends StateNotifier { final Ndk _ndk; + final Ref ref; - WalletPayNotifier({required Ndk ndk}) + WalletPayNotifier({required Ndk ndk, required this.ref}) : _ndk = ndk, super( WalletPayState( @@ -132,18 +134,16 @@ class WalletPayNotifier extends StateNotifier { payToWalletId: null, ), ) { - _loadInitial(); - } - - Future _loadInitial() async { - final balances = await _ndk.wallets.combinedBalances.first; - - final wallets = await _ndk.wallets.walletsStream.first; - - state = state.copyWith( - availableWallets: wallets, - availableBalances: balances, - ); + // listen to state changes + ref.listen(walletCombinedProvider, (previous, next) { + if (next.balances != previous?.balances || + next.wallets != previous?.wallets) { + state = state.copyWith( + availableBalances: next.balances, + availableWallets: next.wallets, + ); + } + }, fireImmediately: true); } bool get valid { @@ -265,5 +265,5 @@ class WalletPayNotifier extends StateNotifier { final walletPayStateProvider = StateNotifierProvider((ref) { final ndk = ref.watch(ndkProvider); - return WalletPayNotifier(ndk: ndk); + return WalletPayNotifier(ndk: ndk, ref: ref); }); From 184fd4cc84f0bf667a8cd009363303ac43f29b54 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:22:16 +0200 Subject: [PATCH 29/52] select wallet on pay --- .../wallet/wallets_select_bottom_sheet.dart | 13 +++++++++ .../routes/wallet/wallet_dashboard.dart | 23 ++++++++++++--- .../wallet_pay_done/wallet_pay_done.dart | 4 +-- .../routes/wallet/wallet_qr_scan.dart | 28 +++++++------------ 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart index 81263249..353fcabc 100644 --- a/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart +++ b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart @@ -7,6 +7,7 @@ Future showWalletsSelectBottomSheet({ required BuildContext context, required List wallets, required List balances, + String? title, String? selectedId, double maxHeightFactor = 0.7, // cap as a fraction of screen height }) { @@ -48,6 +49,18 @@ Future showWalletsSelectBottomSheet({ ), ), const SizedBox(height: 8), + if (title != null) + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + title, + style: TextStyle( + color: Palette.white, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ), Flexible( fit: FlexFit.loose, child: ConstrainedBox( diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index ab901a42..4a2633aa 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -1,4 +1,3 @@ -import 'package:camelus/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,11 +6,13 @@ import '../../atoms/my_profile_picture.dart'; import '../../components/wallet/payment_history_short.dart'; import '../../components/wallet/wallet_actions_strip.dart'; import '../../components/wallet/wallet_friends_strip.dart'; -import '../../components/wallet/wallet_accounts_card.dart'; import '../../components/wallet/wallets_carousel.dart'; +import '../../components/wallet/wallets_select_bottom_sheet.dart'; import '../../providers/metadata_state_provider.dart'; import '../../providers/ndk_provider.dart'; import '../nostr/nostr_drawer.dart'; +import 'wallet_pay/wallet_pay_state_provider.dart'; +import 'wallet_providers/wallet_combined_state_provider.dart'; class WalletDashboard extends ConsumerStatefulWidget { const WalletDashboard({super.key}); @@ -97,8 +98,22 @@ class _WalletDashboardState extends ConsumerState onReceive: () { Navigator.pushNamed(context, '/wallet/receive'); }, - onPay: () { - Navigator.pushNamed(context, '/wallet/pay'); + onPay: () async { + final selectedId = await showWalletsSelectBottomSheet( + context: context, + wallets: combinedWallet.wallets, + balances: combinedWallet.balances, + title: "Select Wallet to Pay From", + ); + if (selectedId != null) { + final payNotifier = + ref.read(walletPayStateProvider.notifier); + payNotifier.reset(); + payNotifier.updatePayFromWalletId(selectedId); + if (mounted) { + Navigator.pushNamed(context, '/wallet/pay'); + } + } }, onHistory: () {}, ), diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart index d2997948..81fc2356 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -19,6 +17,7 @@ class WalletPayDone extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final walletPayState = ref.watch(walletPayStateProvider); + final payNotifier = ref.watch(walletPayStateProvider.notifier); return Scaffold( backgroundColor: Palette.background, @@ -36,6 +35,7 @@ class WalletPayDone extends ConsumerWidget { child: longButton( name: "close", onPressed: () { + payNotifier.reset(); Navigator.of(context).pop(); }, inverted: true), diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart index 6e2a32a3..839e8f16 100644 --- a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -6,6 +6,7 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../config/palette.dart'; import '../../atoms/spinner_center.dart'; +import 'wallet_navigation.dart'; import 'wallet_providers/qr_value_processing_state_provider.dart'; import 'wallet_rcv/wallet_rcv_state_provider.dart'; @@ -83,6 +84,7 @@ class _QrScan extends ConsumerState { context, '/wallet/receive', ); + ref.read(walletNavigationProvider.notifier).changeDashboardPage(1); break; case QRNavigationTarget.sendPage: throw UnimplementedError( @@ -128,26 +130,16 @@ class _QrScan extends ConsumerState { /// scanning indicator Center( child: Container( - width: 250, - height: 250, - decoration: BoxDecoration( - border: Border.all( - color: Palette.white, - width: 2, - ), - borderRadius: BorderRadius.circular(20), - ), - child: const Center( - child: Text( - 'Position QR code here', - style: TextStyle( - color: Colors.white70, - fontSize: 16, + width: 250, + height: 250, + decoration: BoxDecoration( + border: Border.all( + color: Palette.white, + width: 2, ), - textAlign: TextAlign.center, + borderRadius: BorderRadius.circular(20), ), - ), - ), + child: null), ), /// bottom area From e3e224680843861cd2044938ae7098c331fdb660 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:30:36 +0200 Subject: [PATCH 30/52] add mint page --- lib/main.dart | 5 + .../atoms/wallet/mint_info_card_small.dart | 302 ++++++++++++++++++ .../components/wallet/wallets_carousel.dart | 6 +- .../routes/wallet/add_mint/add_mint_page.dart | 223 +++++++++++++ .../routes/wallet/wallet_dashboard.dart | 12 + 5 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 lib/presentation_layer/atoms/wallet/mint_info_card_small.dart create mode 100644 lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart diff --git a/lib/main.dart b/lib/main.dart index dd02cc70..83abc9e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,6 +37,7 @@ import 'presentation_layer/routes/nostr/settings/inital_route/inital_route_setti import 'presentation_layer/routes/nostr/settings/locale/locale_settings.dart'; import 'presentation_layer/routes/nostr/settings/moderation/moderation_settings.dart'; import 'presentation_layer/routes/nostr/settings/settings_page.dart'; +import 'presentation_layer/routes/wallet/add_mint/add_mint_page.dart'; import 'presentation_layer/routes/wallet/wallet_navigation.dart'; import 'presentation_layer/routes/wallet/wallet_pay_page.dart'; import 'presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart'; @@ -296,6 +297,10 @@ class MyApp extends ConsumerWidget { return MaterialPageRoute( builder: (context) => WalletPayPage(), ); + case '/wallet/add_mint': + return MaterialPageRoute( + builder: (context) => const AddMintPage(), + ); } assert(false, 'Need to implement ${settings.name}'); return null; diff --git a/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart new file mode 100644 index 00000000..891cdd45 --- /dev/null +++ b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart @@ -0,0 +1,302 @@ +import 'package:flutter/material.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../../../config/palette.dart'; + +class MintInfoCardSmall extends StatelessWidget { + final ndk_entities.CashuMintInfo mintInfo; + + const MintInfoCardSmall({ + super.key, + required this.mintInfo, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 4, + color: Palette.extraDarkGray, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: Palette.white.withValues(alpha: 0.5), + width: 1, + ), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// header + Row( + children: [ + if (mintInfo.iconUrl != null) ...[ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + mintInfo.iconUrl!, + width: 48, + height: 48, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildDefaultIcon(), + ), + ), + const SizedBox(width: 12), + ] else + _buildDefaultIcon(), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + mintInfo.name ?? 'Unknown Mint', + style: TextStyle( + color: Palette.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (mintInfo.version != null) + Text( + '${mintInfo.version}', + style: TextStyle( + color: Palette.gray, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 12), + + /// description + if (mintInfo.description != null) ...[ + Text( + mintInfo.description!, + style: TextStyle( + color: Palette.lightGray, + fontSize: 14, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 24), + ], + + /// motd + if (mintInfo.motd != null) ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Palette.gray.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Palette.primary.withValues(alpha: 0.5), + ), + ), + child: Row( + children: [ + Icon( + PhosphorIcons.megaphone(), + size: 16, + color: Palette.primary, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + mintInfo.motd!, + style: TextStyle( + color: Palette.white, + fontSize: 16, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + ], + + /// supported units + if (mintInfo.supportedUnits.isNotEmpty) ...[ + Text( + 'Supported Units', + style: TextStyle( + color: Palette.lightGray, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Wrap( + spacing: 6, + runSpacing: 4, + children: mintInfo.supportedUnits.map((unit) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + ), + child: Text( + unit.toUpperCase(), + style: TextStyle( + color: Palette.white, + fontSize: 12, + ), + ), + ); + }).toList(), + ), + const SizedBox(height: 12), + ], + + /// contact info + if (mintInfo.contact.isNotEmpty) ...[ + Text( + 'Contact', + style: TextStyle( + color: Palette.lightGray, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + ...mintInfo.contact.take(2).map((contact) { + return Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Row( + children: [ + Icon( + PhosphorIcons.userCircle(), + size: 14, + color: Palette.lightGray, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + contact.info, + style: TextStyle( + color: Palette.white, + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + }), + const SizedBox(height: 12), + ], + + /// URLs section + if (mintInfo.urls.isNotEmpty) ...[ + Row( + children: [ + Icon( + PhosphorIcons.link(), + size: 16, + color: Palette.lightGray, + ), + const SizedBox(width: 4), + Text( + 'URLs', + ), + ], + ), + const SizedBox(height: 4), + ...mintInfo.urls.map((url) { + return Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + url, + style: TextStyle( + color: Palette.lightGray, + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }), + ], + + /// footer + if (mintInfo.time != null || mintInfo.tosUrl != null) ...[ + const SizedBox(height: 8), + const Divider(height: 1), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox.shrink(), + if (mintInfo.tosUrl != null) + TextButton( + onPressed: () async { + launchUrlString(mintInfo.tosUrl!); + }, + child: Row( + children: [ + Icon( + Icons.description_outlined, + size: 12, + color: Palette.lightGray, + ), + const SizedBox(width: 4), + Text( + 'Terms of Service', + style: TextStyle( + color: Palette.lightGray, + fontSize: 12, + ), + ), + ], + ), + ) + ], + ), + ], + ], + ), + ), + ), + ); + } + + Widget _buildDefaultIcon() { + return Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Palette.darkGray, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + PhosphorIcons.bank(), + size: 24, + color: Palette.gray, + ), + ); + } +} diff --git a/lib/presentation_layer/components/wallet/wallets_carousel.dart b/lib/presentation_layer/components/wallet/wallets_carousel.dart index 83de49c1..688ae2e3 100644 --- a/lib/presentation_layer/components/wallet/wallets_carousel.dart +++ b/lib/presentation_layer/components/wallet/wallets_carousel.dart @@ -63,7 +63,11 @@ class _WalletsCarouselState extends State { child: SizedBox( height: 210, child: _CardMaxWidth( - child: WalletAccountCardPlaceholder(), + child: WalletAccountCardPlaceholder( + onTap: () { + Navigator.pushNamed(context, '/wallet/add_mint'); + }, + ), ), ), ); diff --git a/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart b/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart new file mode 100644 index 00000000..7057f69a --- /dev/null +++ b/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart @@ -0,0 +1,223 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/ndk.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import '../../../../config/palette.dart'; +import '../../../atoms/long_button.dart'; +import 'package:ndk/entities.dart' as ndk_entities; + +import '../../../atoms/wallet/mint_info_card_small.dart'; +import '../../../providers/ndk_provider.dart'; + +class ValidationState { + final String text; + final bool isValidating; + final bool? isValid; + final ndk_entities.CashuMintInfo? mintInfo; + + const ValidationState({ + this.text = '', + this.isValidating = false, + this.isValid, + this.mintInfo, + }); + + ValidationState copyWith({ + String? text, + bool? isValidating, + bool? isValid, + ndk_entities.CashuMintInfo? mintInfo, + bool clearMintInfo = false, + }) { + return ValidationState( + text: text ?? this.text, + isValidating: isValidating ?? this.isValidating, + isValid: isValid ?? this.isValid, + mintInfo: clearMintInfo ? null : (mintInfo ?? this.mintInfo), + ); + } +} + +class AddMintNotifier extends StateNotifier { + final Ndk _ndk; + AddMintNotifier({ + required Ndk ndk, + }) : _ndk = ndk, + super(const ValidationState()); + + Timer? _debounceTimer; + + void updateText(String text) { + state = state.copyWith( + text: text, + isValid: false, + clearMintInfo: true, + ); + + _debounceTimer?.cancel(); + + if (text.isEmpty) { + state = state.copyWith( + isValidating: false, isValid: null, clearMintInfo: true); + return; + } + + _debounceTimer = Timer(const Duration(milliseconds: 500), () { + _validateText(text); + }); + } + + Future _validateText(String text) async { + state = state.copyWith( + isValidating: true, + clearMintInfo: true, + ); + + final isValid = text.length >= 3 && !text.contains(' '); + + try { + final mintInfoNetwork = + await _ndk.cashu.getMintInfoNetwork(mintUrl: buildValidUrl(text)); + + state = state.copyWith( + isValidating: false, + isValid: isValid, + mintInfo: mintInfoNetwork, + ); + } catch (e) { + state = state.copyWith( + isValidating: false, isValid: false, clearMintInfo: true); + return; + } + } + + String buildValidUrl(String urlToCheck) { + final text = urlToCheck.trim(); + if (text.isEmpty) return ''; + + if (!text.startsWith('http://') && !text.startsWith('https://')) { + return 'https://$text'; + } + return text; + } + + @override + void dispose() { + _debounceTimer?.cancel(); + super.dispose(); + } +} + +final addMintProvider = + StateNotifierProvider.autoDispose( + (ref) { + final ndk = ref.watch(ndkProvider); + return AddMintNotifier(ndk: ndk); + }, +); + +class AddMintPage extends ConsumerWidget { + const AddMintPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final validationState = ref.watch(addMintProvider); + final notifier = ref.read(addMintProvider.notifier); + + return Scaffold( + backgroundColor: Palette.background, + appBar: AppBar( + title: const Text('Add Mint'), + backgroundColor: Palette.background, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const Spacer(flex: 10), + + if (validationState.mintInfo != null) + SingleChildScrollView( + child: MintInfoCardSmall( + mintInfo: validationState.mintInfo!, + ), + ), + + const Spacer(flex: 2), + + Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: LinearProgressIndicator( + value: validationState.isValidating ? null : 0.0, + backgroundColor: Palette.background, + color: Palette.white, + borderRadius: BorderRadius.circular(12), + ), + ), + + TextField( + autofocus: true, + onChanged: notifier.updateText, + decoration: InputDecoration( + hintText: 'Enter mint address...', + border: const OutlineInputBorder(), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Palette.white), + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Palette.darkGray), + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + suffixIcon: _buildValidationIcon(validationState), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + + //const Spacer(flex: 1), + ], + ), + ), + bottomNavigationBar: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: longButton( + name: "accept terms & add", + onPressed: () { + if (validationState.isValid == true && + validationState.mintInfo != null) { + final validUrl = notifier.buildValidUrl(validationState.text); + ref.read(ndkProvider).cashu.addMintToKnownMints( + mintUrl: validUrl, + ); + Navigator.pop(context); + } + }, + inverted: true, + disabled: validationState.isValid != true, + ), + ), + ), + ); + } + + Widget? _buildValidationIcon(ValidationState state) { + if (state.text.isEmpty) { + return null; + } + + if (state.isValid == true) { + return Icon( + PhosphorIcons.checkCircle(), + color: Palette.success, + ); + } + + return null; + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index 4a2633aa..3b0dce05 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../config/palette.dart'; import '../../atoms/my_profile_picture.dart'; @@ -69,6 +70,17 @@ class _WalletDashboardState extends ConsumerState ); }, ), + actions: [ + IconButton( + icon: Icon( + PhosphorIcons.plusCircle(), + size: 30, + ), + onPressed: () { + Navigator.pushNamed(context, '/wallet/add_mint'); + }, + ), + ], ), backgroundColor: Palette.background, drawer: NostrDrawer(pubkey: myUserPubkey), From 2b76be8d847e04e9e973691c2582868b4377d689 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:30:14 +0200 Subject: [PATCH 31/52] mint info page --- lib/main.dart | 8 +++ .../atoms/wallet/mint_info_card_small.dart | 2 +- .../components/wallet/wallets_carousel.dart | 23 ++++++-- .../routes/wallet/add_mint/add_mint_page.dart | 1 + .../wallet/mint_info/mint_info_page.dart | 56 +++++++++++++++++++ .../wallet_pay_select_amount.dart | 1 + .../wallet_pay_summary.dart | 1 + 7 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart diff --git a/lib/main.dart b/lib/main.dart index 83abc9e4..836d5a2c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,6 +38,7 @@ import 'presentation_layer/routes/nostr/settings/locale/locale_settings.dart'; import 'presentation_layer/routes/nostr/settings/moderation/moderation_settings.dart'; import 'presentation_layer/routes/nostr/settings/settings_page.dart'; import 'presentation_layer/routes/wallet/add_mint/add_mint_page.dart'; +import 'presentation_layer/routes/wallet/mint_info/mint_info_page.dart'; import 'presentation_layer/routes/wallet/wallet_navigation.dart'; import 'presentation_layer/routes/wallet/wallet_pay_page.dart'; import 'presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart'; @@ -301,6 +302,13 @@ class MyApp extends ConsumerWidget { return MaterialPageRoute( builder: (context) => const AddMintPage(), ); + + case '/wallet/mint_details': + return MaterialPageRoute( + builder: (context) => MintInfoPage( + mintUrl: settings.arguments as String?, + ), + ); } assert(false, 'Need to implement ${settings.name}'); return null; diff --git a/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart index 891cdd45..9d5f584e 100644 --- a/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart +++ b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart @@ -17,7 +17,7 @@ class MintInfoCardSmall extends StatelessWidget { Widget build(BuildContext context) { return Card( elevation: 4, - color: Palette.extraDarkGray, + color: Palette.background, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( diff --git a/lib/presentation_layer/components/wallet/wallets_carousel.dart b/lib/presentation_layer/components/wallet/wallets_carousel.dart index 688ae2e3..86d67209 100644 --- a/lib/presentation_layer/components/wallet/wallets_carousel.dart +++ b/lib/presentation_layer/components/wallet/wallets_carousel.dart @@ -111,12 +111,23 @@ class _WalletsCarouselState extends State { right: index == wallets.length - 1 ? 16 : 8, ), child: _CardMaxWidth( - child: WalletAccountsCard( - walletId: w.id, - title: title, - alias: alias, - balances: myBalances, - nfcAnimController: widget.nfcAnimController!), + child: GestureDetector( + onTap: () { + if (mintUrl != null) { + Navigator.pushNamed( + context, + '/wallet/mint_details', + arguments: mintUrl, + ); + } + }, + child: WalletAccountsCard( + walletId: w.id, + title: title, + alias: alias, + balances: myBalances, + nfcAnimController: widget.nfcAnimController!), + ), ), ); }, diff --git a/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart b/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart index 7057f69a..588c2157 100644 --- a/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart +++ b/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart @@ -88,6 +88,7 @@ class AddMintNotifier extends StateNotifier { } catch (e) { state = state.copyWith( isValidating: false, isValid: false, clearMintInfo: true); + debugPrint('Error validating mint: $e'); return; } } diff --git a/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart b/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart new file mode 100644 index 00000000..fc5ec148 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart'; + +import '../../../../config/palette.dart'; +import '../../../atoms/wallet/mint_info_card_small.dart'; +import '../wallet_providers/wallet_combined_state_provider.dart'; + +class MintInfoPage extends ConsumerWidget { + final String? mintUrl; + + const MintInfoPage({ + super.key, + this.mintUrl, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final walletCombined = ref.watch(walletCombinedProvider); + + final mintInfoFilter = walletCombined.wallets.where((wallet) { + return wallet is CashuWallet && wallet.mintUrl == mintUrl; + }); + + if (mintInfoFilter.isEmpty) { + return Scaffold( + backgroundColor: Palette.background, + appBar: AppBar( + backgroundColor: Palette.background, + title: Text('Mint Info'), + ), + body: Center( + child: Text('Mint not found'), + ), + ); + } + + final myWallet = mintInfoFilter.first as CashuWallet; + + return Scaffold( + backgroundColor: Palette.background, + appBar: AppBar( + backgroundColor: Palette.background, + title: Text('Mint Info'), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), + child: MintInfoCardSmall( + mintInfo: myWallet.mintInfo, + ), + ), + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart index d2a01aed..f521dc61 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart @@ -140,6 +140,7 @@ class _WalletPaySelectAmountState extends ConsumerState { type: ndk_entities.WalletType.CASHU, supportedUnits: {}, mintUrl: '', + mintInfo: ndk_entities.CashuMintInfo(nuts: {}), ), ), balances: payState.availableBalances diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart index 29bc1a90..3fcca52f 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -203,6 +203,7 @@ class WalletPaySummary extends ConsumerWidget { type: ndk_entities.WalletType.CASHU, supportedUnits: {}, mintUrl: '', + mintInfo: ndk_entities.CashuMintInfo(nuts: {}), ), ), balances: state.availableBalances From 21397b868da5ab21b323dbef4a42c73838d41643 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:41:19 +0200 Subject: [PATCH 32/52] refactor: naming --- lib/main.dart | 6 ++--- .../{ => wallet_pay}/wallet_pay_page.dart | 6 ++--- .../routes/wallet/wallet_qr_scan.dart | 5 ++-- .../wallet_rcv_ecash_completer_page.dart} | 16 ++++++------ ...t_rcv_ecash_completer_state_provider.dart} | 25 ++++++++++--------- test/unit_test/helpers/helpers_test.dart | 2 +- 6 files changed, 31 insertions(+), 29 deletions(-) rename lib/presentation_layer/routes/wallet/{ => wallet_pay}/wallet_pay_page.dart (89%) rename lib/presentation_layer/routes/wallet/{wallet_rcv/wallet_rcv_page.dart => wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart} (93%) rename lib/presentation_layer/routes/wallet/{wallet_rcv/wallet_rcv_state_provider.dart => wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart} (83%) diff --git a/lib/main.dart b/lib/main.dart index 836d5a2c..0c63baa3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,8 +40,8 @@ import 'presentation_layer/routes/nostr/settings/settings_page.dart'; import 'presentation_layer/routes/wallet/add_mint/add_mint_page.dart'; import 'presentation_layer/routes/wallet/mint_info/mint_info_page.dart'; import 'presentation_layer/routes/wallet/wallet_navigation.dart'; -import 'presentation_layer/routes/wallet/wallet_pay_page.dart'; -import 'presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart'; +import 'presentation_layer/routes/wallet/wallet_pay/wallet_pay_page.dart'; +import 'presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart'; import 'theme.dart' as theme; const devDeviceFrame = true; @@ -278,7 +278,7 @@ class MyApp extends ConsumerWidget { ); case '/wallet/receive': return MaterialPageRoute( - builder: (context) => WalletReceivePage(), + builder: (context) => WalletReceiveEcashCompleterPage(), ); case '/wallet/mints': diff --git a/lib/presentation_layer/routes/wallet/wallet_pay_page.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_page.dart similarity index 89% rename from lib/presentation_layer/routes/wallet/wallet_pay_page.dart rename to lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_page.dart index 0ac5d8e6..6e0fac67 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart'; -import 'wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart'; -import 'wallet_pay/wallet_pay_summary/wallet_pay_summary.dart'; +import 'wallet_pay_select_amount/wallet_pay_select_amount.dart'; +import 'wallet_pay_select_reciever/wallet_pay_reciever.dart'; +import 'wallet_pay_summary/wallet_pay_summary.dart'; class WalletPayPage extends ConsumerStatefulWidget { const WalletPayPage({ diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart index 839e8f16..1f375802 100644 --- a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -8,7 +8,7 @@ import '../../../config/palette.dart'; import '../../atoms/spinner_center.dart'; import 'wallet_navigation.dart'; import 'wallet_providers/qr_value_processing_state_provider.dart'; -import 'wallet_rcv/wallet_rcv_state_provider.dart'; +import 'wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart'; class WalletQrScan extends ConsumerStatefulWidget { const WalletQrScan({super.key}); @@ -73,7 +73,8 @@ class _QrScan extends ConsumerState { next.navigationData != null) { ref.read(qrScannerProvider.notifier).clearNavigation(); - final rcvProvider = ref.read(walletReceiveProvider.notifier); + final rcvProvider = + ref.read(walletReceiveEcashCompleterProvider.notifier); switch (next.navigationTarget!) { case QRNavigationTarget.rcvPage: diff --git a/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart similarity index 93% rename from lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart rename to lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart index 97a81c4c..ffd4943e 100644 --- a/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart @@ -2,17 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../../../config/palette.dart'; -import '../../../../helpers/wallet_number_formatting.dart'; -import '../../../atoms/long_button.dart'; -import 'wallet_rcv_state_provider.dart'; +import '../../../../../config/palette.dart'; +import '../../../../../helpers/wallet_number_formatting.dart'; +import '../../../../atoms/long_button.dart'; +import 'wallet_rcv_ecash_completer_state_provider.dart'; -class WalletReceivePage extends ConsumerWidget { - const WalletReceivePage({super.key}); +class WalletReceiveEcashCompleterPage extends ConsumerWidget { + const WalletReceiveEcashCompleterPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final walletState = ref.watch(walletReceiveProvider); + final walletState = ref.watch(walletReceiveEcashCompleterProvider); return Scaffold( backgroundColor: Palette.background, @@ -104,7 +104,7 @@ class WalletReceivePage extends ConsumerWidget { ); } - Widget _buildStatusCard(WalletRcvState state) { + Widget _buildStatusCard(WalletRcvEcashCompleterState state) { IconData icon; Color color; String title; diff --git a/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart similarity index 83% rename from lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_state_provider.dart rename to lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart index ed29d455..69267534 100644 --- a/lib/presentation_layer/routes/wallet/wallet_rcv/wallet_rcv_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart @@ -2,13 +2,13 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; import 'package:riverpod/riverpod.dart'; -import '../../../providers/ndk_provider.dart'; +import '../../../../providers/ndk_provider.dart'; enum WalletRcvType { eCash, } -class WalletRcvState { +class WalletRcvEcashCompleterState { final bool isPending; final bool isCompleted; final bool isError; @@ -22,7 +22,7 @@ class WalletRcvState { final int? amount; final String? unit; - WalletRcvState({ + WalletRcvEcashCompleterState({ required this.isPending, required this.isCompleted, required this.isError, @@ -35,7 +35,7 @@ class WalletRcvState { this.unit, }); - WalletRcvState copyWith({ + WalletRcvEcashCompleterState copyWith({ bool? isPending, bool? isCompleted, bool? isError, @@ -47,7 +47,7 @@ class WalletRcvState { int? amount, String? unit, }) { - return WalletRcvState( + return WalletRcvEcashCompleterState( isPending: isPending ?? this.isPending, isCompleted: isCompleted ?? this.isCompleted, isError: isError ?? this.isError, @@ -62,12 +62,13 @@ class WalletRcvState { } } -class WalletRcvNotifier extends StateNotifier { +class WalletRcvEcashCompleterNotifier + extends StateNotifier { final Ndk _ndk; - WalletRcvNotifier({required Ndk ndk}) + WalletRcvEcashCompleterNotifier({required Ndk ndk}) : _ndk = ndk, super( - WalletRcvState( + WalletRcvEcashCompleterState( isPending: false, isCompleted: false, isError: false, @@ -123,7 +124,7 @@ class WalletRcvNotifier extends StateNotifier { } void reset() { - state = WalletRcvState( + state = WalletRcvEcashCompleterState( isPending: false, isCompleted: false, isError: false, @@ -138,10 +139,10 @@ class WalletRcvNotifier extends StateNotifier { } } -final walletReceiveProvider = - StateNotifierProvider( +final walletReceiveEcashCompleterProvider = StateNotifierProvider< + WalletRcvEcashCompleterNotifier, WalletRcvEcashCompleterState>( (ref) { final ndk = ref.watch(ndkProvider); - return WalletRcvNotifier(ndk: ndk); + return WalletRcvEcashCompleterNotifier(ndk: ndk); }, ); diff --git a/test/unit_test/helpers/helpers_test.dart b/test/unit_test/helpers/helpers_test.dart index d4fc7378..d142b23c 100644 --- a/test/unit_test/helpers/helpers_test.dart +++ b/test/unit_test/helpers/helpers_test.dart @@ -35,7 +35,7 @@ void main() { const hex = '1e1a76e8dc8f8c82eacb5ea02a1e87a2f172dd'; const hrp = 'test'; - final bech32 = helpers.encodeBech32(hex, hrp); + final bech32 = Helpers.encodeBech32(hex, hrp); final decoded = helpers.decodeBech32(bech32); expect(decoded[0], hex); From d6f9c6063e9d536955f3ceaa300091341c96143e Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:56:54 +0200 Subject: [PATCH 33/52] pay rcv flow --- lib/main.dart | 6 + .../atoms/copy_to_clipboard.dart | 67 ++++ .../atoms/cowntdown_timer.dart | 74 ++++ .../atoms/currency_picker_bar.dart | 24 +- .../wallet/payment_history_short.dart | 98 ++++- .../routes/wallet/wallet_dashboard.dart | 117 +++--- .../routes/wallet/wallet_mints.dart | 14 - .../routes/wallet/wallet_navigation.dart | 5 +- .../wallet_pay_done/wallet_pay_done.dart | 55 +-- .../wallet_pay_select_amount.dart | 40 +- .../wallet_combined_state_provider.dart | 30 -- .../routes/wallet/wallet_qr_scan.dart | 2 +- .../routes/wallet/wallet_receive.dart | 14 - .../wallet_receive_amount.dart | 349 +++++++++++++++++ .../wallet_receive/wallet_receive_page.dart | 74 ++++ .../wallet_receive_request.dart | 363 ++++++++++++++++++ .../wallet_receive_state_provider.dart | 179 +++++++++ .../wallet_receive_type.dart | 247 ++++++++++++ pubspec.lock | 24 ++ pubspec.yaml | 1 + 20 files changed, 1574 insertions(+), 209 deletions(-) create mode 100644 lib/presentation_layer/atoms/copy_to_clipboard.dart create mode 100644 lib/presentation_layer/atoms/cowntdown_timer.dart delete mode 100644 lib/presentation_layer/routes/wallet/wallet_mints.dart delete mode 100644 lib/presentation_layer/routes/wallet/wallet_receive.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_page.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart diff --git a/lib/main.dart b/lib/main.dart index 0c63baa3..a6a17709 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -42,6 +42,7 @@ import 'presentation_layer/routes/wallet/mint_info/mint_info_page.dart'; import 'presentation_layer/routes/wallet/wallet_navigation.dart'; import 'presentation_layer/routes/wallet/wallet_pay/wallet_pay_page.dart'; import 'presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart'; +import 'presentation_layer/routes/wallet/wallet_receive/wallet_receive_page.dart'; import 'theme.dart' as theme; const devDeviceFrame = true; @@ -277,6 +278,11 @@ class MyApp extends ConsumerWidget { ), ); case '/wallet/receive': + return MaterialPageRoute( + builder: (context) => WalletReceivePage(), + ); + + case '/wallet/receive/ecash': return MaterialPageRoute( builder: (context) => WalletReceiveEcashCompleterPage(), ); diff --git a/lib/presentation_layer/atoms/copy_to_clipboard.dart b/lib/presentation_layer/atoms/copy_to_clipboard.dart new file mode 100644 index 00000000..4acf3a76 --- /dev/null +++ b/lib/presentation_layer/atoms/copy_to_clipboard.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../../config/palette.dart'; + +class CopyClipboardButton extends StatefulWidget { + final String value; + final String copyText; + final String copyDoneText; + + final Color backgroundColor; + final Color primaryColor; + + const CopyClipboardButton({ + super.key, + required this.value, + this.copyText = 'Copy', + this.copyDoneText = 'Copied to Clipboard!', + this.backgroundColor = Palette.white, + this.primaryColor = Palette.primary, + }); + + @override + State createState() => _CopyClipboardButtonState(); +} + +class _CopyClipboardButtonState extends State { + bool _copied = false; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _copied ? null : _copyToClipboard, + icon: + Icon(_copied ? PhosphorIcons.check() : PhosphorIcons.copySimple()), + label: Text(_copied ? widget.copyDoneText : widget.copyText), + style: ElevatedButton.styleFrom( + backgroundColor: + _copied ? widget.backgroundColor : widget.primaryColor, + foregroundColor: widget.backgroundColor, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ); + } + + Future _copyToClipboard() async { + await Clipboard.setData(ClipboardData(text: widget.value)); + setState(() { + _copied = true; + }); + + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + setState(() { + _copied = false; + }); + } + }); + } +} diff --git a/lib/presentation_layer/atoms/cowntdown_timer.dart b/lib/presentation_layer/atoms/cowntdown_timer.dart new file mode 100644 index 00000000..358def87 --- /dev/null +++ b/lib/presentation_layer/atoms/cowntdown_timer.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +class CountdownTimer extends StatefulWidget { + final int unixTimestamp; + + const CountdownTimer({super.key, required this.unixTimestamp}); + + @override + CountdownTimerState createState() => CountdownTimerState(); +} + +class CountdownTimerState extends State { + Timer? _timer; + int _remainingSeconds = 0; + bool _isExpired = false; + + @override + void initState() { + super.initState(); + _calculateRemainingTime(); + _startTimer(); + } + + void _calculateRemainingTime() { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + _remainingSeconds = widget.unixTimestamp - now; + if (_remainingSeconds <= 0) { + _remainingSeconds = 0; + _isExpired = true; + } + } + + void _startTimer() { + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + setState(() { + if (_remainingSeconds > 0) { + _remainingSeconds--; + } else { + _isExpired = true; + _timer?.cancel(); + } + }); + }); + } + + String _formatTime(int seconds) { + final hours = seconds ~/ 3600; + final minutes = (seconds % 3600) ~/ 60; + final secs = seconds % 60; + return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(8), + child: Text( + _isExpired ? 'EXPIRED' : _formatTime(_remainingSeconds), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: _isExpired ? Colors.red : Colors.black, + ), + ), + ); + } +} diff --git a/lib/presentation_layer/atoms/currency_picker_bar.dart b/lib/presentation_layer/atoms/currency_picker_bar.dart index c08f5a1e..0b7b6e69 100644 --- a/lib/presentation_layer/atoms/currency_picker_bar.dart +++ b/lib/presentation_layer/atoms/currency_picker_bar.dart @@ -4,6 +4,16 @@ import 'package:flutter/services.dart'; import '../../config/palette.dart'; +class ChangeResult { + final String currentUnit; + final String previousUnit; + + ChangeResult({ + required this.currentUnit, + required this.previousUnit, + }); +} + class CurrencyPickerBar extends StatefulWidget { const CurrencyPickerBar({ super.key, @@ -24,7 +34,7 @@ class CurrencyPickerBar extends StatefulWidget { final List currencies; final int initialIndex; - final ValueChanged? onChanged; + final ValueChanged? onChanged; final double height; final double lensRadius; final EdgeInsets padding; @@ -123,6 +133,7 @@ class _CurrencyPickerBarState extends State } void _setSelectedIndex(int index, {bool fromUser = true}) { + final previousIndex = _selectedIndex; if (index == _selectedIndex) return; setState(() { _selectedIndex = index; @@ -130,7 +141,16 @@ class _CurrencyPickerBarState extends State if (widget.showHaptics && fromUser) { HapticFeedback.selectionClick(); } - widget.onChanged?.call(_selectedIndex); + + final currentUnit = widget.currencies[_selectedIndex]; + final previousUnit = widget.currencies[previousIndex]; + + widget.onChanged?.call( + ChangeResult( + currentUnit: currentUnit, + previousUnit: previousUnit, + ), + ); } @override diff --git a/lib/presentation_layer/components/wallet/payment_history_short.dart b/lib/presentation_layer/components/wallet/payment_history_short.dart index 03a0f58c..806b8c88 100644 --- a/lib/presentation_layer/components/wallet/payment_history_short.dart +++ b/lib/presentation_layer/components/wallet/payment_history_short.dart @@ -25,8 +25,6 @@ class PaymentHistoryShort extends StatelessWidget { this.onTap, this.showDividers = true, this.emptyText = 'no transactions yet', - - /// default height this.height = 300, this.physics, this.controller, @@ -65,12 +63,7 @@ class PaymentHistoryShort extends StatelessWidget { delegate: SliverChildBuilderDelegate( (context, index) { final tx = visible[index]; - final isPending = pendingTransactions.contains(tx); - final isIncoming = tx.changeAmount >= 0; - final color = isIncoming ? Colors.green : Colors.red; - final icon = isIncoming - ? Icons.arrow_downward_rounded - : Icons.arrow_upward_rounded; + final transactionStatus = _getTransactionStatus(tx); final dateToUse = _bestDate(tx); final now = DateTime.now(); @@ -98,23 +91,21 @@ class PaymentHistoryShort extends StatelessWidget { Widget listTile = ListTile( onTap: onTap == null ? null : () => onTap!(tx), leading: CircleAvatar( - backgroundColor: isPending - ? Colors.blue.withValues(alpha: 0.1) - : color.withValues(alpha: 0.1), + backgroundColor: + transactionStatus.color.withValues(alpha: 0.1), child: Icon( - isPending ? Icons.pending : icon, - color: isPending ? Colors.blue : color, + transactionStatus.icon, + color: transactionStatus.color, ), ), title: Text( - '${isPending ? 'Pending' : (isIncoming ? 'Incoming' : 'Outgoing')} - ${tx.walletType}', + '${transactionStatus.label} - ${tx.walletType}', style: const TextStyle(fontWeight: FontWeight.w600), ), subtitle: Text( [ transactionDateText, _removeHttpPrefix(tx.walletId), - //_enumLabel(tx.state), ].where((e) => e.isNotEmpty).join(' • '), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -127,16 +118,22 @@ class PaymentHistoryShort extends StatelessWidget { amountStr, style: TextStyle( fontWeight: FontWeight.w700, - color: isPending ? Colors.blue : color, + color: transactionStatus.color, + decoration: transactionStatus.isStrikethrough + ? TextDecoration.lineThrough + : null, ), ), if (tx.completionMsg != null && tx.completionMsg!.isNotEmpty) - Text( - tx.completionMsg!, - style: Theme.of(context).textTheme.bodySmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 150), + child: Text( + tx.completionMsg!, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ], ), @@ -164,6 +161,51 @@ class PaymentHistoryShort extends StatelessWidget { : scrollView; } + static TransactionStatus _getTransactionStatus( + ndk_entities.WalletTransaction tx) { + final isDraft = tx.state == ndk_entities.WalletTransactionState.draft; + final isPending = tx.state == ndk_entities.WalletTransactionState.pending; + final isFailed = tx.state == ndk_entities.WalletTransactionState.failed; + final isCanceled = tx.state == ndk_entities.WalletTransactionState.canceled; + final isIncoming = tx.changeAmount >= 0; + + if (isPending || isDraft) { + return TransactionStatus( + label: 'Pending', + icon: Icons.pending, + color: Colors.blue, + isStrikethrough: false, + ); + } else if (isFailed) { + return TransactionStatus( + label: 'Failed ${isIncoming ? 'Incoming' : 'Outgoing'}', + icon: Icons.error_outline, + color: Colors.red, + isStrikethrough: true, + ); + } else if (isCanceled) { + return TransactionStatus( + label: 'Canceled ${isIncoming ? 'Incoming' : 'Outgoing'}', + icon: Icons.cancel_outlined, + color: Colors.orange, + isStrikethrough: true, + ); + } else { + // Successful transaction + final color = isIncoming ? Colors.green : Colors.red; + final icon = isIncoming + ? Icons.arrow_downward_rounded + : Icons.arrow_upward_rounded; + + return TransactionStatus( + label: isIncoming ? 'Incoming' : 'Outgoing', + icon: icon, + color: color, + isStrikethrough: false, + ); + } + } + static int? _bestDate(ndk_entities.WalletTransaction tx) => tx.transactionDate ?? tx.initiatedDate; @@ -183,3 +225,17 @@ class PaymentHistoryShort extends StatelessWidget { return url; } } + +class TransactionStatus { + final String label; + final IconData icon; + final Color color; + final bool isStrikethrough; + + const TransactionStatus({ + required this.label, + required this.icon, + required this.color, + required this.isStrikethrough, + }); +} diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index 3b0dce05..a2782520 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -14,6 +14,7 @@ import '../../providers/ndk_provider.dart'; import '../nostr/nostr_drawer.dart'; import 'wallet_pay/wallet_pay_state_provider.dart'; import 'wallet_providers/wallet_combined_state_provider.dart'; +import 'wallet_receive/wallet_receive_state_provider.dart'; class WalletDashboard extends ConsumerStatefulWidget { const WalletDashboard({super.key}); @@ -85,70 +86,68 @@ class _WalletDashboardState extends ConsumerState backgroundColor: Palette.background, drawer: NostrDrawer(pubkey: myUserPubkey), body: SafeArea( - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 20), - WalletsCarousel( - wallets: wallets, - balances: combinedWallet.balances, - nfcAnimController: _nfcAnimController, - ), - const SizedBox(height: 30), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: WalletFriendsStrip(), - ), - const SizedBox(height: 30), - WalletActionsStrip( - onScan: () { - Navigator.pushNamed(context, '/wallet/scan'); - }, - onReceive: () { - Navigator.pushNamed(context, '/wallet/receive'); - }, - onPay: () async { - final selectedId = await showWalletsSelectBottomSheet( - context: context, - wallets: combinedWallet.wallets, - balances: combinedWallet.balances, - title: "Select Wallet to Pay From", - ); - if (selectedId != null) { - final payNotifier = - ref.read(walletPayStateProvider.notifier); - payNotifier.reset(); - payNotifier.updatePayFromWalletId(selectedId); - if (mounted) { - Navigator.pushNamed(context, '/wallet/pay'); - } + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(height: 20), + WalletsCarousel( + wallets: wallets, + balances: combinedWallet.balances, + nfcAnimController: _nfcAnimController, + ), + const SizedBox(height: 30), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: WalletFriendsStrip(), + ), + const SizedBox(height: 30), + WalletActionsStrip( + onScan: () { + Navigator.pushNamed(context, '/wallet/scan'); + }, + onReceive: () async { + final selectedId = await showWalletsSelectBottomSheet( + context: context, + wallets: combinedWallet.wallets, + balances: combinedWallet.balances, + title: "Select Wallet to Receive To", + ); + if (selectedId != null) { + final rcvNotifier = ref.read(walletRecieverProvider.notifier); + rcvNotifier.reset(); + rcvNotifier.updateRecieveToWalletId(selectedId); + if (mounted) { + Navigator.pushNamed(context, '/wallet/receive'); } - }, - onHistory: () {}, - ), - const SizedBox(height: 30), - TextButton( - onPressed: () { - combinedWalletNotifier.fundWallet(); - }, - child: Text("fund"), - ), - TextButton( - onPressed: () { - combinedWalletNotifier.spend(); - }, - child: Text("spend"), - ), - PaymentHistoryShort( + } + }, + onPay: () async { + final selectedId = await showWalletsSelectBottomSheet( + context: context, + wallets: combinedWallet.wallets, + balances: combinedWallet.balances, + title: "Select Wallet to Pay From", + ); + if (selectedId != null) { + final payNotifier = ref.read(walletPayStateProvider.notifier); + payNotifier.reset(); + payNotifier.updatePayFromWalletId(selectedId); + if (mounted) { + Navigator.pushNamed(context, '/wallet/pay'); + } + } + }, + onHistory: () {}, + ), + Expanded( + child: PaymentHistoryShort( + height: double.infinity, transactions: combinedWallet.recentTransactions, pendingTransactions: combinedWallet.pendingTransactions, showDividers: false, ), - ], - ), + ) + ], ), ), ); diff --git a/lib/presentation_layer/routes/wallet/wallet_mints.dart b/lib/presentation_layer/routes/wallet/wallet_mints.dart deleted file mode 100644 index cb9c1f67..00000000 --- a/lib/presentation_layer/routes/wallet/wallet_mints.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class WalletMints extends ConsumerWidget { - const WalletMints({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: null, - body: ListView(children: [Text("WalletMints")]), - ); - } -} diff --git a/lib/presentation_layer/routes/wallet/wallet_navigation.dart b/lib/presentation_layer/routes/wallet/wallet_navigation.dart index baf78c57..51f05744 100644 --- a/lib/presentation_layer/routes/wallet/wallet_navigation.dart +++ b/lib/presentation_layer/routes/wallet/wallet_navigation.dart @@ -5,9 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../components/wallet/sheet_send_receive.dart'; import 'wallet_dashboard.dart'; -import 'wallet_mints.dart'; + import 'wallet_qr_scan.dart'; -import 'wallet_receive.dart'; // Navigation state class class WalletNavigationState { @@ -281,8 +280,6 @@ class _WalletNavigationState extends ConsumerState WalletDashboard(), ], ), - WalletReceive(), - WalletMints(), ], ); } diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart index 81fc2356..d778743d 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart @@ -6,6 +6,7 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../../../config/palette.dart'; import '../../../../../helpers/wallet_number_formatting.dart'; +import '../../../../atoms/copy_to_clipboard.dart'; import '../../../../atoms/long_button.dart'; import '../../../../components/wallet/animated_qr.dart'; import '../../wallet_providers/wallet_combined_state_provider.dart'; @@ -203,8 +204,9 @@ class SuccessStep extends StatelessWidget { /// copy button SizedBox( width: 250, - child: CopyTokenButton( - token: outputToken!.toV4TokenString(), + child: CopyClipboardButton( + value: outputToken!.toV4TokenString(), + copyText: "Copy Token", ), ), ], @@ -212,55 +214,6 @@ class SuccessStep extends StatelessWidget { } } -class CopyTokenButton extends StatefulWidget { - final String token; - - const CopyTokenButton({super.key, required this.token}); - - @override - State createState() => _CopyTokenButtonState(); -} - -class _CopyTokenButtonState extends State { - bool _copied = false; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _copied ? null : _copyToClipboard, - icon: Icon(_copied ? Icons.check : Icons.copy), - label: Text(_copied ? 'Copied to Clipboard!' : 'Copy Token'), - style: ElevatedButton.styleFrom( - backgroundColor: - _copied ? Palette.white : Palette.primary.withValues(alpha: 0.9), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ); - } - - Future _copyToClipboard() async { - await Clipboard.setData(ClipboardData(text: widget.token)); - setState(() { - _copied = true; - }); - - Future.delayed(const Duration(seconds: 3), () { - if (mounted) { - setState(() { - _copied = false; - }); - } - }); - } -} - class TransactionState extends ConsumerWidget { const TransactionState({super.key}); diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart index f521dc61..f6f58d39 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart @@ -75,19 +75,30 @@ class _WalletPaySelectAmountState extends ConsumerState { super.dispose(); } - _onSwitchCurrency() { + _onSwitchCurrency({ + required String previousUnit, + required String currentUnit, + }) { final state = ref.read(walletPayStateProvider); final stateNotifier = ref.read(walletPayStateProvider.notifier); - if (state.unit == 'sat') { - // remove last two digits from state.amount - final newAmount = - state.amount != null ? (state.amount! / 100).toStringAsFixed(0) : '0'; - _amountController.text = newAmount; - stateNotifier.updateAmount((state.amount! / 100).toInt()); + if (previousUnit == "sat" && currentUnit != "sat") { + /// sat to fiat + final amount = state.amount; + if (amount != null) { + final convertedAmount = (amount).toStringAsFixed(2); + _amountController.text = convertedAmount; + stateNotifier.updateAmount((amount * 100).toInt()); + } + } else if (previousUnit != "sat" && currentUnit == "sat") { + /// fiat to sat + final amount = state.amount; + if (amount != null) { + final convertedAmount = (amount / 100).round(); + _amountController.text = convertedAmount.toString(); + stateNotifier.updateAmount(convertedAmount); + } } else { - // Convert amount to fiat format - final amountInFiat = (state.amount ?? 0) / 100; - _amountController.text = amountInFiat.toStringAsFixed(2); + /// fiat to fiat } } @@ -211,11 +222,14 @@ class _WalletPaySelectAmountState extends ConsumerState { .toList() .indexOf(state.unit!) : 0, - onChanged: (i) => { + onChanged: (r) => { payNotifier.updateUnit( - state.supportedUnitsByWallet!.elementAt(i), + r.currentUnit, + ), + _onSwitchCurrency( + previousUnit: r.previousUnit, + currentUnit: r.currentUnit, ), - _onSwitchCurrency(), }, showHaptics: true, trackColor: Palette.extraDarkGray, diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart index 55307895..9a041a77 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -4,7 +4,6 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; import 'package:riverpod/riverpod.dart'; -import 'package:serverpod_flutter/serverpod_flutter.dart'; import '../../../providers/db_ndk_provider.dart'; import '../../../providers/ndk_provider.dart'; @@ -91,33 +90,4 @@ class WalletCombinedStateNotifier extends StateNotifier { }), ]); } - - void fundWallet() async { - /// todo acc management in wallet usecase - /// just testing - - final draftTransaction = await _ndk.cashu.initiateFund( - mintUrl: "http://$localhost:8086", - amount: 10, - unit: "sat", - method: "bolt11"); - - final transaction = await _ndk.cashu - .retriveFunds(draftTransaction: draftTransaction) - .toList(); - print(transaction); - } - - void spend() async { - final mintUrl = "http://$localhost:8086"; - final unit = "eur"; - - final spend = await _ndk.cashu.initiateSpend( - mintUrl: mintUrl, - amount: 1, - unit: unit, - ); - - print(spend.token.toV4TokenString()); - } } diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart index 1f375802..187cac59 100644 --- a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -83,7 +83,7 @@ class _QrScan extends ConsumerState { rcvProvider.receiveEcash(tokenString: ecashTokenString); Navigator.pushNamed( context, - '/wallet/receive', + '/wallet/receive/ecash', ); ref.read(walletNavigationProvider.notifier).changeDashboardPage(1); break; diff --git a/lib/presentation_layer/routes/wallet/wallet_receive.dart b/lib/presentation_layer/routes/wallet/wallet_receive.dart deleted file mode 100644 index 7f72f77a..00000000 --- a/lib/presentation_layer/routes/wallet/wallet_receive.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class WalletReceive extends ConsumerWidget { - const WalletReceive({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: null, - body: ListView(children: [Text("WalletReceive")]), - ); - } -} diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart new file mode 100644 index 00000000..6b44ca85 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart @@ -0,0 +1,349 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../../../../../config/palette.dart'; +import '../../../../atoms/currency_picker_bar.dart'; +import '../../../../atoms/long_button.dart'; +import '../../../../atoms/wallet/wallet_card.dart'; +import '../../../../components/wallet/wallets_select_bottom_sheet.dart'; +import '../../wallet_providers/wallet_combined_state_provider.dart'; + +import '../wallet_receive_state_provider.dart'; + +class WalletReceiveAmount extends ConsumerStatefulWidget { + final Function doneCallback; + final Function backCallback; + + const WalletReceiveAmount({ + super.key, + required this.doneCallback, + required this.backCallback, + this.title, + }); + + final String? title; + + @override + ConsumerState createState() => + _WalletPaySelectAmountState(); +} + +class _WalletPaySelectAmountState extends ConsumerState { + late final TextEditingController _amountController; + late final TextEditingController _memoController; + final FocusNode _amountFocus = FocusNode(); + final FocusNode _memoFocus = FocusNode(); + + @override + void initState() { + super.initState(); + final state = ref.read(walletRecieverProvider); + _amountController = TextEditingController(text: state.amount?.toString()); + _memoController = TextEditingController(text: state.memo); + + _amountController.addListener(() { + final stateR = ref.read(walletRecieverProvider); + if (_amountController.text.isEmpty) { + return; + } + + final int? parsedAmount; + if (stateR.unit == 'sat') { + parsedAmount = int.tryParse(_amountController.text); + } else { + // Handle decimal input for fiat currencies + final sanitizedInput = _amountController.text.replaceAll(',', '.'); + parsedAmount = ((double.tryParse(sanitizedInput) ?? 0) * 100).toInt(); + } + + ref.read(walletRecieverProvider.notifier).updateAmount(parsedAmount ?? 0); + }); + _memoController.addListener(() { + ref + .read(walletRecieverProvider.notifier) + .updateMemo(_memoController.text); + }); + } + + @override + void dispose() { + _amountController.dispose(); + _memoController.dispose(); + _amountFocus.dispose(); + _memoFocus.dispose(); + super.dispose(); + } + + _onSwitchCurrency({ + required String previousUnit, + required String currentUnit, + }) { + final state = ref.read(walletRecieverProvider); + final stateNotifier = ref.read(walletRecieverProvider.notifier); + + if (previousUnit == "sat" && currentUnit != "sat") { + /// sat to fiat + final amount = state.amount; + if (amount != null) { + final convertedAmount = (amount).toStringAsFixed(2); + _amountController.text = convertedAmount; + stateNotifier.updateAmount((amount * 100).toInt()); + } + } else if (previousUnit != "sat" && currentUnit == "sat") { + /// fiat to sat + final amount = state.amount; + if (amount != null) { + final convertedAmount = (amount / 100).round(); + _amountController.text = convertedAmount.toString(); + stateNotifier.updateAmount(convertedAmount); + } + } else { + /// fiat to fiat + } + } + + showSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Palette.warn, + content: Text( + message, + style: TextStyle(color: Colors.white), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(walletRecieverProvider); + final notifier = ref.read(walletRecieverProvider.notifier); + + final combinedWallets = ref.watch(walletCombinedProvider); + + final List mySelectedWalletList = + combinedWallets.wallets + .where( + (w) => w.id == state.recieveToWalletId, + ) + .toList(); + + final ndk_entities.CashuWallet? mySelectedWallet = + mySelectedWalletList.isNotEmpty + ? mySelectedWalletList.first as ndk_entities.CashuWallet + : null; + + final List supportedUnitsBySelectedWallet = + mySelectedWallet?.supportedUnits.toList() ?? []; + + return Scaffold( + appBar: widget.title != null + ? AppBar( + backgroundColor: Palette.background, + title: Text(widget.title!), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + widget.backCallback(); + }, + ), + ) + : null, + backgroundColor: Palette.background, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), + child: Column( + children: [ + Container( + child: WalletCard( + wallet: combinedWallets.wallets.firstWhere( + (w) => w.id == state.recieveToWalletId, + orElse: () => ndk_entities.CashuWallet( + id: '', + name: 'Select Wallet', + type: ndk_entities.WalletType.CASHU, + supportedUnits: {}, + mintUrl: '', + mintInfo: ndk_entities.CashuMintInfo(nuts: {}), + ), + ), + balances: combinedWallets.balances + .where((b) => b.walletId == state.recieveToWalletId) + .toList(), + onTap: (_) {}, + tralling: IconButton( + icon: Icon( + PhosphorIcons.notePencil(), + size: 25, + ), + color: Palette.primary, + onPressed: () async { + final selectedId = await showWalletsSelectBottomSheet( + context: context, + selectedId: state.recieveToWalletId, + wallets: combinedWallets.wallets, + balances: combinedWallets.balances, + ); + if (selectedId != null) { + notifier.updateRecieveToWalletId(selectedId); + } + }, + ), + ), + ), + + Spacer(flex: 1), + TextField( + controller: _amountController, + focusNode: _amountFocus, + autofocus: true, + keyboardType: TextInputType.numberWithOptions( + decimal: state.unit != 'sat'), + inputFormatters: state.unit == 'sat' + ? [ + FilteringTextInputFormatter.digitsOnly, + ] + : [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9\.,]')), + DecimalTextInputFormatter(decimalRange: 2), + ], + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + ), + decoration: InputDecoration( + border: InputBorder.none, + hintText: state.unit != 'sat' ? '0.00' : '0', + ), + ), + + const SizedBox(height: 12), + Spacer(flex: 1), + + /// currency selector + Align( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: CurrencyPickerBar( + currencies: supportedUnitsBySelectedWallet, + initialIndex: state.unit != null + ? supportedUnitsBySelectedWallet.indexOf(state.unit!) + : 0, + onChanged: (r) => { + notifier.updateUnit( + r.currentUnit, + ), + _onSwitchCurrency( + previousUnit: r.previousUnit, + currentUnit: r.currentUnit, + ), + }, + showHaptics: true, + trackColor: Palette.extraDarkGray, + activeColor: Palette.primary, + ), + ), + ), + + Spacer(flex: 5), + + /// memo input + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: TextField( + controller: _memoController, + focusNode: _memoFocus, + keyboardType: TextInputType.text, + maxLines: 3, + minLines: 1, + decoration: const InputDecoration( + labelText: 'Memo', + hintText: 'Add a memo (optional)', + border: OutlineInputBorder(), + ), + ), + ), + Spacer(flex: 1), + ], + ), + ), + ), + bottomNavigationBar: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), + child: SizedBox( + width: double.infinity, + height: 40, + child: longButton( + name: "create request", + onPressed: () { + if (state.recieveToWalletId == null || + state.recieveToWalletId!.isEmpty) { + showSnackBar( + context, 'Please select a wallet to receive to.'); + return; + } + if (state.amount == null || state.amount! <= 0) { + _amountFocus.requestFocus(); + showSnackBar(context, 'Please enter a valid amount.'); + return; + } + notifier.mintEcashToken(); + widget.doneCallback(); + }, + inverted: true), + ), + ), + ), + ); + } +} + +class DecimalTextInputFormatter extends TextInputFormatter { + DecimalTextInputFormatter({required this.decimalRange}) + : assert(decimalRange > 0); + + final int decimalRange; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + String newText = newValue.text; + + newText = newText.replaceAll(',', '.'); + + if (newText.isEmpty) { + return newValue.copyWith(text: ''); + } + + if (newText == '.') { + return newValue.copyWith(text: '0.'); + } + + if (!RegExp(r'^\d*\.?\d*').hasMatch(newText)) { + return oldValue; + } + + if (newText.contains('.')) { + List parts = newText.split('.'); + if (parts.length > 2) { + return oldValue; + } + if (parts[1].length > decimalRange) { + // too many decimal places + return oldValue; + } + } + + return newValue.copyWith(text: newText); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_page.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_page.dart new file mode 100644 index 00000000..3bd5c02f --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_page.dart @@ -0,0 +1,74 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'wallet_receive_amout/wallet_receive_amount.dart'; +import 'wallet_receive_request/wallet_receive_request.dart'; +import 'wallet_receive_type/wallet_receive_type.dart'; + +class WalletReceivePage extends ConsumerStatefulWidget { + const WalletReceivePage({ + super.key, + }); + + @override + ConsumerState createState() => _WalletReceivePageState(); +} + +class _WalletReceivePageState extends ConsumerState + with TickerProviderStateMixin { + final PageController _horizontalPageController = PageController( + initialPage: 0, + keepPage: true, + ); + + bool horizontalScrollLock = false; + + _navigateToNextPage() { + if (_horizontalPageController.page != null) { + _horizontalPageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + _navigateToPreviousPage() { + if (_horizontalPageController.page != null) { + _horizontalPageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + @override + Widget build(BuildContext context) { + return PageView( + controller: _horizontalPageController, + physics: horizontalScrollLock + ? const NeverScrollableScrollPhysics() + : const AlwaysScrollableScrollPhysics(), + children: [ + WalletReceiveType( + doneCallback: () { + _navigateToNextPage(); + }, + ), + WalletReceiveAmount( + title: "Receive", + backCallback: () { + _navigateToPreviousPage(); + }, + doneCallback: () { + _navigateToNextPage(); + }, + ), + WalletReceiveRequest( + backCallback: () { + _navigateToPreviousPage(); + }, + ), + ], + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart new file mode 100644 index 00000000..29f661dc --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart @@ -0,0 +1,363 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:pretty_qr_code/pretty_qr_code.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../../../config/palette.dart'; +import '../../../../../helpers/wallet_number_formatting.dart'; + +import '../../../../atoms/copy_to_clipboard.dart'; +import '../../../../atoms/long_button.dart'; +import '../wallet_receive_state_provider.dart'; + +class WalletReceiveRequest extends ConsumerWidget { + final Function backCallback; + const WalletReceiveRequest({ + super.key, + required this.backCallback, + }); + + showSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Palette.warn, + content: Text( + message, + style: TextStyle(color: Colors.white), + ), + ), + ); + } + + _launchBolt11Url(String url) async { + final Uri uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.platformDefault); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(walletRecieverProvider); + final rcvNotifier = ref.read(walletRecieverProvider.notifier); + + if (state.request != null && state.method == RecieveMethods.bolt11) { + _launchBolt11Url(state.request!); + } + + return Scaffold( + backgroundColor: Colors.black, + body: Column( + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + color: Palette.primary.withValues(alpha: 0.9), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + /// header back, close + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + Icons.arrow_back, + color: Colors.white, + size: 24, + ), + onPressed: () => backCallback(), + ), + Text( + 'receive', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + IconButton( + icon: Icon( + Icons.close, + color: Palette.lightGray, + size: 24, + ), + onPressed: () { + rcvNotifier.reset(); + Navigator.of(context).pop(); + }, + ), + ], + ), + SizedBox(height: 40), + + if (state.request != null && + state.method == RecieveMethods.bolt11) + Column( + children: [ + Stack( + children: [ + Container( + constraints: BoxConstraints( + maxHeight: 300, + ), + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Palette.white, + borderRadius: BorderRadius.circular(12), + ), + child: PrettyQrView.data( + data: state.request!, + decoration: const PrettyQrDecoration( + shape: PrettyQrSmoothSymbol( + color: Colors.black, + ), + background: Colors.white, + ), + ), + ), + if (state.isSuccess) + Container( + constraints: BoxConstraints( + maxHeight: 300, + maxWidth: 300, + ), + child: AspectRatio( + aspectRatio: 1.0, + child: Container( + decoration: BoxDecoration( + color: Palette.black + .withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + PhosphorIcons.checkCircle(), + color: Palette.success, + size: 64, + ), + SizedBox(height: 8), + Text( + 'Request has been payed', + style: TextStyle( + color: Palette.white, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ), + ], + ), + SizedBox(height: 16), + Container( + constraints: BoxConstraints( + maxWidth: 300, + ), + child: CopyClipboardButton( + value: state.request!, + copyText: "Copy invoice", + backgroundColor: Palette.extraDarkGray, + primaryColor: Palette.extraLightGray, + ), + ), + ], + ), + ], + ), + ), + ), + ), + + /// main + Expanded( + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + SizedBox(height: 16), + _buildDetailRow( + label: "Amount", + value: state.amount != null + ? WalletNumberFormatting.formatAmount( + amount: state.amount!, unit: state.unit!) + : "Not set", + ), + _buildDetailRow( + label: "Unit", + value: state.unit ?? "Not set", + ), + _buildDetailRow( + label: "Memo", + value: state.memo ?? "", + ), + Spacer(), + ], + ), + ), + ), + ], + ), + bottomNavigationBar: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: longButton( + name: "close", + onPressed: () => { + rcvNotifier.reset(), + Navigator.of(context).pop(), + }, + inverted: true), + ), + ), + ); + } + + Widget _buildDetailRow({ + required String label, + required String value, + }) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: TextStyle(color: Colors.grey, fontSize: 12)), + Text(value, style: TextStyle(color: Colors.white, fontSize: 16)), + ], + ), + ], + ), + ); + } +} + +class ContactReciever extends StatelessWidget { + const ContactReciever({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + CircleAvatar( + backgroundColor: Palette.primary.withValues(alpha: 0.8), + child: + Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('receiver', + style: TextStyle(color: Colors.grey, fontSize: 12)), + Text('NOT IMPLEMENTED', + style: TextStyle(color: Colors.white, fontSize: 16)), + Text('send to pubkey not implemented', + style: TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ); + } +} + +class TokenReciever extends StatelessWidget { + const TokenReciever({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + CircleAvatar( + backgroundColor: Palette.primary.withValues(alpha: 0.8), + child: + Text('TK', style: TextStyle(color: Colors.white, fontSize: 12)), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('receiver', + style: TextStyle(color: Palette.gray, fontSize: 12)), + Text('Token', + style: TextStyle( + color: Palette.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + Text('send as a token or qr code', + style: TextStyle(color: Palette.lightGray, fontSize: 14)), + ], + ), + ), + ], + ), + ); + } +} + +class WalletReciever extends StatelessWidget { + const WalletReciever({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + CircleAvatar( + backgroundColor: Palette.primary.withValues(alpha: 0.8), + child: + Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('receiver', + style: TextStyle(color: Colors.grey, fontSize: 12)), + Text('NOT IMPLEMENTED', + style: TextStyle(color: Colors.white, fontSize: 16)), + Text('send to wallet not implemented', + style: TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart new file mode 100644 index 00000000..6a0ee9fc --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart @@ -0,0 +1,179 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:ndk/ndk.dart'; +import 'package:bolt11_decoder/bolt11_decoder.dart'; + +import '../../../providers/ndk_provider.dart'; + +enum RecieveMethods { + ecashToken('ecashToken'), + bolt11('bolt11'), + wallet('wallet'); + + final String value; + + const RecieveMethods(this.value); + + factory RecieveMethods.fromValue(String value) { + return RecieveMethods.values.firstWhere( + (kind) => kind.value == value, + orElse: () => throw ArgumentError('Invalid event kind value: $value'), + ); + } + + @override + String toString() => value; +} + +class WalletPayRecieverState { + final String? recieveToWalletId; + final int? amount; + final String? unit; + final String? memo; + final RecieveMethods? method; + + /// the mint request (in case of bolt11 the invoice) + final String? request; + + final Bolt11PaymentRequest? decodedBolt11Request; + + final String? requestErr; + final bool isPending; + final bool isSuccess; + + WalletPayRecieverState({ + required this.recieveToWalletId, + this.amount, + this.unit, + this.memo, + this.method, + this.request, + this.requestErr, + this.isPending = false, + this.isSuccess = false, + this.decodedBolt11Request, + }); + + WalletPayRecieverState copyWith({ + String? recieveToWalletId, + int? amount, + String? unit, + String? memo, + RecieveMethods? method, + String? request, + Bolt11PaymentRequest? decodedBolt11Request, + String? requestErr, + bool? isPending, + bool? isSuccess, + }) { + return WalletPayRecieverState( + recieveToWalletId: recieveToWalletId ?? this.recieveToWalletId, + amount: amount ?? this.amount, + unit: unit ?? this.unit, + memo: memo ?? this.memo, + method: method ?? this.method, + request: request ?? this.request, + decodedBolt11Request: decodedBolt11Request ?? this.decodedBolt11Request, + requestErr: requestErr ?? this.requestErr, + isPending: isPending ?? this.isPending, + isSuccess: isSuccess ?? this.isSuccess, + ); + } +} + +class WalletPayToNotifier extends StateNotifier { + final Ndk _ndk; + + WalletPayToNotifier({ + String? initialWalletId, + required Ndk ndk, + }) : _ndk = ndk, + super( + WalletPayRecieverState( + recieveToWalletId: initialWalletId, + ), + ); + + void updateRecieveToWalletId(String? walletId) { + state = state.copyWith(recieveToWalletId: walletId); + } + + void updateMethod(RecieveMethods? method) { + state = state.copyWith(method: method); + } + + void updateAmount(int? amount) { + state = state.copyWith(amount: amount); + } + + void updateUnit(String? unit) { + state = state.copyWith(unit: unit); + } + + void updateMemo(String? memo) { + state = state.copyWith(memo: memo); + } + + void reset() { + state = WalletPayRecieverState( + recieveToWalletId: null, + amount: null, + unit: null, + memo: null, + method: null, + ); + } + + Future mintEcashToken() async { + if (state.recieveToWalletId == null || + state.amount == null || + state.unit == null || + state.method != RecieveMethods.bolt11) { + throw ArgumentError('Invalid state for minting ecash token'); + } + + final initTransaction = await _ndk.cashu.initiateFund( + mintUrl: state.recieveToWalletId!, + amount: state.amount!, + unit: state.unit!, + method: state.method!.toString(), + memo: state.memo, + ); + + state = state.copyWith( + request: initTransaction.qoute!.request, + decodedBolt11Request: + Bolt11PaymentRequest(initTransaction.qoute!.request), + isPending: true, + ); + + print(state); + + final resultStream = + _ndk.cashu.retriveFunds(draftTransaction: initTransaction); + + await for (final result in resultStream) { + if (result.state == ndk_entities.WalletTransactionState.completed) { + state = state.copyWith( + isPending: false, + isSuccess: true, + ); + } else if (result.state == ndk_entities.WalletTransactionState.failed) { + state = state.copyWith( + requestErr: result.completionMsg, + isPending: false, + isSuccess: false, + ); + } + } + } +} + +final walletRecieverProvider = + StateNotifierProvider((ref) { + final ndk = ref.watch(ndkProvider); + + return WalletPayToNotifier( + ndk: ndk, + ); +}); diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart new file mode 100644 index 00000000..ffceef72 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart' as ndk_entities; + +import '../../../../../config/palette.dart'; + +import '../../../../../helpers/wallet_number_formatting.dart'; +import '../../../../atoms/long_button.dart'; + +import '../../wallet_providers/wallet_combined_state_provider.dart'; +import '../rcv_completers/wallet_rcv_ecash_completer_state_provider.dart'; +import '../wallet_receive_state_provider.dart'; + +class WalletReceiveType extends ConsumerWidget { + final Function doneCallback; + + const WalletReceiveType({ + super.key, + required this.doneCallback, + }); + + _onPasteToken(BuildContext context, WidgetRef ref) async { + final userClipboard = await _handleReadClipboard(); + if (userClipboard == null || userClipboard.isEmpty) { + _showError(context, 'Clipboard is empty or invalid'); + return; + } + + if (!userClipboard.startsWith("cashuB")) { + _showError(context, 'Invalid ecash token format'); + return; + } + + final ecashCompleter = + ref.read(walletReceiveEcashCompleterProvider.notifier); + + ecashCompleter.receiveEcash(tokenString: userClipboard); + if (!context.mounted) return; + Navigator.pushReplacementNamed( + context, + '/wallet/receive/ecash', + ); + } + + Future _handleReadClipboard() async { + try { + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + return data?.text; + } catch (e) { + return null; + } + } + + _showError(BuildContext context, String message) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message, style: TextStyle(color: Palette.white)), + backgroundColor: Palette.error, + ), + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final combinedWallets = ref.watch(walletCombinedProvider); + final state = ref.watch(walletRecieverProvider); + final stateNotifier = ref.watch(walletRecieverProvider.notifier); + + return Scaffold( + backgroundColor: Palette.background, + appBar: AppBar( + backgroundColor: Palette.background, + title: const Text('Receive'), + leading: Container(), + leadingWidth: 0, + actions: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + stateNotifier.reset(); + Navigator.pop(context); + }, + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + // scrollable results + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), + children: [ + _Island( + title: "Lightning", + child: longButton( + name: "create invoice", + onPressed: () { + stateNotifier.updateMethod(RecieveMethods.bolt11); + doneCallback(); + }, + ), + ), + _Island( + title: "Ecash", + child: longButton( + name: "paste token", + onPressed: () => _onPasteToken(context, ref), + )), + _Island( + title: 'All wallets', + child: _WalletsList( + wallets: combinedWallets.wallets + .where( + (w) => w.id != state.recieveToWalletId, + ) + .toList(), + balances: combinedWallets.balances, + selectedWalletId: state.recieveToWalletId, + onTap: (wallet) { + stateNotifier.updateRecieveToWalletId(wallet.id); + stateNotifier.updateMethod(RecieveMethods.wallet); + doneCallback(); + }, + ), + ), + ], + ), + ), + ], + ), + ), + + /// next button + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), + child: SizedBox( + width: double.infinity, + height: 40, + child: longButton( + name: "next", + onPressed: () { + if (state.memo == null) { + stateNotifier.updateMethod(RecieveMethods.bolt11); + } + + doneCallback(); + }, + inverted: true, + ), + ), + ), + ), + ); + } +} + +class _Island extends StatelessWidget { + final String title; + final Widget child; + const _Island({required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + child, + ], + ), + ), + ); + } +} + +class _WalletsList extends StatelessWidget { + final List wallets; + final List balances; + final String? selectedWalletId; + final void Function(ndk_entities.Wallet) onTap; + + const _WalletsList({ + required this.wallets, + required this.balances, + required this.selectedWalletId, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + if (wallets.isEmpty) { + return const Padding( + padding: EdgeInsets.all(12.0), + child: Text('No wallets found'), + ); + } + return ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: wallets.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, i) { + final w = wallets[i]; + final wBallances = balances.where((b) => b.walletId == w.id); + final selected = w.id == selectedWalletId; + + /// dont show selected wallet + if (selected) { + return Container(); + } + + return ListTile( + title: Text(w.name), + subtitle: Text(w.type.toString()), + + /// show all balances for the wallet + trailing: Column(children: [ + for (final b in wBallances) + Text( + "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", + style: TextStyle( + color: Palette.white, + ), + ), + ]), + + onTap: () => onTap(w), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7da3b649..915ac516 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -180,6 +180,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.10" + bolt11_decoder: + dependency: "direct main" + description: + name: bolt11_decoder + sha256: ce3c4e4311e84c7b6ceb06ffbf84329bf6bdac1244ecfa1fc157c46038021bbc + url: "https://pub.dev" + source: hosted + version: "1.0.2" boolean_selector: dependency: transitive description: @@ -500,6 +508,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" device_info_plus: dependency: transitive description: @@ -1512,6 +1528,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" riverpod: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5f86a347..f810f4e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -103,6 +103,7 @@ dependencies: pretty_qr_code: ^3.5.0 bc_ur_dart: ^0.1.19 mobile_scanner: ^7.0.1 + bolt11_decoder: ^1.0.2 From 4bec82b36f59d110a7e285f6f79a7500f09c7a43 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:03:00 +0200 Subject: [PATCH 34/52] fix: show err on funding --- .../wallet_pay/wallet_pay_state_provider.dart | 3 +- .../wallet_receive_request.dart | 8 ++ .../wallet_receive_state_provider.dart | 76 +++++++++++-------- 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart index 6364b739..edc641dc 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart @@ -164,7 +164,8 @@ class WalletPayNotifier extends StateNotifier { ); state = state.copyWith( supportedUnitsByWallet: state.payFromWallet?.supportedUnits, - unit: "sat", // state.payFromWallet?.supportedUnits.first, + unit: + state.unit == null ? state.payFromWallet?.supportedUnits.first : null, ); } diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart index 29f661dc..8a79730e 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart @@ -190,6 +190,14 @@ class WalletReceiveRequest extends ConsumerWidget { padding: EdgeInsets.all(16), child: Column( children: [ + if (state.requestErr != null) + Text( + state.requestErr!, + style: TextStyle( + color: Palette.error, + fontSize: 14, + ), + ), SizedBox(height: 16), _buildDetailRow( label: "Amount", diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart index 6a0ee9fc..92149238 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart @@ -95,7 +95,10 @@ class WalletPayToNotifier extends StateNotifier { ); void updateRecieveToWalletId(String? walletId) { - state = state.copyWith(recieveToWalletId: walletId); + state = state.copyWith( + recieveToWalletId: walletId, + unit: state.unit == null ? "sat" : null, + ); } void updateMethod(RecieveMethods? method) { @@ -132,39 +135,46 @@ class WalletPayToNotifier extends StateNotifier { throw ArgumentError('Invalid state for minting ecash token'); } - final initTransaction = await _ndk.cashu.initiateFund( - mintUrl: state.recieveToWalletId!, - amount: state.amount!, - unit: state.unit!, - method: state.method!.toString(), - memo: state.memo, - ); - - state = state.copyWith( - request: initTransaction.qoute!.request, - decodedBolt11Request: - Bolt11PaymentRequest(initTransaction.qoute!.request), - isPending: true, - ); - - print(state); - - final resultStream = - _ndk.cashu.retriveFunds(draftTransaction: initTransaction); - - await for (final result in resultStream) { - if (result.state == ndk_entities.WalletTransactionState.completed) { - state = state.copyWith( - isPending: false, - isSuccess: true, - ); - } else if (result.state == ndk_entities.WalletTransactionState.failed) { - state = state.copyWith( - requestErr: result.completionMsg, - isPending: false, - isSuccess: false, - ); + try { + final initTransaction = await _ndk.cashu.initiateFund( + mintUrl: state.recieveToWalletId!, + amount: state.amount!, + unit: state.unit!, + method: state.method!.toString(), + memo: state.memo, + ); + + state = state.copyWith( + request: initTransaction.qoute!.request, + decodedBolt11Request: + Bolt11PaymentRequest(initTransaction.qoute!.request), + isPending: true, + ); + + final resultStream = + _ndk.cashu.retriveFunds(draftTransaction: initTransaction); + + await for (final result in resultStream) { + if (result.state == ndk_entities.WalletTransactionState.completed) { + state = state.copyWith( + isPending: false, + isSuccess: true, + ); + } else if (result.state == ndk_entities.WalletTransactionState.failed) { + state = state.copyWith( + requestErr: result.completionMsg, + isPending: false, + isSuccess: false, + ); + } } + } catch (e) { + state = state.copyWith( + requestErr: e.toString(), + isPending: false, + isSuccess: false, + ); + return; } } } From 40bfe601e66d1e61b67560ee52dd42a3e2b58465 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sun, 24 Aug 2025 15:59:37 +0200 Subject: [PATCH 35/52] transaction list --- lib/config/palette.dart | 2 +- lib/helpers/nip04_encryption.dart | 162 -------------- lib/main.dart | 16 +- .../atoms/wallet/wallet_transaction_card.dart | 203 ++++++++++++++++++ .../wallet/payment_history_short.dart | 167 ++------------ .../routes/wallet/wallet_dashboard.dart | 21 +- .../routes/wallet/wallet_navigation.dart | 2 + ...et_rcv_ecash_completer_state_provider.dart | 4 +- .../wallet_transaction_detail_page.dart | 78 +++++++ .../wallet_transaction_list_page.dart | 180 ++++++++++++++++ ...allet_transaction_list_state_provider.dart | 82 +++++++ pubspec.lock | 159 +++++--------- pubspec.yaml | 17 +- .../helpers/nip04_encryption_test.dart | 62 ------ 14 files changed, 656 insertions(+), 499 deletions(-) delete mode 100644 lib/helpers/nip04_encryption.dart create mode 100644 lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart delete mode 100644 test/unit_test/helpers/nip04_encryption_test.dart diff --git a/lib/config/palette.dart b/lib/config/palette.dart index 039318c5..26a3d8cb 100644 --- a/lib/config/palette.dart +++ b/lib/config/palette.dart @@ -15,7 +15,7 @@ class Palette { static const Color black = Color(0xFF000000); static const Color warn = Color.fromARGB(255, 252, 127, 3); static const Color error = Color.fromARGB(255, 254, 29, 29); - static const Color success = Color.fromARGB(255, 22, 163, 74); + static const Color success = Color.fromARGB(255, 22, 180, 80); static const Color likeActive = Color.fromARGB(255, 230, 40, 85); static const Color repostActive = Color.fromARGB(255, 22, 163, 74); } diff --git a/lib/helpers/nip04_encryption.dart b/lib/helpers/nip04_encryption.dart deleted file mode 100644 index f01c6da3..00000000 --- a/lib/helpers/nip04_encryption.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; -import 'dart:typed_data'; -import 'package:pointycastle/export.dart'; -import 'dart:convert' as convert; -import 'package:kepler/kepler.dart'; - -/// from https://github.com/aniketambore/nostr_tools credits to him -class Nip04Encryption { - /// Decrypts a cipher text message encrypted using AES-256-CBC encryption with a - /// randomly generated initialization vector (IV). - /// - /// Parameters: - /// - privKey: The private key to use for decryption. - /// - pubKey: The public key to use for decryption. - /// - cipherText: The cipher text message to decrypt, in the format - /// "{cipherText}?iv={iv}", where {cipherText} is the Base64-encoded encrypted - /// message and {iv} is the Base64-encoded IV used for encryption. - /// - /// Returns: - /// - The decrypted plain text message. - String decrypt(String privKey, String pubKey, String cipherText) { - // Split the cipher text into the encrypted message and the IV. - final parts = cipherText.split("?iv="); - if (parts.length != 2) { - throw ArgumentError("[!] Invalid cipher text format"); - } - - // Decode the encrypted message and the IV from Base64. - final encodedText = base64.decode(parts[0]); - final iv = base64.decode(parts[1]); - - // Generate the shared secret and use the first 32 bytes as the encryption key. - final secretIV = Kepler.byteSecret(privKey, '02$pubKey'); - final key = Uint8List.fromList(secretIV[0]); - - // Define the decryption parameters using the key and IV. - final params = PaddedBlockCipherParameters( - ParametersWithIV(KeyParameter(key), iv), - null, - ); - - // Initialize the AES-256-CBC cipher with PKCS7 padding. - final cipherImpl = - PaddedBlockCipherImpl(PKCS7Padding(), CBCBlockCipher(AESEngine())); - - // Initialize the cipher with the decryption parameters. - cipherImpl.init(false, params); - - // Allocate space for the decrypted output buffer. - final outputDecodedText = Uint8List.view( - Uint8List(encodedText.length).buffer, - 0, - encodedText.length, - ); - - // Decrypt the encrypted message in blocks and write the decrypted bytes to - // the output buffer. - var offset = 0; - while (offset < encodedText.length) { - offset += cipherImpl.processBlock( - encodedText, - offset, - outputDecodedText, - offset, - ); - } - - // Determine the amount of padding added to the decrypted message. - final padCount = outputDecodedText[outputDecodedText.length - 1]; - - // Strip the padding from the decrypted bytes and convert them to a string. - final unpaddedDecodedText = utf8.decode( - outputDecodedText.sublist(0, outputDecodedText.length - padCount), - ); - - // Return the decrypted plain text message. - return unpaddedDecodedText; - } - - /// Encrypts a plain text message using AES-256-CBC encryption with a randomly - /// generated initialization vector (IV). - /// - /// Parameters: - /// - privKey: The private key to use for encryption. (hex) - /// - pubKey: The public key to use for encryption. (hex) - /// - text: The plain text message to encrypt. - /// - /// Returns: - /// - The encrypted message in the format "{cipherText}?iv={iv}", where - /// {cipherText} is the Base64-encoded encrypted message and {iv} is the - /// Base64-encoded IV used for encryption. - - String encrypt(String privKey, String pubKey, String text) { - Uint8List uintInputText = const convert.Utf8Encoder().convert(text); - - // Generate the shared secret and use the first 32 bytes as the encryption key. - final secretIV = Kepler.byteSecret(privKey, '02$pubKey'); - final key = Uint8List.fromList(secretIV[0]); - - // generate iv https://stackoverflow.com/questions/63630661/aes-engine-not-initialised-with-pointycastle-securerandom - // Generate a random 16-byte initialization vector (IV) using the Fortuna - // random number generator. - FortunaRandom fr = FortunaRandom(); - final sGen = Random.secure(); - fr.seed(KeyParameter( - Uint8List.fromList(List.generate(32, (_) => sGen.nextInt(255))))); - final iv = fr.nextBytes(16); - - // Define the encryption parameters using the key and IV. - CipherParameters params = PaddedBlockCipherParameters( - ParametersWithIV(KeyParameter(key), iv), null); - - // Initialize the AES-256-CBC cipher with PKCS7 padding. - PaddedBlockCipherImpl cipherImpl = - PaddedBlockCipherImpl(PKCS7Padding(), CBCBlockCipher(AESEngine())); - - // Initialize the cipher with the encryption parameters. - cipherImpl.init( - true, // means to encrypt - params - as PaddedBlockCipherParameters, - ); - - // Allocate space for the encrypted output buffer. - final outputEncodedText = Uint8List.view( - Uint8List(uintInputText.length + 16).buffer, - 0, - uintInputText.length + 16, - ); - - // Encrypt the plain text message in blocks and write the encrypted bytes to - // the output buffer. - var offset = 0; - while (offset < uintInputText.length - 16) { - offset += cipherImpl.processBlock( - uintInputText, - offset, - outputEncodedText, - offset, - ); - } - - // Add padding and write the remaining encrypted bytes to the output buffer. - offset += - cipherImpl.doFinal(uintInputText, offset, outputEncodedText, offset); - - // Extract the encrypted bytes from the output buffer and create a new - // Uint8List containing only the encrypted bytes. - final Uint8List finalEncodedText = outputEncodedText.sublist(0, offset); - - // Encode the IV as a Base64 string. - String stringIv = convert.base64.encode(iv); - - // Encode the encrypted bytes as a Base64 string and append the IV to the end - final cipherText = - "${convert.base64.encode(finalEncodedText)}?iv=$stringIv"; - - // Return the encrypted message. - return cipherText; - } -} diff --git a/lib/main.dart b/lib/main.dart index a6a17709..2b8aaa0d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,11 +39,13 @@ import 'presentation_layer/routes/nostr/settings/moderation/moderation_settings. import 'presentation_layer/routes/nostr/settings/settings_page.dart'; import 'presentation_layer/routes/wallet/add_mint/add_mint_page.dart'; import 'presentation_layer/routes/wallet/mint_info/mint_info_page.dart'; +import 'presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart'; import 'presentation_layer/routes/wallet/wallet_navigation.dart'; import 'presentation_layer/routes/wallet/wallet_pay/wallet_pay_page.dart'; import 'presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart'; import 'presentation_layer/routes/wallet/wallet_receive/wallet_receive_page.dart'; import 'theme.dart' as theme; +import 'package:ndk/entities.dart' as ndk_entities; const devDeviceFrame = true; @@ -294,12 +296,6 @@ class MyApp extends ConsumerWidget { ), ); - case '/wallet/qr-scan': - return MaterialPageRoute( - builder: (context) => WalletNavigation( - title: "a", - ), - ); case '/wallet/pay': return MaterialPageRoute( builder: (context) => WalletPayPage(), @@ -315,6 +311,14 @@ class MyApp extends ConsumerWidget { mintUrl: settings.arguments as String?, ), ); + + case '/wallet/transactions/detail': + return MaterialPageRoute( + builder: (context) => WalletTransactionDetailPage( + transaction: + settings.arguments as ndk_entities.WalletTransaction, + ), + ); } assert(false, 'Need to implement ${settings.name}'); return null; diff --git a/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart new file mode 100644 index 00000000..bda21429 --- /dev/null +++ b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:timeago_flutter/timeago_flutter.dart' as timeago; + +import '../../../config/palette.dart'; +import '../../../helpers/wallet_number_formatting.dart'; + +DateTime _fromUnixSeconds(int seconds) => + DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true).toLocal(); + +int? _bestDate(ndk_entities.WalletTransaction tx) => + tx.transactionDate ?? tx.initiatedDate; + +class WalletTransactionCard extends StatelessWidget { + final ndk_entities.WalletTransaction tx; + final bool showDate; + const WalletTransactionCard({ + super.key, + required this.tx, + this.showDate = true, + }); + + @override + Widget build(BuildContext context) { + final amount = tx.changeAmount; + + final transactionStatus = _getTransactionStatus(tx); + + final dt = _fromUnixSeconds(_bestDate(tx) ?? 0); + final formattedDate = _formatDate(dt, includeDate: showDate); + + final ndk_entities.CashuWalletTransaction? cashuTx; + if (tx is ndk_entities.CashuWalletTransaction) { + cashuTx = tx as ndk_entities.CashuWalletTransaction; + } else { + cashuTx = null; + } + + return Card( + elevation: 0, + color: Palette.extraDarkGray.withValues(alpha: 0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Palette.darkGray.withValues(alpha: 0.25)), + ), + margin: const EdgeInsets.symmetric(vertical: 6), + child: ListTile( + leading: CircleAvatar( + backgroundColor: transactionStatus.color.withValues(alpha: 0.1), + child: Icon( + transactionStatus.icon, + color: transactionStatus.color, + size: 20, + ), + ), + title: Text( + transactionStatus.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Palette.white, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + "${cashuTx != null ? _removeHttpPrefix(cashuTx.mintUrl) : ""} • ${cashuTx != null ? _txType(cashuTx) : ''}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Palette.extraLightGray), + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${_formatAmount(amount, tx.unit)} ${tx.unit.toUpperCase()}", + style: TextStyle( + color: transactionStatus.color, + fontWeight: FontWeight.w700, + decoration: transactionStatus.isStrikethrough + ? TextDecoration.lineThrough + : null, + ), + ), + const SizedBox(height: 2), + Text( + formattedDate, + style: TextStyle( + color: Palette.gray, + fontSize: 12, + ), + ), + ], + ), + onTap: () { + Navigator.pushNamed( + context, + '/wallet/transactions/detail', + arguments: tx, + ); + }, + ), + ); + } + + String _formatDate(DateTime dt, + {bool includeDate = true, int minAgeForDate = 24}) { + if (DateTime.now().difference(dt).inHours < minAgeForDate) { + return timeago.format(dt); + } + + if (includeDate) { + return DateFormat('d.M.yy hh:mm').format(dt); + } else { + return DateFormat('hh:mm').format(dt); + } + } + + String _formatAmount(int amount, String unit) { + final prefix = amount >= 0 ? '+' : ''; + + final formattedAmount = WalletNumberFormatting.formatAmount( + amount: amount, + unit: unit, + ); + return '$prefix$formattedAmount'; + } + + String _removeHttpPrefix(String url) { + return url.replaceFirst(RegExp(r'^https?://'), ''); + } + + String _txType(ndk_entities.CashuWalletTransaction tx) { + if (tx.method == "bolt11") { + return "lightning"; + } + + if (tx.token != null && tx.token!.isNotEmpty) { + return "cashuToken"; + } + + return "unknownType"; + } +} + +class TransactionStatus { + final String label; + final IconData icon; + final Color color; + final bool isStrikethrough; + + const TransactionStatus({ + required this.label, + required this.icon, + required this.color, + required this.isStrikethrough, + }); +} + +TransactionStatus _getTransactionStatus(ndk_entities.WalletTransaction tx) { + final isDraft = tx.state == ndk_entities.WalletTransactionState.draft; + final isPending = tx.state == ndk_entities.WalletTransactionState.pending; + final isFailed = tx.state == ndk_entities.WalletTransactionState.failed; + final isCanceled = tx.state == ndk_entities.WalletTransactionState.canceled; + final isIncoming = tx.changeAmount >= 0; + + if (isPending || isDraft) { + return TransactionStatus( + label: 'Pending', + icon: PhosphorIcons.dotsThreeCircle(), + color: Colors.blue, + isStrikethrough: false, + ); + } else if (isFailed) { + return TransactionStatus( + label: 'Failed ${isIncoming ? 'Incoming' : 'Outgoing'}', + icon: PhosphorIcons.warningCircle(), + color: Palette.error, + isStrikethrough: true, + ); + } else if (isCanceled) { + return TransactionStatus( + label: 'Canceled ${isIncoming ? 'Incoming' : 'Outgoing'}', + icon: PhosphorIcons.xCircle(), + color: Palette.warn, + isStrikethrough: true, + ); + } else { + // Successful transaction + final color = isIncoming ? Palette.success : Palette.gray; + final icon = + isIncoming ? PhosphorIcons.arrowDown() : PhosphorIcons.arrowUp(); + + return TransactionStatus( + label: isIncoming ? 'Incoming' : 'Outgoing', + icon: icon, + color: color, + isStrikethrough: false, + ); + } +} diff --git a/lib/presentation_layer/components/wallet/payment_history_short.dart b/lib/presentation_layer/components/wallet/payment_history_short.dart index 806b8c88..2c59abd2 100644 --- a/lib/presentation_layer/components/wallet/payment_history_short.dart +++ b/lib/presentation_layer/components/wallet/payment_history_short.dart @@ -5,15 +5,16 @@ import 'package:timeago/timeago.dart' as timeago; import '../../../config/palette.dart'; import '../../../helpers/wallet_number_formatting.dart'; +import '../../atoms/wallet/wallet_transaction_card.dart'; class PaymentHistoryShort extends StatelessWidget { final List transactions; final List pendingTransactions; final int? maxItems; final void Function(ndk_entities.WalletTransaction tx)? onTap; - final bool showDividers; + final String emptyText; - final double? height; + final ScrollPhysics? physics; final ScrollController? controller; @@ -23,9 +24,7 @@ class PaymentHistoryShort extends StatelessWidget { required this.pendingTransactions, this.maxItems, this.onTap, - this.showDividers = true, this.emptyText = 'no transactions yet', - this.height = 300, this.physics, this.controller, }); @@ -34,7 +33,9 @@ class PaymentHistoryShort extends StatelessWidget { Widget build(BuildContext context) { if (transactions.isEmpty && pendingTransactions.isEmpty) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 24.0), + padding: const EdgeInsets.symmetric( + vertical: 24.0, + ), child: Center(child: Text(emptyText)), ); } @@ -63,92 +64,8 @@ class PaymentHistoryShort extends StatelessWidget { delegate: SliverChildBuilderDelegate( (context, index) { final tx = visible[index]; - final transactionStatus = _getTransactionStatus(tx); - - final dateToUse = _bestDate(tx); - final now = DateTime.now(); - final transactionDate = dateToUse != null - ? DateTime.fromMillisecondsSinceEpoch(dateToUse * 1000) - : null; - final difference = transactionDate != null - ? now.difference(transactionDate) - : Duration.zero; - - final String transactionDateText; - if (difference.inDays < 1) { - transactionDateText = transactionDate != null - ? timeago.format(transactionDate) - : ''; - } else { - transactionDateText = transactionDate != null - ? DateFormat('MMM d, yyyy').format(transactionDate) - : ''; - } - - final amountStr = WalletNumberFormatting.formatAmount( - amount: tx.changeAmount, unit: tx.unit); - - Widget listTile = ListTile( - onTap: onTap == null ? null : () => onTap!(tx), - leading: CircleAvatar( - backgroundColor: - transactionStatus.color.withValues(alpha: 0.1), - child: Icon( - transactionStatus.icon, - color: transactionStatus.color, - ), - ), - title: Text( - '${transactionStatus.label} - ${tx.walletType}', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - subtitle: Text( - [ - transactionDateText, - _removeHttpPrefix(tx.walletId), - ].where((e) => e.isNotEmpty).join(' • '), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - amountStr, - style: TextStyle( - fontWeight: FontWeight.w700, - color: transactionStatus.color, - decoration: transactionStatus.isStrikethrough - ? TextDecoration.lineThrough - : null, - ), - ), - if (tx.completionMsg != null && - tx.completionMsg!.isNotEmpty) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 150), - child: Text( - tx.completionMsg!, - style: Theme.of(context).textTheme.bodySmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - - if (showDividers && index < visible.length - 1) { - return Column( - children: [ - listTile, - const Divider(height: 1, color: Palette.darkGray), - ], - ); - } - - return listTile; + + return WalletTransactionCard(tx: tx); }, childCount: visible.length, ), @@ -156,74 +73,14 @@ class PaymentHistoryShort extends StatelessWidget { ], ); - return height != null - ? SizedBox(height: height, child: scrollView) - : scrollView; - } - - static TransactionStatus _getTransactionStatus( - ndk_entities.WalletTransaction tx) { - final isDraft = tx.state == ndk_entities.WalletTransactionState.draft; - final isPending = tx.state == ndk_entities.WalletTransactionState.pending; - final isFailed = tx.state == ndk_entities.WalletTransactionState.failed; - final isCanceled = tx.state == ndk_entities.WalletTransactionState.canceled; - final isIncoming = tx.changeAmount >= 0; - - if (isPending || isDraft) { - return TransactionStatus( - label: 'Pending', - icon: Icons.pending, - color: Colors.blue, - isStrikethrough: false, - ); - } else if (isFailed) { - return TransactionStatus( - label: 'Failed ${isIncoming ? 'Incoming' : 'Outgoing'}', - icon: Icons.error_outline, - color: Colors.red, - isStrikethrough: true, - ); - } else if (isCanceled) { - return TransactionStatus( - label: 'Canceled ${isIncoming ? 'Incoming' : 'Outgoing'}', - icon: Icons.cancel_outlined, - color: Colors.orange, - isStrikethrough: true, - ); - } else { - // Successful transaction - final color = isIncoming ? Colors.green : Colors.red; - final icon = isIncoming - ? Icons.arrow_downward_rounded - : Icons.arrow_upward_rounded; - - return TransactionStatus( - label: isIncoming ? 'Incoming' : 'Outgoing', - icon: icon, - color: color, - isStrikethrough: false, - ); - } + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: scrollView, + ); } static int? _bestDate(ndk_entities.WalletTransaction tx) => tx.transactionDate ?? tx.initiatedDate; - - static String _enumLabel(Object? value) { - if (value == null) return ''; - final s = value.toString(); - final dot = s.indexOf('.'); - return dot >= 0 ? s.substring(dot + 1) : s; - } - - static String _removeHttpPrefix(String url) { - if (url.startsWith('http://')) { - return url.substring(7); - } else if (url.startsWith('https://')) { - return url.substring(8); - } - return url; - } } class TransactionStatus { diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index a2782520..a548e8fa 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -12,6 +12,7 @@ import '../../components/wallet/wallets_select_bottom_sheet.dart'; import '../../providers/metadata_state_provider.dart'; import '../../providers/ndk_provider.dart'; import '../nostr/nostr_drawer.dart'; +import 'wallet_navigation.dart'; import 'wallet_pay/wallet_pay_state_provider.dart'; import 'wallet_providers/wallet_combined_state_provider.dart'; import 'wallet_receive/wallet_receive_state_provider.dart'; @@ -54,6 +55,7 @@ class _WalletDashboardState extends ConsumerState return Scaffold( appBar: AppBar( backgroundColor: Palette.background, + surfaceTintColor: Palette.background, leading: Builder( builder: (context) { final myMetadata = @@ -103,7 +105,9 @@ class _WalletDashboardState extends ConsumerState const SizedBox(height: 30), WalletActionsStrip( onScan: () { - Navigator.pushNamed(context, '/wallet/scan'); + final navigationNoti = + ref.read(walletNavigationProvider.notifier); + navigationNoti.changeDashboardPage(0); }, onReceive: () async { final selectedId = await showWalletsSelectBottomSheet( @@ -137,14 +141,23 @@ class _WalletDashboardState extends ConsumerState } } }, - onHistory: () {}, + onHistory: () { + final navigationNoti = + ref.read(walletNavigationProvider.notifier); + navigationNoti.changeMainPage(1); + }, ), Expanded( child: PaymentHistoryShort( - height: double.infinity, transactions: combinedWallet.recentTransactions, pendingTransactions: combinedWallet.pendingTransactions, - showDividers: false, + onTap: (tx) { + Navigator.pushNamed( + context, + '/wallet/transactions/detail', + arguments: tx, + ); + }, ), ) ], diff --git a/lib/presentation_layer/routes/wallet/wallet_navigation.dart b/lib/presentation_layer/routes/wallet/wallet_navigation.dart index 51f05744..3e721ccf 100644 --- a/lib/presentation_layer/routes/wallet/wallet_navigation.dart +++ b/lib/presentation_layer/routes/wallet/wallet_navigation.dart @@ -7,6 +7,7 @@ import '../../components/wallet/sheet_send_receive.dart'; import 'wallet_dashboard.dart'; import 'wallet_qr_scan.dart'; +import 'wallet_transaction/wallet_transaction_list_page.dart'; // Navigation state class class WalletNavigationState { @@ -280,6 +281,7 @@ class _WalletNavigationState extends ConsumerState WalletDashboard(), ], ), + WalletTransactionListPage(), ], ); } diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart index 69267534..33c1a56f 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart @@ -108,7 +108,7 @@ class WalletRcvEcashCompleterNotifier isError: true, isCompleted: true, isPending: false, - errorMessage: 'Failed to receive eCash: ${rcvResult.completionMsg}', + errorMessage: '${rcvResult.completionMsg}', ); } } @@ -117,7 +117,7 @@ class WalletRcvEcashCompleterNotifier isPending: false, isError: true, isCompleted: true, - errorMessage: 'Failed to receive eCash: $e', + errorMessage: '$e', ); return; } diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart new file mode 100644 index 00000000..3c58aeba --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart' as ndk_entities; + +import '../../../../config/palette.dart'; + +class WalletTransactionDetailPage extends ConsumerWidget { + final ndk_entities.WalletTransaction transaction; + + const WalletTransactionDetailPage({ + super.key, + required this.transaction, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + backgroundColor: Palette.background, + appBar: AppBar( + backgroundColor: Palette.background, + title: Text('Transaction Detail'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Transaction ID: ${transaction.id}', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + SizedBox(height: 8), + Text( + 'Wallet ID: ${transaction.walletId}', + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 8), + Text( + 'Change Amount: ${transaction.changeAmount} ${transaction.unit}', + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 8), + Text( + 'Wallet Type: ${transaction.walletType}', + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 8), + Text( + 'State: ${transaction.state}', + style: TextStyle(fontSize: 16), + ), + if (transaction.completionMsg != null) ...[ + SizedBox(height: 8), + Text( + 'Completion Message: ${transaction.completionMsg}', + style: TextStyle(fontSize: 16), + ), + ], + if (transaction.transactionDate != null) ...[ + SizedBox(height: 8), + Text( + 'Transaction Date: ${transaction.transactionDate}', + style: TextStyle(fontSize: 16), + ), + ], + if (transaction.initiatedDate != null) ...[ + SizedBox(height: 8), + Text( + 'Initiated Date: ${transaction.initiatedDate}', + style: TextStyle(fontSize: 16), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart new file mode 100644 index 00000000..55a22008 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import 'package:ndk/entities.dart' as ndk_entities; + +import '../../../../config/palette.dart'; +import '../../../atoms/wallet/wallet_transaction_card.dart'; +import '../wallet_navigation.dart'; +import 'wallet_transaction_list_state_provider.dart'; + +class WalletTransactionListPage extends ConsumerWidget { + const WalletTransactionListPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(walletTransactionListProvider); + + final grouped = _groupByDay(state.transactions); + + return Scaffold( + backgroundColor: Palette.background, + appBar: AppBar( + elevation: 0, + backgroundColor: Palette.background, + foregroundColor: Palette.white, + title: const Text('Transactions'), + leading: IconButton( + icon: Icon(PhosphorIcons.caretLeft(), size: 24), + onPressed: () { + ref.read(walletNavigationProvider.notifier).changeMainPage(0); + }, + )), + body: state.transactions.isEmpty + ? const _EmptyTransactions() + : ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + itemCount: grouped.length, + itemBuilder: (context, index) { + final section = grouped[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _DateHeader(label: section.label), + const SizedBox(height: 8), + ...section.items.map( + (tx) => WalletTransactionCard( + tx: tx, + showDate: false, + ), + ), + const SizedBox(height: 16), + ], + ); + }, + ), + ); + } +} + +DateTime _fromUnixSeconds(int seconds) => + DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true).toLocal(); + +String _formatDayLabel(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final thatDay = DateTime(date.year, date.month, date.day); + + if (thatDay == today) return 'Today'; + if (thatDay == today.subtract(const Duration(days: 1))) return 'Yesterday'; + + return DateFormat('d.M.yyyy').format(date); +} + +class _DaySection { + final String label; + final List items; + _DaySection({required this.label, required this.items}); +} + +List<_DaySection> _groupByDay( + List transactions, +) { + final Map> buckets = {}; + final Map labelToDate = {}; + + for (final tx in transactions) { + final dt = _fromUnixSeconds(_bestDate(tx) ?? 0); + // grouping key + final key = DateFormat('yyyy-MM-dd').format(dt); + buckets.putIfAbsent(key, () => []).add(tx); + labelToDate[key] = DateTime(dt.year, dt.month, dt.day); + } + + final keys = buckets.keys.toList() + ..sort((a, b) => labelToDate[b]!.compareTo(labelToDate[a]!)); + + return keys + .map((k) => _DaySection( + label: _formatDayLabel(labelToDate[k]!), + items: buckets[k]!, + )) + .toList(); +} + +int? _bestDate(ndk_entities.WalletTransaction tx) => + tx.transactionDate ?? tx.initiatedDate; + +class _DateHeader extends StatelessWidget { + final String label; + const _DateHeader({required this.label}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Divider(color: Palette.darkGray.withValues(alpha: 0.4))), + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Palette.extraDarkGray, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Palette.darkGray.withValues(alpha: 0.4)), + ), + child: Text( + label, + style: TextStyle( + color: Palette.white, + fontSize: 12.5, + fontWeight: FontWeight.w600, + letterSpacing: 0.2, + ), + ), + ), + Expanded( + child: Divider(color: Palette.darkGray.withValues(alpha: 0.4))), + ], + ); + } +} + +class _EmptyTransactions extends StatelessWidget { + const _EmptyTransactions(); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.receipt_long_outlined, size: 48, color: Palette.gray), + const SizedBox(height: 12), + Text( + 'No transactions available', + style: TextStyle( + color: Palette.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 6), + Text( + 'Your recent activity will show up here.', + textAlign: TextAlign.center, + style: TextStyle( + color: Palette.lightGray, + fontSize: 13.5, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart new file mode 100644 index 00000000..5b5de36d --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart @@ -0,0 +1,82 @@ +import 'package:ndk/entities.dart' as ndk_entities; +import 'package:ndk/ndk.dart'; +import 'package:riverpod/riverpod.dart'; + +import '../../../providers/ndk_provider.dart'; +import '../wallet_providers/wallet_combined_state_provider.dart'; + +const limit = 20; + +class WalletTransactionListState { + final List transactions; + final List pendingTransactions; + final int offset; + + WalletTransactionListState({ + required this.transactions, + required this.pendingTransactions, + this.offset = 0, + }); + + WalletTransactionListState copyWith({ + List? transactions, + List? pendingTransactions, + int? offset, + }) { + return WalletTransactionListState( + transactions: transactions ?? this.transactions, + pendingTransactions: pendingTransactions ?? this.pendingTransactions, + offset: offset ?? this.offset, + ); + } +} + +class WalletTransactionListNotifier + extends StateNotifier { + final Ndk _ndk; + final Ref ref; + WalletTransactionListNotifier({required Ndk ndk, required this.ref}) + : _ndk = ndk, + super( + WalletTransactionListState( + transactions: [], + pendingTransactions: [], + ), + ) { + ref.listen(walletCombinedProvider, (previous, next) { + if (next.pendingTransactions != previous?.pendingTransactions || + next.recentTransactions != previous?.recentTransactions) { + state = state.copyWith( + pendingTransactions: next.pendingTransactions, + ); + } + }, fireImmediately: true); + _loadMore(); + } + + _loadMore() async { + final transactions = await _ndk.wallets.combinedTransactions( + limit: limit, + offset: state.offset, + ); + state = state.copyWith( + transactions: [...state.transactions, ...transactions], + offset: state.offset + transactions.length, + ); + } + + void reset() { + state = WalletTransactionListState( + transactions: [], + pendingTransactions: [], + ); + } +} + +final walletTransactionListProvider = StateNotifierProvider< + WalletTransactionListNotifier, WalletTransactionListState>( + (ref) { + final ndk = ref.watch(ndkProvider); + return WalletTransactionListNotifier(ndk: ndk, ref: ref); + }, +); diff --git a/pubspec.lock b/pubspec.lock index 915ac516..cd47033f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f url: "https://pub.dev" source: hosted - version: "76.0.0" + version: "85.0.0" _flutterfire_internals: dependency: transitive description: @@ -17,11 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.59" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" amberflutter: dependency: "direct main" description: @@ -34,18 +29,18 @@ packages: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.6.0" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.13.4" apipod_client: dependency: "direct main" description: @@ -109,14 +104,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - asn1lib: - dependency: transitive - description: - name: asn1lib - sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" - url: "https://pub.dev" - source: hosted - version: "1.6.5" async: dependency: transitive description: @@ -133,14 +120,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - base58check: - dependency: transitive - description: - name: base58check - sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5" - url: "https://pub.dev" - source: hosted - version: "2.0.0" bc_ur_dart: dependency: "direct main" description: @@ -159,19 +138,21 @@ packages: bip32: dependency: "direct main" description: - name: bip32 - sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" - url: "https://pub.dev" - source: hosted + path: "." + ref: "44f24d495c037f3042f6071be55b9a6c8f5a2492" + resolved-ref: "44f24d495c037f3042f6071be55b9a6c8f5a2492" + url: "https://github.com/ejayjeon/bip32-dart.git" + source: git version: "2.0.0" bip340: dependency: "direct main" description: - name: bip340 - sha256: b7bcd70a860e605046006adaa72bc4f7453f4d31d7ba74a4ad9d5de387a0fc0b - url: "https://pub.dev" - source: hosted - version: "0.3.0" + path: "." + ref: HEAD + resolved-ref: d1157e2aebd3debc8466a30ddf852cb9b69dfe30 + url: "https://github.com/1-leo/dart-bip340.git" + source: git + version: "0.3.1" bip39_mnemonic: dependency: "direct main" description: @@ -456,34 +437,34 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "3486c470bb93313a9417f926c7dd694a2e349220992d7b9d14534dc49c15bba9" + sha256: "9656925637516c5cf0f5da018b33df94025af2088fe09c8ae2ca54c53f2d9a84" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.6" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "42cdc41994eeeddab0d7a722c7093ec52bd0761921eeb2cbdbf33d192a234759" + sha256: "6cdc8e87e51baaaba9c43e283ed8d28e59a0c4732279df62f66f7b5984655414" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.6" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "02450c3e45e2a6e8b26c4d16687596ab3c4644dd5792e3313aa9ceba5a49b7f5" + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.5" custom_lint_visitor: dependency: transitive description: name: custom_lint_visitor - sha256: bfe9b7a09c4775a587b58d10ebb871d4fe618237639b1e84d5ec62d7dfef25f9 + sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2" url: "https://pub.dev" source: hosted - version: "1.0.0+6.11.0" + version: "1.0.0+7.7.0" custom_refresh_indicator: dependency: "direct main" description: @@ -496,10 +477,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.1.1" dbus: dependency: transitive description: @@ -540,14 +521,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.11" - encrypt: - dependency: "direct main" - description: - name: encrypt - sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" - url: "https://pub.dev" - source: hosted - version: "5.0.3" equatable: dependency: transitive description: @@ -908,10 +881,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.1.0" frontend_server_client: dependency: transitive description: @@ -1114,38 +1087,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" - kepler: - dependency: "direct main" - description: - name: kepler - sha256: "8cf9f7df525bd4e5b192d91e52f1c75832b1fefb27fb4f4a09b1412b0f4f23d0" - url: "https://pub.dev" - source: hosted - version: "1.0.3" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" linkify: dependency: transitive description: @@ -1186,14 +1151,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -1256,7 +1213,7 @@ packages: path: "../ndk/packages/ndk" relative: true source: path - version: "0.5.0" + version: "0.5.1" ndk_amber: dependency: "direct main" description: @@ -1300,26 +1257,26 @@ packages: dependency: "direct main" description: name: objectbox - sha256: "3d1cb5f9aa564f95c76ba251299f6cb1591c3dd8ff05fd76fa0549d899d9fe31" + sha256: "25c2e24b417d938decb5598682dc831bc6a21856eaae65affbc57cfad326808d" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.3.0" objectbox_flutter_libs: dependency: "direct main" description: name: objectbox_flutter_libs - sha256: "4f54ebbd7a3b72f1a5ef4fea76cb01cc36440a4cac1f63bfb6719afba400eedb" + sha256: "574b0233ba79a7159fca9049c67974f790a2180b6141d4951112b20bd146016a" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.3.0" objectbox_generator: dependency: "direct dev" description: name: objectbox_generator - sha256: "777924e14daf18603a023b346333acac1c4fce9fe4a715b85d98cfcc723d9a1a" + sha256: "1b17e9168d03706b5bb895b5f36f4301aa7c973ac30ff761b205b1ca3e2e3865" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.3.0" octo_image: dependency: transitive description: @@ -1460,10 +1417,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "4.0.0" pool: dependency: transitive description: @@ -1548,10 +1505,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: c6b8222b2b483cb87ae77ad147d6408f400c64f060df7a225b127f4afef4f8c8 + sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" url: "https://pub.dev" source: hosted - version: "0.5.8" + version: "0.5.10" riverpod_annotation: dependency: "direct main" description: @@ -1564,10 +1521,10 @@ packages: dependency: "direct dev" description: name: riverpod_lint - sha256: "83e4caa337a9840469b7b9bd8c2351ce85abad80f570d84146911b32086fbd99" + sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35" url: "https://pub.dev" source: hosted - version: "2.6.3" + version: "2.6.5" rust_lib_ndk: dependency: transitive description: @@ -1729,10 +1686,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_map_stack_trace: dependency: transitive description: @@ -1873,26 +1830,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.11" timeago: dependency: transitive description: @@ -2041,10 +1998,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" video_player: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f810f4e0..e513215e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,9 @@ dependencies: badges: ^3.1.2 uuid: ^4.4.2 cached_network_image: ^3.3.1 - bip340: ^0.3.0 + bip340: + git: + url: https://github.com/1-leo/dart-bip340.git bech32: ^0.2.1 hex: ^0.2.0 flutter_secure_storage: ^9.2.2 @@ -62,9 +64,7 @@ dependencies: web_socket_channel: ^3.0.1 http_parser: ^4.0.2 path_provider: ^2.1.4 - pointycastle: ^3.7.3 - encrypt: ^5.0.3 - kepler: ^1.0.3 + pointycastle: ^4.0.0 #device_preview: ^1.1.0 bip39_mnemonic: ^3.0.6 bip32: ^2.0.0 @@ -73,7 +73,7 @@ dependencies: rxdart: ^0.28.0 shimmer: ^3.0.0 flutter_force_directed_graph: ^1.0.7 - objectbox: ^4.0.3 + objectbox: ^4.3.0 objectbox_flutter_libs: any amberflutter: ^0.0.9 intl: ^0.19.0 @@ -135,10 +135,15 @@ dependency_overrides: # ref: nip-59-gift-wrap bc_ur_dart: path: ../fx-wallet-packages/packages/bc_ur_dart + bip32: + git: + url: https://github.com/ejayjeon/bip32-dart.git + ref: 44f24d495c037f3042f6071be55b9a6c8f5a2492 + dev_dependencies: - build_runner: ^2.4.13 + build_runner: ^2.5.4 mockito: ^5.4.0 flutter_test: sdk: flutter diff --git a/test/unit_test/helpers/nip04_encryption_test.dart b/test/unit_test/helpers/nip04_encryption_test.dart deleted file mode 100644 index 7d736f12..00000000 --- a/test/unit_test/helpers/nip04_encryption_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:convert'; - -import 'package:camelus/helpers/nip04_encryption.dart'; -import 'package:test/test.dart'; - -void main() { - group('nip04Encryption', () { - test('decrypt', () { - final nip04 = Nip04Encryption(); - const priv = - "fb505c65d4df950f5d28c9e4d285ee12ffaf315deef1fc24e3c7cd1e7e35f2b1"; - const pub = - "b1a5c93edcc8d586566fde53a20bdb50049a97b15483cb763854e57016e0fa3d"; - - const ciphertext = - "VezuSvWak++ASjFMRqBPWS3mK5pZ0vRLL325iuIL4S+r8n9z+DuMau5vMElz1tGC/UqCDmbzE2kwplafaFo/FnIZMdEj4pdxgptyBV1ifZpH3TEF6OMjEtqbYRRqnxgIXsuOSXaerWgpi0pm+raHQPseoELQI/SZ1cvtFqEUCXdXpa5AYaSd+quEuthAEw7V1jP+5TDRCEC8jiLosBVhCtaPpLcrm8HydMYJ2XB6Ixs=?iv=/rtV49RFm0XyFEwG62Eo9A=="; - - final desiredResult = [ - [ - "p", - "9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437" - ], - [ - "p", - "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168" - ] - ]; - - final result = nip04.decrypt(priv, pub, ciphertext); - final parsedResult = json.decode(result); - - expect(parsedResult, desiredResult); - }); - - test('encrypt only type', () { - const priv = - "fb505c65d4df950f5d28c9e4d285ee12ffaf315deef1fc24e3c7cd1e7e35f2b1"; - const pub = - "b1a5c93edcc8d586566fde53a20bdb50049a97b15483cb763854e57016e0fa3d"; - - const clearTextObj = [ - [ - "p", - "9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437" - ], - [ - "p", - "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168" - ] - ]; - - const desiredResult = - "VezuSvWak++ASjFMRqBPWS3mK5pZ0vRLL325iuIL4S+r8n9z+DuMau5vMElz1tGC/UqCDmbzE2kwplafaFo/FnIZMdEj4pdxgptyBV1ifZpH3TEF6OMjEtqbYRRqnxgIXsuOSXaerWgpi0pm+raHQPseoELQI/SZ1cvtFqEUCXdXpa5AYaSd+quEuthAEw7V1jP+5TDRCEC8jiLosBVhCtaPpLcrm8HydMYJ2XB6Ixs=?iv=/rtV49RFm0XyFEwG62Eo9A=="; - - final clearTextString = json.encode(clearTextObj); - final nip04 = Nip04Encryption(); - final result = nip04.encrypt(priv, pub, clearTextString); - - expect(result.runtimeType, desiredResult.runtimeType); - }); - }); -} From 6389d412cc44759729dd0c672989faafe5376785 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:58:08 +0200 Subject: [PATCH 36/52] update sdk 36 --- android/app/build.gradle | 6 +++--- .../atoms/wallet/wallet_transaction_card.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 57ae7a12..db03c771 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -39,7 +39,7 @@ if (flutterVersionName == null) { android { - compileSdk 35 + compileSdk 36 ndkVersion flutter.ndkVersion compileOptions { @@ -67,7 +67,7 @@ android { applicationId "de.lox.dev.camelus" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion 23 + minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -97,4 +97,4 @@ dependencies { //implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation platform('com.google.firebase:firebase-bom:33.14.0') coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' -} \ No newline at end of file +} diff --git a/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart index bda21429..2a7d4bb0 100644 --- a/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart +++ b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart @@ -40,7 +40,7 @@ class WalletTransactionCard extends StatelessWidget { return Card( elevation: 0, - color: Palette.extraDarkGray.withValues(alpha: 0.5), + color: Palette.extraDarkGray.withValues(alpha: 0.75), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: Palette.darkGray.withValues(alpha: 0.25)), From ba6231cb87458225d00662ad0d19519489264c61 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:20:18 +0200 Subject: [PATCH 37/52] upgrade to ndk 0.5.1 --- .../usecases/generate_private_key.dart | 2 +- .../wallet_receive_state_provider.dart | 2 +- pubspec.lock | 22 +++++++++++++------ pubspec.yaml | 6 ++--- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/domain_layer/usecases/generate_private_key.dart b/lib/domain_layer/usecases/generate_private_key.dart index 8cf71ef2..08f79d20 100644 --- a/lib/domain_layer/usecases/generate_private_key.dart +++ b/lib/domain_layer/usecases/generate_private_key.dart @@ -11,7 +11,7 @@ class GeneratePrivateKey { static GeneratedPrivateKey generateKey() { final mnemonic = Mnemonic.generate( Language.english, - entropyLength: 256, + length: MnemonicLength.words24, ); final sentence = mnemonic.sentence; diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart index 92149238..151df57a 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart @@ -152,7 +152,7 @@ class WalletPayToNotifier extends StateNotifier { ); final resultStream = - _ndk.cashu.retriveFunds(draftTransaction: initTransaction); + _ndk.cashu.retrieveFunds(draftTransaction: initTransaction); await for (final result in resultStream) { if (result.state == ndk_entities.WalletTransactionState.completed) { diff --git a/pubspec.lock b/pubspec.lock index cd47033f..fe8c9f05 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -144,23 +144,31 @@ packages: url: "https://github.com/ejayjeon/bip32-dart.git" source: git version: "2.0.0" - bip340: - dependency: "direct main" + bip32_keys: + dependency: transitive description: path: "." ref: HEAD - resolved-ref: d1157e2aebd3debc8466a30ddf852cb9b69dfe30 - url: "https://github.com/1-leo/dart-bip340.git" + resolved-ref: b5a0342220e7ee5aaf64d489a589bdee6ef8de22 + url: "https://github.com/1-leo/dart-bip32-keys" source: git + version: "3.1.2" + bip340: + dependency: "direct main" + description: + name: bip340 + sha256: "4c2df9fa2409d26f1d9334b2801015ebe4dc3978191f186743e60e89a90230c4" + url: "https://pub.dev" + source: hosted version: "0.3.1" bip39_mnemonic: dependency: "direct main" description: name: bip39_mnemonic - sha256: e280b785d40dda3f70186d5a51126c5d3ae2c3b9a1bafdbd20a94b50c8253fb3 + sha256: dd6bdfc2547d986b2c00f99bba209c69c0b6fa5c1a185e1f728998282f1249d5 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "4.0.1" bolt11_decoder: dependency: "direct main" description: @@ -1228,7 +1236,7 @@ packages: path: "../ndk/packages/objectbox" relative: true source: path - version: "0.2.5" + version: "0.2.6" ndk_rust_verifier: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e513215e..72edbc64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,9 +40,7 @@ dependencies: badges: ^3.1.2 uuid: ^4.4.2 cached_network_image: ^3.3.1 - bip340: - git: - url: https://github.com/1-leo/dart-bip340.git + bip340: ^0.3.1 bech32: ^0.2.1 hex: ^0.2.0 flutter_secure_storage: ^9.2.2 @@ -66,7 +64,7 @@ dependencies: path_provider: ^2.1.4 pointycastle: ^4.0.0 #device_preview: ^1.1.0 - bip39_mnemonic: ^3.0.6 + bip39_mnemonic: ^4.0.1 bip32: ^2.0.0 crop_your_image: ^2.0.0 package_info_plus: ^8.0.2 From 9675f278a69c6a4e501e838a449555410e03834f Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 30 Sep 2025 19:06:46 +0200 Subject: [PATCH 38/52] update local notifications --- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index fe8c9f05..ad46bd70 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -751,10 +751,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "20ca0a9c82ce0c855ac62a2e580ab867f3fbea82680a90647f7953832d0850ae" + sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5" url: "https://pub.dev" source: hosted - version: "19.4.0" + version: "19.4.2" flutter_local_notifications_linux: dependency: transitive description: @@ -775,10 +775,10 @@ packages: dependency: transitive description: name: flutter_local_notifications_windows - sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98 + sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" flutter_mentions: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 72edbc64..082b57c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,7 +90,7 @@ dependencies: path: ./apipod/apipod_client firebase_messaging: ^15.2.4 firebase_core: ^3.12.1 - flutter_local_notifications: ^19.2.1 + flutter_local_notifications: ^19.4.2 path: ^1.9.1 share_plus: ^11.0.0 connectivity_plus: ^6.1.4 From 1d859692de463dabc0a713f8f34b25478f42ac84 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:12:53 +0100 Subject: [PATCH 39/52] go router refactor --- .../atoms/copy_to_clipboard.dart | 11 +- .../atoms/currency_picker_bar.dart | 20 ++-- .../atoms/wallet/mint_info_card_small.dart | 43 +++---- .../atoms/wallet/wallet_card.dart | 14 ++- .../atoms/wallet/wallet_transaction_card.dart | 28 +++-- .../components/drawer/nostr_side_menu.dart | 9 +- .../wallet_account_card_placeholder.dart | 4 +- .../wallet/wallet_actions_strip.dart | 4 +- .../components/wallet/wallets_carousel.dart | 9 +- .../wallet/wallets_select_bottom_sheet.dart | 14 ++- .../providers/ndk_provider.dart | 19 ++-- .../routes/wallet/add_mint/add_mint_page.dart | 14 +-- .../wallet/mint_info/mint_info_page.dart | 8 +- .../routes/wallet/wallet_dashboard.dart | 21 ++-- .../wallet_pay_done/wallet_pay_done.dart | 30 ++--- .../wallet_pay_select_amount.dart | 12 +- .../wallet_pay_reciever.dart | 19 ++-- .../wallet_pay_summary.dart | 61 +++++----- .../routes/wallet/wallet_qr_scan.dart | 27 ++--- .../wallet_rcv_ecash_completer_page.dart | 28 ++--- .../wallet_receive_amount.dart | 12 +- .../wallet_receive_request.dart | 46 ++++---- .../wallet_receive_type.dart | 16 ++- .../wallet_transaction_detail_page.dart | 4 +- .../wallet_transaction_list_page.dart | 22 ++-- lib/routes.dart | 55 +++++++++ pubspec.lock | 105 +++++++++++------- pubspec.yaml | 2 +- 28 files changed, 369 insertions(+), 288 deletions(-) diff --git a/lib/presentation_layer/atoms/copy_to_clipboard.dart b/lib/presentation_layer/atoms/copy_to_clipboard.dart index 4acf3a76..e4f31b85 100644 --- a/lib/presentation_layer/atoms/copy_to_clipboard.dart +++ b/lib/presentation_layer/atoms/copy_to_clipboard.dart @@ -2,23 +2,19 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../config/palette.dart'; - class CopyClipboardButton extends StatefulWidget { final String value; final String copyText; final String copyDoneText; final Color backgroundColor; - final Color primaryColor; const CopyClipboardButton({ super.key, required this.value, this.copyText = 'Copy', this.copyDoneText = 'Copied to Clipboard!', - this.backgroundColor = Palette.white, - this.primaryColor = Palette.primary, + this.backgroundColor = Colors.white, }); @override @@ -38,8 +34,9 @@ class _CopyClipboardButtonState extends State { Icon(_copied ? PhosphorIcons.check() : PhosphorIcons.copySimple()), label: Text(_copied ? widget.copyDoneText : widget.copyText), style: ElevatedButton.styleFrom( - backgroundColor: - _copied ? widget.backgroundColor : widget.primaryColor, + backgroundColor: _copied + ? widget.backgroundColor + : Theme.of(context).colorScheme.primary, foregroundColor: widget.backgroundColor, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( diff --git a/lib/presentation_layer/atoms/currency_picker_bar.dart b/lib/presentation_layer/atoms/currency_picker_bar.dart index 0b7b6e69..de1db39f 100644 --- a/lib/presentation_layer/atoms/currency_picker_bar.dart +++ b/lib/presentation_layer/atoms/currency_picker_bar.dart @@ -159,13 +159,13 @@ class _CurrencyPickerBarState extends State final trackColor = widget.trackColor ?? (widget.isDark - ? Palette.extraDarkGray.withValues(alpha: 0.22) - : Palette.lightGray.withValues(alpha: 0.85)); + ? Paletter.extraDarkGray.withValues(alpha: 0.22) + : Paletter.lightGray.withValues(alpha: 0.85)); final inactiveColor = widget.inactiveColor ?? (widget.isDark - ? Palette.primary.withValues(alpha: 0.88) - : Palette.primary.withValues(alpha: 0.9)); + ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.88) + : Theme.of(context).colorScheme.primary.withValues(alpha: 0.9)); final activeColor = widget.activeColor ?? theme.colorScheme.primary; @@ -306,8 +306,8 @@ class _CurrencyPickerBarState extends State isDark: widget.isDark, borderWidth: 3, shadowColor: widget.isDark - ? Palette.black.withValues(alpha: 0.5) - : Palette.black.withValues(alpha: 0.15), + ? Colors.black.withValues(alpha: 0.5) + : Colors.black.withValues(alpha: 0.15), highlightColor: widget.isDark ? Colors.white.withValues(alpha: 0.08) : Colors.white.withValues(alpha: 0.18), @@ -327,8 +327,8 @@ class _LensRing extends StatelessWidget { const _LensRing({ required this.isDark, this.borderWidth = 3, - this.shadowColor = Palette.gray, - this.highlightColor = Palette.gray, + this.shadowColor = Paletter.gray, + this.highlightColor = Paletter.gray, }); final bool isDark; @@ -344,7 +344,9 @@ class _LensRing extends StatelessWidget { shape: BoxShape.circle, border: Border.all( width: borderWidth, - color: isDark ? Palette.darkGray : Palette.background, + color: isDark + ? Paletter.darkGray + : Theme.of(context).colorScheme.surface, ), boxShadow: [ BoxShadow( diff --git a/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart index 9d5f584e..57eac1b4 100644 --- a/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart +++ b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart @@ -17,11 +17,11 @@ class MintInfoCardSmall extends StatelessWidget { Widget build(BuildContext context) { return Card( elevation: 4, - color: Palette.background, + color: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( - color: Palette.white.withValues(alpha: 0.5), + color: Colors.white.withValues(alpha: 0.5), width: 1, ), ), @@ -60,7 +60,7 @@ class MintInfoCardSmall extends StatelessWidget { Text( mintInfo.name ?? 'Unknown Mint', style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, ), @@ -71,7 +71,7 @@ class MintInfoCardSmall extends StatelessWidget { Text( '${mintInfo.version}', style: TextStyle( - color: Palette.gray, + color: Paletter.gray, fontSize: 12, ), ), @@ -88,7 +88,7 @@ class MintInfoCardSmall extends StatelessWidget { Text( mintInfo.description!, style: TextStyle( - color: Palette.lightGray, + color: Paletter.lightGray, fontSize: 14, ), maxLines: 2, @@ -103,10 +103,13 @@ class MintInfoCardSmall extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Palette.gray.withValues(alpha: 0.1), + color: Paletter.gray.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), border: Border.all( - color: Palette.primary.withValues(alpha: 0.5), + color: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.5), ), ), child: Row( @@ -114,14 +117,14 @@ class MintInfoCardSmall extends StatelessWidget { Icon( PhosphorIcons.megaphone(), size: 16, - color: Palette.primary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 6), Expanded( child: Text( mintInfo.motd!, style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 16, ), maxLines: 1, @@ -139,7 +142,7 @@ class MintInfoCardSmall extends StatelessWidget { Text( 'Supported Units', style: TextStyle( - color: Palette.lightGray, + color: Paletter.lightGray, fontSize: 14, fontWeight: FontWeight.bold, ), @@ -160,7 +163,7 @@ class MintInfoCardSmall extends StatelessWidget { child: Text( unit.toUpperCase(), style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 12, ), ), @@ -175,7 +178,7 @@ class MintInfoCardSmall extends StatelessWidget { Text( 'Contact', style: TextStyle( - color: Palette.lightGray, + color: Paletter.lightGray, fontSize: 14, fontWeight: FontWeight.bold, ), @@ -189,14 +192,14 @@ class MintInfoCardSmall extends StatelessWidget { Icon( PhosphorIcons.userCircle(), size: 14, - color: Palette.lightGray, + color: Paletter.lightGray, ), const SizedBox(width: 6), Expanded( child: Text( contact.info, style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 12, ), maxLines: 1, @@ -217,7 +220,7 @@ class MintInfoCardSmall extends StatelessWidget { Icon( PhosphorIcons.link(), size: 16, - color: Palette.lightGray, + color: Paletter.lightGray, ), const SizedBox(width: 4), Text( @@ -232,7 +235,7 @@ class MintInfoCardSmall extends StatelessWidget { child: Text( url, style: TextStyle( - color: Palette.lightGray, + color: Paletter.lightGray, fontSize: 12, ), maxLines: 1, @@ -261,13 +264,13 @@ class MintInfoCardSmall extends StatelessWidget { Icon( Icons.description_outlined, size: 12, - color: Palette.lightGray, + color: Paletter.lightGray, ), const SizedBox(width: 4), Text( 'Terms of Service', style: TextStyle( - color: Palette.lightGray, + color: Paletter.lightGray, fontSize: 12, ), ), @@ -289,13 +292,13 @@ class MintInfoCardSmall extends StatelessWidget { width: 48, height: 48, decoration: BoxDecoration( - color: Palette.darkGray, + color: Paletter.darkGray, borderRadius: BorderRadius.circular(8), ), child: Icon( PhosphorIcons.bank(), size: 24, - color: Palette.gray, + color: Paletter.gray, ), ); } diff --git a/lib/presentation_layer/atoms/wallet/wallet_card.dart b/lib/presentation_layer/atoms/wallet/wallet_card.dart index 2e13691a..e3c0ca1a 100644 --- a/lib/presentation_layer/atoms/wallet/wallet_card.dart +++ b/lib/presentation_layer/atoms/wallet/wallet_card.dart @@ -24,7 +24,7 @@ class WalletCard extends StatelessWidget { required this.onTap, this.isSelected = false, this.isDisabled = false, - this.backgroundColor = Palette.extraDarkGray, + this.backgroundColor = Paletter.extraDarkGray, this.tralling, this.showBalances = true, }); @@ -44,7 +44,10 @@ class WalletCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ CircleAvatar( - backgroundColor: Palette.primary.withValues(alpha: 0.8), + backgroundColor: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.8), child: Text(wallet.name.substring(0, 2).toUpperCase(), style: TextStyle(color: Colors.white, fontSize: 12)), ), @@ -60,14 +63,15 @@ class WalletCard extends StatelessWidget { style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: isDisabled ? Palette.gray : Palette.white, + color: isDisabled ? Paletter.gray : Colors.white, ), ), Text( wallet.id, style: TextStyle( fontSize: 14, - color: isDisabled ? Palette.gray : Palette.lightGray, + color: + isDisabled ? Paletter.gray : Paletter.lightGray, ), ), ], @@ -82,7 +86,7 @@ class WalletCard extends StatelessWidget { Text( "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", style: TextStyle( - color: Palette.white, + color: Colors.white, ), ), ]), diff --git a/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart index 2a7d4bb0..5fee12b4 100644 --- a/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart +++ b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -26,7 +27,7 @@ class WalletTransactionCard extends StatelessWidget { Widget build(BuildContext context) { final amount = tx.changeAmount; - final transactionStatus = _getTransactionStatus(tx); + final transactionStatus = _getTransactionStatus(tx, context); final dt = _fromUnixSeconds(_bestDate(tx) ?? 0); final formattedDate = _formatDate(dt, includeDate: showDate); @@ -40,10 +41,10 @@ class WalletTransactionCard extends StatelessWidget { return Card( elevation: 0, - color: Palette.extraDarkGray.withValues(alpha: 0.75), + color: Paletter.extraDarkGray.withValues(alpha: 0.75), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: BorderSide(color: Palette.darkGray.withValues(alpha: 0.25)), + side: BorderSide(color: Paletter.darkGray.withValues(alpha: 0.25)), ), margin: const EdgeInsets.symmetric(vertical: 6), child: ListTile( @@ -60,7 +61,7 @@ class WalletTransactionCard extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - color: Palette.white, + color: Colors.white, fontWeight: FontWeight.w600, ), ), @@ -68,7 +69,7 @@ class WalletTransactionCard extends StatelessWidget { "${cashuTx != null ? _removeHttpPrefix(cashuTx.mintUrl) : ""} • ${cashuTx != null ? _txType(cashuTx) : ''}", maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(color: Palette.extraLightGray), + style: TextStyle(color: Paletter.extraLightGray), ), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -88,18 +89,14 @@ class WalletTransactionCard extends StatelessWidget { Text( formattedDate, style: TextStyle( - color: Palette.gray, + color: Paletter.gray, fontSize: 12, ), ), ], ), onTap: () { - Navigator.pushNamed( - context, - '/wallet/transactions/detail', - arguments: tx, - ); + context.push('/wallet/transactions/detail', extra: tx); }, ), ); @@ -159,7 +156,8 @@ class TransactionStatus { }); } -TransactionStatus _getTransactionStatus(ndk_entities.WalletTransaction tx) { +TransactionStatus _getTransactionStatus( + ndk_entities.WalletTransaction tx, BuildContext context) { final isDraft = tx.state == ndk_entities.WalletTransactionState.draft; final isPending = tx.state == ndk_entities.WalletTransactionState.pending; final isFailed = tx.state == ndk_entities.WalletTransactionState.failed; @@ -177,19 +175,19 @@ TransactionStatus _getTransactionStatus(ndk_entities.WalletTransaction tx) { return TransactionStatus( label: 'Failed ${isIncoming ? 'Incoming' : 'Outgoing'}', icon: PhosphorIcons.warningCircle(), - color: Palette.error, + color: Theme.of(context).colorScheme.error, isStrikethrough: true, ); } else if (isCanceled) { return TransactionStatus( label: 'Canceled ${isIncoming ? 'Incoming' : 'Outgoing'}', icon: PhosphorIcons.xCircle(), - color: Palette.warn, + color: Theme.of(context).colorScheme.onError, isStrikethrough: true, ); } else { // Successful transaction - final color = isIncoming ? Palette.success : Palette.gray; + final color = isIncoming ? Colors.green : Paletter.gray; final icon = isIncoming ? PhosphorIcons.arrowDown() : PhosphorIcons.arrowUp(); diff --git a/lib/presentation_layer/components/drawer/nostr_side_menu.dart b/lib/presentation_layer/components/drawer/nostr_side_menu.dart index a3ea9c2c..2763bdf7 100644 --- a/lib/presentation_layer/components/drawer/nostr_side_menu.dart +++ b/lib/presentation_layer/components/drawer/nostr_side_menu.dart @@ -220,15 +220,10 @@ class NostrSideMenu extends ConsumerWidget { }), _drawerItem( label: AppLocalizations.of(context)!.payments, - routeName: 'payments', icon: PhosphorIcons.lightning(), + routeName: '/wallet/dashboard', onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text(AppLocalizations.of(context)!.notImplementedYet), - ), - ); + context.push('/wallet/dashboard'); }), _drawerItem( label: AppLocalizations.of(context)!.blocklist, diff --git a/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart b/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart index 1e2f122c..753e6044 100644 --- a/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart +++ b/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart @@ -16,11 +16,11 @@ class WalletAccountCardPlaceholder extends StatelessWidget { onTap: onTap, child: Container( decoration: BoxDecoration( - color: Palette.extraDarkGray, + color: Paletter.extraDarkGray, borderRadius: BorderRadius.circular(18.0), border: Border.all( width: 1, - color: Palette.gray, + color: Paletter.gray, ), ), child: Center( diff --git a/lib/presentation_layer/components/wallet/wallet_actions_strip.dart b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart index 5a3ec5d0..ed31ce5b 100644 --- a/lib/presentation_layer/components/wallet/wallet_actions_strip.dart +++ b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart @@ -87,13 +87,13 @@ Widget _actionButton({ ), child: Icon( iconData, - color: iconColor ?? Palette.gray, + color: iconColor ?? Paletter.gray, size: 23, ), ), Text( text, - style: TextStyle(color: textColor ?? Palette.gray), + style: TextStyle(color: textColor ?? Paletter.gray), ) ], ); diff --git a/lib/presentation_layer/components/wallet/wallets_carousel.dart b/lib/presentation_layer/components/wallet/wallets_carousel.dart index 86d67209..9336aa4c 100644 --- a/lib/presentation_layer/components/wallet/wallets_carousel.dart +++ b/lib/presentation_layer/components/wallet/wallets_carousel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'wallet_account_card_placeholder.dart'; import 'wallet_accounts_card.dart'; @@ -65,7 +66,7 @@ class _WalletsCarouselState extends State { child: _CardMaxWidth( child: WalletAccountCardPlaceholder( onTap: () { - Navigator.pushNamed(context, '/wallet/add_mint'); + context.push('/wallet/add_mint'); }, ), ), @@ -114,11 +115,7 @@ class _WalletsCarouselState extends State { child: GestureDetector( onTap: () { if (mintUrl != null) { - Navigator.pushNamed( - context, - '/wallet/mint_details', - arguments: mintUrl, - ); + context.push('/wallet/mint_details', extra: mintUrl); } }, child: WalletAccountsCard( diff --git a/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart index 353fcabc..48265721 100644 --- a/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart +++ b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart @@ -14,7 +14,7 @@ Future showWalletsSelectBottomSheet({ return showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, builder: (ctx) { final size = MediaQuery.of(ctx).size; @@ -28,10 +28,11 @@ Future showWalletsSelectBottomSheet({ // upper bound constraints: BoxConstraints(maxHeight: size.height * maxHeightFactor), decoration: BoxDecoration( - color: Palette.background, + color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), - boxShadow: const [ - BoxShadow(blurRadius: 16, color: Palette.background) + boxShadow: [ + BoxShadow( + blurRadius: 16, color: Theme.of(context).colorScheme.surface) ], ), child: Material( @@ -55,7 +56,7 @@ Future showWalletsSelectBottomSheet({ child: Text( title, style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600, ), @@ -84,7 +85,8 @@ Future showWalletsSelectBottomSheet({ isSelected: isSelected, onTap: (id) => Navigator.of(ctx).pop(id), tralling: Radio( - activeColor: Palette.primary, + activeColor: + Theme.of(context).colorScheme.primary, value: wallet.id, groupValue: isSelected ? wallet.id : null, onChanged: (value) { diff --git a/lib/presentation_layer/providers/ndk_provider.dart b/lib/presentation_layer/providers/ndk_provider.dart index 3601d4b3..1951a8c7 100644 --- a/lib/presentation_layer/providers/ndk_provider.dart +++ b/lib/presentation_layer/providers/ndk_provider.dart @@ -1,3 +1,4 @@ +import 'package:ndk/domain_layer/entities/cashu/cashu_user_seedphrase.dart'; import 'package:ndk/ndk.dart'; import 'package:riverpod/riverpod.dart'; @@ -12,14 +13,16 @@ final ndkProvider = Provider((ref) { final bloomFilterRef = ref.read(bloomFilterReferenceProvider); final NdkConfig ndkConfig = NdkConfig( - engine: NdkEngine.JIT, - cache: db!, - eventVerifier: eventVerifier, - bootstrapRelays: camelusBootstrapRelays, - logLevel: Logger.logLevels.warning, - defaultBroadcastConsiderDonePercent: 0.2, - eventOutFilters: [bloomFilterRef], - ); + engine: NdkEngine.JIT, + cache: db!, + eventVerifier: eventVerifier, + bootstrapRelays: camelusBootstrapRelays, + logLevel: Logger.logLevels.warning, + defaultBroadcastConsiderDonePercent: 0.2, + eventOutFilters: [bloomFilterRef], + cashuUserSeedphrase: CashuUserSeedphrase( + seedPhrase: + "market grid grocery useless into bag earn dove measure stay elephant bright")); final ndk = Ndk(ndkConfig); return ndk; diff --git a/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart b/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart index 588c2157..50b33202 100644 --- a/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart +++ b/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart @@ -127,10 +127,10 @@ class AddMintPage extends ConsumerWidget { final notifier = ref.read(addMintProvider.notifier); return Scaffold( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.background, appBar: AppBar( title: const Text('Add Mint'), - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.background, ), body: Padding( padding: const EdgeInsets.all(16.0), @@ -151,8 +151,8 @@ class AddMintPage extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 16), child: LinearProgressIndicator( value: validationState.isValidating ? null : 0.0, - backgroundColor: Palette.background, - color: Palette.white, + backgroundColor: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(12), ), ), @@ -164,11 +164,11 @@ class AddMintPage extends ConsumerWidget { hintText: 'Enter mint address...', border: const OutlineInputBorder(), focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Palette.white), + borderSide: BorderSide(color: Paletter.darkGray), borderRadius: BorderRadius.all(Radius.circular(12)), ), enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Palette.darkGray), + borderSide: BorderSide(color: Paletter.darkGray), borderRadius: BorderRadius.all(Radius.circular(12)), ), suffixIcon: _buildValidationIcon(validationState), @@ -215,7 +215,7 @@ class AddMintPage extends ConsumerWidget { if (state.isValid == true) { return Icon( PhosphorIcons.checkCircle(), - color: Palette.success, + color: Colors.green, ); } diff --git a/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart b/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart index fc5ec148..6f23021d 100644 --- a/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart +++ b/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart @@ -24,9 +24,9 @@ class MintInfoPage extends ConsumerWidget { if (mintInfoFilter.isEmpty) { return Scaffold( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, title: Text('Mint Info'), ), body: Center( @@ -38,9 +38,9 @@ class MintInfoPage extends ConsumerWidget { final myWallet = mintInfoFilter.first as CashuWallet; return Scaffold( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, title: Text('Mint Info'), ), body: SingleChildScrollView( diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index a548e8fa..ee5bebfd 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../config/palette.dart'; import '../../atoms/my_profile_picture.dart'; +import '../../components/drawer/nostr_drawer.dart'; import '../../components/wallet/payment_history_short.dart'; import '../../components/wallet/wallet_actions_strip.dart'; import '../../components/wallet/wallet_friends_strip.dart'; @@ -11,7 +13,6 @@ import '../../components/wallet/wallets_carousel.dart'; import '../../components/wallet/wallets_select_bottom_sheet.dart'; import '../../providers/metadata_state_provider.dart'; import '../../providers/ndk_provider.dart'; -import '../nostr/nostr_drawer.dart'; import 'wallet_navigation.dart'; import 'wallet_pay/wallet_pay_state_provider.dart'; import 'wallet_providers/wallet_combined_state_provider.dart'; @@ -54,8 +55,8 @@ class _WalletDashboardState extends ConsumerState return Scaffold( appBar: AppBar( - backgroundColor: Palette.background, - surfaceTintColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, + surfaceTintColor: Theme.of(context).colorScheme.surface, leading: Builder( builder: (context) { final myMetadata = @@ -80,12 +81,12 @@ class _WalletDashboardState extends ConsumerState size: 30, ), onPressed: () { - Navigator.pushNamed(context, '/wallet/add_mint'); + context.push('/wallet/add_mint'); }, ), ], ), - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, drawer: NostrDrawer(pubkey: myUserPubkey), body: SafeArea( child: Column( @@ -121,7 +122,7 @@ class _WalletDashboardState extends ConsumerState rcvNotifier.reset(); rcvNotifier.updateRecieveToWalletId(selectedId); if (mounted) { - Navigator.pushNamed(context, '/wallet/receive'); + context.push('/wallet/receive'); } } }, @@ -137,7 +138,7 @@ class _WalletDashboardState extends ConsumerState payNotifier.reset(); payNotifier.updatePayFromWalletId(selectedId); if (mounted) { - Navigator.pushNamed(context, '/wallet/pay'); + context.push('/wallet/pay'); } } }, @@ -152,11 +153,7 @@ class _WalletDashboardState extends ConsumerState transactions: combinedWallet.recentTransactions, pendingTransactions: combinedWallet.pendingTransactions, onTap: (tx) { - Navigator.pushNamed( - context, - '/wallet/transactions/detail', - arguments: tx, - ); + context.push('/wallet/transactions/detail', extra: tx); }, ), ) diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart index d778743d..66874f3e 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -21,7 +22,7 @@ class WalletPayDone extends ConsumerWidget { final payNotifier = ref.watch(walletPayStateProvider.notifier); return Scaffold( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, appBar: null, body: Padding( padding: const EdgeInsets.all(16.0), @@ -37,7 +38,8 @@ class WalletPayDone extends ConsumerWidget { name: "close", onPressed: () { payNotifier.reset(); - Navigator.of(context).pop(); + + context.pop(); }, inverted: true), ), @@ -101,18 +103,18 @@ class ErrorStep extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.error_outline, - color: Palette.error, + color: Theme.of(context).colorScheme.error, size: 80, ), const SizedBox(height: 24), - const Text( + Text( 'Error', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Palette.error, + color: Theme.of(context).colorScheme.error, ), ), const SizedBox(height: 16), @@ -125,9 +127,9 @@ class ErrorStep extends StatelessWidget { ), child: Text( errorMessage ?? 'An unknown error occurred', - style: const TextStyle( + style: TextStyle( fontSize: 16, - color: Palette.error, + color: Theme.of(context).colorScheme.onError, ), textAlign: TextAlign.center, ), @@ -167,7 +169,7 @@ class SuccessStep extends StatelessWidget { style: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, - color: Palette.white, + color: Colors.white, ), ), const SizedBox(width: 16), @@ -176,7 +178,7 @@ class SuccessStep extends StatelessWidget { style: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, - color: Palette.extraLightGray, + color: Paletter.extraLightGray, ), ), ], @@ -190,7 +192,7 @@ class SuccessStep extends StatelessWidget { width: 240, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Palette.white, + color: Colors.white, borderRadius: BorderRadius.circular(8), ), child: AnimatedQr( @@ -243,16 +245,16 @@ class TransactionState extends ConsumerWidget { if (myTransaction.state == ndk_entities.WalletTransactionState.pending) { icon = PhosphorIcons.hourglass(); - color = Palette.lightGray; + color = Paletter.lightGray; title = 'pending ecash'; } else if (myTransaction.state == ndk_entities.WalletTransactionState.failed) { icon = PhosphorIcons.warningCircle(); - color = Palette.error; + color = Theme.of(context).colorScheme.surface; title = 'failed'; } else { icon = PhosphorIcons.checkCircle(); - color = Palette.success; + color = Colors.green; title = 'ecash claimed'; } diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart index f6f58d39..3749805c 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart @@ -105,7 +105,7 @@ class _WalletPaySelectAmountState extends ConsumerState { showSnackBar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - backgroundColor: Palette.warn, + backgroundColor: Theme.of(context).colorScheme.onError, content: Text( message, style: TextStyle(color: Colors.white), @@ -125,7 +125,7 @@ class _WalletPaySelectAmountState extends ConsumerState { return Scaffold( appBar: widget.title != null ? AppBar( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, title: Text(widget.title!), leading: IconButton( icon: const Icon(Icons.arrow_back), @@ -135,7 +135,7 @@ class _WalletPaySelectAmountState extends ConsumerState { ), ) : null, - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), @@ -163,7 +163,7 @@ class _WalletPaySelectAmountState extends ConsumerState { PhosphorIcons.notePencil(), size: 25, ), - color: Palette.primary, + color: Theme.of(context).colorScheme.primary, onPressed: () async { final selectedId = await showWalletsSelectBottomSheet( context: context, @@ -232,8 +232,8 @@ class _WalletPaySelectAmountState extends ConsumerState { ), }, showHaptics: true, - trackColor: Palette.extraDarkGray, - activeColor: Palette.primary, + trackColor: Paletter.extraDarkGray, + activeColor: Theme.of(context).colorScheme.primary, ), ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart index 14d410b3..2ddd697c 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart @@ -55,9 +55,9 @@ class WalletSelectReciever extends ConsumerWidget { final paymentStateNotifier = ref.watch(walletPayStateProvider.notifier); return Scaffold( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, title: const Text('Pay to'), leading: Container(), leadingWidth: 0, @@ -81,16 +81,17 @@ class WalletSelectReciever extends ConsumerWidget { isDense: true, hintText: ' Search by name', hintStyle: - const TextStyle(color: Palette.white, letterSpacing: 1.1), + const TextStyle(color: Colors.white, letterSpacing: 1.1), filled: true, - fillColor: Palette.background, + fillColor: Theme.of(context).colorScheme.surface, enabledBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(50.0)), - borderSide: BorderSide(color: Palette.extraDarkGray), + borderSide: BorderSide(color: Paletter.extraDarkGray), ), - focusedBorder: const OutlineInputBorder( + focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(25.0)), - borderSide: BorderSide(color: Palette.background), + borderSide: + BorderSide(color: Theme.of(context).colorScheme.surface), ), ), ), @@ -274,7 +275,7 @@ class _ContactsList extends StatelessWidget { Helpers.shortHr( c.pubkey, ), - style: TextStyle(color: Palette.gray), + style: TextStyle(color: Paletter.gray), ), onTap: () => onTap(c), ); @@ -329,7 +330,7 @@ class _WalletsList extends StatelessWidget { Text( "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", style: TextStyle( - color: Palette.white, + color: Colors.white, ), ), ]), diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart index 3fcca52f..56c9dd97 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -1,6 +1,7 @@ import 'package:camelus/presentation_layer/atoms/long_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:ndk/entities.dart' as ndk_entities; @@ -22,7 +23,7 @@ class WalletPaySummary extends ConsumerWidget { showSnackBar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - backgroundColor: Palette.warn, + backgroundColor: Theme.of(context).colorScheme.error, content: Text( message, style: TextStyle(color: Colors.white), @@ -77,11 +78,8 @@ class WalletPaySummary extends ConsumerWidget { stateNotifier.createToken(memo: state.memo); /// navigate to done page - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => WalletPayDone(), - ), - ); + + context.pushReplacement('/wallet/pay/done'); } return Scaffold( @@ -91,7 +89,8 @@ class WalletPaySummary extends ConsumerWidget { Container( width: double.infinity, decoration: BoxDecoration( - color: Palette.primary.withValues(alpha: 0.9), + color: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), @@ -125,12 +124,12 @@ class WalletPaySummary extends ConsumerWidget { IconButton( icon: Icon( Icons.close, - color: Palette.lightGray, + color: Paletter.lightGray, size: 24, ), onPressed: () { payNotifier.reset(); - Navigator.of(context).pop(); + context.pop(); }, ), ], @@ -158,7 +157,7 @@ class WalletPaySummary extends ConsumerWidget { icon: Icon( PhosphorIcons.notePencil(), size: 24, - color: Palette.extraLightGray, + color: Paletter.extraLightGray, ), onPressed: () => backCallback(), ), @@ -215,7 +214,7 @@ class WalletPaySummary extends ConsumerWidget { PhosphorIcons.notePencil(), size: 25, ), - color: Palette.primary, + color: Theme.of(context).colorScheme.primary, onPressed: () async { final selectedId = await showWalletsSelectBottomSheet( @@ -231,7 +230,7 @@ class WalletPaySummary extends ConsumerWidget { ), ), - Divider(color: Palette.darkGray, height: 1), + Divider(color: Paletter.darkGray, height: 1), /// receiver if (state.recieverType == @@ -252,18 +251,18 @@ class WalletPaySummary extends ConsumerWidget { /// details _buildDetailRow( - label: 'transaction type', - value: state.recieverType.toString(), - isEditable: false, - ), + label: 'transaction type', + value: state.recieverType.toString(), + isEditable: false, + context: context), _buildDetailRow( - label: 'Memo', - value: state.memo ?? '', - onEdit: () { - backCallback(); - }, - ), + label: 'Memo', + value: state.memo ?? '', + onEdit: () { + backCallback(); + }, + context: context), SizedBox(height: 24), @@ -290,6 +289,7 @@ class WalletPaySummary extends ConsumerWidget { required String value, bool isEditable = true, Function()? onEdit, + required BuildContext context, }) { return Padding( padding: EdgeInsets.symmetric(vertical: 8), @@ -309,7 +309,7 @@ class WalletPaySummary extends ConsumerWidget { PhosphorIcons.notePencil(), size: 25, ), - color: Palette.primary, + color: Theme.of(context).colorScheme.primary, onPressed: isEditable ? onEdit : null, ), ], @@ -330,7 +330,8 @@ class ContactReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: Palette.primary.withValues(alpha: 0.8), + backgroundColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), child: Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), ), @@ -366,7 +367,8 @@ class TokenReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: Palette.primary.withValues(alpha: 0.8), + backgroundColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), child: Text('TK', style: TextStyle(color: Colors.white, fontSize: 12)), ), @@ -376,14 +378,14 @@ class TokenReciever extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('receiver', - style: TextStyle(color: Palette.gray, fontSize: 12)), + style: TextStyle(color: Paletter.gray, fontSize: 12)), Text('Token', style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), Text('send as a token or qr code', - style: TextStyle(color: Palette.lightGray, fontSize: 14)), + style: TextStyle(color: Paletter.lightGray, fontSize: 14)), ], ), ), @@ -405,7 +407,8 @@ class WalletReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: Palette.primary.withValues(alpha: 0.8), + backgroundColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), child: Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart index 187cac59..2a9eed77 100644 --- a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -81,10 +82,8 @@ class _QrScan extends ConsumerState { final ecashTokenString = next.navigationData!['cashuTokenString'] as String; rcvProvider.receiveEcash(tokenString: ecashTokenString); - Navigator.pushNamed( - context, - '/wallet/receive/ecash', - ); + + context.push('/wallet/receive/ecash'); ref.read(walletNavigationProvider.notifier).changeDashboardPage(1); break; case QRNavigationTarget.sendPage: @@ -106,7 +105,8 @@ class _QrScan extends ConsumerState { return Center( child: Text( 'Error: $p1', - style: const TextStyle(color: Palette.error, fontSize: 16), + style: TextStyle( + color: Theme.of(context).colorScheme.error, fontSize: 16), ), ); }, @@ -135,7 +135,7 @@ class _QrScan extends ConsumerState { height: 250, decoration: BoxDecoration( border: Border.all( - color: Palette.white, + color: Colors.white, width: 2, ), borderRadius: BorderRadius.circular(20), @@ -151,7 +151,7 @@ class _QrScan extends ConsumerState { height: 120, width: double.infinity, decoration: BoxDecoration( - color: Palette.black.withValues(alpha: 0.8), + color: Colors.black.withValues(alpha: 0.8), borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), @@ -180,8 +180,9 @@ class _QrScan extends ConsumerState { ), label: const Text('paste from clipboard'), style: ElevatedButton.styleFrom( - backgroundColor: Palette.primary, - foregroundColor: Palette.white, + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, ), ), ], @@ -198,8 +199,8 @@ class _QrScan extends ConsumerState { right: 20, child: LinearProgressIndicator( borderRadius: BorderRadius.circular(20), - backgroundColor: Palette.extraDarkGray, - color: Palette.white, + backgroundColor: Paletter.extraDarkGray, + color: Colors.white, ), ), @@ -222,8 +223,8 @@ class _QrScan extends ConsumerState { ), child: Text( qrScannerState.error ?? "", - style: const TextStyle( - color: Palette.warn, + style: TextStyle( + color: Theme.of(context).colorScheme.error, fontSize: 16, ), textAlign: TextAlign.center, diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart index ffd4943e..bb33030c 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../../../config/palette.dart'; @@ -15,7 +16,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { final walletState = ref.watch(walletReceiveEcashCompleterProvider); return Scaffold( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, appBar: null, body: SafeArea( child: Padding( @@ -24,7 +25,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { children: [ Spacer(flex: 1), // Status Card - _buildStatusCard(walletState), + _buildStatusCard(walletState, context), Spacer(flex: 7), @@ -37,7 +38,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { Text( '+', style: const TextStyle( - color: Palette.lightGray, + color: Paletter.lightGray, fontSize: 28, ), ), @@ -46,7 +47,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { WalletNumberFormatting.formatAmount( amount: walletState.amount!, unit: walletState.unit!), style: const TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 48, fontWeight: FontWeight.bold, ), @@ -55,7 +56,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { Text( walletState.unit ?? '', style: const TextStyle( - fontSize: 16, color: Palette.lightGray), + fontSize: 16, color: Paletter.lightGray), ), ], ), @@ -73,7 +74,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { child: Text( walletState.memo!, style: const TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 16, ), textAlign: TextAlign.center, @@ -96,15 +97,14 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: longButton( - name: "close", - onPressed: () => {Navigator.of(context).pop()}, - inverted: true), + name: "close", onPressed: () => {context.pop()}, inverted: true), ), ), ); } - Widget _buildStatusCard(WalletRcvEcashCompleterState state) { + Widget _buildStatusCard( + WalletRcvEcashCompleterState state, BuildContext context) { IconData icon; Color color; String title; @@ -112,22 +112,22 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { if (state.isPending) { icon = PhosphorIcons.hourglass(); - color = Palette.primary; + color = Theme.of(context).colorScheme.primary; title = 'Processing'; subtitle = 'Receiving...'; } else if (state.isError) { icon = PhosphorIcons.warningCircle(); - color = Palette.error; + color = Theme.of(context).colorScheme.error; title = 'Failed'; subtitle = 'transaction failed'; } else if (state.isSuccess) { icon = PhosphorIcons.checkCircle(); - color = Palette.success; + color = Colors.green; title = 'Success'; subtitle = 'received successfully'; } else { icon = PhosphorIcons.info(); - color = Palette.gray; + color = Paletter.gray; title = 'Ready'; subtitle = 'waiting for transaction'; } diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart index 6b44ca85..fc45edc8 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart @@ -108,7 +108,7 @@ class _WalletPaySelectAmountState extends ConsumerState { showSnackBar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - backgroundColor: Palette.warn, + backgroundColor: Theme.of(context).colorScheme.error, content: Text( message, style: TextStyle(color: Colors.white), @@ -142,7 +142,7 @@ class _WalletPaySelectAmountState extends ConsumerState { return Scaffold( appBar: widget.title != null ? AppBar( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, title: Text(widget.title!), leading: IconButton( icon: const Icon(Icons.arrow_back), @@ -152,7 +152,7 @@ class _WalletPaySelectAmountState extends ConsumerState { ), ) : null, - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), @@ -180,7 +180,7 @@ class _WalletPaySelectAmountState extends ConsumerState { PhosphorIcons.notePencil(), size: 25, ), - color: Palette.primary, + color: Theme.of(context).colorScheme.primary, onPressed: () async { final selectedId = await showWalletsSelectBottomSheet( context: context, @@ -245,8 +245,8 @@ class _WalletPaySelectAmountState extends ConsumerState { ), }, showHaptics: true, - trackColor: Palette.extraDarkGray, - activeColor: Palette.primary, + trackColor: Paletter.extraDarkGray, + activeColor: Theme.of(context).colorScheme.primary, ), ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart index 8a79730e..69dcda9b 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:ndk/entities.dart' as ndk_entities; @@ -23,7 +24,7 @@ class WalletReceiveRequest extends ConsumerWidget { showSnackBar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - backgroundColor: Palette.warn, + backgroundColor: Theme.of(context).colorScheme.error, content: Text( message, style: TextStyle(color: Colors.white), @@ -55,7 +56,8 @@ class WalletReceiveRequest extends ConsumerWidget { Container( width: double.infinity, decoration: BoxDecoration( - color: Palette.primary.withValues(alpha: 0.9), + color: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), @@ -89,12 +91,13 @@ class WalletReceiveRequest extends ConsumerWidget { IconButton( icon: Icon( Icons.close, - color: Palette.lightGray, + color: Paletter.lightGray, size: 24, ), onPressed: () { rcvNotifier.reset(); - Navigator.of(context).pop(); + + context.pop(); }, ), ], @@ -113,7 +116,7 @@ class WalletReceiveRequest extends ConsumerWidget { ), padding: EdgeInsets.all(16), decoration: BoxDecoration( - color: Palette.white, + color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: PrettyQrView.data( @@ -136,8 +139,8 @@ class WalletReceiveRequest extends ConsumerWidget { aspectRatio: 1.0, child: Container( decoration: BoxDecoration( - color: Palette.black - .withValues(alpha: 0.7), + color: + Colors.black.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(12), ), child: Column( @@ -146,14 +149,14 @@ class WalletReceiveRequest extends ConsumerWidget { children: [ Icon( PhosphorIcons.checkCircle(), - color: Palette.success, + color: Colors.green, size: 64, ), SizedBox(height: 8), Text( 'Request has been payed', style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 16, ), ), @@ -172,8 +175,7 @@ class WalletReceiveRequest extends ConsumerWidget { child: CopyClipboardButton( value: state.request!, copyText: "Copy invoice", - backgroundColor: Palette.extraDarkGray, - primaryColor: Palette.extraLightGray, + backgroundColor: Paletter.extraDarkGray, ), ), ], @@ -194,7 +196,7 @@ class WalletReceiveRequest extends ConsumerWidget { Text( state.requestErr!, style: TextStyle( - color: Palette.error, + color: Theme.of(context).colorScheme.error, fontSize: 14, ), ), @@ -227,10 +229,7 @@ class WalletReceiveRequest extends ConsumerWidget { padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: longButton( name: "close", - onPressed: () => { - rcvNotifier.reset(), - Navigator.of(context).pop(), - }, + onPressed: () => {rcvNotifier.reset(), context.pop()}, inverted: true), ), ), @@ -271,7 +270,8 @@ class ContactReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: Palette.primary.withValues(alpha: 0.8), + backgroundColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), child: Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), ), @@ -307,7 +307,8 @@ class TokenReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: Palette.primary.withValues(alpha: 0.8), + backgroundColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), child: Text('TK', style: TextStyle(color: Colors.white, fontSize: 12)), ), @@ -317,14 +318,14 @@ class TokenReciever extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('receiver', - style: TextStyle(color: Palette.gray, fontSize: 12)), + style: TextStyle(color: Paletter.gray, fontSize: 12)), Text('Token', style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), Text('send as a token or qr code', - style: TextStyle(color: Palette.lightGray, fontSize: 14)), + style: TextStyle(color: Paletter.lightGray, fontSize: 14)), ], ), ), @@ -346,7 +347,8 @@ class WalletReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: Palette.primary.withValues(alpha: 0.8), + backgroundColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), child: Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart index ffceef72..f229af03 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:ndk/entities.dart' as ndk_entities; import '../../../../../config/palette.dart'; @@ -37,10 +38,7 @@ class WalletReceiveType extends ConsumerWidget { ecashCompleter.receiveEcash(tokenString: userClipboard); if (!context.mounted) return; - Navigator.pushReplacementNamed( - context, - '/wallet/receive/ecash', - ); + context.pushReplacement('/wallet/receive/ecash'); } Future _handleReadClipboard() async { @@ -56,8 +54,8 @@ class WalletReceiveType extends ConsumerWidget { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(message, style: TextStyle(color: Palette.white)), - backgroundColor: Palette.error, + content: Text(message, style: TextStyle(color: Colors.white)), + backgroundColor: Theme.of(context).colorScheme.error, ), ); } @@ -69,9 +67,9 @@ class WalletReceiveType extends ConsumerWidget { final stateNotifier = ref.watch(walletRecieverProvider.notifier); return Scaffold( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, title: const Text('Receive'), leading: Container(), leadingWidth: 0, @@ -234,7 +232,7 @@ class _WalletsList extends StatelessWidget { Text( "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", style: TextStyle( - color: Palette.white, + color: Colors.white, ), ), ]), diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart index 3c58aeba..d6e9bfa2 100644 --- a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart @@ -15,9 +15,9 @@ class WalletTransactionDetailPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, title: Text('Transaction Detail'), ), body: Padding( diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart index 55a22008..4d5017be 100644 --- a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart @@ -20,11 +20,11 @@ class WalletTransactionListPage extends ConsumerWidget { final grouped = _groupByDay(state.transactions); return Scaffold( - backgroundColor: Palette.background, + backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( elevation: 0, - backgroundColor: Palette.background, - foregroundColor: Palette.white, + backgroundColor: Theme.of(context).colorScheme.surface, + foregroundColor: Colors.white, title: const Text('Transactions'), leading: IconButton( icon: Icon(PhosphorIcons.caretLeft(), size: 24), @@ -116,19 +116,19 @@ class _DateHeader extends StatelessWidget { return Row( children: [ Expanded( - child: Divider(color: Palette.darkGray.withValues(alpha: 0.4))), + child: Divider(color: Paletter.darkGray.withValues(alpha: 0.4))), Container( margin: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( - color: Palette.extraDarkGray, + color: Paletter.extraDarkGray, borderRadius: BorderRadius.circular(16), - border: Border.all(color: Palette.darkGray.withValues(alpha: 0.4)), + border: Border.all(color: Paletter.darkGray.withValues(alpha: 0.4)), ), child: Text( label, style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.w600, letterSpacing: 0.2, @@ -136,7 +136,7 @@ class _DateHeader extends StatelessWidget { ), ), Expanded( - child: Divider(color: Palette.darkGray.withValues(alpha: 0.4))), + child: Divider(color: Paletter.darkGray.withValues(alpha: 0.4))), ], ); } @@ -153,12 +153,12 @@ class _EmptyTransactions extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.receipt_long_outlined, size: 48, color: Palette.gray), + Icon(Icons.receipt_long_outlined, size: 48, color: Paletter.gray), const SizedBox(height: 12), Text( 'No transactions available', style: TextStyle( - color: Palette.white, + color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), @@ -168,7 +168,7 @@ class _EmptyTransactions extends StatelessWidget { 'Your recent activity will show up here.', textAlign: TextAlign.center, style: TextStyle( - color: Palette.lightGray, + color: Paletter.lightGray, fontSize: 13.5, ), ), diff --git a/lib/routes.dart b/lib/routes.dart index d41efbb3..c0a3b2c4 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,8 +1,17 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; +import 'package:ndk/entities.dart' as ndk_entities; import 'domain_layer/entities/starter_pack_identifier.dart'; import 'lifecycle/app_init_shell.dart'; +import 'presentation_layer/routes/wallet/add_mint/add_mint_page.dart'; +import 'presentation_layer/routes/wallet/mint_info/mint_info_page.dart'; +import 'presentation_layer/routes/wallet/wallet_navigation.dart'; +import 'presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart'; +import 'presentation_layer/routes/wallet/wallet_pay/wallet_pay_page.dart'; +import 'presentation_layer/routes/wallet/wallet_receive/wallet_receive_page.dart'; +import 'presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart'; +import 'presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart'; import 'presentation_layer/components/app_bottom_navigation_bar/app_bottom_navigation_bar.dart'; import 'presentation_layer/components/drawer/nostr_side_menu.dart'; import 'presentation_layer/components/drawer/nostr_side_menu_post_button.dart'; @@ -178,6 +187,52 @@ final routes = [ starterPackIdentifier: state.extra as StarterPackIdentifier, ), ), + // Wallet routes + GoRoute( + path: '/wallet/dashboard', + builder: (context, state) => WalletNavigation( + title: "a", + ), + ), + GoRoute( + path: '/wallet/receive', + builder: (context, state) => WalletReceivePage(), + ), + GoRoute( + path: '/wallet/receive/ecash', + builder: (context, state) => WalletReceiveEcashCompleterPage(), + ), + GoRoute( + path: '/wallet/mints', + builder: (context, state) => WalletNavigation( + title: "a", + ), + ), + GoRoute( + path: '/wallet/pay', + builder: (context, state) => WalletPayPage(), + ), + GoRoute( + path: '/wallet/add_mint', + builder: (context, state) => const AddMintPage(), + ), + GoRoute( + path: '/wallet/mint_details', + builder: (context, state) => MintInfoPage( + mintUrl: state.extra as String?, + ), + ), + GoRoute( + path: '/wallet/transactions/detail', + builder: (context, state) => WalletTransactionDetailPage( + transaction: state.extra as ndk_entities.WalletTransaction, + ), + ), + + GoRoute( + path: '/wallet/pay/done', + builder: (context, state) => WalletPayDone(), + ), ], ), // Routes outside the shell (no persistent layout) diff --git a/pubspec.lock b/pubspec.lock index 0334fedd..9ee6cf36 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -96,6 +96,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" + ascii_qr: + dependency: transitive + description: + name: ascii_qr + sha256: "2046e400a0fa4ea0de5df44c87b992cdd1f76403bb15e64513b89263598750ae" + url: "https://pub.dev" + source: hosted + version: "1.0.1" async: dependency: transitive description: @@ -112,6 +120,13 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + bc_ur_dart: + dependency: "direct main" + description: + path: "../fx-wallet-packages/packages/bc_ur_dart" + relative: true + source: path + version: "0.1.19" bech32: dependency: "direct main" description: @@ -129,6 +144,15 @@ packages: url: "https://github.com/5eeman/bip32-dart.git" source: git version: "2.0.1" + bip32_keys: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: b5a0342220e7ee5aaf64d489a589bdee6ef8de22 + url: "https://github.com/1-leo/dart-bip32-keys" + source: git + version: "3.1.2" bip340: dependency: "direct main" description: @@ -173,10 +197,10 @@ packages: dependency: transitive description: name: build - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "3.1.0" build_cli_annotations: dependency: transitive description: @@ -189,10 +213,10 @@ packages: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: @@ -205,26 +229,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46 url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "3.0.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30 url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.7.1" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b" url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "9.3.1" built_collection: dependency: transitive description: @@ -473,6 +497,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" elliptic: dependency: transitive description: @@ -1100,14 +1132,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" - logger: - dependency: transitive - description: - name: logger - sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 - url: "https://pub.dev" - source: hosted - version: "2.6.2" logging: dependency: transitive description: @@ -1168,19 +1192,17 @@ packages: dependency: "direct dev" description: name: mockito - sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 + sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99" url: "https://pub.dev" source: hosted - version: "5.4.5" + version: "5.5.0" ndk: dependency: "direct main" description: - path: "packages/ndk" - ref: tmp-camelus-1-7-4 - resolved-ref: "1264d9bd65f4cc998ce776951217adb3a7118a9f" - url: "https://github.com/relaystr/ndk.git" - source: git - version: "0.6.0-dev.0" + path: "../ndk/packages/ndk" + relative: true + source: path + version: "0.6.0-dev.4" ndk_amber: dependency: "direct main" description: @@ -1192,11 +1214,10 @@ packages: ndk_objectbox: dependency: "direct main" description: - name: ndk_objectbox - sha256: "4651b2b4575b5bed1d619eed225234f3a68b72049bac1df426d9062625cf8e8a" - url: "https://pub.dev" - source: hosted - version: "0.2.6" + path: "../ndk/packages/objectbox" + relative: true + source: path + version: "0.2.7-dev.9" ndk_rust_verifier: dependency: "direct main" description: @@ -1225,26 +1246,26 @@ packages: dependency: "direct main" description: name: objectbox - sha256: "25c2e24b417d938decb5598682dc831bc6a21856eaae65affbc57cfad326808d" + sha256: "825b3106bbd8b15b2e7a08d75345a218db020fd87bd5dc246acae65ec08ebc06" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "5.0.0" objectbox_flutter_libs: dependency: "direct main" description: name: objectbox_flutter_libs - sha256: "574b0233ba79a7159fca9049c67974f790a2180b6141d4951112b20bd146016a" + sha256: f5fd388ffebda310a1acda5c0076f854d7c0cb49634710eb88937ea4ae6c8e10 url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "5.0.0" objectbox_generator: dependency: "direct dev" description: name: objectbox_generator - sha256: "1b17e9168d03706b5bb895b5f36f4301aa7c973ac30ff761b205b1ca3e2e3865" + sha256: "62c1015e733267eef73b3f70f1b6ab3b483008714d8ad43b4c79af3a4d459474" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "5.0.0" octo_image: dependency: transitive description: @@ -1694,10 +1715,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.1.0" source_map_stack_trace: dependency: transitive description: @@ -1822,7 +1843,7 @@ packages: dependency: transitive description: name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" url: "https://pub.dev" source: hosted version: "3.3.1" @@ -2142,7 +2163,7 @@ packages: dependency: transitive description: name: win32 - sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f url: "https://pub.dev" source: hosted version: "5.12.0" @@ -2195,5 +2216,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index c590893d..333bba0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,7 +70,7 @@ dependencies: rxdart: ^0.28.0 shimmer: ^3.0.0 flutter_force_directed_graph: ^1.0.7 - objectbox: ^4.3.0 + objectbox: ^5.0.0 objectbox_flutter_libs: any amberflutter: ^0.0.9 intl: any From 61152a245e41e08a781731b1cfa273006ac518d7 Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sat, 14 Mar 2026 11:38:06 +0100 Subject: [PATCH 40/52] fix: use theme colors --- lib/config/db_paths.dart | 3 +- lib/objectbox.g.dart | 5 + .../atoms/currency_picker_bar.dart | 99 +++++---- .../atoms/wallet/mint_info_card_small.dart | 94 +++++---- .../atoms/wallet/wallet_card.dart | 52 ++--- .../atoms/wallet/wallet_transaction_card.dart | 46 +++-- .../components/drawer/nostr_side_menu.dart | 8 +- .../wallet/payment_history_short.dart | 38 ++-- .../wallet_account_card_placeholder.dart | 13 +- .../wallet/wallet_actions_strip.dart | 36 ++-- .../wallet/wallets_select_bottom_sheet.dart | 16 +- .../providers/event_verifier.dart | 1 - .../routes/wallet/add_mint/add_mint_page.dart | 73 ++++--- .../wallet/mint_info/mint_info_page.dart | 14 +- .../routes/wallet/wallet_dashboard.dart | 23 +-- .../routes/wallet/wallet_navigation.dart | 36 ++-- .../wallet_pay_done/wallet_pay_done.dart | 40 ++-- .../wallet_pay_select_amount.dart | 63 +++--- .../wallet_pay_reciever.dart | 54 ++--- .../wallet_pay_reciever_state_provider.dart | 67 +++--- .../wallet_pay/wallet_pay_state_provider.dart | 52 +++-- .../wallet_pay_summary.dart | 194 ++++++++++-------- .../qr_value_processing_state_provider.dart | 47 ++--- .../wallet_combined_state_provider.dart | 37 ++-- .../routes/wallet/wallet_qr_scan.dart | 49 ++--- .../wallet_rcv_ecash_completer_page.dart | 53 ++--- ...et_rcv_ecash_completer_state_provider.dart | 42 ++-- .../wallet_receive_amount.dart | 79 ++++--- .../wallet_receive_request.dart | 168 ++++++++------- .../wallet_receive_state_provider.dart | 36 ++-- .../wallet_receive_type.dart | 43 ++-- .../wallet_transaction_detail_page.dart | 12 +- .../wallet_transaction_list_page.dart | 65 +++--- ...allet_transaction_list_state_provider.dart | 48 +++-- pubspec.yaml | 2 +- 35 files changed, 861 insertions(+), 847 deletions(-) diff --git a/lib/config/db_paths.dart b/lib/config/db_paths.dart index 804bbabb..6906277e 100644 --- a/lib/config/db_paths.dart +++ b/lib/config/db_paths.dart @@ -10,7 +10,8 @@ class DbPaths { static const String _camelusDbNameDev = 'camelus-obx-default-dev'; static String get ndkDbName => kDebugMode ? _ndkDbNameDev : _ndkDbNameProd; - static String get camelusDbName => kDebugMode ? _camelusDbNameDev : _camelusDbNameProd; + static String get camelusDbName => + kDebugMode ? _camelusDbNameDev : _camelusDbNameProd; static Future getNdkDbPath() async { final docsDir = await getApplicationSupportDirectory(); diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart index 44c5f46f..73ae50ff 100644 --- a/lib/objectbox.g.dart +++ b/lib/objectbox.g.dart @@ -231,6 +231,11 @@ Future openStore({ /// [obx.Store.new]. obx_int.ModelDefinition getObjectBoxModel() { final model = obx_int.ModelInfo( + // If this version is not found, it means that this file was generated + // with an older version of the ObjectBox Dart generator. + // Please regenerate this file with the current generator version. + // Typically, this is done with `dart run build_runner build`. + generatorVersion: obx_int.GeneratorVersion.v2025_12_16, entities: _entities, lastEntityId: const obx_int.IdUid(9, 5397343319776606099), lastIndexId: const obx_int.IdUid(9, 6864549305007800035), diff --git a/lib/presentation_layer/atoms/currency_picker_bar.dart b/lib/presentation_layer/atoms/currency_picker_bar.dart index de1db39f..dc9b53c7 100644 --- a/lib/presentation_layer/atoms/currency_picker_bar.dart +++ b/lib/presentation_layer/atoms/currency_picker_bar.dart @@ -2,16 +2,11 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../../config/palette.dart'; - class ChangeResult { final String currentUnit; final String previousUnit; - ChangeResult({ - required this.currentUnit, - required this.previousUnit, - }); + ChangeResult({required this.currentUnit, required this.previousUnit}); } class CurrencyPickerBar extends StatefulWidget { @@ -64,11 +59,15 @@ class _CurrencyPickerBarState extends State super.initState(); _selectedIndex = widget.currencies.isEmpty ? 0 - : widget.initialIndex - .clamp(0, math.max(0, widget.currencies.length - 1)); + : widget.initialIndex.clamp( + 0, + math.max(0, widget.currencies.length - 1), + ); _lensX = 0; // will be set on first layout - _snapController = - AnimationController(vsync: this, duration: widget.snapDuration); + _snapController = AnimationController( + vsync: this, + duration: widget.snapDuration, + ); } @override @@ -121,9 +120,10 @@ class _CurrencyPickerBarState extends State final targetX = _xForIndex(index, width); _snapController.stop(); final start = _lensX; - _snapAnim = Tween(begin: start, end: targetX) - .chain(CurveTween(curve: Curves.easeOutCubic)) - .animate(_snapController); + _snapAnim = Tween( + begin: start, + end: targetX, + ).chain(CurveTween(curve: Curves.easeOutCubic)).animate(_snapController); _snapAnim!.addListener(() { setState(() { _lensX = _snapAnim!.value; @@ -146,10 +146,7 @@ class _CurrencyPickerBarState extends State final previousUnit = widget.currencies[previousIndex]; widget.onChanged?.call( - ChangeResult( - currentUnit: currentUnit, - previousUnit: previousUnit, - ), + ChangeResult(currentUnit: currentUnit, previousUnit: previousUnit), ); } @@ -157,17 +154,20 @@ class _CurrencyPickerBarState extends State Widget build(BuildContext context) { final theme = Theme.of(context); - final trackColor = widget.trackColor ?? + final trackColor = + widget.trackColor ?? (widget.isDark - ? Paletter.extraDarkGray.withValues(alpha: 0.22) - : Paletter.lightGray.withValues(alpha: 0.85)); + ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.22) + : Theme.of(context).colorScheme.surface.withValues(alpha: 0.85)); - final inactiveColor = widget.inactiveColor ?? + final inactiveColor = + widget.inactiveColor ?? (widget.isDark - ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.88) - : Theme.of(context).colorScheme.primary.withValues(alpha: 0.9)); + ? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.88) + : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.9)); - final activeColor = widget.activeColor ?? theme.colorScheme.primary; + final activeColor = + widget.activeColor ?? theme.colorScheme.primaryContainer; return LayoutBuilder( builder: (context, constraints) { @@ -180,8 +180,10 @@ class _CurrencyPickerBarState extends State } void handleTapOrPanTo(Offset localPos) { - final clamped = - localPos.dx.clamp(_leftBound(width), _rightBound(width)); + final clamped = localPos.dx.clamp( + _leftBound(width), + _rightBound(width), + ); setState(() => _lensX = clamped.toDouble()); final nearest = _nearestIndexForX(_lensX, width); _setSelectedIndex(nearest); @@ -231,7 +233,9 @@ class _CurrencyPickerBarState extends State boxShadow: [ if (!widget.isDark) BoxShadow( - color: Colors.black.withOpacity(0.06), + color: Theme.of( + context, + ).colorScheme.shadow.withOpacity(0.06), blurRadius: 10, offset: const Offset(0, 3), ), @@ -256,10 +260,12 @@ class _CurrencyPickerBarState extends State return Semantics( label: 'Currency picker', value: hasItems ? widget.currencies[_selectedIndex] : '', - increasedValue: - canIncrease ? widget.currencies[_selectedIndex + 1] : null, - decreasedValue: - canDecrease ? widget.currencies[_selectedIndex - 1] : null, + increasedValue: canIncrease + ? widget.currencies[_selectedIndex + 1] + : null, + decreasedValue: canDecrease + ? widget.currencies[_selectedIndex - 1] + : null, onIncrease: canIncrease ? () { _setSelectedIndex(_selectedIndex + 1); @@ -281,8 +287,10 @@ class _CurrencyPickerBarState extends State _animateLensToIndex(_selectedIndex, width); }, onHorizontalDragUpdate: (d) { - final moved = (_lensX + d.delta.dx) - .clamp(_leftBound(width), _rightBound(width)); + final moved = (_lensX + d.delta.dx).clamp( + _leftBound(width), + _rightBound(width), + ); setState(() => _lensX = moved.toDouble()); final nearest = _nearestIndexForX(_lensX, width); _setSelectedIndex(nearest); @@ -305,12 +313,14 @@ class _CurrencyPickerBarState extends State child: _LensRing( isDark: widget.isDark, borderWidth: 3, - shadowColor: widget.isDark - ? Colors.black.withValues(alpha: 0.5) - : Colors.black.withValues(alpha: 0.15), + highlightColor: widget.isDark - ? Colors.white.withValues(alpha: 0.08) - : Colors.white.withValues(alpha: 0.18), + ? Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.08) + : Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.18), ), ), ], @@ -327,14 +337,13 @@ class _LensRing extends StatelessWidget { const _LensRing({ required this.isDark, this.borderWidth = 3, - this.shadowColor = Paletter.gray, - this.highlightColor = Paletter.gray, + this.highlightColor, }); final bool isDark; final double borderWidth; - final Color shadowColor; - final Color highlightColor; + + final Color? highlightColor; @override Widget build(BuildContext context) { @@ -345,7 +354,7 @@ class _LensRing extends StatelessWidget { border: Border.all( width: borderWidth, color: isDark - ? Paletter.darkGray + ? Theme.of(context).colorScheme.onSurface : Theme.of(context).colorScheme.surface, ), boxShadow: [ @@ -355,7 +364,9 @@ class _LensRing extends StatelessWidget { offset: const Offset(0, 4), ), BoxShadow( - color: highlightColor, + color: + highlightColor ?? + Theme.of(context).colorScheme.onSurfaceVariant, blurRadius: 8, spreadRadius: -2, offset: const Offset(0, -1), diff --git a/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart index 57eac1b4..18f2210d 100644 --- a/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart +++ b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart @@ -3,15 +3,10 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../../../config/palette.dart'; - class MintInfoCardSmall extends StatelessWidget { final ndk_entities.CashuMintInfo mintInfo; - const MintInfoCardSmall({ - super.key, - required this.mintInfo, - }); + const MintInfoCardSmall({super.key, required this.mintInfo}); @override Widget build(BuildContext context) { @@ -20,15 +15,10 @@ class MintInfoCardSmall extends StatelessWidget { color: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - side: BorderSide( - color: Colors.white.withValues(alpha: 0.5), - width: 1, - ), + side: BorderSide(color: Colors.white.withValues(alpha: 0.5), width: 1), ), child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -46,12 +36,12 @@ class MintInfoCardSmall extends StatelessWidget { height: 48, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => - _buildDefaultIcon(), + _buildDefaultIcon(context), ), ), const SizedBox(width: 12), ] else - _buildDefaultIcon(), + _buildDefaultIcon(context), const SizedBox(width: 12), Expanded( child: Column( @@ -71,7 +61,9 @@ class MintInfoCardSmall extends StatelessWidget { Text( '${mintInfo.version}', style: TextStyle( - color: Paletter.gray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), fontSize: 12, ), ), @@ -88,7 +80,9 @@ class MintInfoCardSmall extends StatelessWidget { Text( mintInfo.description!, style: TextStyle( - color: Paletter.lightGray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 14, ), maxLines: 2, @@ -103,13 +97,14 @@ class MintInfoCardSmall extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Paletter.gray.withValues(alpha: 0.1), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), border: Border.all( - color: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.5), + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.5), ), ), child: Row( @@ -123,10 +118,7 @@ class MintInfoCardSmall extends StatelessWidget { Expanded( child: Text( mintInfo.motd!, - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), + style: TextStyle(color: Colors.white, fontSize: 16), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -142,7 +134,9 @@ class MintInfoCardSmall extends StatelessWidget { Text( 'Supported Units', style: TextStyle( - color: Paletter.lightGray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 14, fontWeight: FontWeight.bold, ), @@ -162,10 +156,7 @@ class MintInfoCardSmall extends StatelessWidget { ), child: Text( unit.toUpperCase(), - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), + style: TextStyle(color: Colors.white, fontSize: 12), ), ); }).toList(), @@ -178,7 +169,9 @@ class MintInfoCardSmall extends StatelessWidget { Text( 'Contact', style: TextStyle( - color: Paletter.lightGray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 14, fontWeight: FontWeight.bold, ), @@ -192,16 +185,15 @@ class MintInfoCardSmall extends StatelessWidget { Icon( PhosphorIcons.userCircle(), size: 14, - color: Paletter.lightGray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), ), const SizedBox(width: 6), Expanded( child: Text( contact.info, - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), + style: TextStyle(color: Colors.white, fontSize: 12), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -220,12 +212,12 @@ class MintInfoCardSmall extends StatelessWidget { Icon( PhosphorIcons.link(), size: 16, - color: Paletter.lightGray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), ), const SizedBox(width: 4), - Text( - 'URLs', - ), + Text('URLs'), ], ), const SizedBox(height: 4), @@ -235,7 +227,9 @@ class MintInfoCardSmall extends StatelessWidget { child: Text( url, style: TextStyle( - color: Paletter.lightGray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 12, ), maxLines: 1, @@ -264,19 +258,23 @@ class MintInfoCardSmall extends StatelessWidget { Icon( Icons.description_outlined, size: 12, - color: Paletter.lightGray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), ), const SizedBox(width: 4), Text( 'Terms of Service', style: TextStyle( - color: Paletter.lightGray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 12, ), ), ], ), - ) + ), ], ), ], @@ -287,18 +285,18 @@ class MintInfoCardSmall extends StatelessWidget { ); } - Widget _buildDefaultIcon() { + Widget _buildDefaultIcon(BuildContext context) { return Container( width: 48, height: 48, decoration: BoxDecoration( - color: Paletter.darkGray, + color: Theme.of(context).colorScheme.surfaceVariant, borderRadius: BorderRadius.circular(8), ), child: Icon( PhosphorIcons.bank(), size: 24, - color: Paletter.gray, + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ), ); } diff --git a/lib/presentation_layer/atoms/wallet/wallet_card.dart b/lib/presentation_layer/atoms/wallet/wallet_card.dart index e3c0ca1a..934b3e29 100644 --- a/lib/presentation_layer/atoms/wallet/wallet_card.dart +++ b/lib/presentation_layer/atoms/wallet/wallet_card.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:ndk/entities.dart' as ndk_entities; -import '../../../config/palette.dart'; import '../../../helpers/wallet_number_formatting.dart'; class WalletCard extends StatelessWidget { @@ -15,7 +14,7 @@ class WalletCard extends StatelessWidget { final bool showBalances; - final Color backgroundColor; + final Color? backgroundColor; const WalletCard({ super.key, @@ -24,7 +23,7 @@ class WalletCard extends StatelessWidget { required this.onTap, this.isSelected = false, this.isDisabled = false, - this.backgroundColor = Paletter.extraDarkGray, + this.backgroundColor, this.tralling, this.showBalances = true, }); @@ -35,7 +34,9 @@ class WalletCard extends StatelessWidget { onTap: isDisabled ? null : () => onTap(wallet.id), child: Container( decoration: BoxDecoration( - color: backgroundColor, + color: + backgroundColor ?? + Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Padding( @@ -44,12 +45,13 @@ class WalletCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ CircleAvatar( - backgroundColor: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.8), - child: Text(wallet.name.substring(0, 2).toUpperCase(), - style: TextStyle(color: Colors.white, fontSize: 12)), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.8), + child: Text( + wallet.name.substring(0, 2).toUpperCase(), + style: TextStyle(color: Colors.white, fontSize: 12), + ), ), const SizedBox(width: 12), Row( @@ -63,15 +65,18 @@ class WalletCard extends StatelessWidget { style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: isDisabled ? Paletter.gray : Colors.white, + color: isDisabled + ? Theme.of(context).colorScheme.onSurfaceVariant + : Colors.white, ), ), Text( wallet.id, style: TextStyle( fontSize: 14, - color: - isDisabled ? Paletter.gray : Paletter.lightGray, + color: isDisabled + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.outline, ), ), ], @@ -81,19 +86,16 @@ class WalletCard extends StatelessWidget { const SizedBox(width: 12), Spacer(flex: 1), if (showBalances) - Column(children: [ - for (final b in balances) - Text( - "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", - style: TextStyle( - color: Colors.white, + Column( + children: [ + for (final b in balances) + Text( + "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", + style: TextStyle(color: Colors.white), ), - ), - ]), - if (tralling != null) ...[ - Spacer(flex: 2), - tralling!, - ], + ], + ), + if (tralling != null) ...[Spacer(flex: 2), tralling!], ], ), ), diff --git a/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart index 5fee12b4..3cd13b14 100644 --- a/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart +++ b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart @@ -5,7 +5,6 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:timeago_flutter/timeago_flutter.dart' as timeago; -import '../../../config/palette.dart'; import '../../../helpers/wallet_number_formatting.dart'; DateTime _fromUnixSeconds(int seconds) => @@ -41,10 +40,14 @@ class WalletTransactionCard extends StatelessWidget { return Card( elevation: 0, - color: Paletter.extraDarkGray.withValues(alpha: 0.75), + color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.75), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: BorderSide(color: Paletter.darkGray.withValues(alpha: 0.25)), + side: BorderSide( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.25), + ), ), margin: const EdgeInsets.symmetric(vertical: 6), child: ListTile( @@ -60,16 +63,17 @@ class WalletTransactionCard extends StatelessWidget { transactionStatus.label, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600), ), subtitle: Text( "${cashuTx != null ? _removeHttpPrefix(cashuTx.mintUrl) : ""} • ${cashuTx != null ? _txType(cashuTx) : ''}", maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(color: Paletter.extraLightGray), + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), + ), ), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -89,7 +93,9 @@ class WalletTransactionCard extends StatelessWidget { Text( formattedDate, style: TextStyle( - color: Paletter.gray, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), fontSize: 12, ), ), @@ -102,8 +108,11 @@ class WalletTransactionCard extends StatelessWidget { ); } - String _formatDate(DateTime dt, - {bool includeDate = true, int minAgeForDate = 24}) { + String _formatDate( + DateTime dt, { + bool includeDate = true, + int minAgeForDate = 24, + }) { if (DateTime.now().difference(dt).inHours < minAgeForDate) { return timeago.format(dt); } @@ -142,6 +151,8 @@ class WalletTransactionCard extends StatelessWidget { } } +mixin Paletter {} + class TransactionStatus { final String label; final IconData icon; @@ -157,7 +168,9 @@ class TransactionStatus { } TransactionStatus _getTransactionStatus( - ndk_entities.WalletTransaction tx, BuildContext context) { + ndk_entities.WalletTransaction tx, + BuildContext context, +) { final isDraft = tx.state == ndk_entities.WalletTransactionState.draft; final isPending = tx.state == ndk_entities.WalletTransactionState.pending; final isFailed = tx.state == ndk_entities.WalletTransactionState.failed; @@ -187,9 +200,12 @@ TransactionStatus _getTransactionStatus( ); } else { // Successful transaction - final color = isIncoming ? Colors.green : Paletter.gray; - final icon = - isIncoming ? PhosphorIcons.arrowDown() : PhosphorIcons.arrowUp(); + final color = isIncoming + ? Colors.green + : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.8); + final icon = isIncoming + ? PhosphorIcons.arrowDown() + : PhosphorIcons.arrowUp(); return TransactionStatus( label: isIncoming ? 'Incoming' : 'Outgoing', diff --git a/lib/presentation_layer/components/drawer/nostr_side_menu.dart b/lib/presentation_layer/components/drawer/nostr_side_menu.dart index b21e1ebb..2a4f779e 100644 --- a/lib/presentation_layer/components/drawer/nostr_side_menu.dart +++ b/lib/presentation_layer/components/drawer/nostr_side_menu.dart @@ -280,13 +280,7 @@ class NostrSideMenu extends ConsumerWidget { routeName: 'payments', icon: PhosphorIcons.lightning(), onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - AppLocalizations.of(context)!.notImplementedYet, - ), - ), - ); + context.push('/wallet/dashboard'); }, ), _drawerItem( diff --git a/lib/presentation_layer/components/wallet/payment_history_short.dart b/lib/presentation_layer/components/wallet/payment_history_short.dart index 2c59abd2..14fd5437 100644 --- a/lib/presentation_layer/components/wallet/payment_history_short.dart +++ b/lib/presentation_layer/components/wallet/payment_history_short.dart @@ -3,7 +3,6 @@ import 'package:intl/intl.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:timeago/timeago.dart' as timeago; -import '../../../config/palette.dart'; import '../../../helpers/wallet_number_formatting.dart'; import '../../atoms/wallet/wallet_transaction_card.dart'; @@ -33,22 +32,24 @@ class PaymentHistoryShort extends StatelessWidget { Widget build(BuildContext context) { if (transactions.isEmpty && pendingTransactions.isEmpty) { return Padding( - padding: const EdgeInsets.symmetric( - vertical: 24.0, - ), + padding: const EdgeInsets.symmetric(vertical: 24.0), child: Center(child: Text(emptyText)), ); } - final sortedPending = - List.from(pendingTransactions); - sortedPending - .sort((a, b) => (_bestDate(b) ?? 0).compareTo(_bestDate(a) ?? 0)); + final sortedPending = List.from( + pendingTransactions, + ); + sortedPending.sort( + (a, b) => (_bestDate(b) ?? 0).compareTo(_bestDate(a) ?? 0), + ); - final sortedTransactions = - List.from(transactions); - sortedTransactions - .sort((a, b) => (_bestDate(b) ?? 0).compareTo(_bestDate(a) ?? 0)); + final sortedTransactions = List.from( + transactions, + ); + sortedTransactions.sort( + (a, b) => (_bestDate(b) ?? 0).compareTo(_bestDate(a) ?? 0), + ); final allTransactions = [...sortedPending, ...sortedTransactions]; @@ -61,14 +62,11 @@ class PaymentHistoryShort extends StatelessWidget { physics: physics, slivers: [ SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final tx = visible[index]; - - return WalletTransactionCard(tx: tx); - }, - childCount: visible.length, - ), + delegate: SliverChildBuilderDelegate((context, index) { + final tx = visible[index]; + + return WalletTransactionCard(tx: tx); + }, childCount: visible.length), ), ], ); diff --git a/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart b/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart index 753e6044..a548b493 100644 --- a/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart +++ b/lib/presentation_layer/components/wallet/wallet_account_card_placeholder.dart @@ -1,12 +1,7 @@ import 'package:flutter/material.dart'; -import '../../../config/palette.dart'; - class WalletAccountCardPlaceholder extends StatelessWidget { - const WalletAccountCardPlaceholder({ - super.key, - this.onTap, - }); + const WalletAccountCardPlaceholder({super.key, this.onTap}); final VoidCallback? onTap; @@ -16,11 +11,11 @@ class WalletAccountCardPlaceholder extends StatelessWidget { onTap: onTap, child: Container( decoration: BoxDecoration( - color: Paletter.extraDarkGray, + color: Theme.of(context).colorScheme.surfaceVariant, borderRadius: BorderRadius.circular(18.0), border: Border.all( width: 1, - color: Paletter.gray, + color: Theme.of(context).colorScheme.outline, ), ), child: Center( @@ -28,7 +23,7 @@ class WalletAccountCardPlaceholder extends StatelessWidget { 'add wallet +', style: TextStyle( fontSize: 16, - color: Colors.grey[700], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), diff --git a/lib/presentation_layer/components/wallet/wallet_actions_strip.dart b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart index ed31ce5b..1dc5dc73 100644 --- a/lib/presentation_layer/components/wallet/wallet_actions_strip.dart +++ b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../../config/palette.dart'; - class WalletActionsStrip extends StatelessWidget { final void Function() onScan; final void Function() onReceive; @@ -34,6 +32,7 @@ class WalletActionsStrip extends StatelessWidget { Container( padding: const EdgeInsets.all(2), child: _actionButton( + context: context, iconData: PhosphorIcons.barcode(), onTab: () => onScan(), text: "scan", @@ -42,6 +41,7 @@ class WalletActionsStrip extends StatelessWidget { Container( padding: const EdgeInsets.all(2), child: _actionButton( + context: context, iconData: PhosphorIcons.wallet(), onTab: () => onPay(), text: "pay", @@ -50,17 +50,21 @@ class WalletActionsStrip extends StatelessWidget { Container( padding: const EdgeInsets.all(2), child: _actionButton( - iconData: PhosphorIcons.piggyBank(), - onTab: () => onReceive(), - text: "receive"), + context: context, + iconData: PhosphorIcons.piggyBank(), + onTab: () => onReceive(), + text: "receive", + ), ), Container( - padding: const EdgeInsets.all(2), - child: _actionButton( - iconData: PhosphorIcons.receipt(), - text: "history", - onTab: () => onHistory(), - )), + padding: const EdgeInsets.all(2), + child: _actionButton( + context: context, + iconData: PhosphorIcons.receipt(), + text: "history", + onTab: () => onHistory(), + ), + ), ], ), ), @@ -72,6 +76,7 @@ Widget _actionButton({ required Function onTab, required IconData iconData, required String text, + required BuildContext context, Color? iconColor, Color? textColor, }) { @@ -87,14 +92,17 @@ Widget _actionButton({ ), child: Icon( iconData, - color: iconColor ?? Paletter.gray, + color: iconColor ?? Theme.of(context).colorScheme.onSurfaceVariant, size: 23, ), ), Text( text, - style: TextStyle(color: textColor ?? Paletter.gray), - ) + style: TextStyle( + color: textColor ?? Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), + ), ], ); } diff --git a/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart index 48265721..1cbf65b6 100644 --- a/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart +++ b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:ndk/entities.dart' as ndk_entities; -import '../../../config/palette.dart'; import '../../atoms/wallet/wallet_card.dart'; Future showWalletsSelectBottomSheet({ @@ -32,7 +31,9 @@ Future showWalletsSelectBottomSheet({ borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), boxShadow: [ BoxShadow( - blurRadius: 16, color: Theme.of(context).colorScheme.surface) + blurRadius: 16, + color: Theme.of(context).colorScheme.surface, + ), ], ), child: Material( @@ -66,8 +67,10 @@ Future showWalletsSelectBottomSheet({ fit: FlexFit.loose, child: ConstrainedBox( constraints: BoxConstraints( - maxHeight: - maxScrollableHeight.clamp(120.0, double.infinity), + maxHeight: maxScrollableHeight.clamp( + 120.0, + double.infinity, + ), ), child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(12, 8, 12, 16), @@ -85,8 +88,9 @@ Future showWalletsSelectBottomSheet({ isSelected: isSelected, onTap: (id) => Navigator.of(ctx).pop(id), tralling: Radio( - activeColor: - Theme.of(context).colorScheme.primary, + activeColor: Theme.of( + context, + ).colorScheme.primary, value: wallet.id, groupValue: isSelected ? wallet.id : null, onChanged: (value) { diff --git a/lib/presentation_layer/providers/event_verifier.dart b/lib/presentation_layer/providers/event_verifier.dart index 02411a7f..305f8d2b 100644 --- a/lib/presentation_layer/providers/event_verifier.dart +++ b/lib/presentation_layer/providers/event_verifier.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:ndk/ndk.dart'; -import 'package:ndk_rust_verifier/ndk_rust_verifier.dart'; import 'package:ndk_flutter/ndk_flutter.dart' show WebEventVerifier; import 'package:ndk/entities.dart' as ndk_entities; import 'package:riverpod/riverpod.dart'; diff --git a/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart b/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart index 50b33202..0973414d 100644 --- a/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart +++ b/lib/presentation_layer/routes/wallet/add_mint/add_mint_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/ndk.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../../../config/palette.dart'; +import 'package:riverpod/legacy.dart'; import '../../../atoms/long_button.dart'; import 'package:ndk/entities.dart' as ndk_entities; @@ -41,25 +41,23 @@ class ValidationState { class AddMintNotifier extends StateNotifier { final Ndk _ndk; - AddMintNotifier({ - required Ndk ndk, - }) : _ndk = ndk, - super(const ValidationState()); + AddMintNotifier({required Ndk ndk}) + : _ndk = ndk, + super(const ValidationState()); Timer? _debounceTimer; void updateText(String text) { - state = state.copyWith( - text: text, - isValid: false, - clearMintInfo: true, - ); + state = state.copyWith(text: text, isValid: false, clearMintInfo: true); _debounceTimer?.cancel(); if (text.isEmpty) { state = state.copyWith( - isValidating: false, isValid: null, clearMintInfo: true); + isValidating: false, + isValid: null, + clearMintInfo: true, + ); return; } @@ -69,16 +67,14 @@ class AddMintNotifier extends StateNotifier { } Future _validateText(String text) async { - state = state.copyWith( - isValidating: true, - clearMintInfo: true, - ); + state = state.copyWith(isValidating: true, clearMintInfo: true); final isValid = text.length >= 3 && !text.contains(' '); try { - final mintInfoNetwork = - await _ndk.cashu.getMintInfoNetwork(mintUrl: buildValidUrl(text)); + final mintInfoNetwork = await _ndk.cashu.getMintInfoNetwork( + mintUrl: buildValidUrl(text), + ); state = state.copyWith( isValidating: false, @@ -87,7 +83,10 @@ class AddMintNotifier extends StateNotifier { ); } catch (e) { state = state.copyWith( - isValidating: false, isValid: false, clearMintInfo: true); + isValidating: false, + isValid: false, + clearMintInfo: true, + ); debugPrint('Error validating mint: $e'); return; } @@ -111,12 +110,10 @@ class AddMintNotifier extends StateNotifier { } final addMintProvider = - StateNotifierProvider.autoDispose( - (ref) { - final ndk = ref.watch(ndkProvider); - return AddMintNotifier(ndk: ndk); - }, -); + StateNotifierProvider.autoDispose((ref) { + final ndk = ref.watch(ndkProvider); + return AddMintNotifier(ndk: ndk); + }); class AddMintPage extends ConsumerWidget { const AddMintPage({super.key}); @@ -140,9 +137,7 @@ class AddMintPage extends ConsumerWidget { if (validationState.mintInfo != null) SingleChildScrollView( - child: MintInfoCardSmall( - mintInfo: validationState.mintInfo!, - ), + child: MintInfoCardSmall(mintInfo: validationState.mintInfo!), ), const Spacer(flex: 2), @@ -163,12 +158,16 @@ class AddMintPage extends ConsumerWidget { decoration: InputDecoration( hintText: 'Enter mint address...', border: const OutlineInputBorder(), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Paletter.darkGray), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), borderRadius: BorderRadius.all(Radius.circular(12)), ), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Paletter.darkGray), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), borderRadius: BorderRadius.all(Radius.circular(12)), ), suffixIcon: _buildValidationIcon(validationState), @@ -193,9 +192,10 @@ class AddMintPage extends ConsumerWidget { if (validationState.isValid == true && validationState.mintInfo != null) { final validUrl = notifier.buildValidUrl(validationState.text); - ref.read(ndkProvider).cashu.addMintToKnownMints( - mintUrl: validUrl, - ); + ref + .read(ndkProvider) + .cashu + .addMintToKnownMints(mintUrl: validUrl); Navigator.pop(context); } }, @@ -213,10 +213,7 @@ class AddMintPage extends ConsumerWidget { } if (state.isValid == true) { - return Icon( - PhosphorIcons.checkCircle(), - color: Colors.green, - ); + return Icon(PhosphorIcons.checkCircle(), color: Colors.green); } return null; diff --git a/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart b/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart index 6f23021d..c9408913 100644 --- a/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart +++ b/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart @@ -2,17 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/entities.dart'; -import '../../../../config/palette.dart'; import '../../../atoms/wallet/mint_info_card_small.dart'; import '../wallet_providers/wallet_combined_state_provider.dart'; class MintInfoPage extends ConsumerWidget { final String? mintUrl; - const MintInfoPage({ - super.key, - this.mintUrl, - }); + const MintInfoPage({super.key, this.mintUrl}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -29,9 +25,7 @@ class MintInfoPage extends ConsumerWidget { backgroundColor: Theme.of(context).colorScheme.surface, title: Text('Mint Info'), ), - body: Center( - child: Text('Mint not found'), - ), + body: Center(child: Text('Mint not found')), ); } @@ -46,9 +40,7 @@ class MintInfoPage extends ConsumerWidget { body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), - child: MintInfoCardSmall( - mintInfo: myWallet.mintInfo, - ), + child: MintInfoCardSmall(mintInfo: myWallet.mintInfo), ), ), ); diff --git a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart index ee5bebfd..579c7811 100644 --- a/lib/presentation_layer/routes/wallet/wallet_dashboard.dart +++ b/lib/presentation_layer/routes/wallet/wallet_dashboard.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../../config/palette.dart'; import '../../atoms/my_profile_picture.dart'; import '../../components/drawer/nostr_drawer.dart'; import '../../components/wallet/payment_history_short.dart'; @@ -59,8 +58,9 @@ class _WalletDashboardState extends ConsumerState surfaceTintColor: Theme.of(context).colorScheme.surface, leading: Builder( builder: (context) { - final myMetadata = - ref.watch(metadataStateProvider(myUserPubkey)).userMetadata; + final myMetadata = ref + .watch(metadataStateProvider(myUserPubkey)) + .userMetadata; return InkWell( borderRadius: BorderRadius.circular(100), onTap: () => Scaffold.of(context).openDrawer(), @@ -76,10 +76,7 @@ class _WalletDashboardState extends ConsumerState ), actions: [ IconButton( - icon: Icon( - PhosphorIcons.plusCircle(), - size: 30, - ), + icon: Icon(PhosphorIcons.plusCircle(), size: 30), onPressed: () { context.push('/wallet/add_mint'); }, @@ -106,8 +103,9 @@ class _WalletDashboardState extends ConsumerState const SizedBox(height: 30), WalletActionsStrip( onScan: () { - final navigationNoti = - ref.read(walletNavigationProvider.notifier); + final navigationNoti = ref.read( + walletNavigationProvider.notifier, + ); navigationNoti.changeDashboardPage(0); }, onReceive: () async { @@ -143,8 +141,9 @@ class _WalletDashboardState extends ConsumerState } }, onHistory: () { - final navigationNoti = - ref.read(walletNavigationProvider.notifier); + final navigationNoti = ref.read( + walletNavigationProvider.notifier, + ); navigationNoti.changeMainPage(1); }, ), @@ -156,7 +155,7 @@ class _WalletDashboardState extends ConsumerState context.push('/wallet/transactions/detail', extra: tx); }, ), - ) + ), ], ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_navigation.dart b/lib/presentation_layer/routes/wallet/wallet_navigation.dart index 3e721ccf..35ea19d5 100644 --- a/lib/presentation_layer/routes/wallet/wallet_navigation.dart +++ b/lib/presentation_layer/routes/wallet/wallet_navigation.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod/legacy.dart'; import '../../components/wallet/sheet_send_receive.dart'; import 'wallet_dashboard.dart'; @@ -46,14 +47,16 @@ class WalletNavigationState { // StateNotifier for managing navigation class WalletNavigationNotifier extends StateNotifier { WalletNavigationNotifier() - : super(WalletNavigationState( + : super( + WalletNavigationState( selectedIndex: 0, dashboardIndex: 1, // Start at dashboard (index 1 in vertical PageView) mainPageController: PageController(initialPage: 0), dashboardPageController: PageController(initialPage: 1), pageChangeController: StreamController.broadcast(), - )); + ), + ); // Route mappings final Map _routeToIndex = { @@ -70,8 +73,11 @@ class WalletNavigationNotifier extends StateNotifier { }; // Change main page (horizontal navigation) - void changeMainPage(int index, - {bool animate = true, bool updateRoute = true}) { + void changeMainPage( + int index, { + bool animate = true, + bool updateRoute = true, + }) { if (index == state.selectedIndex) return; state = state.copyWith(selectedIndex: index); @@ -154,10 +160,11 @@ class WalletNavigationNotifier extends StateNotifier { // Provider final walletNavigationProvider = - StateNotifierProvider( - (ref) { - return WalletNavigationNotifier(); -}); + StateNotifierProvider(( + ref, + ) { + return WalletNavigationNotifier(); + }); // Convenience providers for easy access final selectedIndexProvider = Provider((ref) { @@ -204,9 +211,7 @@ class _WalletNavigationState extends ConsumerState showModalBottomSheet( backgroundColor: Colors.black, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), context: myContext, builder: (context) => WalletSheetSendReceive( @@ -239,7 +244,9 @@ class _WalletNavigationState extends ConsumerState void initState() { super.initState(); animationController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 200)); + vsync: this, + duration: const Duration(milliseconds: 200), + ); animationController.forward(); // current route from navigator @@ -276,10 +283,7 @@ class _WalletNavigationState extends ConsumerState scrollDirection: Axis.vertical, controller: dashboardPageController, onPageChanged: _onDashboardPageSwipe, - children: [ - WalletQrScan(), - WalletDashboard(), - ], + children: [WalletQrScan(), WalletDashboard()], ), WalletTransactionListPage(), ], diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart index 66874f3e..d4155eeb 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart @@ -5,7 +5,6 @@ import 'package:go_router/go_router.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../../../../config/palette.dart'; import '../../../../../helpers/wallet_number_formatting.dart'; import '../../../../atoms/copy_to_clipboard.dart'; import '../../../../atoms/long_button.dart'; @@ -26,22 +25,21 @@ class WalletPayDone extends ConsumerWidget { appBar: null, body: Padding( padding: const EdgeInsets.all(16.0), - child: Center( - child: _buildCurrentStep(walletPayState), - ), + child: Center(child: _buildCurrentStep(walletPayState)), ), bottomNavigationBar: SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: longButton( - name: "close", - onPressed: () { - payNotifier.reset(); + name: "close", + onPressed: () { + payNotifier.reset(); - context.pop(); - }, - inverted: true), + context.pop(); + }, + inverted: true, + ), ), ), ); @@ -77,16 +75,11 @@ class ProcessingStep extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator( - strokeWidth: 3, - ), + CircularProgressIndicator(strokeWidth: 3), SizedBox(height: 24), Text( 'Creating Token...', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), ), ], ); @@ -162,10 +155,7 @@ class SuccessStep extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - WalletNumberFormatting.formatAmount( - amount: amount, - unit: unit, - ), + WalletNumberFormatting.formatAmount(amount: amount, unit: unit), style: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, @@ -178,7 +168,7 @@ class SuccessStep extends StatelessWidget { style: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, - color: Paletter.extraLightGray, + color: Theme.of(context).colorScheme.surfaceContainerHighest, ), ), ], @@ -195,9 +185,7 @@ class SuccessStep extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(8), ), - child: AnimatedQr( - qrCodeData: outputToken!.toV4TokenString(), - ), + child: AnimatedQr(qrCodeData: outputToken!.toV4TokenString()), ), const SizedBox(height: 32), @@ -245,7 +233,7 @@ class TransactionState extends ConsumerWidget { if (myTransaction.state == ndk_entities.WalletTransactionState.pending) { icon = PhosphorIcons.hourglass(); - color = Paletter.lightGray; + color = Theme.of(context).colorScheme.surface; title = 'pending ecash'; } else if (myTransaction.state == ndk_entities.WalletTransactionState.failed) { diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart index 3749805c..e610a67b 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../../../../config/palette.dart'; import '../../../../atoms/currency_picker_bar.dart'; import '../../../../atoms/long_button.dart'; import '../../../../components/wallet/wallets_select_bottom_sheet.dart'; @@ -106,10 +105,7 @@ class _WalletPaySelectAmountState extends ConsumerState { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Theme.of(context).colorScheme.onError, - content: Text( - message, - style: TextStyle(color: Colors.white), - ), + content: Text(message, style: TextStyle(color: Colors.white)), ), ); } @@ -159,10 +155,7 @@ class _WalletPaySelectAmountState extends ConsumerState { .toList(), onTap: (_) {}, tralling: IconButton( - icon: Icon( - PhosphorIcons.notePencil(), - size: 25, - ), + icon: Icon(PhosphorIcons.notePencil(), size: 25), color: Theme.of(context).colorScheme.primary, onPressed: () async { final selectedId = await showWalletsSelectBottomSheet( @@ -185,11 +178,10 @@ class _WalletPaySelectAmountState extends ConsumerState { focusNode: _amountFocus, autofocus: true, keyboardType: TextInputType.numberWithOptions( - decimal: state.unit != 'sat'), + decimal: state.unit != 'sat', + ), inputFormatters: state.unit == 'sat' - ? [ - FilteringTextInputFormatter.digitsOnly, - ] + ? [FilteringTextInputFormatter.digitsOnly] : [ FilteringTextInputFormatter.allow(RegExp(r'[0-9\.,]')), DecimalTextInputFormatter(decimalRange: 2), @@ -218,21 +210,19 @@ class _WalletPaySelectAmountState extends ConsumerState { ? state.supportedUnitsByWallet!.toList() : [], initialIndex: state.unit != null - ? state.supportedUnitsByWallet! - .toList() - .indexOf(state.unit!) + ? state.supportedUnitsByWallet!.toList().indexOf( + state.unit!, + ) : 0, onChanged: (r) => { - payNotifier.updateUnit( - r.currentUnit, - ), + payNotifier.updateUnit(r.currentUnit), _onSwitchCurrency( previousUnit: r.previousUnit, currentUnit: r.currentUnit, ), }, showHaptics: true, - trackColor: Paletter.extraDarkGray, + trackColor: Theme.of(context).colorScheme.onSurface, activeColor: Theme.of(context).colorScheme.primary, ), ), @@ -266,21 +256,22 @@ class _WalletPaySelectAmountState extends ConsumerState { child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: longButton( - name: "next", - onPressed: () { - if (state.payFromWalletId == null || - state.payFromWalletId!.isEmpty) { - showSnackBar(context, 'Please select a wallet to pay from.'); - return; - } - if (state.amount == null || state.amount! <= 0) { - _amountFocus.requestFocus(); - showSnackBar(context, 'Please enter a valid amount.'); - return; - } - widget.doneCallback(); - }, - inverted: true), + name: "next", + onPressed: () { + if (state.payFromWalletId == null || + state.payFromWalletId!.isEmpty) { + showSnackBar(context, 'Please select a wallet to pay from.'); + return; + } + if (state.amount == null || state.amount! <= 0) { + _amountFocus.requestFocus(); + showSnackBar(context, 'Please enter a valid amount.'); + return; + } + widget.doneCallback(); + }, + inverted: true, + ), ), ), ); @@ -289,7 +280,7 @@ class _WalletPaySelectAmountState extends ConsumerState { class DecimalTextInputFormatter extends TextInputFormatter { DecimalTextInputFormatter({required this.decimalRange}) - : assert(decimalRange > 0); + : assert(decimalRange > 0); final int decimalRange; diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart index 2ddd697c..84b0e900 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/entities.dart' as ndk_entities; -import '../../../../../config/palette.dart'; import '../../../../../domain_layer/entities/user_metadata.dart'; import '../../../../../helpers/helpers.dart'; @@ -73,25 +72,30 @@ class WalletSelectReciever extends ConsumerWidget { bottom: PreferredSize( preferredSize: const Size.fromHeight(64), child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), child: TextField( onChanged: notifier.setSearchQuery, decoration: InputDecoration( isDense: true, hintText: ' Search by name', - hintStyle: - const TextStyle(color: Colors.white, letterSpacing: 1.1), + hintStyle: const TextStyle( + color: Colors.white, + letterSpacing: 1.1, + ), filled: true, fillColor: Theme.of(context).colorScheme.surface, enabledBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(50.0)), - borderSide: BorderSide(color: Paletter.extraDarkGray), + borderSide: BorderSide(color: Colors.grey), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(25.0)), - borderSide: - BorderSide(color: Theme.of(context).colorScheme.surface), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.surface, + ), ), ), ), @@ -194,11 +198,12 @@ class WalletSelectReciever extends ConsumerWidget { width: double.infinity, height: 45, child: longButton( - name: "next", - onPressed: () { - _onTokenSelected(paymentStateNotifier); - }, - inverted: true), + name: "next", + onPressed: () { + _onTokenSelected(paymentStateNotifier); + }, + inverted: true, + ), ), ), ), @@ -271,11 +276,8 @@ class _ContactsList extends StatelessWidget { ), title: Text(c.name ?? ''), subtitle: Text( - c.nip05 ?? - Helpers.shortHr( - c.pubkey, - ), - style: TextStyle(color: Paletter.gray), + c.nip05 ?? Helpers.shortHr(c.pubkey), + style: TextStyle(color: Colors.grey), ), onTap: () => onTap(c), ); @@ -325,15 +327,15 @@ class _WalletsList extends StatelessWidget { subtitle: Text(w.type.toString()), /// show all balances for the wallet - trailing: Column(children: [ - for (final b in wBallances) - Text( - "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", - style: TextStyle( - color: Colors.white, + trailing: Column( + children: [ + for (final b in wBallances) + Text( + "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", + style: TextStyle(color: Colors.white), ), - ), - ]), + ], + ), onTap: () => onTap(w), ); diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart index 91962eb4..ad2f1647 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever_state_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; +import 'package:riverpod/legacy.dart'; import '../../../../../domain_layer/entities/user_metadata.dart'; import '../../../../../domain_layer/usecases/get_user_metadata.dart'; @@ -53,9 +54,11 @@ class WalletPayRecieverState { if (q.isEmpty) return const []; return allContacts - .where((c) => - (c.name != null && c.name!.toLowerCase().contains(q)) || - (c.pubkey.toLowerCase().contains(q))) + .where( + (c) => + (c.name != null && c.name!.toLowerCase().contains(q)) || + (c.pubkey.toLowerCase().contains(q)), + ) .toList(); } @@ -78,20 +81,20 @@ class WalletPayToNotifier extends StateNotifier { required Ndk ndk, required List contactPubkeys, required GetUserMetadata getUserMetadata, - }) : _ndk = ndk, - _contactPubkeys = contactPubkeys, - _getUserMetadata = getUserMetadata, - super( - WalletPayRecieverState( - wallets: const [], - balances: const [], - allContacts: const [], - recentContacts: const [], - selectedWalletId: initialWalletId, - searchQuery: '', - isLoading: true, - ), - ) { + }) : _ndk = ndk, + _contactPubkeys = contactPubkeys, + _getUserMetadata = getUserMetadata, + super( + WalletPayRecieverState( + wallets: const [], + balances: const [], + allContacts: const [], + recentContacts: const [], + selectedWalletId: initialWalletId, + searchQuery: '', + isLoading: true, + ), + ) { _loadInitial(); } @@ -136,20 +139,22 @@ class WalletPayToNotifier extends StateNotifier { } } -final walletPayRecieverProvider = StateNotifierProvider.family< - WalletPayToNotifier, - WalletPayRecieverState, - String?>((ref, initialWalletId) { - final ndk = ref.watch(ndkProvider); +final walletPayRecieverProvider = + StateNotifierProvider.family< + WalletPayToNotifier, + WalletPayRecieverState, + String? + >((ref, initialWalletId) { + final ndk = ref.watch(ndkProvider); - final contacts = ref.watch(contactListSelfStateProvider); + final contacts = ref.watch(contactListSelfStateProvider); - final getUserMetadata = ref.watch(metadataProvider); + final getUserMetadata = ref.watch(metadataProvider); - return WalletPayToNotifier( - initialWalletId: initialWalletId, - ndk: ndk, - contactPubkeys: contacts.contactList.contacts, - getUserMetadata: getUserMetadata, - ); -}); + return WalletPayToNotifier( + initialWalletId: initialWalletId, + ndk: ndk, + contactPubkeys: contacts.contactList.contacts, + getUserMetadata: getUserMetadata, + ); + }); diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart index edc641dc..3fdca5a0 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_state_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; +import 'package:riverpod/legacy.dart'; import '../../../providers/ndk_provider.dart'; import '../wallet_providers/wallet_combined_state_provider.dart'; @@ -120,20 +121,20 @@ class WalletPayNotifier extends StateNotifier { final Ref ref; WalletPayNotifier({required Ndk ndk, required this.ref}) - : _ndk = ndk, - super( - WalletPayState( - availableWallets: [], - availableBalances: [], - payFromWalletId: null, - amount: null, - unit: null, - memo: null, - recieverType: null, - payToPubkey: null, - payToWalletId: null, - ), - ) { + : _ndk = ndk, + super( + WalletPayState( + availableWallets: [], + availableBalances: [], + payFromWalletId: null, + amount: null, + unit: null, + memo: null, + recieverType: null, + payToPubkey: null, + payToWalletId: null, + ), + ) { // listen to state changes ref.listen(walletCombinedProvider, (previous, next) { if (next.balances != previous?.balances || @@ -154,18 +155,17 @@ class WalletPayNotifier extends StateNotifier { (state.recieverType == PaymentRecieverType.contact ? state.payToPubkey != null : state.recieverType == PaymentRecieverType.wallet - ? state.payToWalletId != null - : true); + ? state.payToWalletId != null + : true); } void updatePayFromWalletId(String walletId) { - state = state.copyWith( - payFromWalletId: walletId, - ); + state = state.copyWith(payFromWalletId: walletId); state = state.copyWith( supportedUnitsByWallet: state.payFromWallet?.supportedUnits, - unit: - state.unit == null ? state.payFromWallet?.supportedUnits.first : null, + unit: state.unit == null + ? state.payFromWallet?.supportedUnits.first + : null, ); } @@ -234,9 +234,7 @@ class WalletPayNotifier extends StateNotifier { transactionId: result.transaction.id, ); } catch (e) { - setError( - errorMessage: e.toString(), - ); + setError(errorMessage: e.toString()); return; } @@ -265,6 +263,6 @@ class WalletPayNotifier extends StateNotifier { final walletPayStateProvider = StateNotifierProvider((ref) { - final ndk = ref.watch(ndkProvider); - return WalletPayNotifier(ndk: ndk, ref: ref); -}); + final ndk = ref.watch(ndkProvider); + return WalletPayNotifier(ndk: ndk, ref: ref); + }); diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart index 56c9dd97..6315802b 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -6,7 +6,6 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:ndk/entities.dart' as ndk_entities; -import '../../../../../config/palette.dart'; import '../../../../../helpers/wallet_number_formatting.dart'; import '../../../../atoms/wallet/wallet_card.dart'; import '../../../../components/wallet/wallets_select_bottom_sheet.dart'; @@ -15,19 +14,13 @@ import '../wallet_pay_state_provider.dart'; class WalletPaySummary extends ConsumerWidget { final Function backCallback; - const WalletPaySummary({ - super.key, - required this.backCallback, - }); + const WalletPaySummary({super.key, required this.backCallback}); showSnackBar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Theme.of(context).colorScheme.error, - content: Text( - message, - style: TextStyle(color: Colors.white), - ), + content: Text(message, style: TextStyle(color: Colors.white)), ), ); } @@ -63,8 +56,10 @@ class WalletPaySummary extends ConsumerWidget { ); if (availableBalanceForUnit.isEmpty || availableBalanceForUnit.first.amount < state.amount!) { - showSnackBar(context, - 'Insufficient balance in the selected wallet for the specified unit'); + showSnackBar( + context, + 'Insufficient balance in the selected wallet for the specified unit', + ); return false; } @@ -89,8 +84,9 @@ class WalletPaySummary extends ConsumerWidget { Container( width: double.infinity, decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.9), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), @@ -124,7 +120,7 @@ class WalletPaySummary extends ConsumerWidget { IconButton( icon: Icon( Icons.close, - color: Paletter.lightGray, + color: Theme.of(context).colorScheme.onSurface, size: 24, ), onPressed: () { @@ -144,7 +140,9 @@ class WalletPaySummary extends ConsumerWidget { Text( state.amount != null && state.unit != null ? WalletNumberFormatting.formatAmount( - amount: state.amount!, unit: state.unit!) + amount: state.amount!, + unit: state.unit!, + ) : '0', style: TextStyle( color: Colors.white, @@ -157,7 +155,7 @@ class WalletPaySummary extends ConsumerWidget { icon: Icon( PhosphorIcons.notePencil(), size: 24, - color: Paletter.extraLightGray, + color: Theme.of(context).colorScheme.primary, ), onPressed: () => backCallback(), ), @@ -210,19 +208,16 @@ class WalletPaySummary extends ConsumerWidget { .toList(), onTap: (_) {}, tralling: IconButton( - icon: Icon( - PhosphorIcons.notePencil(), - size: 25, - ), + icon: Icon(PhosphorIcons.notePencil(), size: 25), color: Theme.of(context).colorScheme.primary, onPressed: () async { final selectedId = await showWalletsSelectBottomSheet( - context: context, - selectedId: state.payFromWalletId, - wallets: state.availableWallets, - balances: state.availableBalances, - ); + context: context, + selectedId: state.payFromWalletId, + wallets: state.availableWallets, + balances: state.availableBalances, + ); if (selectedId != null) { payNotifier.updatePayFromWalletId(selectedId); } @@ -230,7 +225,10 @@ class WalletPaySummary extends ConsumerWidget { ), ), - Divider(color: Paletter.darkGray, height: 1), + Divider( + color: Theme.of(context).colorScheme.surface, + height: 1, + ), /// receiver if (state.recieverType == @@ -251,18 +249,20 @@ class WalletPaySummary extends ConsumerWidget { /// details _buildDetailRow( - label: 'transaction type', - value: state.recieverType.toString(), - isEditable: false, - context: context), + label: 'transaction type', + value: state.recieverType.toString(), + isEditable: false, + context: context, + ), _buildDetailRow( - label: 'Memo', - value: state.memo ?? '', - onEdit: () { - backCallback(); - }, - context: context), + label: 'Memo', + value: state.memo ?? '', + onEdit: () { + backCallback(); + }, + context: context, + ), SizedBox(height: 24), @@ -278,7 +278,10 @@ class WalletPaySummary extends ConsumerWidget { child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: longButton( - name: "send", onPressed: () => onSend(), inverted: true), + name: "send", + onPressed: () => onSend(), + inverted: true, + ), ), ), ); @@ -305,10 +308,7 @@ class WalletPaySummary extends ConsumerWidget { ), if (isEditable && onEdit != null) IconButton( - icon: Icon( - PhosphorIcons.notePencil(), - size: 25, - ), + icon: Icon(PhosphorIcons.notePencil(), size: 25), color: Theme.of(context).colorScheme.primary, onPressed: isEditable ? onEdit : null, ), @@ -319,9 +319,7 @@ class WalletPaySummary extends ConsumerWidget { } class ContactReciever extends StatelessWidget { - const ContactReciever({ - super.key, - }); + const ContactReciever({super.key}); @override Widget build(BuildContext context) { @@ -330,22 +328,31 @@ class ContactReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), - child: - Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.8), + child: Text( + 'NA', + style: TextStyle(color: Colors.white, fontSize: 12), + ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('receiver', - style: TextStyle(color: Colors.grey, fontSize: 12)), - Text('NOT IMPLEMENTED', - style: TextStyle(color: Colors.white, fontSize: 16)), - Text('send to pubkey not implemented', - style: TextStyle(color: Colors.grey, fontSize: 14)), + Text( + 'receiver', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + Text( + 'NOT IMPLEMENTED', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + Text( + 'send to pubkey not implemented', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), ], ), ), @@ -356,9 +363,7 @@ class ContactReciever extends StatelessWidget { } class TokenReciever extends StatelessWidget { - const TokenReciever({ - super.key, - }); + const TokenReciever({super.key}); @override Widget build(BuildContext context) { @@ -367,25 +372,41 @@ class TokenReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), - child: - Text('TK', style: TextStyle(color: Colors.white, fontSize: 12)), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.8), + child: Text( + 'TK', + style: TextStyle(color: Colors.white, fontSize: 12), + ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('receiver', - style: TextStyle(color: Paletter.gray, fontSize: 12)), - Text('Token', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold)), - Text('send as a token or qr code', - style: TextStyle(color: Paletter.lightGray, fontSize: 14)), + Text( + 'receiver', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 12, + ), + ), + Text( + 'Token', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'send as a token or qr code', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 14, + ), + ), ], ), ), @@ -396,9 +417,7 @@ class TokenReciever extends StatelessWidget { } class WalletReciever extends StatelessWidget { - const WalletReciever({ - super.key, - }); + const WalletReciever({super.key}); @override Widget build(BuildContext context) { @@ -407,22 +426,31 @@ class WalletReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), - child: - Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.8), + child: Text( + 'NA', + style: TextStyle(color: Colors.white, fontSize: 12), + ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('receiver', - style: TextStyle(color: Colors.grey, fontSize: 12)), - Text('NOT IMPLEMENTED', - style: TextStyle(color: Colors.white, fontSize: 16)), - Text('send to wallet not implemented', - style: TextStyle(color: Colors.grey, fontSize: 14)), + Text( + 'receiver', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + Text( + 'NOT IMPLEMENTED', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + Text( + 'send to wallet not implemented', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), ], ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart index 49bd34cd..4a113368 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/qr_value_processing_state_provider.dart @@ -1,3 +1,4 @@ +import 'package:flutter_riverpod/legacy.dart'; import 'package:riverpod/riverpod.dart'; class QRScannerState { @@ -28,37 +29,27 @@ class QRScannerState { } } -enum QRNavigationTarget { - rcvPage, - sendPage, -} +enum QRNavigationTarget { rcvPage, sendPage } class QRScanTypeResult { final String value; final QRScanTypes type; - QRScanTypeResult({ - required this.value, - required this.type, - }); + QRScanTypeResult({required this.value, required this.type}); } -enum QRScanTypes { - cashuToken, - lightningInvoice, - unknown, -} +enum QRScanTypes { cashuToken, lightningInvoice, unknown } class QRScannerNotifier extends StateNotifier { QRScannerNotifier() - : super( - QRScannerState( - isProcessing: false, - error: null, - navigationTarget: null, - navigationData: null, - ), - ); + : super( + QRScannerState( + isProcessing: false, + error: null, + navigationTarget: null, + navigationData: null, + ), + ); void setError(String error) { state = state.copyWith(error: error); @@ -105,24 +96,18 @@ class QRScannerNotifier extends StateNotifier { Future _analyzeQRData(String qrData) async { // Analyze the QR data and return the appropriate type if (qrData.startsWith('cashuB')) { - return QRScanTypeResult( - value: qrData, - type: QRScanTypes.cashuToken, - ); + return QRScanTypeResult(value: qrData, type: QRScanTypes.cashuToken); } else if (qrData.startsWith('lightning:')) { return QRScanTypeResult( value: qrData, type: QRScanTypes.lightningInvoice, ); } - return QRScanTypeResult( - value: qrData, - type: QRScanTypes.unknown, - ); + return QRScanTypeResult(value: qrData, type: QRScanTypes.unknown); } } final qrScannerProvider = StateNotifierProvider.autoDispose( - (ref) => QRScannerNotifier(), -); + (ref) => QRScannerNotifier(), + ); diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart index 9a041a77..c01f22fc 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; +import 'package:riverpod/legacy.dart'; import 'package:riverpod/riverpod.dart'; @@ -37,15 +38,16 @@ class WalletCombinedState { } } -final walletCombinedProvider = StateNotifierProvider.autoDispose< - WalletCombinedStateNotifier, WalletCombinedState>( - (ref) { - final ndk = ref.watch(ndkProvider); - final ndkDb = ref.watch(dbNdkProvider)!; +final walletCombinedProvider = + StateNotifierProvider.autoDispose< + WalletCombinedStateNotifier, + WalletCombinedState + >((ref) { + final ndk = ref.watch(ndkProvider); + final ndkDb = ref.watch(dbNdkProvider)!; - return WalletCombinedStateNotifier(ndk, ndkDb); - }, -); + return WalletCombinedStateNotifier(ndk, ndkDb); + }); class WalletCombinedStateNotifier extends StateNotifier { final Ndk _ndk; @@ -53,16 +55,15 @@ class WalletCombinedStateNotifier extends StateNotifier { final _subscriptions = []; - WalletCombinedStateNotifier( - this._ndk, - this.ndkDb, - ) : super( - WalletCombinedState( - balances: [], - recentTransactions: [], - pendingTransactions: [], - wallets: []), - ) { + WalletCombinedStateNotifier(this._ndk, this.ndkDb) + : super( + WalletCombinedState( + balances: [], + recentTransactions: [], + pendingTransactions: [], + wallets: [], + ), + ) { _initializeState(); } diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart index 2a9eed77..002fc9e4 100644 --- a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -5,7 +5,6 @@ import 'package:go_router/go_router.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../../config/palette.dart'; import '../../atoms/spinner_center.dart'; import 'wallet_navigation.dart'; import 'wallet_providers/qr_value_processing_state_provider.dart'; @@ -74,8 +73,9 @@ class _QrScan extends ConsumerState { next.navigationData != null) { ref.read(qrScannerProvider.notifier).clearNavigation(); - final rcvProvider = - ref.read(walletReceiveEcashCompleterProvider.notifier); + final rcvProvider = ref.read( + walletReceiveEcashCompleterProvider.notifier, + ); switch (next.navigationTarget!) { case QRNavigationTarget.rcvPage: @@ -88,7 +88,8 @@ class _QrScan extends ConsumerState { break; case QRNavigationTarget.sendPage: throw UnimplementedError( - 'Send page navigation is not implemented yet'); + 'Send page navigation is not implemented yet', + ); break; } } @@ -106,7 +107,9 @@ class _QrScan extends ConsumerState { child: Text( 'Error: $p1', style: TextStyle( - color: Theme.of(context).colorScheme.error, fontSize: 16), + color: Theme.of(context).colorScheme.error, + fontSize: 16, + ), ), ); }, @@ -131,16 +134,14 @@ class _QrScan extends ConsumerState { /// scanning indicator Center( child: Container( - width: 250, - height: 250, - decoration: BoxDecoration( - border: Border.all( - color: Colors.white, - width: 2, - ), - borderRadius: BorderRadius.circular(20), - ), - child: null), + width: 250, + height: 250, + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2), + borderRadius: BorderRadius.circular(20), + ), + child: null, + ), ), /// bottom area @@ -174,14 +175,12 @@ class _QrScan extends ConsumerState { } }); }, - icon: Icon( - PhosphorIcons.clipboardText(), - size: 18, - ), + icon: Icon(PhosphorIcons.clipboardText(), size: 18), label: const Text('paste from clipboard'), style: ElevatedButton.styleFrom( - backgroundColor: - Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of( + context, + ).colorScheme.primary, foregroundColor: Colors.white, ), ), @@ -199,7 +198,9 @@ class _QrScan extends ConsumerState { right: 20, child: LinearProgressIndicator( borderRadius: BorderRadius.circular(20), - backgroundColor: Paletter.extraDarkGray, + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.5), color: Colors.white, ), ), @@ -216,7 +217,9 @@ class _QrScan extends ConsumerState { qrScannerState.error != null ? Container( padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(50), diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart index bb33030c..aeac14ca 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../../../../config/palette.dart'; import '../../../../../helpers/wallet_number_formatting.dart'; import '../../../../atoms/long_button.dart'; import 'wallet_rcv_ecash_completer_state_provider.dart'; @@ -37,15 +36,17 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { children: [ Text( '+', - style: const TextStyle( - color: Paletter.lightGray, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontSize: 28, ), ), const SizedBox(width: 8), Text( WalletNumberFormatting.formatAmount( - amount: walletState.amount!, unit: walletState.unit!), + amount: walletState.amount!, + unit: walletState.unit!, + ), style: const TextStyle( color: Colors.white, fontSize: 48, @@ -55,8 +56,10 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { const SizedBox(width: 8), Text( walletState.unit ?? '', - style: const TextStyle( - fontSize: 16, color: Paletter.lightGray), + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), ), ], ), @@ -73,10 +76,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { child: SingleChildScrollView( child: Text( walletState.memo!, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), + style: const TextStyle(color: Colors.white, fontSize: 16), textAlign: TextAlign.center, ), ), @@ -97,14 +97,19 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: longButton( - name: "close", onPressed: () => {context.pop()}, inverted: true), + name: "close", + onPressed: () => {context.pop()}, + inverted: true, + ), ), ), ); } Widget _buildStatusCard( - WalletRcvEcashCompleterState state, BuildContext context) { + WalletRcvEcashCompleterState state, + BuildContext context, + ) { IconData icon; Color color; String title; @@ -127,7 +132,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { subtitle = 'received successfully'; } else { icon = PhosphorIcons.info(); - color = Paletter.gray; + color = Colors.grey; title = 'Ready'; subtitle = 'waiting for transaction'; } @@ -144,11 +149,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(50), ), - child: Icon( - icon, - color: color, - size: 32, - ), + child: Icon(icon, color: color, size: 32), ), const SizedBox(width: 16), Expanded( @@ -165,10 +166,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { const SizedBox(height: 4), Text( subtitle, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ], ), @@ -194,11 +192,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.error_outline, - color: Colors.red[700], - size: 24, - ), + Icon(Icons.error_outline, color: Colors.red[700], size: 24), const SizedBox(width: 12), Expanded( child: Column( @@ -215,10 +209,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { const SizedBox(height: 4), Text( errorMessage, - style: TextStyle( - fontSize: 14, - color: Colors.red[600], - ), + style: TextStyle(fontSize: 14, color: Colors.red[600]), ), ], ), diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart index 33c1a56f..ca40786f 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_state_provider.dart @@ -1,12 +1,11 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; +import 'package:riverpod/legacy.dart'; import 'package:riverpod/riverpod.dart'; import '../../../../providers/ndk_provider.dart'; -enum WalletRcvType { - eCash, -} +enum WalletRcvType { eCash } class WalletRcvEcashCompleterState { final bool isPending; @@ -66,19 +65,17 @@ class WalletRcvEcashCompleterNotifier extends StateNotifier { final Ndk _ndk; WalletRcvEcashCompleterNotifier({required Ndk ndk}) - : _ndk = ndk, - super( - WalletRcvEcashCompleterState( - isPending: false, - isCompleted: false, - isError: false, - isSuccess: false, - ), - ); + : _ndk = ndk, + super( + WalletRcvEcashCompleterState( + isPending: false, + isCompleted: false, + isError: false, + isSuccess: false, + ), + ); - void receiveEcash({ - required String tokenString, - }) async { + void receiveEcash({required String tokenString}) async { reset(); state = state.copyWith(isPending: true); @@ -139,10 +136,11 @@ class WalletRcvEcashCompleterNotifier } } -final walletReceiveEcashCompleterProvider = StateNotifierProvider< - WalletRcvEcashCompleterNotifier, WalletRcvEcashCompleterState>( - (ref) { - final ndk = ref.watch(ndkProvider); - return WalletRcvEcashCompleterNotifier(ndk: ndk); - }, -); +final walletReceiveEcashCompleterProvider = + StateNotifierProvider< + WalletRcvEcashCompleterNotifier, + WalletRcvEcashCompleterState + >((ref) { + final ndk = ref.watch(ndkProvider); + return WalletRcvEcashCompleterNotifier(ndk: ndk); + }); diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart index fc45edc8..6430a972 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../../../../../config/palette.dart'; import '../../../../atoms/currency_picker_bar.dart'; import '../../../../atoms/long_button.dart'; import '../../../../atoms/wallet/wallet_card.dart'; @@ -109,10 +108,7 @@ class _WalletPaySelectAmountState extends ConsumerState { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Theme.of(context).colorScheme.error, - content: Text( - message, - style: TextStyle(color: Colors.white), - ), + content: Text(message, style: TextStyle(color: Colors.white)), ), ); } @@ -124,17 +120,15 @@ class _WalletPaySelectAmountState extends ConsumerState { final combinedWallets = ref.watch(walletCombinedProvider); - final List mySelectedWalletList = - combinedWallets.wallets - .where( - (w) => w.id == state.recieveToWalletId, - ) - .toList(); + final List mySelectedWalletList = combinedWallets + .wallets + .where((w) => w.id == state.recieveToWalletId) + .toList(); final ndk_entities.CashuWallet? mySelectedWallet = mySelectedWalletList.isNotEmpty - ? mySelectedWalletList.first as ndk_entities.CashuWallet - : null; + ? mySelectedWalletList.first as ndk_entities.CashuWallet + : null; final List supportedUnitsBySelectedWallet = mySelectedWallet?.supportedUnits.toList() ?? []; @@ -176,10 +170,7 @@ class _WalletPaySelectAmountState extends ConsumerState { .toList(), onTap: (_) {}, tralling: IconButton( - icon: Icon( - PhosphorIcons.notePencil(), - size: 25, - ), + icon: Icon(PhosphorIcons.notePencil(), size: 25), color: Theme.of(context).colorScheme.primary, onPressed: () async { final selectedId = await showWalletsSelectBottomSheet( @@ -202,11 +193,10 @@ class _WalletPaySelectAmountState extends ConsumerState { focusNode: _amountFocus, autofocus: true, keyboardType: TextInputType.numberWithOptions( - decimal: state.unit != 'sat'), + decimal: state.unit != 'sat', + ), inputFormatters: state.unit == 'sat' - ? [ - FilteringTextInputFormatter.digitsOnly, - ] + ? [FilteringTextInputFormatter.digitsOnly] : [ FilteringTextInputFormatter.allow(RegExp(r'[0-9\.,]')), DecimalTextInputFormatter(decimalRange: 2), @@ -236,16 +226,16 @@ class _WalletPaySelectAmountState extends ConsumerState { ? supportedUnitsBySelectedWallet.indexOf(state.unit!) : 0, onChanged: (r) => { - notifier.updateUnit( - r.currentUnit, - ), + notifier.updateUnit(r.currentUnit), _onSwitchCurrency( previousUnit: r.previousUnit, currentUnit: r.currentUnit, ), }, showHaptics: true, - trackColor: Paletter.extraDarkGray, + trackColor: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.1), activeColor: Theme.of(context).colorScheme.primary, ), ), @@ -282,23 +272,26 @@ class _WalletPaySelectAmountState extends ConsumerState { width: double.infinity, height: 40, child: longButton( - name: "create request", - onPressed: () { - if (state.recieveToWalletId == null || - state.recieveToWalletId!.isEmpty) { - showSnackBar( - context, 'Please select a wallet to receive to.'); - return; - } - if (state.amount == null || state.amount! <= 0) { - _amountFocus.requestFocus(); - showSnackBar(context, 'Please enter a valid amount.'); - return; - } - notifier.mintEcashToken(); - widget.doneCallback(); - }, - inverted: true), + name: "create request", + onPressed: () { + if (state.recieveToWalletId == null || + state.recieveToWalletId!.isEmpty) { + showSnackBar( + context, + 'Please select a wallet to receive to.', + ); + return; + } + if (state.amount == null || state.amount! <= 0) { + _amountFocus.requestFocus(); + showSnackBar(context, 'Please enter a valid amount.'); + return; + } + notifier.mintEcashToken(); + widget.doneCallback(); + }, + inverted: true, + ), ), ), ), @@ -308,7 +301,7 @@ class _WalletPaySelectAmountState extends ConsumerState { class DecimalTextInputFormatter extends TextInputFormatter { DecimalTextInputFormatter({required this.decimalRange}) - : assert(decimalRange > 0); + : assert(decimalRange > 0); final int decimalRange; diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart index 69dcda9b..a0829ecc 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_request/wallet_receive_request.dart @@ -7,7 +7,6 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:pretty_qr_code/pretty_qr_code.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../../../config/palette.dart'; import '../../../../../helpers/wallet_number_formatting.dart'; import '../../../../atoms/copy_to_clipboard.dart'; @@ -16,19 +15,13 @@ import '../wallet_receive_state_provider.dart'; class WalletReceiveRequest extends ConsumerWidget { final Function backCallback; - const WalletReceiveRequest({ - super.key, - required this.backCallback, - }); + const WalletReceiveRequest({super.key, required this.backCallback}); showSnackBar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Theme.of(context).colorScheme.error, - content: Text( - message, - style: TextStyle(color: Colors.white), - ), + content: Text(message, style: TextStyle(color: Colors.white)), ), ); } @@ -56,8 +49,9 @@ class WalletReceiveRequest extends ConsumerWidget { Container( width: double.infinity, decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.9), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), @@ -91,7 +85,7 @@ class WalletReceiveRequest extends ConsumerWidget { IconButton( icon: Icon( Icons.close, - color: Paletter.lightGray, + color: Theme.of(context).colorScheme.onSurface, size: 24, ), onPressed: () { @@ -111,9 +105,7 @@ class WalletReceiveRequest extends ConsumerWidget { Stack( children: [ Container( - constraints: BoxConstraints( - maxHeight: 300, - ), + constraints: BoxConstraints(maxHeight: 300), padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, @@ -139,8 +131,9 @@ class WalletReceiveRequest extends ConsumerWidget { aspectRatio: 1.0, child: Container( decoration: BoxDecoration( - color: - Colors.black.withValues(alpha: 0.7), + color: Colors.black.withValues( + alpha: 0.7, + ), borderRadius: BorderRadius.circular(12), ), child: Column( @@ -169,13 +162,13 @@ class WalletReceiveRequest extends ConsumerWidget { ), SizedBox(height: 16), Container( - constraints: BoxConstraints( - maxWidth: 300, - ), + constraints: BoxConstraints(maxWidth: 300), child: CopyClipboardButton( value: state.request!, copyText: "Copy invoice", - backgroundColor: Paletter.extraDarkGray, + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.8), ), ), ], @@ -205,17 +198,16 @@ class WalletReceiveRequest extends ConsumerWidget { label: "Amount", value: state.amount != null ? WalletNumberFormatting.formatAmount( - amount: state.amount!, unit: state.unit!) + amount: state.amount!, + unit: state.unit!, + ) : "Not set", ), _buildDetailRow( label: "Unit", value: state.unit ?? "Not set", ), - _buildDetailRow( - label: "Memo", - value: state.memo ?? "", - ), + _buildDetailRow(label: "Memo", value: state.memo ?? ""), Spacer(), ], ), @@ -228,18 +220,16 @@ class WalletReceiveRequest extends ConsumerWidget { child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: longButton( - name: "close", - onPressed: () => {rcvNotifier.reset(), context.pop()}, - inverted: true), + name: "close", + onPressed: () => {rcvNotifier.reset(), context.pop()}, + inverted: true, + ), ), ), ); } - Widget _buildDetailRow({ - required String label, - required String value, - }) { + Widget _buildDetailRow({required String label, required String value}) { return Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Row( @@ -259,9 +249,7 @@ class WalletReceiveRequest extends ConsumerWidget { } class ContactReciever extends StatelessWidget { - const ContactReciever({ - super.key, - }); + const ContactReciever({super.key}); @override Widget build(BuildContext context) { @@ -270,22 +258,31 @@ class ContactReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), - child: - Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.8), + child: Text( + 'NA', + style: TextStyle(color: Colors.white, fontSize: 12), + ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('receiver', - style: TextStyle(color: Colors.grey, fontSize: 12)), - Text('NOT IMPLEMENTED', - style: TextStyle(color: Colors.white, fontSize: 16)), - Text('send to pubkey not implemented', - style: TextStyle(color: Colors.grey, fontSize: 14)), + Text( + 'receiver', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + Text( + 'NOT IMPLEMENTED', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + Text( + 'send to pubkey not implemented', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), ], ), ), @@ -296,9 +293,7 @@ class ContactReciever extends StatelessWidget { } class TokenReciever extends StatelessWidget { - const TokenReciever({ - super.key, - }); + const TokenReciever({super.key}); @override Widget build(BuildContext context) { @@ -307,25 +302,41 @@ class TokenReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), - child: - Text('TK', style: TextStyle(color: Colors.white, fontSize: 12)), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.8), + child: Text( + 'TK', + style: TextStyle(color: Colors.white, fontSize: 12), + ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('receiver', - style: TextStyle(color: Paletter.gray, fontSize: 12)), - Text('Token', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold)), - Text('send as a token or qr code', - style: TextStyle(color: Paletter.lightGray, fontSize: 14)), + Text( + 'receiver', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 12, + ), + ), + Text( + 'Token', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'send as a token or qr code', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + ), ], ), ), @@ -336,9 +347,7 @@ class TokenReciever extends StatelessWidget { } class WalletReciever extends StatelessWidget { - const WalletReciever({ - super.key, - }); + const WalletReciever({super.key}); @override Widget build(BuildContext context) { @@ -347,22 +356,31 @@ class WalletReciever extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), - child: - Text('NA', style: TextStyle(color: Colors.white, fontSize: 12)), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.8), + child: Text( + 'NA', + style: TextStyle(color: Colors.white, fontSize: 12), + ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('receiver', - style: TextStyle(color: Colors.grey, fontSize: 12)), - Text('NOT IMPLEMENTED', - style: TextStyle(color: Colors.white, fontSize: 16)), - Text('send to wallet not implemented', - style: TextStyle(color: Colors.grey, fontSize: 14)), + Text( + 'receiver', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + Text( + 'NOT IMPLEMENTED', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + Text( + 'send to wallet not implemented', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), ], ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart index 151df57a..f5e1b74b 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_state_provider.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; import 'package:bolt11_decoder/bolt11_decoder.dart'; +import 'package:riverpod/legacy.dart'; import '../../../providers/ndk_provider.dart'; @@ -84,15 +85,9 @@ class WalletPayRecieverState { class WalletPayToNotifier extends StateNotifier { final Ndk _ndk; - WalletPayToNotifier({ - String? initialWalletId, - required Ndk ndk, - }) : _ndk = ndk, - super( - WalletPayRecieverState( - recieveToWalletId: initialWalletId, - ), - ); + WalletPayToNotifier({String? initialWalletId, required Ndk ndk}) + : _ndk = ndk, + super(WalletPayRecieverState(recieveToWalletId: initialWalletId)); void updateRecieveToWalletId(String? walletId) { state = state.copyWith( @@ -146,20 +141,19 @@ class WalletPayToNotifier extends StateNotifier { state = state.copyWith( request: initTransaction.qoute!.request, - decodedBolt11Request: - Bolt11PaymentRequest(initTransaction.qoute!.request), + decodedBolt11Request: Bolt11PaymentRequest( + initTransaction.qoute!.request, + ), isPending: true, ); - final resultStream = - _ndk.cashu.retrieveFunds(draftTransaction: initTransaction); + final resultStream = _ndk.cashu.retrieveFunds( + draftTransaction: initTransaction, + ); await for (final result in resultStream) { if (result.state == ndk_entities.WalletTransactionState.completed) { - state = state.copyWith( - isPending: false, - isSuccess: true, - ); + state = state.copyWith(isPending: false, isSuccess: true); } else if (result.state == ndk_entities.WalletTransactionState.failed) { state = state.copyWith( requestErr: result.completionMsg, @@ -181,9 +175,7 @@ class WalletPayToNotifier extends StateNotifier { final walletRecieverProvider = StateNotifierProvider((ref) { - final ndk = ref.watch(ndkProvider); + final ndk = ref.watch(ndkProvider); - return WalletPayToNotifier( - ndk: ndk, - ); -}); + return WalletPayToNotifier(ndk: ndk); + }); diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart index f229af03..c548ccab 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_type/wallet_receive_type.dart @@ -4,8 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:ndk/entities.dart' as ndk_entities; -import '../../../../../config/palette.dart'; - import '../../../../../helpers/wallet_number_formatting.dart'; import '../../../../atoms/long_button.dart'; @@ -16,10 +14,7 @@ import '../wallet_receive_state_provider.dart'; class WalletReceiveType extends ConsumerWidget { final Function doneCallback; - const WalletReceiveType({ - super.key, - required this.doneCallback, - }); + const WalletReceiveType({super.key, required this.doneCallback}); _onPasteToken(BuildContext context, WidgetRef ref) async { final userClipboard = await _handleReadClipboard(); @@ -33,8 +28,9 @@ class WalletReceiveType extends ConsumerWidget { return; } - final ecashCompleter = - ref.read(walletReceiveEcashCompleterProvider.notifier); + final ecashCompleter = ref.read( + walletReceiveEcashCompleterProvider.notifier, + ); ecashCompleter.receiveEcash(tokenString: userClipboard); if (!context.mounted) return; @@ -102,18 +98,17 @@ class WalletReceiveType extends ConsumerWidget { ), ), _Island( - title: "Ecash", - child: longButton( - name: "paste token", - onPressed: () => _onPasteToken(context, ref), - )), + title: "Ecash", + child: longButton( + name: "paste token", + onPressed: () => _onPasteToken(context, ref), + ), + ), _Island( title: 'All wallets', child: _WalletsList( wallets: combinedWallets.wallets - .where( - (w) => w.id != state.recieveToWalletId, - ) + .where((w) => w.id != state.recieveToWalletId) .toList(), balances: combinedWallets.balances, selectedWalletId: state.recieveToWalletId, @@ -227,15 +222,15 @@ class _WalletsList extends StatelessWidget { subtitle: Text(w.type.toString()), /// show all balances for the wallet - trailing: Column(children: [ - for (final b in wBallances) - Text( - "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", - style: TextStyle( - color: Colors.white, + trailing: Column( + children: [ + for (final b in wBallances) + Text( + "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", + style: TextStyle(color: Colors.white), ), - ), - ]), + ], + ), onTap: () => onTap(w), ); diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart index d6e9bfa2..8e7d2a2f 100644 --- a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart @@ -2,15 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ndk/entities.dart' as ndk_entities; -import '../../../../config/palette.dart'; - class WalletTransactionDetailPage extends ConsumerWidget { final ndk_entities.WalletTransaction transaction; - const WalletTransactionDetailPage({ - super.key, - required this.transaction, - }); + const WalletTransactionDetailPage({super.key, required this.transaction}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -45,10 +40,7 @@ class WalletTransactionDetailPage extends ConsumerWidget { style: TextStyle(fontSize: 16), ), SizedBox(height: 8), - Text( - 'State: ${transaction.state}', - style: TextStyle(fontSize: 16), - ), + Text('State: ${transaction.state}', style: TextStyle(fontSize: 16)), if (transaction.completionMsg != null) ...[ SizedBox(height: 8), Text( diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart index 4d5017be..57d44336 100644 --- a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart @@ -5,7 +5,6 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:ndk/entities.dart' as ndk_entities; -import '../../../../config/palette.dart'; import '../../../atoms/wallet/wallet_transaction_card.dart'; import '../wallet_navigation.dart'; import 'wallet_transaction_list_state_provider.dart'; @@ -22,16 +21,17 @@ class WalletTransactionListPage extends ConsumerWidget { return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( - elevation: 0, - backgroundColor: Theme.of(context).colorScheme.surface, - foregroundColor: Colors.white, - title: const Text('Transactions'), - leading: IconButton( - icon: Icon(PhosphorIcons.caretLeft(), size: 24), - onPressed: () { - ref.read(walletNavigationProvider.notifier).changeMainPage(0); - }, - )), + elevation: 0, + backgroundColor: Theme.of(context).colorScheme.surface, + foregroundColor: Colors.white, + title: const Text('Transactions'), + leading: IconButton( + icon: Icon(PhosphorIcons.caretLeft(), size: 24), + onPressed: () { + ref.read(walletNavigationProvider.notifier).changeMainPage(0); + }, + ), + ), body: state.transactions.isEmpty ? const _EmptyTransactions() : ListView.builder( @@ -45,10 +45,7 @@ class WalletTransactionListPage extends ConsumerWidget { _DateHeader(label: section.label), const SizedBox(height: 8), ...section.items.map( - (tx) => WalletTransactionCard( - tx: tx, - showDate: false, - ), + (tx) => WalletTransactionCard(tx: tx, showDate: false), ), const SizedBox(height: 16), ], @@ -97,10 +94,12 @@ List<_DaySection> _groupByDay( ..sort((a, b) => labelToDate[b]!.compareTo(labelToDate[a]!)); return keys - .map((k) => _DaySection( - label: _formatDayLabel(labelToDate[k]!), - items: buckets[k]!, - )) + .map( + (k) => _DaySection( + label: _formatDayLabel(labelToDate[k]!), + items: buckets[k]!, + ), + ) .toList(); } @@ -116,14 +115,21 @@ class _DateHeader extends StatelessWidget { return Row( children: [ Expanded( - child: Divider(color: Paletter.darkGray.withValues(alpha: 0.4))), + child: Divider( + color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.4), + ), + ), Container( margin: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( - color: Paletter.extraDarkGray, + color: Theme.of(context).colorScheme.surfaceVariant, borderRadius: BorderRadius.circular(16), - border: Border.all(color: Paletter.darkGray.withValues(alpha: 0.4)), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withValues(alpha: 0.4), + ), ), child: Text( label, @@ -136,7 +142,10 @@ class _DateHeader extends StatelessWidget { ), ), Expanded( - child: Divider(color: Paletter.darkGray.withValues(alpha: 0.4))), + child: Divider( + color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.4), + ), + ), ], ); } @@ -153,12 +162,16 @@ class _EmptyTransactions extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.receipt_long_outlined, size: 48, color: Paletter.gray), + Icon( + Icons.receipt_long_outlined, + size: 48, + color: Theme.of(context).colorScheme.outline, + ), const SizedBox(height: 12), Text( 'No transactions available', style: TextStyle( - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, fontSize: 16, fontWeight: FontWeight.w600, ), @@ -168,7 +181,7 @@ class _EmptyTransactions extends StatelessWidget { 'Your recent activity will show up here.', textAlign: TextAlign.center, style: TextStyle( - color: Paletter.lightGray, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 13.5, ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart index 5b5de36d..9ec82619 100644 --- a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart @@ -1,5 +1,6 @@ import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; +import 'package:riverpod/legacy.dart'; import 'package:riverpod/riverpod.dart'; import '../../../providers/ndk_provider.dart'; @@ -36,33 +37,29 @@ class WalletTransactionListNotifier final Ndk _ndk; final Ref ref; WalletTransactionListNotifier({required Ndk ndk, required this.ref}) - : _ndk = ndk, - super( - WalletTransactionListState( - transactions: [], - pendingTransactions: [], - ), - ) { + : _ndk = ndk, + super( + WalletTransactionListState(transactions: [], pendingTransactions: []), + ) { ref.listen(walletCombinedProvider, (previous, next) { if (next.pendingTransactions != previous?.pendingTransactions || next.recentTransactions != previous?.recentTransactions) { - state = state.copyWith( - pendingTransactions: next.pendingTransactions, - ); + state = state.copyWith(pendingTransactions: next.pendingTransactions); } }, fireImmediately: true); _loadMore(); } _loadMore() async { - final transactions = await _ndk.wallets.combinedTransactions( - limit: limit, - offset: state.offset, - ); - state = state.copyWith( - transactions: [...state.transactions, ...transactions], - offset: state.offset + transactions.length, - ); + throw UnimplementedError("Loading more transactions is currently disabled"); + // final transactions = await _ndk.wallets.combinedTransactions( + // limit: limit, + // offset: state.offset, + // ); + // state = state.copyWith( + // transactions: [...state.transactions, ...transactions], + // offset: state.offset + transactions.length, + // ); } void reset() { @@ -73,10 +70,11 @@ class WalletTransactionListNotifier } } -final walletTransactionListProvider = StateNotifierProvider< - WalletTransactionListNotifier, WalletTransactionListState>( - (ref) { - final ndk = ref.watch(ndkProvider); - return WalletTransactionListNotifier(ndk: ndk, ref: ref); - }, -); +final walletTransactionListProvider = + StateNotifierProvider< + WalletTransactionListNotifier, + WalletTransactionListState + >((ref) { + final ndk = ref.watch(ndkProvider); + return WalletTransactionListNotifier(ndk: ndk, ref: ref); + }); diff --git a/pubspec.yaml b/pubspec.yaml index 3cf3e9c7..5aeb49a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,7 +72,7 @@ dependencies: rxdart: ^0.28.0 shimmer: ^3.0.0 flutter_force_directed_graph: ^1.0.7 - objectbox: ^5.0.0 + objectbox: ^5.2.0 objectbox_flutter_libs: any sembast: ^3.8.5+1 sembast_web: ^2.4.1 From baabef5ebdd508ed8afd39db88835152e2c5c7fd Mon Sep 17 00:00:00 2001 From: 1leo <58687994+1-leo@users.noreply.github.com> Date: Sat, 14 Mar 2026 11:44:12 +0100 Subject: [PATCH 41/52] fix: side menu payments --- .../components/drawer/nostr_side_menu.dart | 2 +- lib/presentation_layer/routing/routes.dart | 82 +++++++++---------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/presentation_layer/components/drawer/nostr_side_menu.dart b/lib/presentation_layer/components/drawer/nostr_side_menu.dart index 2a4f779e..2fd24c55 100644 --- a/lib/presentation_layer/components/drawer/nostr_side_menu.dart +++ b/lib/presentation_layer/components/drawer/nostr_side_menu.dart @@ -277,7 +277,7 @@ class NostrSideMenu extends ConsumerWidget { ), _drawerItem( label: AppLocalizations.of(context)!.payments, - routeName: 'payments', + routeName: '/wallet/dashboard', icon: PhosphorIcons.lightning(), onTap: () { context.push('/wallet/dashboard'); diff --git a/lib/presentation_layer/routing/routes.dart b/lib/presentation_layer/routing/routes.dart index d7b4d690..081f1922 100644 --- a/lib/presentation_layer/routing/routes.dart +++ b/lib/presentation_layer/routing/routes.dart @@ -256,6 +256,47 @@ final routes = [ starterPackIdentifier: state.extra as StarterPackIdentifier, ), ), + + GoRoute( + path: '/wallet/dashboard', + builder: (context, state) => WalletNavigation(title: "a"), + ), + GoRoute( + path: '/wallet/receive', + builder: (context, state) => WalletReceivePage(), + ), + GoRoute( + path: '/wallet/receive/ecash', + builder: (context, state) => WalletReceiveEcashCompleterPage(), + ), + GoRoute( + path: '/wallet/mints', + builder: (context, state) => WalletNavigation(title: "a"), + ), + GoRoute( + path: '/wallet/pay', + builder: (context, state) => WalletPayPage(), + ), + GoRoute( + path: '/wallet/add_mint', + builder: (context, state) => const AddMintPage(), + ), + GoRoute( + path: '/wallet/mint_details', + builder: (context, state) => + MintInfoPage(mintUrl: state.extra as String?), + ), + GoRoute( + path: '/wallet/transactions/detail', + builder: (context, state) => WalletTransactionDetailPage( + transaction: state.extra as ndk_entities.WalletTransaction, + ), + ), + + GoRoute( + path: '/wallet/pay/done', + builder: (context, state) => WalletPayDone(), + ), ], ), // Routes outside the shell (no persistent layout) @@ -279,47 +320,6 @@ final routes = [ return EventGalleryPage(eventId: eventId, startIndex: start); }, ), - - GoRoute( - path: '/wallet/dashboard', - builder: (context, state) => WalletNavigation(title: "a"), - ), - GoRoute( - path: '/wallet/receive', - builder: (context, state) => WalletReceivePage(), - ), - GoRoute( - path: '/wallet/receive/ecash', - builder: (context, state) => WalletReceiveEcashCompleterPage(), - ), - GoRoute( - path: '/wallet/mints', - builder: (context, state) => WalletNavigation(title: "a"), - ), - GoRoute( - path: '/wallet/pay', - builder: (context, state) => WalletPayPage(), - ), - GoRoute( - path: '/wallet/add_mint', - builder: (context, state) => const AddMintPage(), - ), - GoRoute( - path: '/wallet/mint_details', - builder: (context, state) => - MintInfoPage(mintUrl: state.extra as String?), - ), - GoRoute( - path: '/wallet/transactions/detail', - builder: (context, state) => WalletTransactionDetailPage( - transaction: state.extra as ndk_entities.WalletTransaction, - ), - ), - - GoRoute( - path: '/wallet/pay/done', - builder: (context, state) => WalletPayDone(), - ), ], ), From 47777e7487169f4c1cdfc645f58b9a05376e8cd1 Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:19:50 +0200 Subject: [PATCH 42/52] feat: wallet setup overlay --- .../wallet_seed_state_provider.dart | 150 ++++++++ .../wallet/wallet_seed_setup_overlay.dart | 350 ++++++++++++++++++ .../routes/wallet/wallet_shell_page.dart | 14 + lib/presentation_layer/routing/routes.dart | 101 +++-- 4 files changed, 575 insertions(+), 40 deletions(-) create mode 100644 lib/presentation_layer/routes/wallet/wallet_providers/wallet_seed_state_provider.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_seed_setup_overlay.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_shell_page.dart diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_seed_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_seed_state_provider.dart new file mode 100644 index 00000000..0f11fa3f --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_seed_state_provider.dart @@ -0,0 +1,150 @@ +// ignore_for_file: experimental_member_use + +import 'dart:convert'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:ndk/entities.dart'; +import 'package:ndk/ndk.dart'; + +import '../../../providers/ndk_provider.dart'; + +final walletSeedStateProvider = + NotifierProvider( + WalletSeedInitialState.new, + ); + +class LocalStoredSeed { + final String seedPhrase; + final Language language; + final bool passphraseRequired; + + LocalStoredSeed({ + required this.seedPhrase, + this.language = Language.english, + this.passphraseRequired = false, + }); + + Map toJson() { + return { + 'seedPhrase': seedPhrase, + 'language': language.name, + 'passphraseRequired': passphraseRequired, + }; + } + + String toJsonString() { + return json.encode(toJson()); + } + + factory LocalStoredSeed.fromJson(Map json) { + return LocalStoredSeed( + seedPhrase: json['seedPhrase'], + language: Language.values.firstWhere((e) => e.name == json['language']), + passphraseRequired: json['passphraseRequired'], + ); + } + + factory LocalStoredSeed.fromCashuUserSeedphrase(CashuUserSeedphrase seed) { + return LocalStoredSeed( + seedPhrase: seed.seedPhrase, + language: seed.language, + passphraseRequired: seed.passphrase.isNotEmpty, + ); + } +} + +class WalletSeedState { + final CashuUserSeedphrase? cashuSeed; + final bool passwordRequired; + final bool isLoading; + + WalletSeedState({ + this.cashuSeed, + this.passwordRequired = false, + this.isLoading = true, + }); + + WalletSeedState copyWith({ + CashuUserSeedphrase? cashuSeed, + bool? passwordRequired, + bool? isLoading, + }) { + return WalletSeedState( + cashuSeed: cashuSeed ?? this.cashuSeed, + passwordRequired: passwordRequired ?? this.passwordRequired, + isLoading: isLoading ?? this.isLoading, + ); + } +} + +class WalletSeedInitialState extends Notifier { + FlutterSecureStorage secureStorage = const FlutterSecureStorage(); + + Ndk get _ndk => ref.read(ndkProvider); + + @override + WalletSeedState build() { + getSeedFromStorage(); + return WalletSeedState(cashuSeed: null, isLoading: true); + } + + Future getSeedFromStorage({String? password}) async { + final seed = await secureStorage.read(key: 'cashu_seed'); + + if (seed != null) { + final storedSeed = LocalStoredSeed.fromJson(json.decode(seed)); + + if (storedSeed.passphraseRequired && password == null) { + state = state.copyWith(passwordRequired: true, isLoading: false); + return null; + } + + state = state.copyWith( + passwordRequired: false, + isLoading: false, + cashuSeed: CashuUserSeedphrase( + seedPhrase: storedSeed.seedPhrase, + language: storedSeed.language, + passphrase: password ?? '', + ), + ); + + if (state.cashuSeed != null) { + _ndk.cashu.setCashuSeedPhrase(state.cashuSeed!); + } + + return state; + } + + state = state.copyWith(isLoading: false); + return null; + } + + Future setSeedStorage(CashuUserSeedphrase seed) async { + await secureStorage.write( + key: 'cashu_seed', + value: LocalStoredSeed.fromCashuUserSeedphrase(seed).toJsonString(), + ); + } + + void setSeed(CashuUserSeedphrase seed) { + state = state.copyWith( + cashuSeed: seed, + isLoading: false, + passwordRequired: false, + ); + _ndk.cashu.setCashuSeedPhrase(seed); + } + + Future generateSeedPhrase({ + Language language = Language.english, + String passphrase = '', + }) async { + final newSeed = CashuSeed.generateSeedPhrase( + length: MnemonicLength.words24, + ); + + return newSeed; + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_seed_setup_overlay.dart b/lib/presentation_layer/routes/wallet/wallet_seed_setup_overlay.dart new file mode 100644 index 00000000..fc1aa0ef --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_seed_setup_overlay.dart @@ -0,0 +1,350 @@ +import 'package:camelus/presentation_layer/atoms/long_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:ndk/entities.dart'; + +import 'wallet_providers/wallet_seed_state_provider.dart'; + +class WalletSeedSetupOverlay extends ConsumerStatefulWidget { + const WalletSeedSetupOverlay({super.key}); + + @override + ConsumerState createState() => + _WalletSeedSetupOverlayState(); +} + +class _WalletSeedSetupOverlayState + extends ConsumerState { + late Future _generatedSeedFuture; + + final _passwordController = TextEditingController(); + bool _protectWithPassword = false; + bool _isSubmitting = false; + bool _showSeedPhrase = false; + String? _errorText; + + @override + void initState() { + super.initState(); + _generatedSeedFuture = ref + .read(walletSeedStateProvider.notifier) + .generateSeedPhrase(); + } + + @override + void dispose() { + _passwordController.dispose(); + super.dispose(); + } + + Future _confirmSeed(String seedPhrase) async { + final notifier = ref.read(walletSeedStateProvider.notifier); + final password = _passwordController.text.trim(); + + if (_protectWithPassword && password.isEmpty) { + setState(() { + _errorText = 'Please enter a password to protect this wallet seed.'; + }); + return; + } + + setState(() { + _isSubmitting = true; + _errorText = null; + }); + + final seed = CashuUserSeedphrase( + seedPhrase: seedPhrase, + language: Language.english, + passphrase: _protectWithPassword ? password : '', + ); + + await notifier.setSeedStorage(seed); + notifier.setSeed(seed); + + if (mounted) { + setState(() { + _isSubmitting = false; + }); + } + } + + Future _unlockStoredSeed() async { + final notifier = ref.read(walletSeedStateProvider.notifier); + + setState(() { + _isSubmitting = true; + _errorText = null; + }); + + final unlocked = await notifier.getSeedFromStorage( + password: _passwordController.text.trim(), + ); + + if (mounted) { + setState(() { + _isSubmitting = false; + if (unlocked?.cashuSeed == null) { + _errorText = 'Unable to unlock wallet seed. Please try again.'; + } + }); + } + } + + void _cancelSetup() { + context.go('/home'); + } + + @override + Widget build(BuildContext context) { + final seedState = ref.watch(walletSeedStateProvider); + + if (seedState.cashuSeed != null) { + return const SizedBox.shrink(); + } + + if (seedState.isLoading) { + return _WalletOverlayCard( + child: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading wallet setup...'), + ], + ), + ); + } + + if (seedState.passwordRequired) { + return _WalletOverlayCard( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Experimental Wallet', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 8), + const Text( + 'This wallet feature is experimental. Your stored seed is password-protected, enter your password to continue.', + ), + const SizedBox(height: 16), + TextField( + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Password', + ), + ), + if (_errorText != null) ...[ + const SizedBox(height: 12), + Text( + _errorText!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ], + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isSubmitting ? null : _unlockStoredSeed, + child: Text(_isSubmitting ? 'Unlocking...' : 'Unlock Wallet'), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: _isSubmitting ? null : _cancelSetup, + child: const Text('Cancel'), + ), + ), + ], + ), + ); + } + + return FutureBuilder( + future: _generatedSeedFuture, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return _WalletOverlayCard( + child: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Generating secure seed phrase...'), + ], + ), + ); + } + + final seedPhrase = snapshot.data!; + + return _WalletOverlayCard( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Wallet Setup', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 8), + const Text( + 'The wallet feature is experimental! Save your seed phrase safely, it is the only way to recover this wallet.', + ), + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + _showSeedPhrase + ? seedPhrase + : '•••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• •••••••• ••••••••', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + IconButton( + icon: Icon( + _showSeedPhrase + ? Icons.visibility_off + : Icons.visibility, + ), + onPressed: () { + setState(() { + _showSeedPhrase = !_showSeedPhrase; + }); + }, + ), + ], + ), + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: OutlinedButton.icon( + onPressed: () async { + await Clipboard.setData(ClipboardData(text: seedPhrase)); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Seed phrase copied')), + ); + }, + icon: const Icon(Icons.copy_rounded), + label: const Text('Copy seed phrase'), + ), + ), + const SizedBox(height: 10), + Text( + 'Hint: Write these words down offline and never share them. Anyone with this phrase can spend your funds.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Protect seed with password'), + subtitle: const Text( + 'You will need this password each time this wallet is unlocked.', + ), + value: _protectWithPassword, + onChanged: _isSubmitting + ? null + : (value) { + setState(() { + _protectWithPassword = value; + if (!value) { + _passwordController.clear(); + } + }); + }, + ), + if (_protectWithPassword) + TextField( + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Password', + ), + ), + if (_errorText != null) ...[ + const SizedBox(height: 12), + Text( + _errorText!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ], + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: longButton( + name: _isSubmitting ? 'Saving...' : 'I saved my seed phrase', + onPressed: () { + _isSubmitting + ? null + : () { + _confirmSeed(seedPhrase); + }; + }, + inverted: true, + ), + ), + + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: longButton( + name: "Cancel", + onPressed: () { + _isSubmitting ? null : _cancelSetup(); + }, + ), + ), + ], + ), + ); + }, + ); + } +} + +class _WalletOverlayCard extends StatelessWidget { + const _WalletOverlayCard({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.55), + padding: const EdgeInsets.all(20), + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 540), + child: Card( + elevation: 8, + child: Padding(padding: const EdgeInsets.all(16), child: child), + ), + ), + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_shell_page.dart b/lib/presentation_layer/routes/wallet/wallet_shell_page.dart new file mode 100644 index 00000000..d32a70db --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_shell_page.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +import 'wallet_seed_setup_overlay.dart'; + +class WalletShellPage extends StatelessWidget { + const WalletShellPage({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Stack(children: [child, const WalletSeedSetupOverlay()]); + } +} diff --git a/lib/presentation_layer/routing/routes.dart b/lib/presentation_layer/routing/routes.dart index 544ebf62..cf4fd650 100644 --- a/lib/presentation_layer/routing/routes.dart +++ b/lib/presentation_layer/routing/routes.dart @@ -49,6 +49,7 @@ import '../routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart'; import '../routes/wallet/wallet_pay/wallet_pay_page.dart'; import '../routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart'; import '../routes/wallet/wallet_receive/wallet_receive_page.dart'; +import '../routes/wallet/wallet_shell_page.dart'; import '../routes/wallet/wallet_transaction/wallet_transaction_detail_page.dart'; String? decodeProfileIdentifierToPubkey(String rawIdentifier) { @@ -256,46 +257,66 @@ final routes = [ starterPackIdentifier: state.extra as StarterPackIdentifier, ), ), - - GoRoute( - path: '/wallet/dashboard', - builder: (context, state) => WalletNavigation(title: "a"), - ), - GoRoute( - path: '/wallet/receive', - builder: (context, state) => WalletReceivePage(), - ), - GoRoute( - path: '/wallet/receive/ecash', - builder: (context, state) => WalletReceiveEcashCompleterPage(), - ), - GoRoute( - path: '/wallet/mints', - builder: (context, state) => WalletNavigation(title: "a"), - ), - GoRoute( - path: '/wallet/pay', - builder: (context, state) => WalletPayPage(), - ), - GoRoute( - path: '/wallet/add_mint', - builder: (context, state) => const AddMintPage(), - ), - GoRoute( - path: '/wallet/mint_details', - builder: (context, state) => - MintInfoPage(mintUrl: state.extra as String?), - ), - GoRoute( - path: '/wallet/transactions/detail', - builder: (context, state) => WalletTransactionDetailPage( - transaction: state.extra as ndk_entities.WalletTransaction, - ), - ), - - GoRoute( - path: '/wallet/pay/done', - builder: (context, state) => WalletPayDone(), + ShellRoute( + builder: (context, state, child) => WalletShellPage(child: child), + routes: [ + GoRoute( + path: '/wallet', + redirect: (context, state) { + if (state.uri.path == '/wallet') { + return '/wallet/dashboard'; + } + return null; + }, + routes: [ + GoRoute( + path: 'dashboard', + builder: (context, state) => WalletNavigation(title: 'a'), + ), + GoRoute( + path: 'receive', + builder: (context, state) => WalletReceivePage(), + routes: [ + GoRoute( + path: 'ecash', + builder: (context, state) => + WalletReceiveEcashCompleterPage(), + ), + ], + ), + GoRoute( + path: 'mints', + builder: (context, state) => WalletNavigation(title: 'a'), + ), + GoRoute( + path: 'pay', + builder: (context, state) => WalletPayPage(), + routes: [ + GoRoute( + path: 'done', + builder: (context, state) => WalletPayDone(), + ), + ], + ), + GoRoute( + path: 'add_mint', + builder: (context, state) => const AddMintPage(), + ), + GoRoute( + path: 'mint_details', + builder: (context, state) => + MintInfoPage(mintUrl: state.extra as String?), + ), + GoRoute( + path: 'transactions/detail', + builder: (context, state) => WalletTransactionDetailPage( + transaction: + state.extra as ndk_entities.WalletTransaction, + ), + ), + ], + ), + ], ), ], ), From 6e41b95dbc970a303611e4bad38116f0e8971037 Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:33:43 +0200 Subject: [PATCH 43/52] fix: ux warn for wrong seed pw --- .../routes/wallet/wallet_seed_setup_overlay.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_seed_setup_overlay.dart b/lib/presentation_layer/routes/wallet/wallet_seed_setup_overlay.dart index fc1aa0ef..e5985a16 100644 --- a/lib/presentation_layer/routes/wallet/wallet_seed_setup_overlay.dart +++ b/lib/presentation_layer/routes/wallet/wallet_seed_setup_overlay.dart @@ -125,12 +125,12 @@ class _WalletSeedSetupOverlayState crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'Experimental Wallet', + 'Unlock Wallet', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700), ), const SizedBox(height: 8), const Text( - 'This wallet feature is experimental. Your stored seed is password-protected, enter your password to continue.', + 'Your stored seed is password-protected, enter your password to continue. Warning: Entering a wrong password will not raise an error, technically a new wallet seed will be generated (not advised!)', ), const SizedBox(height: 16), TextField( @@ -297,11 +297,7 @@ class _WalletSeedSetupOverlayState child: longButton( name: _isSubmitting ? 'Saving...' : 'I saved my seed phrase', onPressed: () { - _isSubmitting - ? null - : () { - _confirmSeed(seedPhrase); - }; + _isSubmitting ? null : _confirmSeed(seedPhrase); }, inverted: true, ), From 7929ff40c17f8cb3bc6378e391cd6706f20b1945 Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:57:02 +0200 Subject: [PATCH 44/52] refactor: wallet state to modern riverpod api --- .../wallet_combined_state_provider.dart | 52 +++++++--------- ...allet_transaction_list_state_provider.dart | 60 ++++++++++--------- 2 files changed, 54 insertions(+), 58 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart index c01f22fc..c914efaa 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_combined_state_provider.dart @@ -1,10 +1,10 @@ +// ignore_for_file: experimental_member_use + import 'dart:async'; import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; -import 'package:riverpod/legacy.dart'; - -import 'package:riverpod/riverpod.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../providers/db_ndk_provider.dart'; import '../../../providers/ndk_provider.dart'; @@ -39,40 +39,34 @@ class WalletCombinedState { } final walletCombinedProvider = - StateNotifierProvider.autoDispose< + NotifierProvider.autoDispose< WalletCombinedStateNotifier, WalletCombinedState - >((ref) { - final ndk = ref.watch(ndkProvider); - final ndkDb = ref.watch(dbNdkProvider)!; - - return WalletCombinedStateNotifier(ndk, ndkDb); - }); + >(WalletCombinedStateNotifier.new); -class WalletCombinedStateNotifier extends StateNotifier { - final Ndk _ndk; - final CacheManager ndkDb; +class WalletCombinedStateNotifier extends Notifier { + Ndk get _ndk => ref.watch(ndkProvider); final _subscriptions = []; - WalletCombinedStateNotifier(this._ndk, this.ndkDb) - : super( - WalletCombinedState( - balances: [], - recentTransactions: [], - pendingTransactions: [], - wallets: [], - ), - ) { + @override + WalletCombinedState build() { + ref.watch(dbNdkProvider)!; + + ref.onDispose(() { + for (final subscription in _subscriptions) { + subscription.cancel(); + } + }); + _initializeState(); - } - @override - void dispose() { - for (final subscription in _subscriptions) { - subscription.cancel(); - } - super.dispose(); + return WalletCombinedState( + balances: [], + recentTransactions: [], + pendingTransactions: [], + wallets: [], + ); } Future _initializeState() async { diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart index 9ec82619..914ee8c7 100644 --- a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_state_provider.dart @@ -1,7 +1,8 @@ +// ignore_for_file: experimental_member_use + import 'package:ndk/entities.dart' as ndk_entities; import 'package:ndk/ndk.dart'; -import 'package:riverpod/legacy.dart'; -import 'package:riverpod/riverpod.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../providers/ndk_provider.dart'; import '../wallet_providers/wallet_combined_state_provider.dart'; @@ -33,33 +34,38 @@ class WalletTransactionListState { } class WalletTransactionListNotifier - extends StateNotifier { - final Ndk _ndk; - final Ref ref; - WalletTransactionListNotifier({required Ndk ndk, required this.ref}) - : _ndk = ndk, - super( - WalletTransactionListState(transactions: [], pendingTransactions: []), - ) { + extends Notifier { + Ndk get _ndk => ref.watch(ndkProvider); + + @override + WalletTransactionListState build() { + final combinedState = ref.read(walletCombinedProvider); + ref.listen(walletCombinedProvider, (previous, next) { if (next.pendingTransactions != previous?.pendingTransactions || next.recentTransactions != previous?.recentTransactions) { state = state.copyWith(pendingTransactions: next.pendingTransactions); } - }, fireImmediately: true); - _loadMore(); + }); + + Future.microtask(_loadMore); + return WalletTransactionListState( + transactions: [], + pendingTransactions: combinedState.pendingTransactions, + ); } - _loadMore() async { - throw UnimplementedError("Loading more transactions is currently disabled"); - // final transactions = await _ndk.wallets.combinedTransactions( - // limit: limit, - // offset: state.offset, - // ); - // state = state.copyWith( - // transactions: [...state.transactions, ...transactions], - // offset: state.offset + transactions.length, - // ); + Future _loadMore() async { + if (!ref.mounted) return; + final transactions = await _ndk.wallets.combinedTransactions( + limit: limit, + offset: state.offset, + ); + if (!ref.mounted) return; + state = state.copyWith( + transactions: [...state.transactions, ...transactions], + offset: state.offset + transactions.length, + ); } void reset() { @@ -71,10 +77,6 @@ class WalletTransactionListNotifier } final walletTransactionListProvider = - StateNotifierProvider< - WalletTransactionListNotifier, - WalletTransactionListState - >((ref) { - final ndk = ref.watch(ndkProvider); - return WalletTransactionListNotifier(ndk: ndk, ref: ref); - }); + NotifierProvider( + WalletTransactionListNotifier.new, + ); From ceb53158c26ea67acde34eea57a09685e2dd01eb Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:04:01 +0200 Subject: [PATCH 45/52] fix: lightmode --- .../atoms/copy_to_clipboard.dart | 11 +-- .../atoms/wallet/wallet_card.dart | 22 ++--- .../atoms/wallet/wallet_transaction_card.dart | 5 +- .../wallet/wallet_accounts_card.dart | 31 +++--- .../wallet/wallet_actions_strip.dart | 3 - .../wallet/wallet_friends_strip.dart | 38 ++++---- .../wallet/wallets_select_bottom_sheet.dart | 12 +-- .../wallet_pay_done/wallet_pay_done.dart | 13 +-- .../wallet_pay_select_amount.dart | 14 ++- .../wallet_pay_reciever.dart | 14 +-- .../wallet_pay_summary.dart | 96 +++++++++++++++---- .../wallet_receive_amount.dart | 2 +- .../wallet_transaction_list_page.dart | 4 +- pubspec.lock | 2 +- 14 files changed, 167 insertions(+), 100 deletions(-) diff --git a/lib/presentation_layer/atoms/copy_to_clipboard.dart b/lib/presentation_layer/atoms/copy_to_clipboard.dart index e4f31b85..bd9a1070 100644 --- a/lib/presentation_layer/atoms/copy_to_clipboard.dart +++ b/lib/presentation_layer/atoms/copy_to_clipboard.dart @@ -30,18 +30,17 @@ class _CopyClipboardButtonState extends State { width: double.infinity, child: ElevatedButton.icon( onPressed: _copied ? null : _copyToClipboard, - icon: - Icon(_copied ? PhosphorIcons.check() : PhosphorIcons.copySimple()), + icon: Icon( + _copied ? PhosphorIcons.check() : PhosphorIcons.copySimple(), + ), label: Text(_copied ? widget.copyDoneText : widget.copyText), style: ElevatedButton.styleFrom( backgroundColor: _copied ? widget.backgroundColor - : Theme.of(context).colorScheme.primary, + : Theme.of(context).colorScheme.secondaryContainer, foregroundColor: widget.backgroundColor, padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ), ); diff --git a/lib/presentation_layer/atoms/wallet/wallet_card.dart b/lib/presentation_layer/atoms/wallet/wallet_card.dart index 934b3e29..8c5c292e 100644 --- a/lib/presentation_layer/atoms/wallet/wallet_card.dart +++ b/lib/presentation_layer/atoms/wallet/wallet_card.dart @@ -30,13 +30,12 @@ class WalletCard extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return GestureDetector( onTap: isDisabled ? null : () => onTap(wallet.id), child: Container( decoration: BoxDecoration( - color: - backgroundColor ?? - Theme.of(context).colorScheme.surfaceContainerHighest, + color: backgroundColor ?? colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Padding( @@ -45,12 +44,10 @@ class WalletCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ CircleAvatar( - backgroundColor: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.8), + backgroundColor: colorScheme.primary.withValues(alpha: 0.8), child: Text( wallet.name.substring(0, 2).toUpperCase(), - style: TextStyle(color: Colors.white, fontSize: 12), + style: TextStyle(color: colorScheme.onPrimary, fontSize: 12), ), ), const SizedBox(width: 12), @@ -66,8 +63,8 @@ class WalletCard extends StatelessWidget { fontSize: 18, fontWeight: FontWeight.bold, color: isDisabled - ? Theme.of(context).colorScheme.onSurfaceVariant - : Colors.white, + ? colorScheme.onSurfaceVariant + : colorScheme.onSurface, ), ), Text( @@ -75,8 +72,8 @@ class WalletCard extends StatelessWidget { style: TextStyle( fontSize: 14, color: isDisabled - ? Theme.of(context).colorScheme.onSurfaceVariant - : Theme.of(context).colorScheme.outline, + ? colorScheme.onSurfaceVariant + : colorScheme.outline, ), ), ], @@ -87,11 +84,12 @@ class WalletCard extends StatelessWidget { Spacer(flex: 1), if (showBalances) Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ for (final b in balances) Text( "${WalletNumberFormatting.formatAmount(amount: b.amount, unit: b.unit)} ${b.unit}", - style: TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), ), ], ), diff --git a/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart index 3cd13b14..d4a5853c 100644 --- a/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart +++ b/lib/presentation_layer/atoms/wallet/wallet_transaction_card.dart @@ -63,7 +63,10 @@ class WalletTransactionCard extends StatelessWidget { transactionStatus.label, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), ), subtitle: Text( "${cashuTx != null ? _removeHttpPrefix(cashuTx.mintUrl) : ""} • ${cashuTx != null ? _txType(cashuTx) : ''}", diff --git a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart index 220dddf7..471502e3 100644 --- a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart +++ b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart @@ -28,6 +28,8 @@ class WalletAccountsCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + return Column( children: [ Column( @@ -48,10 +50,8 @@ class WalletAccountsCard extends ConsumerWidget { // focal: Alignment.center, // radius: 2, // ), - border: Border.all( - width: 1, - color: Colors.white, - ), + color: colorScheme.surface, + border: Border.all(width: 1, color: colorScheme.outline), // Make rounded corners borderRadius: BorderRadius.circular(18.0), ), @@ -64,16 +64,20 @@ class WalletAccountsCard extends ConsumerWidget { children: [ Text( alias, - style: TextStyle(fontSize: 18), + style: TextStyle( + fontSize: 18, + color: colorScheme.onSurfaceVariant, + ), ), Container( margin: const EdgeInsets.fromLTRB(10.0, 10, 0, 0), child: Text( title, style: TextStyle( - fontSize: 35, - color: Colors.white, - fontWeight: FontWeight.normal), + fontSize: 35, + color: colorScheme.onSurface, + fontWeight: FontWeight.normal, + ), ), ), // const SizedBox(height: 5), @@ -85,10 +89,10 @@ class WalletAccountsCard extends ConsumerWidget { for (final balance in balances) Text( "${WalletNumberFormatting.formatAmount(amount: balance.amount, unit: balance.unit)} ${balance.unit}", - style: const TextStyle( + style: TextStyle( fontSize: 20, fontWeight: FontWeight.normal, - color: Colors.white60, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -101,9 +105,10 @@ class WalletAccountsCard extends ConsumerWidget { right: -50, child: Transform( transform: Matrix4.translationValues( - MediaQuery.of(context).size.width * 0, - -20.0, - -20.0), + MediaQuery.of(context).size.width * 0, + -20.0, + -20.0, + ), child: Lottie.asset( 'assets/animations/nfc-mood.json', width: 150, diff --git a/lib/presentation_layer/components/wallet/wallet_actions_strip.dart b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart index 1dc5dc73..c24f42c0 100644 --- a/lib/presentation_layer/components/wallet/wallet_actions_strip.dart +++ b/lib/presentation_layer/components/wallet/wallet_actions_strip.dart @@ -15,9 +15,6 @@ class WalletActionsStrip extends StatelessWidget { required this.onHistory, }); - final myIconColor = Colors.white70; - final myTextColor = Colors.white60; - @override Widget build(BuildContext context) { return Padding( diff --git a/lib/presentation_layer/components/wallet/wallet_friends_strip.dart b/lib/presentation_layer/components/wallet/wallet_friends_strip.dart index 0e407a93..da6e1cc1 100644 --- a/lib/presentation_layer/components/wallet/wallet_friends_strip.dart +++ b/lib/presentation_layer/components/wallet/wallet_friends_strip.dart @@ -11,15 +11,19 @@ class WalletFriendsStrip extends StatelessWidget { width: MediaQuery.of(context).size.width, height: 130, decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(10))), + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), child: Column( children: [ - const Padding( - padding: EdgeInsets.fromLTRB(15, 4, 15, 0), + Padding( + padding: const EdgeInsets.fromLTRB(15, 4, 15, 0), child: Text( "friends", - style: TextStyle(color: Colors.white, fontSize: 24), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 24, + ), ), ), Container( @@ -34,13 +38,11 @@ class WalletFriendsStrip extends StatelessWidget { children: const [ CircleAvatar( backgroundImage: NetworkImage( - "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg"), + "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", + ), radius: 30, ), - Text( - "user1", - style: TextStyle(fontSize: 15), - ), + Text("user1", style: TextStyle(fontSize: 15)), ], ), ), @@ -52,13 +54,11 @@ class WalletFriendsStrip extends StatelessWidget { children: const [ CircleAvatar( backgroundImage: NetworkImage( - "https://www.venmond.com/demo/vendroid/img/avatar/big.jpg"), + "https://www.venmond.com/demo/vendroid/img/avatar/big.jpg", + ), radius: 30, ), - Text( - "user2", - style: TextStyle(fontSize: 15), - ), + Text("user2", style: TextStyle(fontSize: 15)), ], ), ), @@ -66,15 +66,17 @@ class WalletFriendsStrip extends StatelessWidget { InkWell( borderRadius: const BorderRadius.all(Radius.circular(50)), onTap: () => {log("todo")}, - child: const CircleAvatar( - backgroundColor: Colors.white60, + child: CircleAvatar( + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, radius: 30, child: Icon(Icons.add), ), ), ], ), - ) + ), ], ), ); diff --git a/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart index 1cbf65b6..a5653394 100644 --- a/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart +++ b/lib/presentation_layer/components/wallet/wallets_select_bottom_sheet.dart @@ -13,7 +13,7 @@ Future showWalletsSelectBottomSheet({ return showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Theme.of(context).colorScheme.surface, + backgroundColor: Theme.of(context).colorScheme.primary, builder: (ctx) { final size = MediaQuery.of(ctx).size; @@ -27,12 +27,12 @@ Future showWalletsSelectBottomSheet({ // upper bound constraints: BoxConstraints(maxHeight: size.height * maxHeightFactor), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.surfaceDim, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), boxShadow: [ BoxShadow( blurRadius: 16, - color: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.surfaceDim, ), ], ), @@ -46,7 +46,7 @@ Future showWalletsSelectBottomSheet({ width: 40, height: 4, decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, borderRadius: BorderRadius.circular(2), ), ), @@ -57,7 +57,7 @@ Future showWalletsSelectBottomSheet({ child: Text( title, style: TextStyle( - color: Colors.white, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 20, fontWeight: FontWeight.w600, ), @@ -73,7 +73,7 @@ Future showWalletsSelectBottomSheet({ ), ), child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(12, 8, 12, 16), + padding: const EdgeInsets.fromLTRB(10, 8, 10, 16), child: Wrap( spacing: 12, runSpacing: 12, diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart index d4155eeb..6b0d251c 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_done/wallet_pay_done.dart @@ -114,7 +114,7 @@ class ErrorStep extends StatelessWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.red.shade50, + color: Theme.of(context).colorScheme.errorContainer, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200), ), @@ -159,7 +159,7 @@ class SuccessStep extends StatelessWidget { style: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, ), ), const SizedBox(width: 16), @@ -168,7 +168,7 @@ class SuccessStep extends StatelessWidget { style: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.surfaceContainerHighest, + color: Theme.of(context).colorScheme.onSurface, ), ), ], @@ -182,7 +182,7 @@ class SuccessStep extends StatelessWidget { width: 240, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: AnimatedQr(qrCodeData: outputToken!.toV4TokenString()), @@ -197,6 +197,7 @@ class SuccessStep extends StatelessWidget { child: CopyClipboardButton( value: outputToken!.toV4TokenString(), copyText: "Copy Token", + backgroundColor: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], @@ -233,12 +234,12 @@ class TransactionState extends ConsumerWidget { if (myTransaction.state == ndk_entities.WalletTransactionState.pending) { icon = PhosphorIcons.hourglass(); - color = Theme.of(context).colorScheme.surface; + color = Theme.of(context).colorScheme.onSurfaceVariant; title = 'pending ecash'; } else if (myTransaction.state == ndk_entities.WalletTransactionState.failed) { icon = PhosphorIcons.warningCircle(); - color = Theme.of(context).colorScheme.surface; + color = Theme.of(context).colorScheme.error; title = 'failed'; } else { icon = PhosphorIcons.checkCircle(); diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart index e610a67b..a7ccc24e 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_amount/wallet_pay_select_amount.dart @@ -104,8 +104,11 @@ class _WalletPaySelectAmountState extends ConsumerState { showSnackBar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - backgroundColor: Theme.of(context).colorScheme.onError, - content: Text(message, style: TextStyle(color: Colors.white)), + backgroundColor: Theme.of(context).colorScheme.onSurface, + content: Text( + message, + style: TextStyle(color: Theme.of(context).colorScheme.surface), + ), ), ); } @@ -134,7 +137,7 @@ class _WalletPaySelectAmountState extends ConsumerState { backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( child: Padding( - padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), + padding: const EdgeInsets.fromLTRB(10, 24, 10, 16), child: Column( children: [ Container( @@ -222,8 +225,11 @@ class _WalletPaySelectAmountState extends ConsumerState { ), }, showHaptics: true, - trackColor: Theme.of(context).colorScheme.onSurface, + trackColor: Theme.of(context).colorScheme.surfaceContainer, activeColor: Theme.of(context).colorScheme.primary, + inactiveColor: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), ), diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart index 84b0e900..bd5e9315 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_select_reciever/wallet_pay_reciever.dart @@ -81,18 +81,20 @@ class WalletSelectReciever extends ConsumerWidget { decoration: InputDecoration( isDense: true, hintText: ' Search by name', - hintStyle: const TextStyle( - color: Colors.white, + hintStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, letterSpacing: 1.1, ), filled: true, fillColor: Theme.of(context).colorScheme.surface, - enabledBorder: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(50.0)), - borderSide: BorderSide(color: Colors.grey), + enabledBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(50.0)), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), + borderRadius: const BorderRadius.all(Radius.circular(25.0)), borderSide: BorderSide( color: Theme.of(context).colorScheme.surface, ), diff --git a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart index 6315802b..f7623ee8 100644 --- a/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart +++ b/lib/presentation_layer/routes/wallet/wallet_pay/wallet_pay_summary/wallet_pay_summary.dart @@ -20,7 +20,10 @@ class WalletPaySummary extends ConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Theme.of(context).colorScheme.error, - content: Text(message, style: TextStyle(color: Colors.white)), + content: Text( + message, + style: TextStyle(color: Theme.of(context).colorScheme.onError), + ), ), ); } @@ -78,7 +81,7 @@ class WalletPaySummary extends ConsumerWidget { } return Scaffold( - backgroundColor: Colors.black, + backgroundColor: Theme.of(context).colorScheme.surface, body: Column( children: [ Container( @@ -104,7 +107,9 @@ class WalletPaySummary extends ConsumerWidget { IconButton( icon: Icon( Icons.arrow_back, - color: Colors.white, + color: Theme.of( + context, + ).colorScheme.onInverseSurface, size: 24, ), onPressed: () => backCallback(), @@ -112,7 +117,9 @@ class WalletPaySummary extends ConsumerWidget { Text( 'summary', style: TextStyle( - color: Colors.white, + color: Theme.of( + context, + ).colorScheme.onInverseSurface, fontSize: 20, fontWeight: FontWeight.w500, ), @@ -120,7 +127,9 @@ class WalletPaySummary extends ConsumerWidget { IconButton( icon: Icon( Icons.close, - color: Theme.of(context).colorScheme.onSurface, + color: Theme.of( + context, + ).colorScheme.onInverseSurface, size: 24, ), onPressed: () { @@ -145,7 +154,9 @@ class WalletPaySummary extends ConsumerWidget { ) : '0', style: TextStyle( - color: Colors.white, + color: Theme.of( + context, + ).colorScheme.onInverseSurface, fontSize: 48, fontWeight: FontWeight.bold, ), @@ -155,7 +166,9 @@ class WalletPaySummary extends ConsumerWidget { icon: Icon( PhosphorIcons.notePencil(), size: 24, - color: Theme.of(context).colorScheme.primary, + color: Theme.of( + context, + ).colorScheme.primaryContainer, ), onPressed: () => backCallback(), ), @@ -164,7 +177,7 @@ class WalletPaySummary extends ConsumerWidget { Text( state.unit ?? '', style: TextStyle( - color: Colors.white, + color: Theme.of(context).colorScheme.onInverseSurface, fontSize: 18, fontWeight: FontWeight.w400, ), @@ -184,7 +197,9 @@ class WalletPaySummary extends ConsumerWidget { children: [ Container( decoration: BoxDecoration( - color: Color(0xFF2A2A2A), + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Column( @@ -302,8 +317,20 @@ class WalletPaySummary extends ConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: TextStyle(color: Colors.grey, fontSize: 12)), - Text(value, style: TextStyle(color: Colors.white, fontSize: 16)), + Text( + label, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 12, + ), + ), + Text( + value, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 16, + ), + ), ], ), if (isEditable && onEdit != null) @@ -333,7 +360,10 @@ class ContactReciever extends StatelessWidget { ).colorScheme.primary.withValues(alpha: 0.8), child: Text( 'NA', - style: TextStyle(color: Colors.white, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontSize: 12, + ), ), ), SizedBox(width: 12), @@ -343,15 +373,24 @@ class ContactReciever extends StatelessWidget { children: [ Text( 'receiver', - style: TextStyle(color: Colors.grey, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 12, + ), ), Text( 'NOT IMPLEMENTED', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 16, + ), ), Text( 'send to pubkey not implemented', - style: TextStyle(color: Colors.grey, fontSize: 14), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + ), ), ], ), @@ -377,7 +416,10 @@ class TokenReciever extends StatelessWidget { ).colorScheme.primary.withValues(alpha: 0.8), child: Text( 'TK', - style: TextStyle(color: Colors.white, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontSize: 12, + ), ), ), SizedBox(width: 12), @@ -395,7 +437,7 @@ class TokenReciever extends StatelessWidget { Text( 'Token', style: TextStyle( - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, fontSize: 16, fontWeight: FontWeight.bold, ), @@ -431,7 +473,10 @@ class WalletReciever extends StatelessWidget { ).colorScheme.primary.withValues(alpha: 0.8), child: Text( 'NA', - style: TextStyle(color: Colors.white, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontSize: 12, + ), ), ), SizedBox(width: 12), @@ -441,15 +486,24 @@ class WalletReciever extends StatelessWidget { children: [ Text( 'receiver', - style: TextStyle(color: Colors.grey, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), ), Text( 'NOT IMPLEMENTED', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16, + ), ), Text( 'send to wallet not implemented', - style: TextStyle(color: Colors.grey, fontSize: 14), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + ), ), ], ), diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart index 6430a972..bef925a6 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/wallet_receive_amout/wallet_receive_amount.dart @@ -149,7 +149,7 @@ class _WalletPaySelectAmountState extends ConsumerState { backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( child: Padding( - padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), + padding: const EdgeInsets.fromLTRB(10, 24, 10, 16), child: Column( children: [ Container( diff --git a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart index 57d44336..346767e0 100644 --- a/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_transaction/wallet_transaction_list_page.dart @@ -23,7 +23,7 @@ class WalletTransactionListPage extends ConsumerWidget { appBar: AppBar( elevation: 0, backgroundColor: Theme.of(context).colorScheme.surface, - foregroundColor: Colors.white, + foregroundColor: Theme.of(context).colorScheme.onSurface, title: const Text('Transactions'), leading: IconButton( icon: Icon(PhosphorIcons.caretLeft(), size: 24), @@ -134,7 +134,7 @@ class _DateHeader extends StatelessWidget { child: Text( label, style: TextStyle( - color: Colors.white, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12.5, fontWeight: FontWeight.w600, letterSpacing: 0.2, diff --git a/pubspec.lock b/pubspec.lock index e4abc809..abe431db 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1247,7 +1247,7 @@ packages: path: "../ndk/packages/objectbox" relative: true source: path - version: "0.2.11-dev.2" + version: "0.2.11-dev.3" nip07_event_signer: dependency: transitive description: From 9bd0a12fa56d8dcc2c58d32f684b28d43e2d747a Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:21:34 +0200 Subject: [PATCH 46/52] fix: wallet color --- .../atoms/wallet/mint_info_card_small.dart | 12 +++++++++--- .../components/wallet/wallet_accounts_card.dart | 5 +---- .../routes/wallet/mint_info/mint_info_page.dart | 17 ++++++++++++++--- .../routes/wallet/wallet_qr_scan.dart | 8 +++++--- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart index 18f2210d..1be18344 100644 --- a/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart +++ b/lib/presentation_layer/atoms/wallet/mint_info_card_small.dart @@ -15,7 +15,10 @@ class MintInfoCardSmall extends StatelessWidget { color: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - side: BorderSide(color: Colors.white.withValues(alpha: 0.5), width: 1), + side: BorderSide( + color: Theme.of(context).colorScheme.onSurface.withAlpha(128), + width: 1, + ), ), child: Container( decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), @@ -50,7 +53,7 @@ class MintInfoCardSmall extends StatelessWidget { Text( mintInfo.name ?? 'Unknown Mint', style: TextStyle( - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, fontSize: 22, fontWeight: FontWeight.bold, ), @@ -118,7 +121,10 @@ class MintInfoCardSmall extends StatelessWidget { Expanded( child: Text( mintInfo.motd!, - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 16, + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart index 471502e3..773d54aa 100644 --- a/lib/presentation_layer/components/wallet/wallet_accounts_card.dart +++ b/lib/presentation_layer/components/wallet/wallet_accounts_card.dart @@ -43,10 +43,7 @@ class WalletAccountsCard extends ConsumerWidget { // Color.fromARGB(255, 7, 238, 176), // Color.fromARGB(255, 11, 189, 243), // ], - // stops: [ - // 0, - // 1, - // ], + // stops: [0, 1], // focal: Alignment.center, // radius: 2, // ), diff --git a/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart b/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart index c9408913..5d9a7514 100644 --- a/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart +++ b/lib/presentation_layer/routes/wallet/mint_info/mint_info_page.dart @@ -23,9 +23,17 @@ class MintInfoPage extends ConsumerWidget { backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.surface, - title: Text('Mint Info'), + title: Text( + 'Mint Info', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), + ), + body: Center( + child: Text( + 'Mint not found', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), ), - body: Center(child: Text('Mint not found')), ); } @@ -35,7 +43,10 @@ class MintInfoPage extends ConsumerWidget { backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.surface, - title: Text('Mint Info'), + title: Text( + 'Mint Info', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), ), body: SingleChildScrollView( child: Padding( diff --git a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart index 002fc9e4..dfe84733 100644 --- a/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart +++ b/lib/presentation_layer/routes/wallet/wallet_qr_scan.dart @@ -152,7 +152,7 @@ class _QrScan extends ConsumerState { height: 120, width: double.infinity, decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.8), + color: Theme.of(context).colorScheme.surface.withAlpha(200), borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), @@ -181,7 +181,9 @@ class _QrScan extends ConsumerState { backgroundColor: Theme.of( context, ).colorScheme.primary, - foregroundColor: Colors.white, + foregroundColor: Theme.of( + context, + ).colorScheme.onPrimary, ), ), ], @@ -201,7 +203,7 @@ class _QrScan extends ConsumerState { backgroundColor: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.5), - color: Colors.white, + color: Theme.of(context).colorScheme.onPrimary, ), ), From e2b1941de20727549c3ef44a95edd0f00ac338bb Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:38:12 +0200 Subject: [PATCH 47/52] fix: colors --- .../wallet_rcv_ecash_completer_page.dart | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart index aeac14ca..ea328748 100644 --- a/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart +++ b/lib/presentation_layer/routes/wallet/wallet_receive/rcv_completers/wallet_rcv_ecash_completer_page.dart @@ -47,8 +47,8 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { amount: walletState.amount!, unit: walletState.unit!, ), - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontSize: 48, fontWeight: FontWeight.bold, ), @@ -76,7 +76,10 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { child: SingleChildScrollView( child: Text( walletState.memo!, - style: const TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 16, + ), textAlign: TextAlign.center, ), ), @@ -85,7 +88,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { if (walletState.errorMessage != null) ...[ Spacer(flex: 1), - _buildErrorCard(walletState.errorMessage!), + _ErrorCard(errorMessage: walletState.errorMessage!), ], Spacer(flex: 10), ], @@ -146,7 +149,7 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), + color: color.withAlpha(25), borderRadius: BorderRadius.circular(50), ), child: Icon(icon, color: color, size: 32), @@ -166,7 +169,12 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { const SizedBox(height: 4), Text( subtitle, - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + style: TextStyle( + fontSize: 14, + color: Theme.of( + context, + ).colorScheme.onSurface.withAlpha(153), + ), ), ], ), @@ -182,17 +190,28 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { ), ); } +} + +class _ErrorCard extends StatelessWidget { + const _ErrorCard({required this.errorMessage}); - Widget _buildErrorCard(String errorMessage) { + final String errorMessage; + + @override + Widget build(BuildContext context) { return Card( elevation: 2, - color: Colors.red[50], + color: Theme.of(context).colorScheme.onError, child: Padding( padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.error_outline, color: Colors.red[700], size: 24), + Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.error, + size: 24, + ), const SizedBox(width: 12), Expanded( child: Column( @@ -203,13 +222,16 @@ class WalletReceiveEcashCompleterPage extends ConsumerWidget { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: Colors.red[700], + color: Theme.of(context).colorScheme.error, ), ), const SizedBox(height: 4), Text( errorMessage, - style: TextStyle(fontSize: 14, color: Colors.red[600]), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.error, + ), ), ], ), From ab4d0c2823c1ec13a935acea666513827331f1b3 Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:27:03 +0200 Subject: [PATCH 48/52] feat: restore wallet --- .../routes/nostr/settings/settings_page.dart | 8 + .../nostr/settings/wallet_settings.dart | 23 + .../wallet_seed_state_provider.dart | 11 + .../wallet/wallet_restore/wallet_restore.dart | 674 ++++++++++++++++++ .../wallet_restore_state_provider.dart | 175 +++++ lib/presentation_layer/routing/routes.dart | 12 + 6 files changed, 903 insertions(+) create mode 100644 lib/presentation_layer/routes/nostr/settings/wallet_settings.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_restore/wallet_restore.dart create mode 100644 lib/presentation_layer/routes/wallet/wallet_restore/wallet_restore_state_provider.dart diff --git a/lib/presentation_layer/routes/nostr/settings/settings_page.dart b/lib/presentation_layer/routes/nostr/settings/settings_page.dart index 765cc197..8ee27827 100644 --- a/lib/presentation_layer/routes/nostr/settings/settings_page.dart +++ b/lib/presentation_layer/routes/nostr/settings/settings_page.dart @@ -34,6 +34,10 @@ class _SettingsPageState extends ConsumerState { context.push('/settings/file-servers'); } + void _navigateToWalletSettings() { + context.push('/settings/wallet'); + } + void _navigateToInitalRoute() { context.push('/settings/initial-route'); } @@ -68,6 +72,10 @@ class _SettingsPageState extends ConsumerState { context.push('/settings/relays'); }, ), + ListTile( + title: const Text('Wallet'), + onTap: _navigateToWalletSettings, + ), ListTile( title: Text(AppLocalizations.of(context)!.initialRoute), diff --git a/lib/presentation_layer/routes/nostr/settings/wallet_settings.dart b/lib/presentation_layer/routes/nostr/settings/wallet_settings.dart new file mode 100644 index 00000000..b3db237b --- /dev/null +++ b/lib/presentation_layer/routes/nostr/settings/wallet_settings.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class WalletSettingsPage extends StatelessWidget { + const WalletSettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Wallet settings')), + body: ListView( + children: [ + ListTile( + title: const Text('Restore wallet'), + onTap: () { + context.push('/settings/wallet/restore'); + }, + ), + ], + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_seed_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_seed_state_provider.dart index 0f11fa3f..001e06a9 100644 --- a/lib/presentation_layer/routes/wallet/wallet_providers/wallet_seed_state_provider.dart +++ b/lib/presentation_layer/routes/wallet/wallet_providers/wallet_seed_state_provider.dart @@ -137,6 +137,17 @@ class WalletSeedInitialState extends Notifier { _ndk.cashu.setCashuSeedPhrase(seed); } + Future deleteSeed() async { + // Remove all known wallets first + final wallets = await _ndk.wallets.getWallets(); + for (final wallet in wallets) { + await _ndk.wallets.removeWallet(wallet.id); + } + + await secureStorage.delete(key: 'cashu_seed'); + state = WalletSeedState(cashuSeed: null, isLoading: false); + } + Future generateSeedPhrase({ Language language = Language.english, String passphrase = '', diff --git a/lib/presentation_layer/routes/wallet/wallet_restore/wallet_restore.dart b/lib/presentation_layer/routes/wallet/wallet_restore/wallet_restore.dart new file mode 100644 index 00000000..5e6af8ef --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_restore/wallet_restore.dart @@ -0,0 +1,674 @@ +// ignore_for_file: experimental_member_use +import 'dart:async'; + +import 'package:camelus/presentation_layer/atoms/long_button.dart'; +import 'package:camelus/presentation_layer/atoms/wallet/mint_info_card_small.dart'; +import 'package:camelus/presentation_layer/providers/ndk_provider.dart'; +import 'package:camelus/presentation_layer/routes/wallet/wallet_providers/wallet_seed_state_provider.dart'; +import 'package:camelus/presentation_layer/routes/wallet/wallet_restore/wallet_restore_state_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ndk/entities.dart'; + +enum _PageStep { loading, warnExistingSeed, enterSeed, restoring } + +class WalletRestorePage extends ConsumerStatefulWidget { + const WalletRestorePage({super.key}); + + @override + ConsumerState createState() => _WalletRestorePageState(); +} + +class _WalletRestorePageState extends ConsumerState { + _PageStep _step = _PageStep.loading; + bool _continuingWithExistingSeed = false; + + final _seedController = TextEditingController(); + final _mintUrlController = TextEditingController(); + final _passphraseController = TextEditingController(); + bool _usePassphrase = false; + bool _showSeedPhrase = false; + String? _errorText; + + // Mint validation + CashuMintInfo? _mintInfo; + bool _isValidatingMint = false; + Timer? _debounceTimer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _syncStep()); + } + + @override + void dispose() { + _seedController.dispose(); + _mintUrlController.dispose(); + _passphraseController.dispose(); + _debounceTimer?.cancel(); + super.dispose(); + } + + void _syncStep() { + if (!mounted || _step == _PageStep.restoring) return; + final seedState = ref.read(walletSeedStateProvider); + if (seedState.isLoading) return; + setState(() { + _step = seedState.cashuSeed != null + ? _PageStep.warnExistingSeed + : _PageStep.enterSeed; + }); + } + + void _continueWithExistingSeed() { + setState(() { + _continuingWithExistingSeed = true; + _step = _PageStep.enterSeed; + }); + } + + String _buildValidUrl(String url) { + final text = url.trim(); + if (!text.startsWith('http://') && !text.startsWith('https://')) { + return 'https://$text'; + } + return text; + } + + void _onMintUrlChanged(String text) { + setState(() { + _mintInfo = null; + _isValidatingMint = false; + }); + _debounceTimer?.cancel(); + if (text.trim().isEmpty) return; + setState(() => _isValidatingMint = true); + _debounceTimer = Timer( + const Duration(milliseconds: 600), + () => _validateMintUrl(text), + ); + } + + Future _validateMintUrl(String text) async { + try { + final ndk = ref.read(ndkProvider); + final info = await ndk.cashu.getMintInfoNetwork( + mintUrl: _buildValidUrl(text), + ); + if (mounted) { + setState(() { + _mintInfo = info; + _isValidatingMint = false; + }); + } + } catch (_) { + if (mounted) { + setState(() { + _mintInfo = null; + _isValidatingMint = false; + }); + } + } + } + + Future _deleteSeed() async { + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Delete wallet seed & all wallets?'), + content: const Text( + 'This will permanently delete all wallets and remove the stored seed phrase from this device. ' + 'All wallet data will be lost. Ensure you have your seed phrase backed up — ' + 'there is no way to recover it afterwards.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text('Cancel'), + ), + TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(ctx).colorScheme.error, + ), + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text('Delete'), + ), + ], + ), + ); + if (confirmed == true && mounted) { + await ref.read(walletSeedStateProvider.notifier).deleteSeed(); + setState(() => _step = _PageStep.enterSeed); + } + } + + Future _startRestore() async { + final mintUrl = _mintUrlController.text.trim(); + + if (!_continuingWithExistingSeed) { + final seedPhrase = _seedController.text.trim(); + if (seedPhrase.isEmpty) { + setState(() => _errorText = 'Please enter your seed phrase.'); + return; + } + } + + if (mintUrl.isEmpty || _mintInfo == null) { + setState( + () => _errorText = + 'Please enter a valid mint URL and wait for validation.', + ); + return; + } + + setState(() { + _errorText = null; + _step = _PageStep.restoring; + }); + + if (!_continuingWithExistingSeed) { + final seedNotifier = ref.read(walletSeedStateProvider.notifier); + final seed = CashuUserSeedphrase( + seedPhrase: _seedController.text.trim(), + language: Language.english, + passphrase: _usePassphrase ? _passphraseController.text : '', + ); + await seedNotifier.setSeedStorage(seed); + seedNotifier.setSeed(seed); + } + + final units = _mintInfo!.supportedUnits.toList(); + final effectiveUnits = units.isEmpty ? ['sat'] : units; + + await ref + .read(restoreStateProvider.notifier) + .startRestoreAllUnits( + mintUrl: _buildValidUrl(mintUrl), + units: effectiveUnits, + ); + } + + void _stopRestore() { + ref.read(restoreStateProvider.notifier).reset(); + setState(() => _step = _PageStep.enterSeed); + } + + void _restoreAnotherMint() { + ref.read(restoreStateProvider.notifier).reset(); + setState(() => _step = _PageStep.enterSeed); + } + + @override + Widget build(BuildContext context) { + final seedState = ref.watch(walletSeedStateProvider); + final restoreState = ref.watch(restoreStateProvider); + + // Sync step once seed finishes loading + if (_step == _PageStep.loading && !seedState.isLoading) { + WidgetsBinding.instance.addPostFrameCallback((_) => _syncStep()); + } + + return Scaffold( + appBar: AppBar(title: const Text('Restore wallet')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: _buildContent(restoreState), + ), + ); + } + + Widget _buildContent(RestoreState restoreState) { + return switch (_step) { + _PageStep.loading => const Center( + child: Padding( + padding: EdgeInsets.only(top: 64), + child: CircularProgressIndicator(), + ), + ), + _PageStep.warnExistingSeed => _WarnExistingSeedStep( + onDeleteSeed: _deleteSeed, + onContinue: _continueWithExistingSeed, + ), + _PageStep.enterSeed => _EnterSeedStep( + seedController: _seedController, + mintUrlController: _mintUrlController, + passphraseController: _passphraseController, + usePassphrase: _usePassphrase, + showSeedPhrase: _showSeedPhrase, + isValidatingMint: _isValidatingMint, + mintInfo: _mintInfo, + errorText: _errorText, + showSeedSection: !_continuingWithExistingSeed, + onUsePassphraseChanged: (v) => setState(() { + _usePassphrase = v; + if (!v) _passphraseController.clear(); + }), + onToggleSeedVisibility: () => + setState(() => _showSeedPhrase = !_showSeedPhrase), + onMintUrlChanged: _onMintUrlChanged, + onStartRestore: _startRestore, + ), + _PageStep.restoring => _RestoringStep( + restoreState: restoreState, + onStop: _stopRestore, + onRestoreAnotherMint: _restoreAnotherMint, + ), + }; + } +} + +// ── Step 1: warn about existing seed ────────────────────────────────────── + +class _WarnExistingSeedStep extends StatelessWidget { + const _WarnExistingSeedStep({ + required this.onDeleteSeed, + required this.onContinue, + }); + + final VoidCallback onDeleteSeed; + final VoidCallback onContinue; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline_rounded, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Existing seed phrase detected', + style: TextStyle( + fontWeight: FontWeight.w700, + color: Theme.of( + context, + ).colorScheme.onSecondaryContainer, + ), + ), + const SizedBox(height: 4), + Text( + 'You already have a wallet seed stored. You can restore additional mints with your existing seed, or delete it and restore from a different seed phrase.', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSecondaryContainer, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: longButton( + name: 'Continue with existing seed', + onPressed: onContinue, + inverted: true, + ), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: longButton( + name: 'Delete seed & use a different one', + onPressed: onDeleteSeed, + ), + ), + ], + ); + } +} + +// ── Step 2: enter seed phrase ───────────────────────────────────────────── + +class _EnterSeedStep extends StatelessWidget { + const _EnterSeedStep({ + required this.seedController, + required this.mintUrlController, + required this.passphraseController, + required this.usePassphrase, + required this.showSeedPhrase, + required this.isValidatingMint, + required this.onUsePassphraseChanged, + required this.onToggleSeedVisibility, + required this.onMintUrlChanged, + required this.onStartRestore, + this.mintInfo, + this.errorText, + this.showSeedSection = true, + }); + + final TextEditingController seedController; + final TextEditingController mintUrlController; + final TextEditingController passphraseController; + final bool usePassphrase; + final bool showSeedPhrase; + final bool isValidatingMint; + final CashuMintInfo? mintInfo; + final ValueChanged onUsePassphraseChanged; + final VoidCallback onToggleSeedVisibility; + final ValueChanged onMintUrlChanged; + final VoidCallback onStartRestore; + final String? errorText; + final bool showSeedSection; + + @override + Widget build(BuildContext context) { + final canStart = mintInfo != null && !isValidatingMint; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Seed phrase section (hidden when continuing with existing seed) ── + if (showSeedSection) ...[ + const Text( + 'Enter seed phrase', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 8), + const Text( + 'Enter the 12 or 24-word seed phrase you want to restore.', + ), + const SizedBox(height: 20), + + Stack( + alignment: Alignment.topRight, + children: [ + TextField( + controller: seedController, + minLines: showSeedPhrase ? 3 : 1, + maxLines: showSeedPhrase ? 6 : 1, + obscureText: !showSeedPhrase, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Seed phrase', + hintText: 'word1 word2 word3 ...', + alignLabelWithHint: true, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 4, right: 4), + child: IconButton( + icon: Icon( + showSeedPhrase ? Icons.visibility_off : Icons.visibility, + ), + onPressed: onToggleSeedVisibility, + ), + ), + ], + ), + const SizedBox(height: 16), + + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Use BIP39 passphrase'), + subtitle: const Text( + 'Enable only if your original seed was created with a passphrase.', + ), + value: usePassphrase, + onChanged: onUsePassphraseChanged, + ), + if (usePassphrase) ...[ + const SizedBox(height: 8), + TextField( + controller: passphraseController, + obscureText: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Passphrase', + ), + ), + ], + const SizedBox(height: 24), + ] else ...[ + const Text( + 'Restore from mint', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 8), + const Text( + 'Your existing seed phrase will be used. Enter a mint URL to restore funds from.', + ), + const SizedBox(height: 20), + ], + + // ── Mint URL ── + const Text('Mint URL', style: TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + TextField( + controller: mintUrlController, + keyboardType: TextInputType.url, + autocorrect: false, + onChanged: onMintUrlChanged, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'Mint URL', + hintText: 'https://mint.example.com', + suffixIcon: isValidatingMint + ? const Padding( + padding: EdgeInsets.all(12), + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : mintInfo != null + ? const Icon(Icons.check_circle_outline, color: Colors.green) + : null, + ), + ), + + // ── Mint info card ── + if (mintInfo != null) ...[ + const SizedBox(height: 16), + MintInfoCardSmall(mintInfo: mintInfo!), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + for (final unit in mintInfo!.supportedUnits) + Chip(label: Text(unit)), + ], + ), + ], + + if (errorText != null) ...[ + const SizedBox(height: 12), + Text( + errorText!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ], + const SizedBox(height: 28), + SizedBox( + width: double.infinity, + child: longButton( + name: 'Start restore', + onPressed: onStartRestore, + inverted: true, + disabled: !canStart, + ), + ), + ], + ); + } +} + +// ── Step 3: restore in progress / complete ──────────────────────────────── + +class _RestoringStep extends StatelessWidget { + const _RestoringStep({ + required this.restoreState, + required this.onStop, + required this.onRestoreAnotherMint, + }); + + final RestoreState restoreState; + final VoidCallback onStop; + final VoidCallback onRestoreAnotherMint; + + @override + Widget build(BuildContext context) { + final isDone = restoreState.isComplete && !restoreState.isRestoring; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (restoreState.isRestoring) ...[ + const Text( + 'Restoring wallet…', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + ), + if (restoreState.pendingUnits.length > 1 && + restoreState.restoringUnit != null) ...[ + const SizedBox(height: 6), + Text( + 'Unit ${restoreState.pendingUnits.indexOf(restoreState.restoringUnit!) + 1} ' + 'of ${restoreState.pendingUnits.length}: ${restoreState.restoringUnit}', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ] else if (restoreState.restoringUnit != null) ...[ + const SizedBox(height: 6), + Text( + 'Restoring unit: ${restoreState.restoringUnit}', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 12), + const LinearProgressIndicator(), + ] else if (isDone) ...[ + Row( + children: [ + Icon( + Icons.check_circle_outline, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + const Text( + 'Restore complete', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + ), + ], + ), + ] else ...[ + const Text( + 'Starting restore…', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 12), + const LinearProgressIndicator(), + ], + + if (restoreState.error != null) ...[ + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + restoreState.error!, + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ], + + const SizedBox(height: 28), + + if (restoreState.restoredAmountByUnit.isEmpty) + const _StatCard(label: 'Restored amount', value: '—') + else + ...restoreState.restoredAmountByUnit.entries.map( + (e) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _StatCard( + label: 'Restored (${e.key})', + value: '${e.value}', + ), + ), + ), + const SizedBox(height: 12), + _StatCard( + label: 'Proofs restored', + value: '${restoreState.totalProofCount}', + ), + const SizedBox(height: 12), + _StatCard( + label: 'Current counter', + value: '${restoreState.currentCounter}', + ), + + const SizedBox(height: 28), + + SizedBox( + width: double.infinity, + child: restoreState.isRestoring + ? longButton(name: 'Stop restore', onPressed: onStop) + : longButton( + name: 'Restore another mint', + onPressed: onRestoreAnotherMint, + inverted: true, + ), + ), + ], + ); + } +} + +class _StatCard extends StatelessWidget { + const _StatCard({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontWeight: FontWeight.w500)), + Text( + value, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700), + ), + ], + ), + ); + } +} diff --git a/lib/presentation_layer/routes/wallet/wallet_restore/wallet_restore_state_provider.dart b/lib/presentation_layer/routes/wallet/wallet_restore/wallet_restore_state_provider.dart new file mode 100644 index 00000000..6f963da4 --- /dev/null +++ b/lib/presentation_layer/routes/wallet/wallet_restore/wallet_restore_state_provider.dart @@ -0,0 +1,175 @@ +// ignore_for_file: experimental_member_use + +import 'dart:async'; + +import 'package:ndk/ndk.dart'; + +import 'package:riverpod/legacy.dart'; + +import '../../../providers/ndk_provider.dart'; + +final restoreStateProvider = + StateNotifierProvider((ref) { + final ndk = ref.read(ndkProvider); + return RestoreNotifier(ndk: ndk); + }); + +class RestoreState { + final bool isRestoring; + final bool isComplete; + + /// Accumulated restored amount keyed by unit (e.g. 'sat', 'eur'). + final Map restoredAmountByUnit; + final int totalProofCount; + final int currentCounter; + + /// The unit currently being restored. + final String? restoringUnit; + + /// All units queued for this restore session. + final List pendingUnits; + final String? error; + + const RestoreState({ + this.isRestoring = false, + this.isComplete = false, + this.restoredAmountByUnit = const {}, + this.totalProofCount = 0, + this.currentCounter = 0, + this.restoringUnit, + this.pendingUnits = const [], + this.error, + }); + + RestoreState copyWith({ + bool? isRestoring, + bool? isComplete, + Map? restoredAmountByUnit, + int? totalProofCount, + int? currentCounter, + String? restoringUnit, + bool clearRestoringUnit = false, + List? pendingUnits, + String? error, + }) { + return RestoreState( + isRestoring: isRestoring ?? this.isRestoring, + isComplete: isComplete ?? this.isComplete, + restoredAmountByUnit: restoredAmountByUnit ?? this.restoredAmountByUnit, + totalProofCount: totalProofCount ?? this.totalProofCount, + currentCounter: currentCounter ?? this.currentCounter, + restoringUnit: clearRestoringUnit + ? null + : (restoringUnit ?? this.restoringUnit), + pendingUnits: pendingUnits ?? this.pendingUnits, + error: error, + ); + } +} + +class RestoreNotifier extends StateNotifier { + final Ndk ndk; + StreamSubscription? _restoreSubscription; + String _currentUnit = ''; + + RestoreNotifier({required this.ndk}) : super(const RestoreState()); + + /// Restores all [units] sequentially from the given [mintUrl]. + Future startRestoreAllUnits({ + required String mintUrl, + required List units, + }) async { + await _restoreSubscription?.cancel(); + state = RestoreState( + isRestoring: true, + pendingUnits: List.unmodifiable(units), + ); + + ndk.cashu.addMintToKnownMints(mintUrl: mintUrl); + + for (final unit in units) { + if (!state.isRestoring) break; + state = state.copyWith(restoringUnit: unit, currentCounter: 0); + await _restoreSingleUnit(mintUrl: mintUrl, unit: unit); + if (state.error != null) break; + } + + if (state.isRestoring) { + state = state.copyWith( + isRestoring: false, + isComplete: true, + clearRestoringUnit: true, + ); + } + } + + Future _restoreSingleUnit({ + required String mintUrl, + required String unit, + int startCounter = 0, + int batchSize = 100, + int gapLimit = 16, + }) async { + _currentUnit = unit; + final completer = Completer(); + try { + final restoreStream = ndk.cashu.restore( + mintUrl: mintUrl, + unit: unit, + startCounter: startCounter, + batchSize: batchSize, + gapLimit: gapLimit, + ); + _restoreSubscription = restoreStream.listen( + (result) => _handleRestoreResult(result), + onError: (error) { + state = state.copyWith(error: error.toString()); + if (!completer.isCompleted) completer.complete(); + }, + onDone: () { + if (!completer.isCompleted) completer.complete(); + }, + cancelOnError: false, + ); + } catch (e) { + state = state.copyWith(error: e.toString()); + if (!completer.isCompleted) completer.complete(); + } + await completer.future; + } + + void _handleRestoreResult(CashuRestoreResult result) { + var batchAmount = 0; + var maxCounter = 0; + + for (var keysetResult in result.keysetResults) { + for (var proof in keysetResult.restoredProofs) { + batchAmount += proof.amount; + } + if (keysetResult.lastUsedCounter > maxCounter) { + maxCounter = keysetResult.lastUsedCounter; + } + } + + final updatedAmounts = Map.from(state.restoredAmountByUnit); + updatedAmounts[_currentUnit] = + (updatedAmounts[_currentUnit] ?? 0) + batchAmount; + + state = state.copyWith( + restoredAmountByUnit: updatedAmounts, + totalProofCount: state.totalProofCount + result.totalProofsRestored, + currentCounter: maxCounter, + ); + } + + void reset() { + _restoreSubscription?.cancel(); + state = const RestoreState(); + } + + @override + void dispose() { + _restoreSubscription?.cancel(); + super.dispose(); + } +} diff --git a/lib/presentation_layer/routing/routes.dart b/lib/presentation_layer/routing/routes.dart index 04017bbf..be6177f2 100644 --- a/lib/presentation_layer/routing/routes.dart +++ b/lib/presentation_layer/routing/routes.dart @@ -33,6 +33,8 @@ import '../routes/nostr/settings/developer/developer_settings.dart'; import '../routes/nostr/settings/relays/relays_settings.dart'; import '../routes/nostr/settings/notifications/notifications_settings.dart'; import '../routes/nostr/settings/theme/theme_settings.dart'; +import '../routes/wallet/wallet_restore/wallet_restore.dart'; +import '../routes/nostr/settings/wallet_settings.dart'; import '../routes/notification_page.dart'; import '../routes/search/search_page.dart'; import '../routes/nostr/settings/file_servers/settings_file_servers.dart'; @@ -180,6 +182,16 @@ final routes = [ path: 'relays', builder: (context, state) => const Nip65RelaysSettings(), ), + GoRoute( + path: 'wallet', + builder: (context, state) => const WalletSettingsPage(), + routes: [ + GoRoute( + path: 'restore', + builder: (context, state) => const WalletRestorePage(), + ), + ], + ), GoRoute( path: 'notifications', builder: (context, state) => const NotificationsSettingsPage(), From ac5ba77017c916c5e7cb6a7e77497ef3ebc7a96b Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:05:43 +0200 Subject: [PATCH 49/52] fix: wallet web setup hack --- .../db/ndk_cache/ndk_cache_factory_web.dart | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/lib/data_layer/db/ndk_cache/ndk_cache_factory_web.dart b/lib/data_layer/db/ndk_cache/ndk_cache_factory_web.dart index be17cf0e..c836c0e0 100644 --- a/lib/data_layer/db/ndk_cache/ndk_cache_factory_web.dart +++ b/lib/data_layer/db/ndk_cache/ndk_cache_factory_web.dart @@ -1,3 +1,12 @@ +import 'package:ndk/data_layer/repositories/wallets/sembast_wallets_repo.dart'; +import 'package:ndk/domain_layer/entities/cashu/cashu_keyset.dart'; +import 'package:ndk/domain_layer/entities/cashu/cashu_mint_info.dart'; +import 'package:ndk/domain_layer/entities/cashu/cashu_proof.dart'; +import 'package:ndk/domain_layer/entities/nip_05.dart'; +import 'package:ndk/domain_layer/entities/user_relay_list.dart'; +import 'package:ndk/domain_layer/entities/wallet/wallet.dart'; +import 'package:ndk/domain_layer/entities/wallet/wallet_transaction.dart'; +import 'package:ndk/domain_layer/entities/wallet/wallet_type.dart'; import 'package:ndk/ndk.dart'; import 'package:sembast_web/sembast_web.dart'; @@ -5,5 +14,88 @@ Future createNdkCacheManager() async { final Database database = await databaseFactoryWeb.openDatabase( 'camelus_ndk_cache.db', ); - return SembastCacheManager(database); + + final walletsRepo = SembastWalletsRepo(database); + + MyCombinedDb combinedDb = MyCombinedDb(database, walletsRepo: walletsRepo); + + return combinedDb; +} + +class MyCombinedDb extends SembastCacheManager implements SembastWalletsRepo { + final SembastWalletsRepo _walletsRepo; + MyCombinedDb(super.database, {required SembastWalletsRepo walletsRepo}) + : _walletsRepo = walletsRepo; + + @override + Future clearWalletRepoData() { + return _walletsRepo.clearWalletRepoData(); + } + + @override + String? getDefaultWalletIdForReceiving() { + return _walletsRepo.getDefaultWalletIdForReceiving(); + } + + @override + String? getDefaultWalletIdForSending() { + return _walletsRepo.getDefaultWalletIdForSending(); + } + + @override + Future> getTransactions({ + int? limit, + int? offset, + String? walletId, + String? unit, + WalletType? walletType, + }) { + return _walletsRepo.getTransactions( + limit: limit, + offset: offset, + walletId: walletId, + unit: unit, + walletType: walletType, + ); + } + + @override + Future getWallet(String id) { + return _walletsRepo.getWallet(id); + } + + @override + Future> getWallets({List? ids}) { + return _walletsRepo.getWallets(ids: ids); + } + + @override + Future initializeWalletDefaults() { + return _walletsRepo.initializeWalletDefaults(); + } + + @override + Future removeWallet(String walletId) { + return _walletsRepo.removeWallet(walletId); + } + + @override + Future saveTransactions(List transactions) { + return _walletsRepo.saveTransactions(transactions); + } + + @override + void setDefaultWalletForReceiving(String? walletId) { + _walletsRepo.setDefaultWalletForReceiving(walletId); + } + + @override + void setDefaultWalletForSending(String? walletId) { + _walletsRepo.setDefaultWalletForSending(walletId); + } + + @override + Future storeWallet(Wallet wallet) { + return _walletsRepo.storeWallet(wallet); + } } From 40135bc56150014b2704e9ef25cca5e886a6cedd Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:24:30 +0200 Subject: [PATCH 50/52] chore: remove unused file --- web/index copy.html | 164 -------------------------------------------- 1 file changed, 164 deletions(-) delete mode 100644 web/index copy.html diff --git a/web/index copy.html b/web/index copy.html deleted file mode 100644 index 47f27cd9..00000000 --- a/web/index copy.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - camelus - - - - - - - - - -
- Camelus logo -
loading...
-
- - - - - \ No newline at end of file From ea9f4a95b71ce2a6e08acacea7a0d3e8a7f70227 Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:54:10 +0200 Subject: [PATCH 51/52] fix: vapid key, fb-sw --- .gitignore | 1 + dart_defines.json.example | 39 ++++++++++--------- .../notification_settings_provider.dart | 4 +- web/firebase-messaging-sw.js.example | 25 ++++++++++++ web/index.html | 13 +++++-- 5 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 web/firebase-messaging-sw.js.example diff --git a/.gitignore b/.gitignore index a15edc62..8d579caa 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ migrate_working_dir/ /build/ # Web related +/web/firebase-messaging-sw.js # Symbolication related app.*.symbols diff --git a/dart_defines.json.example b/dart_defines.json.example index 3929d38a..ec00551c 100644 --- a/dart_defines.json.example +++ b/dart_defines.json.example @@ -1,20 +1,21 @@ { - "FIREBASE_API_KEY_WEB": "", - "FIREBASE_APPID_WEB": "", - "FIREBASE_API_KEY_ANDROID": "", - "FIREBASE_APPID_ANDROID": "", - "FIREBASE_API_KEY_IOS": "", - "FIREBASE_APPID_IOS": "", - "FIREBASE_BUNDLE_ID_IOS": "", - "FIREBASE_API_KEY_MACOS": "", - "FIREBASE_APPID_MACOS": "", - "FIREBASE_BUNDLE_ID_MACOS": "", - "FIREBASE_API_KEY_WINDOWS": "", - "FIREBASE_APPID_WINDOWS": "", - "FIREBASE_API_KEY_LINUX": "", - "FIREBASE_APPID_LINUX": "", - "FIREBASE_MESSAGING_SENDER_ID": "", - "FIREBASE_PROJECT_ID": "", - "FIREBASE_AUTH_DOMAIN": "", - "FIREBASE_STORAGE_BUCKET": "" -} \ No newline at end of file + "FIREBASE_VAPID_KEY": "", + "FIREBASE_API_KEY_WEB": "", + "FIREBASE_APPID_WEB": "", + "FIREBASE_API_KEY_ANDROID": "", + "FIREBASE_APPID_ANDROID": "", + "FIREBASE_API_KEY_IOS": "", + "FIREBASE_APPID_IOS": "", + "FIREBASE_BUNDLE_ID_IOS": "", + "FIREBASE_API_KEY_MACOS": "", + "FIREBASE_APPID_MACOS": "", + "FIREBASE_BUNDLE_ID_MACOS": "", + "FIREBASE_API_KEY_WINDOWS": "", + "FIREBASE_APPID_WINDOWS": "", + "FIREBASE_API_KEY_LINUX": "", + "FIREBASE_APPID_LINUX": "", + "FIREBASE_MESSAGING_SENDER_ID": "", + "FIREBASE_PROJECT_ID": "", + "FIREBASE_AUTH_DOMAIN": "", + "FIREBASE_STORAGE_BUCKET": "" +} diff --git a/lib/presentation_layer/providers/notification_settings_provider.dart b/lib/presentation_layer/providers/notification_settings_provider.dart index c07485aa..1589754a 100644 --- a/lib/presentation_layer/providers/notification_settings_provider.dart +++ b/lib/presentation_layer/providers/notification_settings_provider.dart @@ -409,7 +409,9 @@ class NotificationSettingsNotifier extends Notifier { /// needed for iOS to ensure the APNs token is generated and linked to FCM before getting the FCM token await FirebaseMessaging.instance.getAPNSToken(); - final token = await FirebaseMessaging.instance.getToken(); + final token = await FirebaseMessaging.instance.getToken( + vapidKey: kIsWeb ? String.fromEnvironment('FIREBASE_VAPID_KEY') : null, + ); if (token != null) { final appDb = ref.read(dbAppProvider); await appDb.save(key: _dbTokenKey, value: token); diff --git a/web/firebase-messaging-sw.js.example b/web/firebase-messaging-sw.js.example new file mode 100644 index 00000000..31bb48e8 --- /dev/null +++ b/web/firebase-messaging-sw.js.example @@ -0,0 +1,25 @@ +importScripts('https://www.gstatic.com/firebasejs/12.12.1/firebase-app-compat.js'); +importScripts('https://www.gstatic.com/firebasejs/12.12.1/firebase-messaging-compat.js'); + +firebase.initializeApp({ + apiKey: '', + authDomain: '', + projectId: '', + storageBucket: '', + messagingSenderId: '', + appId: '', +}); + +const messaging = firebase.messaging(); + +// Optional: Handle background messages +messaging.onBackgroundMessage((payload) => { + console.log('Received background message ', payload); + const notificationTitle = payload.notification.title; + const notificationOptions = { + body: payload.notification.body, + icon: '/icons/Icon-192.png' + }; + + return self.registration.showNotification(notificationTitle, notificationOptions); +}); diff --git a/web/index.html b/web/index.html index d58ec767..f894069e 100644 --- a/web/index.html +++ b/web/index.html @@ -101,9 +101,7 @@ - +
Camelus logo
loading...
@@ -138,6 +136,15 @@ } }); + + + if ('serviceWorker' in navigator) { + window.addEventListener('load', function () { + navigator.serviceWorker.register('firebase-messaging-sw.js', { + scope: '/firebase-cloud-messaging-push-scope', + }); + }); + } From b7aa87dcf12b252c1b1a1ea5797cf0131b6d3c3c Mon Sep 17 00:00:00 2001 From: Leo <58687994+1-leo@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:20:13 +0200 Subject: [PATCH 52/52] chore: vapid key --- .../providers/notification_settings_provider.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/presentation_layer/providers/notification_settings_provider.dart b/lib/presentation_layer/providers/notification_settings_provider.dart index 1589754a..1d1ab5c9 100644 --- a/lib/presentation_layer/providers/notification_settings_provider.dart +++ b/lib/presentation_layer/providers/notification_settings_provider.dart @@ -409,8 +409,9 @@ class NotificationSettingsNotifier extends Notifier { /// needed for iOS to ensure the APNs token is generated and linked to FCM before getting the FCM token await FirebaseMessaging.instance.getAPNSToken(); + final vapidKey = String.fromEnvironment('FIREBASE_VAPID_KEY'); final token = await FirebaseMessaging.instance.getToken( - vapidKey: kIsWeb ? String.fromEnvironment('FIREBASE_VAPID_KEY') : null, + vapidKey: kIsWeb ? vapidKey : null, ); if (token != null) { final appDb = ref.read(dbAppProvider);