Compare commits

...

1 Commits

Author SHA1 Message Date
Mark Sinclair 1955e03a35 Test and earn
- add logging and clear local storage menu items
- bump nym-connect version
- add app version number
- add reset trigger when clearing SP storage
- bump tauri version (nym-connect and nym-wallet)
- fix webpack config for prod builds
- new selector for services, with an advanced section for service providers
2022-12-17 05:33:23 +00:00
91 changed files with 9562 additions and 626 deletions
+13
View File
@@ -30,6 +30,19 @@ module.exports = {
use: ['@svgr/webpack'],
});
config.module.rules.unshift({
test: /\.ya?ml$/,
type: 'json',
use: [
{
loader: 'yaml-loader',
options: {
asJSON: true,
},
},
],
});
config.resolve.extensions = ['.tsx', '.ts', '.js'];
config.resolve.plugins = [new TsconfigPathsPlugin()];
@@ -0,0 +1,10 @@
/**
* This is a mock for Tauri's API package (@tauri-apps/api/notification), to prevent stories from being excluded, because they either use
* or import dependencies that use Tauri.
*/
module.exports = {
isPermissionGranted: () => undefined,
requestPermission: () => undefined,
sendNotification: () => undefined,
};
+228 -90
View File
@@ -14,12 +14,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aes"
version = "0.7.5"
@@ -469,12 +463,11 @@ dependencies = [
[[package]]
name = "cargo_toml"
version = "0.11.6"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4419e9adae9fd7e231b60d50467481bf8181ddeef6ed54683b23ae925c74c9c"
checksum = "aa0e3586af56b3bfa51fca452bd56e8dbbbd5d8d81cbf0b7e4e35b695b537eb8"
dependencies = [
"serde",
"serde_derive",
"toml",
]
@@ -754,6 +747,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colored"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59"
dependencies = [
"atty",
"lazy_static",
"winapi",
]
[[package]]
name = "colored"
version = "2.0.0"
@@ -1316,13 +1320,14 @@ dependencies = [
]
[[package]]
name = "deflate"
version = "0.7.20"
name = "dbus"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6"
dependencies = [
"adler32",
"byteorder",
"libc",
"libdbus-sys",
"winapi",
]
[[package]]
@@ -1471,6 +1476,12 @@ dependencies = [
"dtoa",
]
[[package]]
name = "dunce"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c"
[[package]]
name = "dyn-clone"
version = "1.0.9"
@@ -1632,6 +1643,16 @@ dependencies = [
"instant",
]
[[package]]
name = "fern"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a"
dependencies = [
"colored 1.9.3",
"log",
]
[[package]]
name = "ff"
version = "0.10.1"
@@ -2620,12 +2641,12 @@ dependencies = [
[[package]]
name = "ico"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a4b3331534254a9b64095ae60d3dc2a8225a7a70229cd5888be127cdc1f6804"
checksum = "031530fe562d8c8d71c0635013d6d155bbfe8ba0aa4b4d2d24ce8af6b71047bd"
dependencies = [
"byteorder",
"png 0.11.0",
"png",
]
[[package]]
@@ -2702,15 +2723,6 @@ dependencies = [
"cfb",
]
[[package]]
name = "inflate"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4"
dependencies = [
"adler32",
]
[[package]]
name = "inout"
version = "0.1.3"
@@ -2749,9 +2761,9 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
[[package]]
name = "itertools"
version = "0.10.4"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
@@ -2793,9 +2805,9 @@ dependencies = [
[[package]]
name = "jni"
version = "0.19.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
dependencies = [
"cesu8",
"combine",
@@ -2914,6 +2926,15 @@ version = "0.2.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
[[package]]
name = "libdbus-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b"
dependencies = [
"pkg-config",
]
[[package]]
name = "libgit2-sys"
version = "0.14.0+1.5.0"
@@ -2974,6 +2995,12 @@ dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "lioness"
version = "0.1.2"
@@ -3003,6 +3030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
"serde",
]
[[package]]
@@ -3034,6 +3062,19 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mac-notification-sys"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e72d50edb17756489e79d52eb146927bec8eba9dd48faadf9ef08bca3791ad5"
dependencies = [
"cc",
"dirs-next",
"objc-foundation",
"objc_id",
"time 0.3.17",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -3255,6 +3296,17 @@ dependencies = [
"wasm-timer",
]
[[package]]
name = "notify-rust"
version = "4.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368e89ea58df747ce88be669ae44e79783c1d30bfd540ad0fc520b3f41f0b3b0"
dependencies = [
"dbus",
"mac-notification-sys",
"tauri-winrt-notification",
]
[[package]]
name = "num-derive"
version = "0.3.3"
@@ -3276,17 +3328,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
dependencies = [
"autocfg 1.1.0",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
@@ -3357,21 +3398,28 @@ dependencies = [
name = "nym-connect"
version = "1.1.4"
dependencies = [
"anyhow",
"bip39",
"chrono",
"client-core",
"config",
"crypto",
"dirs",
"eyre",
"fern",
"fix-path-env",
"futures",
"itertools",
"log",
"logging",
"nym-socks5-client",
"pretty_env_logger",
"rand 0.8.5",
"reqwest",
"rust-embed",
"serde",
"serde_json",
"serde_repr",
"tap",
"task",
"tauri",
@@ -3385,6 +3433,7 @@ dependencies = [
"topology",
"ts-rs",
"url",
"yaml-rust",
]
[[package]]
@@ -3612,9 +3661,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.14.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "opaque-debug"
@@ -4065,18 +4114,6 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "png"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925"
dependencies = [
"bitflags",
"deflate",
"inflate",
"num-iter",
]
[[package]]
name = "png"
version = "0.17.6"
@@ -4221,6 +4258,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.21"
@@ -4473,9 +4519,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [
"aho-corasick",
"memchr",
@@ -4535,6 +4581,7 @@ dependencies = [
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-socks",
"tower-service",
"url",
"wasm-bindgen",
@@ -4604,6 +4651,41 @@ dependencies = [
"opaque-debug 0.3.0",
]
[[package]]
name = "rust-embed"
version = "6.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "6.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "7.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054"
dependencies = [
"globset",
"sha2 0.10.6",
"walkdir",
]
[[package]]
name = "rustc_version"
version = "0.3.3"
@@ -5459,6 +5541,27 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb"
dependencies = [
"heck 0.3.3",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "subtle"
version = "1.0.0"
@@ -5531,9 +5634,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.14.0"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43336f5d1793543ba96e2a1e75f3a5c7dcd592743be06a0ab3a190f4fcb4b934"
checksum = "a1fa15735311b4816d030ff54da58560b047daca0970e1031aed5502e84231a8"
dependencies = [
"bitflags",
"cairo-rs",
@@ -5566,12 +5669,12 @@ dependencies = [
"once_cell",
"parking_lot 0.12.1",
"paste",
"png 0.17.6",
"png",
"raw-window-handle",
"scopeguard",
"serde",
"unicode-segmentation",
"uuid 1.1.2",
"uuid 1.2.2",
"windows 0.39.0",
"windows-implement",
"x11-dl",
@@ -5608,9 +5711,9 @@ dependencies = [
[[package]]
name = "tauri"
version = "1.1.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efbf22abd61d95ca9b2becd77f9db4c093892f73e8a07d21d8b0b2bf71a7bcea"
checksum = "d8ea1d785ab2164373703817bff144c4610a69ad3f659becaca0e1ea004b98d8"
dependencies = [
"anyhow",
"attohttpc",
@@ -5628,6 +5731,7 @@ dependencies = [
"http",
"ignore",
"minisign-verify",
"notify-rust",
"objc",
"once_cell",
"open",
@@ -5652,7 +5756,7 @@ dependencies = [
"time 0.3.17",
"tokio",
"url",
"uuid 1.1.2",
"uuid 1.2.2",
"webkit2gtk",
"webview2-com",
"windows 0.39.0",
@@ -5661,9 +5765,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "1.1.1"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0991fb306849897439dbd4a72e4cbed2413e2eb26cb4b3ba220b94edba8b4b88"
checksum = "8807c85d656b2b93927c19fe5a5f1f1f348f96c2de8b90763b3c2d561511f9b4"
dependencies = [
"anyhow",
"cargo_toml",
@@ -5677,16 +5781,16 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "1.1.1"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "356fa253e40ae4d6ff02075011f2f2bb4066f5c9d8c1e16ca6912d7b75903ba6"
checksum = "14388d484b6b1b5dc0f6a7d6cc6433b3b230bec85eaa576adcdf3f9fafa49251"
dependencies = [
"base64",
"brotli",
"ico",
"json-patch",
"plist",
"png 0.17.6",
"png",
"proc-macro2",
"quote",
"regex",
@@ -5697,15 +5801,15 @@ dependencies = [
"tauri-utils",
"thiserror",
"time 0.3.17",
"uuid 1.1.2",
"uuid 1.2.2",
"walkdir",
]
[[package]]
name = "tauri-macros"
version = "1.1.1"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6051fd6940ddb22af452340d03c66a3e2f5d72e0788d4081d91e31528ccdc4d"
checksum = "069319e5ecbe653a799b94b0690d9f9bf5d00f7b1d3989aa331c524d4e354075"
dependencies = [
"heck 0.4.0",
"proc-macro2",
@@ -5717,30 +5821,29 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.11.1"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49439a5ea47f474572b854972f42eda2e02a470be5ca9609cc83bb66945abe2"
checksum = "c507d954d08ac8705d235bc70ec6975b9054fb95ff7823af72dbb04186596f3b"
dependencies = [
"gtk",
"http",
"http-range",
"infer",
"rand 0.8.5",
"raw-window-handle",
"serde",
"serde_json",
"tauri-utils",
"thiserror",
"uuid 1.1.2",
"uuid 1.2.2",
"webview2-com",
"windows 0.39.0",
]
[[package]]
name = "tauri-runtime-wry"
version = "0.11.1"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dce920995fd49907aa9bea7249ed1771454f11f7611924c920a1f75fb614d4"
checksum = "36b1c5764a41a13176a4599b5b7bd0881bea7d94dfe45e1e755f789b98317e30"
dependencies = [
"cocoa",
"gtk",
@@ -5749,7 +5852,7 @@ dependencies = [
"raw-window-handle",
"tauri-runtime",
"tauri-utils",
"uuid 1.1.2",
"uuid 1.2.2",
"webkit2gtk",
"webview2-com",
"windows 0.39.0",
@@ -5758,15 +5861,16 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.1.1"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8fdae6f29cef959809a3c3afef510c5b715a446a597ab8b791497585363f39"
checksum = "5abbc109a6eb45127956ffcc26ef0e875d160150ac16cfa45d26a6b2871686f1"
dependencies = [
"brotli",
"ctor",
"glob",
"heck 0.4.0",
"html5ever",
"infer",
"json-patch",
"kuchiki",
"memchr",
@@ -5783,6 +5887,17 @@ dependencies = [
"windows 0.39.0",
]
[[package]]
name = "tauri-winrt-notification"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c58de036c4d2e20717024de2a3c4bf56c301f07b21bc8ef9b57189fce06f1f3b"
dependencies = [
"quick-xml",
"strum",
"windows 0.39.0",
]
[[package]]
name = "tempfile"
version = "3.3.0"
@@ -6055,6 +6170,18 @@ dependencies = [
"webpki 0.22.0",
]
[[package]]
name = "tokio-socks"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
dependencies = [
"either",
"futures-util",
"thiserror",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.10"
@@ -6346,9 +6473,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "uuid"
version = "1.1.2"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
dependencies = [
"getrandom 0.2.7",
]
@@ -6363,7 +6490,7 @@ dependencies = [
"coconut-bandwidth-contract-common",
"coconut-dkg-common",
"coconut-interface",
"colored",
"colored 2.0.0",
"config",
"contracts-common",
"cosmrs",
@@ -6634,9 +6761,9 @@ dependencies = [
[[package]]
name = "webkit2gtk"
version = "0.18.0"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29952969fb5e10fe834a52eb29ad0814ccdfd8387159b0933edf1344a1c9cdcc"
checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370"
dependencies = [
"bitflags",
"cairo-rs",
@@ -7011,15 +7138,16 @@ dependencies = [
[[package]]
name = "wry"
version = "0.21.1"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945"
checksum = "4c1ad8e2424f554cc5bdebe8aa374ef5b433feff817aebabca0389961fc7ef98"
dependencies = [
"base64",
"block",
"cocoa",
"core-graphics",
"crossbeam-channel",
"dunce",
"gdk",
"gio",
"glib",
@@ -7035,6 +7163,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.6",
"soup2",
"tao",
"thiserror",
"url",
@@ -7101,6 +7230,15 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "zeroize"
version = "1.5.7"
@@ -7124,9 +7262,9 @@ dependencies = [
[[package]]
name = "zip"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf225bcf73bb52cbb496e70475c7bd7a3f769df699c0020f6c7bd9a96dcf0b8d"
checksum = "537ce7411d25e54e8ae21a7ce0b15840e7bfcff15b51d697ec3266cc76bdf080"
dependencies = [
"byteorder",
"crc32fast",
+7 -3
View File
@@ -6,6 +6,7 @@
"scripts": {
"prewebpack:dev": "yarn --cwd .. build",
"webpack:dev": "yarn webpack serve --config webpack.dev.js",
"webpack:dev:onlyThis": "yarn webpack serve --config webpack.dev.js",
"webpack:prod": "yarn webpack --progress --config webpack.prod.js",
"tauri:dev": "RUST_DEBUG=1 yarn tauri dev",
"tauri:build": "yarn tauri build",
@@ -30,7 +31,7 @@
"@mui/material": "^5.2.2",
"@mui/styles": "^5.2.2",
"@nymproject/react": "^1.0.0",
"@tauri-apps/api": "^1.1.0",
"@tauri-apps/api": "^1.2.0",
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
"clsx": "^1.1.1",
"luxon": "^2.3.0",
@@ -39,6 +40,7 @@
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",
"react-hook-form": "^7.14.2",
"react-markdown": "^8.0.4",
"react-router-dom": "^5.2.0",
"semver": "^6.3.0",
"yup": "^0.32.9"
@@ -49,11 +51,12 @@
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@mdx-js/loader": "^2.1.5",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@storybook/react": "^6.5.8",
"@svgr/webpack": "^6.1.1",
"@tauri-apps/cli": "^1.1.0",
"@tauri-apps/cli": "^1.2.2",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/jest": "^27.0.1",
@@ -105,6 +108,7 @@
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
"webpack-merge": "^5.8.0",
"yaml-loader": "^0.8.0"
}
}
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nym Connect</title>
</head>
<body style="background: rgb(29, 33, 37);">
<div id="root-growth"></div>
</body>
</html>
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>nym-connect</title>
<title>Nym Connect</title>
</head>
<body>
<div id="root"></div>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nym Wallet Logs</title>
</head>
<body>
<div id="root-log"></div>
</body>
</html>
+14 -7
View File
@@ -13,33 +13,40 @@ rust-version = "1.58"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "^1.1.1", features = [] }
tauri-build = { version = "^1.2.1", features = [] }
tauri-codegen = "^1.1.1"
tauri-macros = "^1.1.1"
tauri-codegen = "^1.2.1"
tauri-macros = "^1.2.1"
[dependencies]
anyhow = "1.0"
bip39 = "1.0"
chrono = "0.4"
dirs = "4.0"
eyre = "0.6.5"
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", branch = "release"}
futures = "0.3"
log = "0.4"
fern = { version = "0.6.1", features = ["colored"] }
itertools = "0.10.5"
log = { version = "0.4", features = ["serde"] }
pretty_env_logger = "0.4.0"
rand = "0.8"
reqwest = { version = "0.11", features = ["json"] }
reqwest = { version = "0.11", features = ["json", "socks"] }
rust-embed = { version = "6.4.2", features = ["include-exclude"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
tap = "1.0.1"
tauri = { version = "^1.1.1", features = ["clipboard-write-text", "macos-private-api", "shell-open", "system-tray", "updater", "window-close", "window-minimize", "window-start-dragging"] }
tauri = { version = "^1.2.2", features = ["clipboard-write-text", "macos-private-api", "notification-all", "shell-open", "system-tray", "updater", "window-close", "window-minimize", "window-start-dragging"] }
tendermint-rpc = "0.23.0"
thiserror = "1.0"
tokio = { version = "1.21.2", features = ["sync", "time"] }
url = "2.2"
yaml-rust = "0.4"
client-core = { path = "../../clients/client-core" }
config-common = { path = "../../common/config", package = "config" }
crypto = { path = "../../common/crypto" }
logging = { path = "../../common/logging"}
nym-socks5-client = { path = "../../clients/socks5" }
task = { path = "../../common/task" }
+7
View File
@@ -44,6 +44,13 @@ impl Config {
}
}
#[allow(unused)]
pub fn new_with_port<S: Into<String>>(id: S, provider_mix_address: S, port: u16) -> Self {
Config {
socks5: Socks5Config::new(id, provider_mix_address).with_port(port),
}
}
pub fn get_socks5(&self) -> &Socks5Config {
&self.socks5
}
+12
View File
@@ -27,6 +27,11 @@ pub enum BackendError {
source: tauri::Error,
},
#[error("{source}")]
TauriApiError {
#[from]
source: tauri::api::Error,
},
#[error("{source}")]
SerdeJsonError {
#[from]
source: serde_json::Error,
@@ -36,6 +41,11 @@ pub enum BackendError {
#[from]
source: ClientCoreError<fs_backend::Backend>,
},
#[error("{source}")]
ApiClientError {
#[from]
source: crate::operations::growth::api_client::ApiClientError,
},
#[error("Could not send disconnect signal to the SOCKS5 client")]
CoundNotSendDisconnectSignal,
@@ -57,6 +67,8 @@ pub enum BackendError {
CouldNotGetConfigFilename,
#[error("Could not load existing gateway configuration")]
CouldNotLoadExistingGatewayConfiguration(std::io::Error),
#[error("Unable to open a new window")]
NewWindowError,
}
impl Serialize for BackendError {
+113
View File
@@ -0,0 +1,113 @@
use std::str::FromStr;
use fern::colors::ColoredLevelConfig;
use serde::Serialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use tauri::Manager;
pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerError> {
let colors = ColoredLevelConfig::new();
let base_config = fern::Dispatch::new()
.level(global_level())
.filter_lowlevel_external_components()
.show_operations();
let stdout_config = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
colors.color(record.level()),
message,
))
})
.chain(std::io::stdout());
let tauri_event_config = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"{}[{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
message,
))
})
.chain(fern::Output::call(move |record| {
let msg = LogMessage {
message: record.args().to_string(),
level: record.level().into(),
};
app_handle.emit_all("log://log", msg).unwrap();
}));
base_config
.chain(stdout_config)
.chain(tauri_event_config)
.apply()
}
trait FernExt {
fn show_operations(self) -> Self;
fn filter_lowlevel_external_components(self) -> Self;
}
impl FernExt for fern::Dispatch {
fn show_operations(self) -> Self {
if ::std::env::var("RUST_TRACE_OPERATIONS").is_ok() {
self.level_for("nym_connect::operations", log::LevelFilter::Trace)
} else {
self
}
}
fn filter_lowlevel_external_components(self) -> Self {
self.level_for("hyper", log::LevelFilter::Warn)
.level_for("tokio_reactor", log::LevelFilter::Warn)
.level_for("reqwest", log::LevelFilter::Warn)
.level_for("mio", log::LevelFilter::Warn)
.level_for("want", log::LevelFilter::Warn)
.level_for("sled", log::LevelFilter::Warn)
.level_for("tungstenite", log::LevelFilter::Warn)
.level_for("tokio_tungstenite", log::LevelFilter::Warn)
.level_for("rustls", log::LevelFilter::Warn)
.level_for("tokio_util", log::LevelFilter::Warn)
}
}
fn global_level() -> log::LevelFilter {
if let Ok(s) = ::std::env::var("RUST_LOG") {
log::LevelFilter::from_str(&s).unwrap_or(log::LevelFilter::Info)
} else {
log::LevelFilter::Info
}
}
#[derive(Debug, Serialize, Clone)]
struct LogMessage {
message: String,
level: LogLevel,
}
// Serialize to u16 instead of strings.
#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)]
#[repr(u16)]
enum LogLevel {
Trace = 1,
Debug,
Info,
Warn,
Error,
}
impl From<log::Level> for LogLevel {
fn from(level: log::Level) -> Self {
match level {
log::Level::Trace => LogLevel::Trace,
log::Level::Debug => LogLevel::Debug,
log::Level::Info => LogLevel::Info,
log::Level::Warn => LogLevel::Warn,
log::Level::Error => LogLevel::Error,
}
}
}
+27 -5
View File
@@ -6,8 +6,7 @@
use std::sync::Arc;
use config_common::defaults::setup_env;
use logging::setup_logging;
use tauri::Menu;
use tauri::{Manager, Menu};
use tokio::sync::RwLock;
use crate::menu::AddDefaultSubmenus;
@@ -17,6 +16,7 @@ use crate::window::window_toggle;
mod config;
mod error;
mod logging;
mod menu;
mod models;
mod operations;
@@ -25,7 +25,6 @@ mod tasks;
mod window;
fn main() {
setup_logging();
setup_env(None);
println!("Starting up...");
@@ -35,11 +34,13 @@ fn main() {
log::warn!("Failed to fix PATH: {error}");
}
let context = tauri::generate_context!();
tauri::Builder::default()
.manage(Arc::new(RwLock::new(State::new())))
.invoke_handler(tauri::generate_handler![
crate::config::get_config_file_location,
crate::config::get_config_id,
crate::operations::connection::status::get_connection_status,
crate::operations::connection::connect::get_gateway,
crate::operations::connection::connect::get_service_provider,
crate::operations::connection::connect::set_gateway,
@@ -49,10 +50,31 @@ fn main() {
crate::operations::directory::get_services,
crate::operations::export::export_keys,
crate::operations::window::hide_window,
crate::operations::growth::test_and_earn::growth_tne_get_client_id,
crate::operations::growth::test_and_earn::growth_tne_take_part,
crate::operations::growth::test_and_earn::growth_tne_get_draws,
crate::operations::growth::test_and_earn::growth_tne_ping,
crate::operations::growth::test_and_earn::growth_tne_submit_wallet_address,
crate::operations::growth::test_and_earn::growth_tne_enter_draw,
crate::operations::growth::test_and_earn::growth_tne_toggle_window,
crate::operations::help::log::help_log_toggle_window,
])
.menu(Menu::new().add_default_app_submenu_if_macos())
.menu(Menu::os_default(&context.package_info().name).add_default_app_submenus())
.on_menu_event(|event| {
if event.menu_item_id() == menu::SHOW_LOG_WINDOW {
let _r = crate::operations::help::log::help_log_toggle_window(
event.window().app_handle(),
);
}
if event.menu_item_id() == menu::CLEAR_STORAGE {
let _r = crate::operations::help::storage::help_clear_storage(
event.window().app_handle(),
);
}
})
.setup(|app| Ok(crate::logging::setup_logging(app.app_handle())?))
.system_tray(create_tray_menu())
.on_system_tray_event(tray_menu_event_handler)
.run(tauri::generate_context!())
.run(context)
.expect("error while running tauri application");
}
+15 -29
View File
@@ -1,40 +1,26 @@
use crate::window_toggle;
use tauri::{
AppHandle, CustomMenuItem, Menu, SystemTray, SystemTrayEvent, SystemTrayMenu,
AppHandle, CustomMenuItem, Menu, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu,
SystemTrayMenuItem, Wry,
};
#[cfg(target_os = "macos")]
use tauri::{MenuItem, Submenu};
use crate::window_toggle;
pub const SHOW_LOG_WINDOW: &str = "show_log_window";
pub const CLEAR_STORAGE: &str = "clear_storage";
pub trait AddDefaultSubmenus {
fn add_default_app_submenu_if_macos(self) -> Self;
fn add_default_app_submenus(self) -> Self;
}
impl AddDefaultSubmenus for Menu {
fn add_default_app_submenu_if_macos(self) -> Menu {
#[cfg(target_os = "macos")]
return self
.add_submenu(Submenu::new(
"File",
Menu::new().add_native_item(MenuItem::Quit),
))
.add_submenu(Submenu::new(
"Edit",
Menu::new()
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::SelectAll),
))
.add_submenu(Submenu::new(
"Window",
Menu::new()
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::ShowAll),
));
#[cfg(not(target_os = "macos"))]
return self;
fn add_default_app_submenus(self) -> Self {
let submenu = Submenu::new(
"Help",
Menu::new()
.add_item(CustomMenuItem::new(SHOW_LOG_WINDOW, "Show logs"))
.add_item(CustomMenuItem::new(CLEAR_STORAGE, "Clear all settings")),
);
self.add_submenu(submenu)
}
}
+21 -3
View File
@@ -39,21 +39,31 @@ impl fmt::Display for ConnectionStatusKind {
pub const APP_EVENT_CONNECTION_STATUS_CHANGED: &str = "app:connection-status-changed";
#[cfg_attr(test, derive(ts_rs::TS))]
#[derive(Clone, serde::Serialize)]
#[derive(Clone, Serialize)]
pub struct AppEventConnectionStatusChangedPayload {
pub status: ConnectionStatusKind,
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DirectoryService {
pub id: String,
pub description: String,
pub items: Vec<DirectoryServiceProvider>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HarbourMasterService {
pub service_provider_client_id: String,
pub gateway_identity_key: String,
pub ip_address: String,
pub last_successful_ping_utc: String,
pub last_updated_utc: String,
pub routing_score: f32,
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DirectoryServiceProvider {
pub id: String,
pub description: String,
@@ -63,3 +73,11 @@ pub struct DirectoryServiceProvider {
/// Address of the gateway, e.g. 2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh
pub gateway: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PagedResult<T> {
pub page: u32,
pub size: u32,
pub total: i32,
pub items: Vec<T>,
}
@@ -37,10 +37,10 @@ pub async fn get_service_provider(state: tauri::State<'_, Arc<RwLock<State>>>) -
#[tauri::command]
pub async fn set_service_provider(
service_provider: String,
service_provider: Option<String>,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<()> {
log::trace!("Setting service_provider: {service_provider}");
log::trace!("Setting service_provider: {:?}", &service_provider);
let mut guard = state.write().await;
guard.set_service_provider(service_provider);
Ok(())
@@ -57,10 +57,10 @@ pub async fn get_gateway(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<
#[tauri::command]
pub async fn set_gateway(
gateway: String,
gateway: Option<String>,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<()> {
log::trace!("Setting gateway: {gateway}");
log::trace!("Setting gateway: {:?}", &gateway);
let mut guard = state.write().await;
guard.set_gateway(gateway);
Ok(())
@@ -1,2 +1,3 @@
pub mod connect;
pub mod disconnect;
pub mod status;
@@ -0,0 +1,15 @@
use crate::error::Result;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::models::ConnectionStatusKind;
use crate::state::State;
#[tauri::command]
pub async fn get_connection_status(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<ConnectionStatusKind> {
let state = state.read().await;
Ok(state.get_status())
}
@@ -1,9 +1,13 @@
use itertools::Itertools;
use crate::error::Result;
use crate::models::DirectoryService;
use crate::models::{DirectoryService, HarbourMasterService, PagedResult};
static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
"https://nymtech.net/.wellknown/connect/service-providers.json";
static HARBOUR_MASTER_URL: &str = "https://harbourmaster.nymtech.net/v1/services/?size=100";
#[tauri::command]
pub async fn get_services() -> Result<Vec<DirectoryService>> {
log::trace!("Fetching services");
@@ -12,5 +16,36 @@ pub async fn get_services() -> Result<Vec<DirectoryService>> {
.json::<Vec<DirectoryService>>()
.await?;
log::trace!("Received: {:#?}", res);
Ok(res)
// TODO: get paged
log::trace!("Fetching active services");
let active_services = reqwest::get(HARBOUR_MASTER_URL)
.await?
.json::<PagedResult<HarbourMasterService>>()
.await?;
log::trace!("Active: {:#?}", active_services);
let mut filtered: Vec<DirectoryService> = vec![];
for service in &res {
let items: _ = service
.items
.clone()
.into_iter()
.filter(|sp| {
active_services
.items
.iter()
.any(|active| active.service_provider_client_id == sp.address)
})
.collect_vec();
log::trace!("service = {} has {} items", service.id, items.len());
filtered.push(DirectoryService {
id: service.id.clone(),
description: service.description.clone(),
items,
})
}
Ok(filtered)
}
@@ -5,6 +5,24 @@ use crate::{
error::{BackendError, Result},
state::State,
};
use client_core::client::key_manager::KeyManager;
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
use crypto::asymmetric::identity;
pub async fn get_identity_key(
state: &tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<Arc<identity::KeyPair>> {
let config = {
let state = state.read().await;
state.load_socks5_config()?
};
let pathfinder = ClientKeyPathfinder::new_from_config(config.get_base());
let key_manager = KeyManager::load_keys(&pathfinder)?;
let identity_keypair = key_manager.identity_keypair();
Ok(identity_keypair)
}
/// Export the gateway keys as a JSON string blob
#[tauri::command]
@@ -0,0 +1,271 @@
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[allow(unused)]
#[derive(Error, Debug)]
pub enum ApiClientError {
#[error("{source}")]
Reqwest {
#[from]
source: reqwest::Error,
},
#[error("{source}")]
SerdeJson {
#[from]
source: serde_json::Error,
},
#[error("{0}")]
Status(String),
}
const API_BASE_URL: &str = "https://growth-api.nymtech.net";
// For development mode, switch to this
// const API_BASE_URL: &str = "http://localhost:8000";
#[derive(Debug, Clone)]
pub struct GrowthApiClient {
base_url: String,
}
impl GrowthApiClient {
pub fn new(resource_base: &str) -> Self {
let base_url = std::env::var("API_BASE_URL").unwrap_or_else(|_| API_BASE_URL.to_string());
GrowthApiClient {
base_url: format!("{}{}", base_url, resource_base),
}
}
pub fn registrations() -> Registrations {
Registrations::new(GrowthApiClient::new("/v1/tne"))
}
pub fn daily_draws() -> DailyDraws {
DailyDraws::new(GrowthApiClient::new("/v1/tne/daily_draw"))
}
pub(crate) async fn get<T: DeserializeOwned>(&self, url: &str) -> Result<T, ApiClientError> {
log::info!(">>> GET {}", url);
let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:1080")?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(10))
.build()?;
match client.get(format!("{}{}", self.base_url, url)).send().await {
Ok(res) => {
if res.status().is_client_error() || res.status().is_server_error() {
log::error!("<<< {}", res.status());
return Err(ApiClientError::Status(res.status().to_string()));
}
match res.text().await {
Ok(response_body) => {
log::info!("<<< {}", response_body);
match serde_json::from_str(&response_body) {
Ok(res) => Ok(res),
Err(e) => {
log::error!("<<< JSON parsing error: {}", e);
Err(e.into())
}
}
}
Err(e) => {
log::error!("<<< Request error: {}", e);
Err(e.into())
}
}
}
Err(e) => {
log::error!("<<< Response parsing error: {}", e);
Err(e.into())
}
}
}
pub(crate) async fn post<REQ: Serialize + ?Sized, RESP: DeserializeOwned>(
&self,
url: &str,
body: &REQ,
) -> Result<RESP, ApiClientError> {
log::info!(">>> POST {}", url);
let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:1080")?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(10))
.build()?;
match client
.post(format!("{}{}", self.base_url, url))
.json(body)
.send()
.await
{
Ok(res) => {
if res.status().is_client_error() || res.status().is_server_error() {
log::error!("<<< {}", res.status());
return Err(ApiClientError::Status(res.status().to_string()));
}
match res.text().await {
Ok(response_body) => {
log::info!("<<< {}", response_body);
match serde_json::from_str(&response_body) {
Ok(res) => Ok(res),
Err(e) => {
log::error!("<<< JSON parsing error: {}", e);
Err(e.into())
}
}
}
Err(e) => {
log::error!("<<< Request error: {}", e);
Err(e.into())
}
}
}
Err(e) => {
log::error!("<<< Response parsing error: {}", e);
Err(e.into())
}
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ClientIdPartial {
pub client_id: String,
pub client_id_signature: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Registration {
pub id: String,
pub client_id: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Ping {
pub client_id: String,
pub timestamp: String,
}
pub struct Registrations {
client: GrowthApiClient,
}
impl Registrations {
pub fn new(client: GrowthApiClient) -> Self {
Registrations { client }
}
pub async fn register(
&self,
registration: &ClientIdPartial,
) -> Result<Registration, ApiClientError> {
self.client.post("/register", &registration).await
}
#[allow(dead_code)]
pub async fn unregister(&self, registration: &ClientIdPartial) -> Result<(), ApiClientError> {
self.client.post("/unregister", &registration).await
}
#[allow(dead_code)]
pub async fn status(&self, registration: &ClientIdPartial) -> Result<(), ApiClientError> {
self.client.post("/status", &registration).await
}
pub async fn ping(&self, registration: &ClientIdPartial) -> Result<(), ApiClientError> {
self.client.post("/ping", &registration).await
}
#[allow(dead_code)]
pub async fn health(&self) -> Result<(), ApiClientError> {
self.client.get("/health").await
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DrawEntryPartial {
pub draw_id: String,
pub client_id: String,
pub client_id_signature: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DrawEntry {
pub id: String,
pub draw_id: String,
pub timestamp: String,
pub status: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DrawWithWordOfTheDay {
pub id: String,
pub start_utc: String,
pub end_utc: String,
pub word_of_the_day: Option<String>,
pub last_modified: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ClaimPartial {
pub draw_id: String,
pub registration_id: String,
pub client_id: String,
pub client_id_signature: String,
pub wallet_address: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Winner {
pub id: String,
pub client_id: String,
pub draw_id: String,
pub timestamp: String,
pub winner_reg_id: String,
pub winner_wallet_address: Option<String>,
pub winner_claim_timestamp: Option<String>,
}
pub struct DailyDraws {
client: GrowthApiClient,
}
impl DailyDraws {
pub fn new(client: GrowthApiClient) -> Self {
DailyDraws { client }
}
pub async fn current(&self) -> Result<DrawWithWordOfTheDay, ApiClientError> {
self.client.get("/current").await
}
pub async fn next(&self) -> Result<DrawWithWordOfTheDay, ApiClientError> {
self.client.get("/next").await
}
#[allow(dead_code)]
pub async fn status(&self, draw_id: &str) -> Result<DrawWithWordOfTheDay, ApiClientError> {
self.client
.get(format!("/status/{}", draw_id).as_str())
.await
}
pub async fn enter(&self, entry: &DrawEntryPartial) -> Result<DrawEntry, ApiClientError> {
self.client.post("/enter", entry).await
}
pub async fn entries(
&self,
client_id: &ClientIdPartial,
) -> Result<Vec<DrawEntry>, ApiClientError> {
self.client.post("/entries", client_id).await
}
pub async fn claim(&self, claim: &ClaimPartial) -> Result<Winner, ApiClientError> {
self.client.post("/claim", claim).await
}
}
@@ -0,0 +1,57 @@
use rust_embed::RustEmbed;
extern crate yaml_rust;
use yaml_rust::YamlLoader;
#[derive(RustEmbed)]
#[folder = "../src/components/Growth/content/"]
#[include = "*.yaml"]
#[exclude = "*.mdx"]
struct Asset;
#[derive(Debug)]
pub struct NotificationContent {
pub title: String,
pub body: String,
}
#[derive(Debug)]
pub struct Notifications {
pub you_are_in_draw: NotificationContent,
pub take_part: NotificationContent,
}
pub struct Content {}
const RESOURCE_ERROR: &str = "❌ RESOURCE ERROR";
fn get_as_string_or_error_message(value: &yaml_rust::Yaml) -> String {
value.as_str().unwrap_or(RESOURCE_ERROR).to_string()
}
impl Content {
pub fn get_notifications() -> Notifications {
let content = Asset::get("en.yaml").unwrap();
let s = std::str::from_utf8(content.data.as_ref()).unwrap();
let content = YamlLoader::load_from_str(s).unwrap();
let content = &content[0];
Notifications {
you_are_in_draw: NotificationContent {
title: get_as_string_or_error_message(
&content["testAndEarn"]["notifications"]["youAreInDraw"]["title"],
),
body: get_as_string_or_error_message(
&content["testAndEarn"]["notifications"]["youAreInDraw"]["body"],
),
},
take_part: NotificationContent {
title: get_as_string_or_error_message(
&content["testAndEarn"]["notifications"]["takePart"]["title"],
),
body: get_as_string_or_error_message(
&content["testAndEarn"]["notifications"]["takePart"]["body"],
),
},
}
}
}
@@ -0,0 +1,3 @@
pub mod assets;
pub mod test_and_earn;
pub mod api_client;
@@ -0,0 +1,156 @@
use crate::error::BackendError;
use crate::operations::export::get_identity_key;
use crate::operations::growth::api_client::{
ClaimPartial, ClientIdPartial, DrawEntry, DrawEntryPartial, DrawWithWordOfTheDay,
GrowthApiClient, Registration, Winner,
};
use crate::State;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::api::notification::Notification;
use tauri::Manager;
use tokio::sync::RwLock;
async fn get_client_id(
state: &tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<ClientIdPartial, BackendError> {
let keypair = get_identity_key(state).await?;
let client_id = keypair.public_key().to_base58_string();
let client_id_signature = keypair
.private_key()
.sign(client_id.as_bytes())
.to_base58_string();
Ok(ClientIdPartial {
client_id,
client_id_signature,
})
}
#[tauri::command]
pub async fn growth_tne_get_client_id(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<ClientIdPartial, BackendError> {
get_client_id(&state).await
}
#[tauri::command]
pub async fn growth_tne_take_part(
app_handle: tauri::AppHandle,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<Registration, BackendError> {
let notifications = super::assets::Content::get_notifications();
let client_id = get_client_id(&state).await?;
let registration = GrowthApiClient::registrations()
.register(&client_id)
.await?;
log::info!("<<< Test&Earn: registration details: {:?}", registration);
if let Err(e) = Notification::new(&app_handle.config().tauri.bundle.identifier)
.title(notifications.take_part.title)
.body(notifications.take_part.body)
.show()
{
log::error!("Could not show notification. Error = {:?}", e);
}
Ok(registration)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Draws {
pub current: Option<DrawWithWordOfTheDay>,
pub next: Option<DrawWithWordOfTheDay>,
pub draws: Vec<DrawEntry>,
}
#[tauri::command]
pub async fn growth_tne_get_draws(client_details: ClientIdPartial) -> Result<Draws, BackendError> {
let draws_api = GrowthApiClient::daily_draws();
let current = draws_api.current().await.ok();
let next = draws_api.next().await.ok();
let draws = draws_api.entries(&client_details).await?;
Ok(Draws {
current,
next,
draws,
})
}
#[tauri::command]
pub async fn growth_tne_enter_draw(
client_details: ClientIdPartial,
draw_id: String,
) -> Result<DrawEntry, BackendError> {
Ok(GrowthApiClient::daily_draws()
.enter(&DrawEntryPartial {
draw_id,
client_id: client_details.client_id,
client_id_signature: client_details.client_id_signature,
})
.await?)
}
#[tauri::command]
pub async fn growth_tne_submit_wallet_address(
client_details: ClientIdPartial,
draw_id: String,
wallet_address: String,
registration_id: String,
) -> Result<Winner, BackendError> {
Ok(GrowthApiClient::daily_draws()
.claim(&ClaimPartial {
draw_id,
client_id: client_details.client_id,
client_id_signature: client_details.client_id_signature,
wallet_address,
registration_id,
})
.await?)
}
#[tauri::command]
pub async fn growth_tne_ping(client_details: ClientIdPartial) -> Result<(), BackendError> {
log::info!("Test&Earn is sending a ping...");
Ok(GrowthApiClient::registrations()
.ping(&client_details)
.await?)
}
#[tauri::command]
pub async fn growth_tne_toggle_window(
app_handle: tauri::AppHandle,
window_title: Option<String>,
) -> Result<(), BackendError> {
if let Some(window) = app_handle.windows().get("growth") {
log::info!("Closing growth window...");
if let Err(e) = window.close() {
log::error!("Unable to close growth window: {:?}", e);
}
return Ok(());
}
log::info!("Creating growth window...");
match tauri::WindowBuilder::new(
&app_handle,
"growth",
tauri::WindowUrl::App("growth.html".into()),
)
.title(window_title.unwrap_or_else(|| "NymConnect Test&Earn".to_string()))
.build()
{
Ok(window) => {
if let Err(e) = window.set_focus() {
log::error!("Unable to focus growth window: {:?}", e);
}
Ok(())
}
Err(e) => {
log::error!("Unable to create growth window: {:?}", e);
Err(BackendError::NewWindowError)
}
}
}
@@ -0,0 +1,30 @@
use crate::error::BackendError;
use tauri::Manager;
#[tauri::command]
pub fn help_log_toggle_window(app_handle: tauri::AppHandle) -> Result<(), BackendError> {
if let Some(current_log_window) = app_handle.windows().get("log") {
log::info!("Closing log window...");
if let Err(e) = current_log_window.close() {
log::error!("Unable to close log window: {:?}", e);
}
return Ok(());
}
log::info!("Creating log window...");
match tauri::WindowBuilder::new(&app_handle, "log", tauri::WindowUrl::App("log.html".into()))
.title("Nym Connect Logs")
.build()
{
Ok(window) => {
if let Err(e) = window.set_focus() {
log::error!("Unable to focus log window: {:?}", e);
}
Ok(())
}
Err(e) => {
log::error!("Unable to create log window: {:?}", e);
Err(BackendError::NewWindowError)
}
}
}
@@ -0,0 +1,2 @@
pub mod log;
pub mod storage;
@@ -0,0 +1,20 @@
use crate::error::BackendError;
use serde::Serialize;
use tauri::Manager;
#[derive(Debug, Serialize, Clone)]
struct ClearStorageEvent {
kind: String,
}
#[tauri::command]
pub fn help_clear_storage(app_handle: tauri::AppHandle) -> Result<(), BackendError> {
log::info!("Sending event to clear local storage...");
let event = ClearStorageEvent {
kind: "local_storage".to_string(),
};
app_handle.emit_all("help://clear-storage", event)?;
Ok(())
}
@@ -1,4 +1,6 @@
pub mod connection;
pub mod directory;
pub mod export;
pub mod growth;
pub mod help;
pub mod window;
+4 -4
View File
@@ -64,16 +64,16 @@ impl State {
&self.service_provider
}
pub fn set_service_provider(&mut self, provider: String) {
self.service_provider = Some(provider);
pub fn set_service_provider(&mut self, provider: Option<String>) {
self.service_provider = provider;
}
pub fn get_gateway(&self) -> &Option<String> {
&self.gateway
}
pub fn set_gateway(&mut self, gateway: String) {
self.gateway = Some(gateway);
pub fn set_gateway(&mut self, gateway: Option<String>) {
self.gateway = gateway;
}
/// The effective config id is the static config id appended with the id of the gateway
+4 -1
View File
@@ -59,13 +59,16 @@
"startDragging": true,
"close": true,
"minimize": true
},
"notification": {
"all": true
}
},
"windows": [
{
"title": "NymConnect",
"width": 240,
"height": 575,
"height": 605,
"resizable": false,
"decorations": false,
"transparent": true
+12 -1
View File
@@ -1,15 +1,27 @@
import React, { useEffect } from 'react';
import { DateTime } from 'luxon';
import { forage } from '@tauri-apps/tauri-forage';
import { ConnectionStatusKind } from './types';
import { useClientContext } from './context/main';
import { DefaultLayout } from './layouts/DefaultLayout';
import { ConnectedLayout } from './layouts/ConnectedLayout';
import { HelpGuideLayout } from './layouts/HelpGuideLayout';
import { useTauriEvents } from './utils';
export const App: React.FC = () => {
const context = useClientContext();
const [busy, setBusy] = React.useState<boolean>();
const [showInfoModal, setShowInfoModal] = React.useState(false);
useTauriEvents('help://clear-storage', (_event) => {
console.log('About to clear local storage...');
// clear local storage
try {
forage.clear()();
console.log('Local storage cleared');
} catch (e) {
console.error('Failed to clear local storage', e);
}
});
const handleConnectClick = React.useCallback(async () => {
const currentStatus = context.connectionStatus;
@@ -49,7 +61,6 @@ export const App: React.FC = () => {
busy={busy}
onConnectClick={handleConnectClick}
services={context.services}
onServiceProviderChange={context.setServiceProvider}
/>
);
}
-17
View File
@@ -1,17 +0,0 @@
import { Typography } from '@mui/material';
import React from 'react';
import { useClientContext } from 'src/context/main';
export const AppVersion = () => {
const { appVersion } = useClientContext();
return (
<Typography
fontSize="small"
textAlign="center"
sx={{ color: 'grey.600', position: 'absolute', bottom: 10, width: '100%' }}
>
Version {appVersion}
</Typography>
);
};
@@ -1,7 +1,6 @@
import React from 'react';
import { Box } from '@mui/material';
import { CustomTitleBar } from './CustomTitleBar';
import { AppVersion } from './AppVersion';
export const AppWindowFrame: React.FC = ({ children }) => (
<Box
@@ -12,10 +11,10 @@ export const AppWindowFrame: React.FC = ({ children }) => (
gridTemplateRows: '40px 1fr 30px',
bgcolor: 'nym.background.dark',
height: '100vh',
overflowY: 'hidden',
}}
>
<CustomTitleBar />
<Box style={{ padding: '16px' }}>{children}</Box>
<AppVersion />
</Box>
);
@@ -14,9 +14,13 @@ export const CopyToClipboard = ({
}) => {
const [copied, setCopied] = useState(false);
const handleCopy = async (text: string) => {
const handleCopy = async (textToCopy: string) => {
try {
await clipboard.writeText(text);
if (clipboard) {
await clipboard.writeText(textToCopy);
} else {
await navigator.clipboard.writeText(textToCopy);
}
setCopied(true);
} catch (e) {
console.log(`failed to copy: ${e}`);
@@ -0,0 +1,17 @@
import React from 'react';
import { FallbackProps } from 'react-error-boundary';
import { Alert, AlertTitle, Button } from '@mui/material';
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => (
<div>
<Alert severity="error" data-testid="error-message">
<AlertTitle>{error.name}</AlertTitle>
{error.message}
</Alert>
<Alert severity="error" data-testid="stack-trace">
<AlertTitle>Stack trace</AlertTitle>
{error.stack}
</Alert>
<Button onClick={resetErrorBoundary}>Back to safety</Button>
</div>
);
@@ -0,0 +1,83 @@
import React from 'react';
import { Badge, Box, Button, Tooltip } from '@mui/material';
import MonetizationOnIcon from '@mui/icons-material/MonetizationOn';
import { invoke } from '@tauri-apps/api';
import Content from './content/en.yaml';
import { useClientContext } from '../../context/main';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { NymShipyardTheme } from '../../theme';
import { ConnectionStatusKind } from '../../types';
export const Wrapper: React.FC<{ disabled: boolean }> = ({ disabled, children }) => {
if (disabled) {
return (
<Badge badgeContent="!" color="warning">
<Tooltip arrow title={disabled ? Content.testAndEarn.mainWindow.button.popup.disconnected : undefined}>
<div>{children}</div>
</Tooltip>
</Badge>
);
}
return <>{children}</>;
};
export const TestAndEarnButtonArea: React.FC = () => {
const clientContext = useClientContext();
const context = useTestAndEarnContext();
const disabled = clientContext.connectionStatus !== ConnectionStatusKind.connected;
const pinger = React.useRef<NodeJS.Timer | null>();
const doPing = async () => {
if (context.clientDetails) {
try {
await invoke('growth_tne_ping', { clientDetails: context.clientDetails });
} catch (_e) {
// console.error('Failed to ping: ', e);
}
}
};
React.useEffect(() => {
(async () => {
if (!disabled) {
// sleep a little until the SOCKS5 proxy connects
setTimeout(() => {
doPing();
}, 1000 * 10);
// update every 15 mins
pinger.current = setInterval(doPing, 1000 * 60 * 15);
} else if (pinger.current) {
clearInterval(pinger.current);
pinger.current = null;
}
})();
}, [disabled, context.clientDetails]);
const handleClick = async () => {
if (!disabled) {
await context.toggleGrowthWindow(Content.testAndEarn.popupWindow.title);
}
};
return (
<NymShipyardTheme>
<Box justifyContent="center" display="grid">
<Wrapper disabled={disabled}>
<Button
color={disabled ? 'secondary' : undefined}
variant="contained"
size="small"
endIcon={<MonetizationOnIcon />}
sx={{ width: '150px', mb: 4, opacity: disabled ? 0.4 : undefined }}
onClick={handleClick}
>
{context.registration
? Content.testAndEarn.mainWindow.button.text.entered
: Content.testAndEarn.mainWindow.button.text.default}
</Button>
</Wrapper>
</Box>
</NymShipyardTheme>
);
};
@@ -0,0 +1,94 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { DateTime, Duration } from 'luxon';
import {
TestAndEarnCurrentDraw,
TestAndEarnCurrentDrawEntered,
TestAndEarnCurrentDrawFuture,
} from './TestAndEarnCurrentDraw';
import { NymShipyardTheme } from '../../theme';
import { DrawEntryStatus } from './context/types';
import { testMarkdown } from './context/mocks/TestAndEarnContext';
export default {
title: 'Growth/TestAndEarn/Components/Cards/Current Draw',
component: TestAndEarnCurrentDraw,
} as ComponentMeta<typeof TestAndEarnCurrentDraw>;
export const Valid = () => (
<NymShipyardTheme>
<TestAndEarnCurrentDraw
draw={{
id: '1',
start_utc: DateTime.now().toISO(),
end_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600))
.toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: 'words words words',
}}
/>
</NymShipyardTheme>
);
export const EnteredMalformedDraw = () => (
<NymShipyardTheme>
<TestAndEarnCurrentDrawEntered
draw={{
id: '1',
start_utc: DateTime.now().toISO(),
end_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600))
.toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: undefined,
entry: {
draw_id: '1',
status: DrawEntryStatus.pending,
id: 'aaaa',
timestamp: DateTime.now().toISO(),
},
}}
/>
</NymShipyardTheme>
);
export const EnteredDraw = () => (
<NymShipyardTheme>
<TestAndEarnCurrentDrawEntered
draw={{
id: '1',
start_utc: DateTime.now().toISO(),
end_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600))
.toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: testMarkdown,
entry: {
draw_id: '1',
status: DrawEntryStatus.pending,
id: 'aaaa',
timestamp: DateTime.now().toISO(),
},
}}
/>
</NymShipyardTheme>
);
export const Future = () => (
<NymShipyardTheme>
<TestAndEarnCurrentDrawFuture
draw={{
id: '1',
start_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600))
.toISO(),
end_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600 * 2))
.toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: 'words words words',
}}
/>
</NymShipyardTheme>
);
@@ -0,0 +1,192 @@
import React from 'react';
import LoadingButton from '@mui/lab/LoadingButton';
import { Alert, AlertTitle, Box, Card, CardContent, CardMedia, Link, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import { DateTime } from 'luxon';
import ReactMarkdown from 'react-markdown';
import assetAnimation from './content/assets/matrix.webp';
import { CopyToClipboard } from '../CopyToClipboard';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { DrawEntryStatus, DrawWithWordOfTheDay } from './context/types';
import Content from './content/en.yaml';
export const TestAndEarnCurrentDrawFuture: React.FC<{ draw?: DrawWithWordOfTheDay }> = ({ draw }) => {
const startsUtc = React.useMemo(() => draw && DateTime.fromISO(draw.start_utc), [draw?.start_utc]);
const startsIn = React.useMemo(() => {
if (draw && startsUtc) {
return startsUtc.toRelative();
}
return undefined;
}, [draw?.start_utc]);
if (!draw || !startsUtc) {
return null;
}
return (
<Card sx={{ mb: 2 }} elevation={10}>
<CardContent>
<h3>
{Content.testAndEarn.draw.next.header} {startsIn}
</h3>
<p>on {startsUtc.toLocaleString(DateTime.DATETIME_FULL)}</p>
</CardContent>
</Card>
);
};
export const TestAndEarnCurrentDrawEnter: React.FC<{ draw?: DrawWithWordOfTheDay }> = ({ draw }) => {
const context = useTestAndEarnContext();
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState<string>();
const handleClick = async () => {
if (!draw) {
setError('No draw selected');
return;
}
setBusy(true);
try {
await context.enterDraw(draw.id);
} catch (e) {
const message = `${e}`;
console.error('Could not enter draw', message);
setError(message);
}
setBusy(false);
};
return (
<Box display="flex" flexDirection="column" alignItems="center" py={3} px={2} mx={6} my={2}>
<Typography mb={4}>Complete todays task for the chance to earn 1000 NYMs.</Typography>
<LoadingButton variant="contained" size="large" loading={busy} onClick={handleClick}>
Start task
</LoadingButton>
{error && (
<Box mt={2}>
<Alert variant="filled" severity="error">
<AlertTitle>Oh no! Something went wrong.</AlertTitle>
{error}
</Alert>
</Box>
)}
</Box>
);
};
export const TestAndEarnCurrentDrawEntered: React.FC<{ draw?: DrawWithWordOfTheDay }> = ({ draw }) => {
if (!draw || !draw.entry) {
return null;
}
if (!draw.word_of_the_day) {
return (
<Alert severity="error" variant="filled">
<AlertTitle>Oh no! Something is wrong</AlertTitle>
Someone configured the wrong instructions for the task, you will not be able to see it until this is fixed
</Alert>
);
}
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
sx={{ background: 'rgba(255,255,255,0.1)' }}
py={4}
mx={6}
my={2}
borderRadius={2}
>
<Box py={2} px={4} color="warning.light">
<ReactMarkdown>{draw.word_of_the_day}</ReactMarkdown>
</Box>
<Typography>{Content.testAndEarn.task.afterText}</Typography>
<Typography mt={2} fontFamily="monospace" fontWeight="bold" color="warning.main">
{draw.entry.id} <CopyToClipboard iconButton light text={draw.entry.id} />
</Typography>
<Typography mt={2}>{Content.testAndEarn.task.beforeSocials}</Typography>
<Typography mt={2} mx={1} textAlign="center">
<Typography component="span" color="info.light" fontWeight="bold">
Twitter
</Typography>{' '}
- remember to
<Typography component="span" color="info.light">
@nymproject
</Typography>{' '}
and use the hashtag{' '}
<Typography component="span" color="info.light">
#PrivacyLovesCompany
</Typography>
</Typography>
<Typography mt={2}>or</Typography>
<Typography textAlign="center" fontWeight="bold">
Nym{' '}
<Link target="_blank" href="https://t.me/nymchan" color="info.light">
Telegram channel
</Link>
</Typography>
</Box>
);
};
export const TestAndEarnCurrentDraw: React.FC<{
draw?: DrawWithWordOfTheDay;
sx?: SxProps;
}> = ({ draw, sx }) => {
const [trigger, setTrigger] = React.useState(DateTime.now().toISO());
const endsUtc = React.useMemo(() => draw && DateTime.fromISO(draw.end_utc), [draw?.end_utc]);
const closesIn = React.useMemo(() => {
if (draw && endsUtc) {
return endsUtc.toRelative();
}
return undefined;
}, [trigger, endsUtc]);
React.useEffect(() => {
const timer = setInterval(() => setTrigger(DateTime.now().toISO()), 1000 * 3600 * 15);
return () => clearInterval(timer);
}, []);
if (draw && closesIn && endsUtc) {
return (
<Card sx={{ mb: 2, ...sx }} elevation={10}>
<CardContent>
<h3>
{"Today's task ends "}
{closesIn}
<Typography sx={{ opacity: 0.5 }}>
{endsUtc.weekdayLong} {endsUtc.toLocaleString(DateTime.DATETIME_FULL)}
</Typography>
</h3>
{!draw.entry && <TestAndEarnCurrentDrawEnter draw={draw} />}
{draw.entry && <TestAndEarnCurrentDrawEntered draw={draw} />}
</CardContent>
<CardMedia component="img" height="150" image={assetAnimation} alt="lottery" />
</Card>
);
}
return null;
};
export const TestAndEarnCurrentDrawWithState: React.FC<{
sx?: SxProps;
}> = ({ sx }) => {
const context = useTestAndEarnContext();
if (
context.draws?.current?.entry?.status === DrawEntryStatus.winner ||
context.draws?.current?.entry?.status === DrawEntryStatus.claimed ||
context.draws?.current?.entry?.status === DrawEntryStatus.noWin
) {
return null;
}
if (!context.draws?.current) {
return <TestAndEarnCurrentDrawFuture draw={context.draws?.next} />;
}
return <TestAndEarnCurrentDraw sx={sx} draw={context.draws.current} />;
};
@@ -0,0 +1,18 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { NymShipyardTheme } from 'src/theme';
import { TestAndEarnDraws } from './TestAndEarnDraws';
import { MockTestAndEarnProvider_RegisteredWithAllDraws } from './context/mocks/TestAndEarnContext';
export default {
title: 'Growth/TestAndEarn/Components/Cards/Draws',
component: TestAndEarnDraws,
} as ComponentMeta<typeof TestAndEarnDraws>;
export const Draws = () => (
<NymShipyardTheme>
<MockTestAndEarnProvider_RegisteredWithAllDraws>
<TestAndEarnDraws />
</MockTestAndEarnProvider_RegisteredWithAllDraws>
</NymShipyardTheme>
);
@@ -0,0 +1,195 @@
import React from 'react';
import LoadingButton from '@mui/lab/LoadingButton';
import {
Alert,
AlertTitle,
Button,
Card,
CardContent,
Chip,
Dialog,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
Tooltip,
Typography,
} from '@mui/material';
import { SxProps } from '@mui/system';
import { DateTime } from 'luxon';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { DrawEntry, DrawEntryStatus } from './context/types';
import { CopyToClipboard } from '../CopyToClipboard';
import { TestAndEarnEnterWalletAddress } from './TestAndEarnEnterWalletAddress';
import Content from './content/en.yaml';
const statusToText = (status: string): string => Content.testAndEarn.status.chip[status] || '-';
const statusToColor = (status: string): 'info' | 'success' | 'warning' | undefined => {
switch (status) {
case DrawEntryStatus.pending:
return 'info';
case DrawEntryStatus.winner:
return 'warning';
case DrawEntryStatus.claimed:
return 'success';
default:
return undefined;
}
};
const StatusText: React.FC<{ entry: DrawEntry }> = ({ entry }) => {
const context = useTestAndEarnContext();
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState<string>();
const [showWalletCapture, setShowWalletCapture] = React.useState(false);
const clear = () => {
setShowWalletCapture(false);
setError(undefined);
setBusy(false);
};
const handleStartWalletCapture = async () => {
setBusy(true);
setShowWalletCapture(true);
};
const cancelEndWalletCapture = async () => {
setBusy(false);
setShowWalletCapture(false);
};
const handleEndWalletCapture = async () => {
setBusy(true);
setShowWalletCapture(false);
if (!context.walletAddress) {
setError('Wallet address is not set');
return;
}
if (!entry.draw_id) {
setError('Task id is not set');
return;
}
try {
await context.claim(entry.draw_id, context.walletAddress);
} catch (e) {
const message = `${e}`;
console.error('Failed to submit claim');
setError(message);
}
setBusy(false);
};
if (error) {
return (
<Alert severity="error" variant="filled">
<AlertTitle>Oh no! Failed to submit claim</AlertTitle>
{error}
<Button variant="contained" color="secondary" size="small" onClick={() => clear()} sx={{ mx: 2 }}>
Try again!
</Button>
</Alert>
);
}
if (showWalletCapture) {
return (
<Dialog open fullWidth onBackdropClick={cancelEndWalletCapture}>
<TestAndEarnEnterWalletAddress onSubmit={handleEndWalletCapture} />
</Dialog>
);
}
switch (entry.status) {
case DrawEntryStatus.pending:
return <>{Content.testAndEarn.status.text.Pending}</>;
case DrawEntryStatus.winner:
return (
<>
{Content.testAndEarn.status.text.Winner}
<LoadingButton
loading={busy}
disabled={busy}
variant="contained"
sx={{ ml: 2 }}
size="small"
onClick={handleStartWalletCapture}
>
{Content.testAndEarn.winner.claimButton.text}
</LoadingButton>
</>
);
case DrawEntryStatus.claimed:
return <>{Content.testAndEarn.status.text.Claimed}</>;
case DrawEntryStatus.noWin:
return <>{Content.testAndEarn.status.text.NoWin}</>;
default:
return null;
}
};
export const TestAndEarnDraws: React.FC<{
sx?: SxProps;
}> = ({ sx }) => {
const context = useTestAndEarnContext();
const draws = React.useMemo<DrawEntry[]>(
() =>
(context.draws?.draws || []).map((item) => ({
...item,
timestamp: DateTime.fromISO(item.timestamp).toLocaleString(DateTime.DATETIME_FULL),
})),
[context.draws?.draws],
);
if (!context.draws) {
return null;
}
return (
<Card sx={{ mb: 2 }}>
<CardContent>
<Typography mb={2}>Here is a history of the tasks you have completed:</Typography>
<TableContainer>
<Table>
<TableBody>
{draws.map((entry) => (
<TableRow key={entry.draw_id}>
<TableCell width="150px">{entry.timestamp}</TableCell>
<TableCell width="150px">
<Tooltip arrow title={`Task Id: ${entry.draw_id}`}>
<Chip label={statusToText(entry.status)} color={statusToColor(entry.status)} />
</Tooltip>
</TableCell>
<TableCell>
<StatusText entry={entry} />
</TableCell>
<TableCell>
{entry.id} <CopyToClipboard iconButton light text={entry.id} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
);
};
export const TestAndEarnDrawsWithState: React.FC<{
sx?: SxProps;
}> = ({ sx }) => {
const context = useTestAndEarnContext();
const drawCount = context.draws?.draws?.length || 0;
if (drawCount < 1) {
return null;
}
return <TestAndEarnDraws sx={sx} />;
};
@@ -0,0 +1,41 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { Box } from '@mui/material';
import { TestAndEarnEnterWalletAddress } from './TestAndEarnEnterWalletAddress';
import { TestAndEarnContextProvider } from './context/TestAndEarnContext';
import { NymShipyardTheme } from '../../theme';
export default {
title: 'Growth/TestAndEarn/Components/Enter wallet address',
component: TestAndEarnEnterWalletAddress,
} as ComponentMeta<typeof TestAndEarnEnterWalletAddress>;
export const Empty = () => (
<NymShipyardTheme>
<TestAndEarnContextProvider>
<Box minWidth="25vw" maxWidth={500}>
<TestAndEarnEnterWalletAddress sx={{ width: '100%' }} />
</Box>
</TestAndEarnContextProvider>
</NymShipyardTheme>
);
export const ErrorValue = () => (
<NymShipyardTheme>
<TestAndEarnContextProvider>
<Box minWidth="25vw" maxWidth={500}>
<TestAndEarnEnterWalletAddress initialValue="this is a bad value" sx={{ width: '100%' }} />
</Box>
</TestAndEarnContextProvider>
</NymShipyardTheme>
);
export const ValidValue = () => (
<NymShipyardTheme>
<TestAndEarnContextProvider>
<Box minWidth="25vw" maxWidth={500}>
<TestAndEarnEnterWalletAddress initialValue="n1xr4w0kddak8d8zlfmu8sl6dk2r4p9uhhzzlaec" sx={{ width: '100%' }} />
</Box>
</TestAndEarnContextProvider>
</NymShipyardTheme>
);
@@ -0,0 +1,37 @@
import React from 'react';
import { WalletAddressFormField } from '@nymproject/react/account/WalletAddressFormField';
import { SxProps } from '@mui/system';
import { Paper, Stack, Button, Box } from '@mui/material';
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
export const TestAndEarnEnterWalletAddress: React.FC<{
initialValue?: string;
placeholder?: string;
onSubmit?: () => Promise<void> | void;
sx?: SxProps;
}> = ({ initialValue, placeholder, onSubmit, sx }) => {
const context = useTestAndEarnContext();
const [isAddressValid, setAddressIsValid] = React.useState(false);
return (
<Paper sx={{ py: 4, px: 2 }}>
<Stack spacing={4}>
<Box>
<WalletAddressFormField
label="Wallet address"
initialValue={initialValue}
placeholder={placeholder || 'Please enter your wallet address'}
onChanged={context.setWalletAddress}
onValidate={setAddressIsValid}
sx={{ width: '80%' }}
/>
</Box>
<Box>
<Button variant="contained" endIcon={<ArrowCircleRightIcon />} disabled={!isAddressValid} onClick={onSubmit}>
Submit
</Button>
</Box>
</Stack>
</Paper>
);
};
@@ -0,0 +1,13 @@
import React from 'react';
import { Box, Button } from '@mui/material';
export const TestAndEarnError: React.FC<{ error?: string }> = ({ error = 'An error has occurred' }) => (
<Box>
<Box mb={4} fontWeight="bold">
{error}
</Box>
<Button variant="outlined" color="secondary">
Send us an error report
</Button>
</Box>
);
@@ -0,0 +1,167 @@
/* eslint-disable react/jsx-pascal-case */
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { Alert, Box } from '@mui/material';
import { NymShipyardTheme } from 'src/theme';
import { TestAndEarnPopup, TestAndEarnPopupContent } from './TestAndEarnPopup';
import { TestAndEarnContextProvider } from './context/TestAndEarnContext';
import { MockProvider } from '../../context/mocks/main';
import { ConnectionStatusKind } from '../../types';
import { TestAndEarnCurrentDraw } from './TestAndEarnCurrentDraw';
import { TestAndEarnWinner } from './TestAndEarnWinner';
import { TestAndEarnDraws } from './TestAndEarnDraws';
import { TestAndEarnWinnerWalletAddress } from './TestAndEarnWinnerWalletAddress';
import {
MockTestAndEarnProvider_NotRegistered,
MockTestAndEarnProvider_Registered,
MockTestAndEarnProvider_RegisteredAndError,
MockTestAndEarnProvider_RegisteredWithDraws,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntry,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndNoWinner,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerClaimed,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerCollectWallet,
MockTestAndEarnProvider_RegisteredWithDrawsNoCurrent,
} from './context/mocks/TestAndEarnContext';
export default {
title: 'Growth/TestAndEarn/Content/Popup',
component: TestAndEarnPopupContent,
} as ComponentMeta<typeof TestAndEarnPopupContent>;
const MacOSWindow: React.FC<{ width?: string | number; height?: string | number; title?: string }> = ({
title,
width,
height,
children,
}) => (
<Box sx={{ border: '1px solid #EEEEEE', width, height }}>
<Box sx={{ background: '#EEEEEE', display: 'grid', gridTemplateColumns: 'auto auto', gridTemplateRows: 'auto' }}>
<Box ml={1}>
<svg width="52px" height="12px" viewBox="0 0 52 12" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="Components" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="macOS" transform="translate(-600.000000, -220.000000)">
<g id="Group" transform="translate(600.000000, 220.000000)" strokeWidth="0.5">
<g id="Traffic-Lights">
<circle id="Traffic-Light---Zoom" stroke="#1BAC2C" fill="#2ACB42" cx="46" cy="6" r="5.75" />
<circle id="Traffic-Light---Minimise" stroke="#DFA023" fill="#FFC12F" cx="26" cy="6" r="5.75" />
<circle id="Traffic-Light---Close" stroke="#E24640" fill="#FF6157" cx="6" cy="6" r="5.75" />
</g>
</g>
</g>
</g>
</svg>
</Box>
<Box
sx={{
alignSelf: 'center',
color: '#000000',
opacity: 0.848675272,
fontSize: 13,
}}
>
{title || 'Window title'}
</Box>
</Box>
<Box sx={{ overflowY: 'scroll', height: 'calc(100% - 25px)' }}>{children}</Box>
</Box>
);
const Wrapper: React.FC<{ text: React.ReactNode }> = ({ text }) => (
<NymShipyardTheme>
<Alert severity="info" sx={{ mb: 4 }}>
{text}
</Alert>
<MacOSWindow width={700} height={600} title="Test&Earn">
<TestAndEarnPopup />
</MacOSWindow>
</NymShipyardTheme>
);
export const Stage0 = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_NotRegistered>
<Wrapper text="The user sees this content when they have not joined Test&Earn." />
</MockTestAndEarnProvider_NotRegistered>
</MockProvider>
);
export const Stage1EnterDraw = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDraws>
<Wrapper text="The user has signed up and can see the next draw and choose the enter." />
</MockTestAndEarnProvider_RegisteredWithDraws>
</MockProvider>
);
export const Stage2GetTask = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntry>
<Wrapper text="The user has entered a draw and can view the word of the day if they missed the popup notification." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntry>
</MockProvider>
);
export const Stage3Winner = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner>
<Wrapper text="The user has won and can claim their prize." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner>
</MockProvider>
);
export const Stage3NoPrize = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndNoWinner>
<Wrapper text="The user has not won. A winner has been announced." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndNoWinner>
</MockProvider>
);
export const Stage4EnterWalletAddress = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerCollectWallet>
<Wrapper text="The user is a winner, claims their prize and enters their wallet address." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerCollectWallet>
</MockProvider>
);
export const Stage5ClaimedPrize = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerClaimed>
<Wrapper text="The user is a winner and has claimed their prize." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerClaimed>
</MockProvider>
);
export const Stage6DrawsFinished = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsNoCurrent>
<Wrapper text="There are no more draws. The user can see their entries and prizes they have claimed." />
</MockTestAndEarnProvider_RegisteredWithDrawsNoCurrent>
</MockProvider>
);
export const Connecting = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connecting}>
<TestAndEarnContextProvider>
<Wrapper text="Test&Earn requires the user to be connected to talk the API. This is shown while connecting." />
</TestAndEarnContextProvider>
</MockProvider>
);
export const Disconnected = () => (
<MockProvider connectionStatus={ConnectionStatusKind.disconnected}>
<TestAndEarnContextProvider>
<Wrapper text="Test&Earn requires the user to be connected to talk the API. This is shown when not connected." />
</TestAndEarnContextProvider>
</MockProvider>
);
export const Error = () => (
<MockProvider>
<MockTestAndEarnProvider_RegisteredAndError>
<Wrapper text="The user see this with details about errors. They can submit an error report." />
</MockTestAndEarnProvider_RegisteredAndError>
</MockProvider>
);
@@ -0,0 +1,118 @@
import React from 'react';
import { Box, CircularProgress, LinearProgress, Stack, Typography } from '@mui/material';
import { useClientContext } from '../../context/main';
import ErrorContent from './content/TestAndEarn/Error.mdx';
import ContentStep0 from './content/TestAndEarn/Stage0_intro.mdx';
import ContentNotAvailable from './content/TestAndEarnNotAvaialble.mdx';
import { ConnectionStatusKind } from '../../types';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { TestAndEarnWinnerWithState } from './TestAndEarnWinner';
import { TestAndEarnCurrentDrawWithState } from './TestAndEarnCurrentDraw';
import { TestAndEarnDrawsWithState } from './TestAndEarnDraws';
enum Stages {
mustRegister = 'mustRegister',
registered = 'registered',
}
export const TestAndEarnPopupContent: React.FC<{
stage?: string;
connectionStatus?: ConnectionStatusKind;
error?: string;
}> = ({ connectionStatus, error, stage = Stages.mustRegister }) => {
if (error) {
return (
<Box p={4}>
<ErrorContent error={error} />
</Box>
);
}
if (!connectionStatus || connectionStatus === ConnectionStatusKind.disconnected) {
return (
<Box p={4}>
<ContentNotAvailable />
</Box>
);
}
if (connectionStatus === ConnectionStatusKind.connecting || connectionStatus === ConnectionStatusKind.disconnecting) {
return (
<Box p={4} justifyContent="center" alignItems="center" display="flex">
<CircularProgress />
<Typography ml={3}>Please wait...</Typography>
</Box>
);
}
switch (stage) {
case Stages.mustRegister:
return (
<Box p={4}>
<ContentStep0 />
</Box>
);
case Stages.registered:
return (
<Box p={4}>
<TestAndEarnWinnerWithState />
<TestAndEarnCurrentDrawWithState />
<TestAndEarnDrawsWithState />
</Box>
);
default:
return (
<Box p={4}>
<Stack direction="row" spacing={2} display="flex" alignItems="center">
<CircularProgress />
<Box>Waiting for task information...</Box>
</Stack>
</Box>
);
}
};
export const TestAndEarnPopup: React.FC = () => {
const clientContext = useClientContext();
const context = useTestAndEarnContext();
React.useEffect(() => {
if (clientContext.connectionStatus === ConnectionStatusKind.connected) {
context.refresh();
}
}, [clientContext.connectionStatus]);
const stage = React.useMemo<Stages>(() => {
if (context.registration) {
return Stages.registered;
}
return Stages.mustRegister;
}, [context.registration?.id]);
React.useEffect(() => {
const interval = setInterval(context.refresh, 1000 * 60 * 5);
return () => clearInterval(interval);
}, []);
if (!context.loadedOnce && clientContext.connectionStatus === ConnectionStatusKind.connected) {
const message = 'Waiting for data to be transferred over the mixnet...';
return (
<Box p={4}>
<Stack direction="row" spacing={2} display="flex" alignItems="center">
<CircularProgress />
<Box>{message}</Box>
{/* {process.env.NODE_ENV === 'development' && <pre>{JSON.stringify(context, null, 2)}</pre>} */}
</Stack>
</Box>
);
}
return (
<>
{context.loading && <LinearProgress />}
{/* <Button onClick={context.refresh}>Refresh</Button> */}
<TestAndEarnPopupContent connectionStatus={clientContext.connectionStatus} stage={stage} error={context.error} />
{/* {process.env.NODE_ENV === 'development' && <pre>{JSON.stringify(context, null, 2)}</pre>} */}
</>
);
};
@@ -0,0 +1,75 @@
import React from 'react';
import { Alert, AlertTitle, Box, Checkbox, Link, Stack } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import { SxProps } from '@mui/system';
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
import { invoke } from '@tauri-apps/api';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { ClientId, Registration } from './context/types';
export const TestAndEarnTakePart: React.FC<{
websiteLinkUrl: string;
websiteLinkText: string;
content: string;
sx?: SxProps;
}> = ({ content, websiteLinkText, websiteLinkUrl, sx }) => {
const [agree, setAgree] = React.useState(false);
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState<string>();
const context = useTestAndEarnContext();
const handleNext = async () => {
try {
setBusy(true);
if (context.clientDetails) {
const registration: Registration = await invoke('growth_tne_take_part');
console.log('Registration: ', { registration });
await context.setAndStoreRegistration(registration);
if (registration) {
console.log('Registered...');
} else {
setError('Failed to get registration details');
}
} else {
setError('Failed to get client details');
}
} catch (e) {
const message = `${e}`;
console.error('An error occurred', message);
setError(message);
setBusy(false); // the busy state only resets on errors, for success stats, the context will navigate the window away
}
};
return (
<>
<Stack direction="row" spacing={6} alignItems="center" sx={sx}>
<Stack alignItems="center" direction="row">
<Checkbox onChange={(_event, checked) => setAgree(checked)} />
<Box color="primary.light" fontWeight="bold">
{content}
</Box>
</Stack>
<Box>
<Link href={websiteLinkUrl} target="_blank" color="secondary" sx={{ opacity: 0.5 }}>
{websiteLinkText}
</Link>
</Box>
<LoadingButton
loading={busy}
disabled={!agree || busy}
variant="contained"
sx={{ justifySelf: 'end' }}
endIcon={<ArrowCircleRightIcon />}
onClick={handleNext}
>
Next
</LoadingButton>
</Stack>
{error && (
<Alert severity="error" variant="filled">
<AlertTitle>Oh no! Something went wrong</AlertTitle>
{error}
</Alert>
)}
</>
);
};
@@ -0,0 +1,18 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { NymShipyardTheme } from 'src/theme';
import { TestAndEarnWinner, TestAndEarnWinnerWithState } from './TestAndEarnWinner';
import { MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner } from './context/mocks/TestAndEarnContext';
export default {
title: 'Growth/TestAndEarn/Components/Cards/Winner',
component: TestAndEarnWinner,
} as ComponentMeta<typeof TestAndEarnWinner>;
export const Winner = () => (
<NymShipyardTheme>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner>
<TestAndEarnWinnerWithState />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner>
</NymShipyardTheme>
);
@@ -0,0 +1,114 @@
import React from 'react';
import { Alert, AlertTitle, Button, Card, CardContent, CardMedia, Dialog, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import LoadingButton from '@mui/lab/LoadingButton';
import winner from './content/assets/winner.webp';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { DrawEntry, DrawEntryStatus } from './context/types';
import { TestAndEarnEnterWalletAddress } from './TestAndEarnEnterWalletAddress';
import Content from './content/en.yaml';
export const TestAndEarnWinner: React.FC<{
sx?: SxProps;
entry?: DrawEntry;
}> = ({ sx, entry }) => {
const context = useTestAndEarnContext();
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState<string>();
const [showWalletCapture, setShowWalletCapture] = React.useState(false);
const clear = () => {
setShowWalletCapture(false);
setError(undefined);
setBusy(false);
};
const handleStartWalletCapture = async () => {
setBusy(true);
setShowWalletCapture(true);
};
const cancelEndWalletCapture = async () => {
setBusy(false);
setShowWalletCapture(false);
};
const handleEndWalletCapture = async () => {
setBusy(true);
setShowWalletCapture(false);
if (!context.walletAddress) {
setError('Wallet address is not set');
return;
}
if (!entry?.draw_id) {
setError('Draw id is not set');
return;
}
try {
await context.claim(entry.draw_id, context.walletAddress);
} catch (e) {
const message = `${e}`;
console.error('Failed to submit claim', entry.draw_id, context.walletAddress);
setError(message);
}
setBusy(false);
};
return (
<>
{showWalletCapture && (
<Dialog open fullWidth onBackdropClick={cancelEndWalletCapture}>
<TestAndEarnEnterWalletAddress onSubmit={handleEndWalletCapture} />
</Dialog>
)}
<Card sx={{ mb: 2 }}>
<CardMedia component="img" height="165" image={winner} alt="winner" />
<CardContent>
<Typography color="warning.main" fontSize={20} fontWeight="bold">
{Content.testAndEarn.winner.card.header}
</Typography>
<Typography mt={2}>
{entry && (
<>
{Content.testAndEarn.winner.card.text} {entry.draw_id}.
</>
)}
<LoadingButton
loading={busy}
variant="contained"
sx={{ ml: 2, my: 2 }}
size="small"
onClick={handleStartWalletCapture}
>
{Content.testAndEarn.winner.claimButton.text}
</LoadingButton>
</Typography>
{error && (
<Alert severity="error" variant="filled">
<AlertTitle>Oh no! Failed to submit claim</AlertTitle>
{error}
<Button variant="contained" color="secondary" size="small" onClick={() => clear()} sx={{ mx: 2 }}>
Try again!
</Button>
</Alert>
)}
</CardContent>
</Card>
</>
);
};
export const TestAndEarnWinnerWithState: React.FC<{
sx?: SxProps;
}> = ({ sx }) => {
const context = useTestAndEarnContext();
if (context.draws?.current?.entry?.status === DrawEntryStatus.winner) {
return <TestAndEarnWinner sx={sx} entry={context.draws.current.entry} />;
}
// when the user does not have any unclaimed prizes, don't render anything
return null;
};
@@ -0,0 +1,12 @@
import React from 'react';
import { Box, Button, Card, CardContent, CardMedia, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import Content from './content/TestAndEarn/WinnerEntersWalletAddress.mdx';
export const TestAndEarnWinnerWalletAddress: React.FC<{
sx?: SxProps;
}> = ({ sx }) => (
<Box>
<Content />
</Box>
);
@@ -0,0 +1,17 @@
import { Alert, AlertTitle, Link } from '@mui/material';
import { TestAndEarnError } from '../../TestAndEarnError';
<Alert severity="error" sx={{ mb: 4 }}>
<AlertTitle>Oh no! Something went wrong</AlertTitle>
Sorry about that. Here is some more information about the error that occurred:
<TestAndEarnError/>
Any error reports that you send us will contain information about your client, the gateway you're using and your IP address.
We need this information to make Nym better for everyone.
</Alert>
If you'd like more information about Test&Earn please look on the <Link href="http://shipyard.nymtech.net/test-and-win">Shipyard website</Link>.
@@ -0,0 +1,25 @@
import { Card, CardContent, Link, Typography } from '@mui/material';
import { TestAndEarnTakePart } from '../../TestAndEarnTakePart';
### Test privacy & Earn tokens!
<Typography color="primary.light" component="span">
Help us stress test the Nym privacy system and have the chance to earn 1000 NYMs per day!
</Typography>
All you need to do is:
1. Make sure you're running NymConnect and it is connected.
2. Note your reference number.
3. NymConnect will ping you a task every day.
Complete the task, post it on Twitter <Typography color="primary.light" component="span">#PrivacyLovesCompany</Typography> or <Link target="_blank" href="https://t.me/nymchan">Telegram</Link> along with your reference number!
Thank you for being part of the Nym community and helping to build a flourishing and free digital society. #PrivacyLovesCompany and we love you!
<Card>
<CardContent sx={{ py: 2 }}>
<TestAndEarnTakePart content={"I want to take part"} websiteLinkText={"Terms and conditions"}
websiteLinkUrl={"https://shipyard.nymtech.net/test-and-win"} sx={{ py: 2 }} />
</CardContent>
</Card>
@@ -0,0 +1,9 @@
import { TestAndEarnEnterWalletAddress } from '../../TestAndEarnEnterWalletAddress';
### 🎉 Congratulations! One more thing...
We need one more thing from you, and that is your wallet address:
<TestAndEarnEnterWalletAddress/>
Once you've submitting your wallet address over the mixnet, we will be in touch to arrange sending your tokens to you.
@@ -0,0 +1,3 @@
## 😕 Sorry, Test&Earn is only accessible while you are connected to the mixnet
Please connect to any service provider and try again once the connection has been established.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

@@ -0,0 +1,43 @@
testAndEarn:
mainWindow:
button:
text:
default: Join Test&Earn
entered: Test&Earn
claim: Claim your reward
popup:
disconnected: Test&Earn is only available when connected. Please connect to any service provider.
help:
url: https://shipyard.nymtech.net/test-and-win
popupWindow:
title: NymConnect Test&Earn 🌈
notifications:
takePart:
title: Thanks for taking part in Ter&Earn
body: Watch out for new tasks 👀 and take part to earn
youAreInDraw:
title: Thanks for completing the task ✨
body: Post a message on Telegram, Discord or Twitter for a chance to to be selected 🤞 good luck
task:
afterText: Copy your reference number
beforeSocials: "And include it in your post to:"
draw:
next:
header: The next task starts
winner:
claimButton:
text: Claim your reward!
card:
header: You are a top contributor!
text: Congratulations, you have earned the reward for
status:
chip:
Pending: Good luck 🤞
Winner: Rewarded!
Claimed: Claimed
NoWin: No rewards
text:
Pending: Task completed. Good luck 🤞
Winner: Well done 🎉
Claimed: You have claimed the reward 💰
NoWin: Sorry you were not a top contributor, better luck next time!
@@ -0,0 +1,272 @@
/* eslint-disable @typescript-eslint/naming-convention */
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { forage } from '@tauri-apps/tauri-forage';
import { invoke } from '@tauri-apps/api';
import { ClientId, DrawEntry, Draws, Registration } from './types';
import { useClientContext } from '../../../context/main';
import { ConnectionStatusKind } from '../../../types';
export type TTestAndEarnContext = {
loadedOnce: boolean;
loading: boolean;
clientDetails?: ClientId;
registration?: Registration;
walletAddress?: string;
draws?: Draws;
isWinnerWithUnclaimedPrize?: boolean;
isEnterWallet?: boolean;
error?: string;
setWalletAddress: (newWalletAddress: string) => void;
clearStorage: () => Promise<void>;
toggleGrowthWindow: (windowTitle?: string) => Promise<void>;
setAndStoreClientId: (newClientId: ClientId) => Promise<void>;
setAndStoreRegistration: (registration: Registration) => Promise<void>;
enterDraw: (drawId: string) => Promise<DrawEntry>;
claim: (drawId: string, walletAddress: string) => Promise<void>;
refresh: () => Promise<void>;
};
const defaultValue: TTestAndEarnContext = {
loadedOnce: false,
loading: true,
setWalletAddress: () => undefined,
clearStorage: async () => undefined,
toggleGrowthWindow: async () => undefined,
setAndStoreRegistration: async () => undefined,
setAndStoreClientId: async () => undefined,
enterDraw: async () => ({} as DrawEntry),
claim: async () => undefined,
refresh: async () => undefined,
};
export const TestAndEarnContext = createContext(defaultValue);
const CLIENT_ID_KEY = 'tne_client_id';
const REGISTRATION_KEY = 'tne_registration';
export const TestAndEarnContextProvider = ({ children }: { children: React.ReactNode }) => {
const clientContext = useClientContext();
const [loadedOnce, setLoadedOnce] = useState(false);
const [loading, setLoading] = useState(true);
const [walletAddress, setWalletAddress] = useState<string>();
const [registration, setRegistration] = useState<Registration>();
const [clientDetails, setClientDetails] = useState<ClientId>();
const [draws, setDraws] = useState<Draws>();
const setAndStoreClientId = async (newClientId: ClientId) => {
await forage.setItem({ key: CLIENT_ID_KEY, value: newClientId } as any)();
setClientDetails((prevState) => {
if (
prevState?.client_id !== newClientId.client_id ||
prevState?.client_id_signature !== newClientId.client_id_signature
) {
console.log('Setting client details');
return newClientId;
}
console.log('Skipping client details');
return prevState;
});
};
const loadClientDetails = async () => {
const data: ClientId | undefined = await forage.getItem({ key: CLIENT_ID_KEY })();
if (data) {
try {
setClientDetails((prevState) => {
if (prevState?.client_id !== data.client_id || prevState?.client_id_signature !== data.client_id_signature) {
console.log('Setting client details');
return data;
}
console.log('Skipping client details');
return prevState;
});
} catch (e) {
console.error('Failed to get registration');
}
} else {
const clientId: ClientId = await invoke('growth_tne_get_client_id');
await setAndStoreClientId(clientId);
}
};
const loadRegistration = async () => {
const data: Registration | undefined = await forage.getItem({ key: REGISTRATION_KEY })();
if (data) {
try {
setRegistration((prevState) => {
if (
prevState?.timestamp !== data.timestamp ||
prevState.client_id_signature !== data.client_id_signature ||
prevState.id !== data.id
) {
console.log('Setting registration');
return data;
}
console.log('Skipping registration');
return prevState;
});
} catch (e) {
console.error('Failed to get registration');
}
}
};
const loadDraws = React.useCallback(async () => {
setLoading(true);
let clientDetailsForDraws = clientDetails;
try {
if (!clientDetailsForDraws) {
console.log('[loadDraws] client details not set, trying to get...');
clientDetailsForDraws = await invoke('growth_tne_get_client_id');
}
if (!clientDetailsForDraws) {
console.log('[loadDraws] failed to get client details not set, skipping...');
setLoading(false);
setLoadedOnce(true);
return;
}
const newDraws: Draws = await invoke('growth_tne_get_draws', { clientDetails: clientDetailsForDraws });
console.log('[loadDraws] draws = ', newDraws);
// find the entered draw and keep a reference
const entered = newDraws.draws.find((draw) => draw.draw_id === newDraws.current?.id);
if (newDraws.current) {
newDraws.current.entry = entered;
}
console.log('[loadDraws] setting draws');
setDraws(newDraws);
} catch (e) {
console.error('Could not get draws: ', e);
}
setLoading(false);
setLoadedOnce(true);
console.log('[loadDraws] done, loaded once');
}, [clientDetails]);
React.useEffect(() => {
loadClientDetails().catch(console.error);
loadRegistration().catch(console.error);
}, []);
React.useEffect(() => {
if (registration && clientContext.connectionStatus === ConnectionStatusKind.connected) {
setTimeout(() => {
loadDraws().catch(console.error);
}, 1000 * 3);
}
}, [registration?.id, registration?.timestamp, clientContext.connectionStatus]);
const refresh = React.useCallback(async () => {
console.log('Refreshing...');
console.log('Loading client details...');
await loadClientDetails();
console.log('Loading registration...');
await loadRegistration();
console.log('Loading draws...');
await loadDraws();
console.log('Refresh complete.');
}, [clientDetails]);
const clearStorage = async () => {
await forage.setItem({ key: REGISTRATION_KEY, value: undefined })();
};
const toggleGrowthWindow = useCallback(async (windowTitle?: string) => {
try {
await invoke('growth_tne_toggle_window', { windowTitle });
} catch (e) {
console.error('Failed to toggle growth window', e);
}
}, []);
const setAndStoreRegistration = async (newRegistration: Registration) => {
await forage.setItem({ key: REGISTRATION_KEY, value: newRegistration } as any)();
setRegistration(newRegistration);
};
const enterDraw = async (drawId: string): Promise<DrawEntry> => {
if (!clientDetails) {
throw new Error('No client details set');
}
if (!draws) {
throw new Error('No draws set');
}
const existingEntry: DrawEntry | undefined = draws.draws.filter((d) => d.draw_id === drawId)[0];
if (existingEntry) {
throw new Error('Already entered into draw');
}
const entry: DrawEntry = await invoke('growth_tne_enter_draw', { clientDetails, drawId });
console.log('Entered draw', { entry });
await loadDraws();
return entry;
};
const claim = async (drawId: string, newWalletAddress: string) => {
if (!clientDetails) {
throw new Error('No client details set');
}
if (!draws) {
throw new Error('No draws set');
}
if (!registration) {
throw new Error('No registration set');
}
const registrationId = registration.id;
const args = {
registrationId,
clientDetails,
drawId,
walletAddress: newWalletAddress,
};
console.log({ args });
await invoke('growth_tne_submit_wallet_address', args);
await loadDraws();
};
const contextValue = useMemo<TTestAndEarnContext>(
() => ({
loadedOnce,
loading,
clientDetails,
registration,
walletAddress,
draws,
clearStorage,
toggleGrowthWindow,
setWalletAddress,
setAndStoreClientId,
setAndStoreRegistration,
enterDraw,
refresh,
claim,
}),
[
loadedOnce,
loading,
walletAddress,
registration,
refresh,
draws,
draws?.current?.last_modified,
draws?.current?.entry,
draws?.draws.length,
clientDetails,
],
);
return <TestAndEarnContext.Provider value={contextValue}>{children}</TestAndEarnContext.Provider>;
};
export const useTestAndEarnContext = () => useContext(TestAndEarnContext);
@@ -0,0 +1,262 @@
/* eslint-disable @typescript-eslint/naming-convention */
import React from 'react';
import { DateTime } from 'luxon';
import { TTestAndEarnContext, TestAndEarnContext } from '../TestAndEarnContext';
import { DrawEntry, DrawEntryStatus, DrawWithWordOfTheDay } from '../types';
const methodDefaults = {
loadedOnce: true,
loading: false,
refresh: async () => undefined,
setAndStoreClientId: async () => undefined,
setAndStoreRegistration: async () => undefined,
clearStorage: async () => undefined,
toggleGrowthWindow: async () => undefined,
setWalletAddress: async () => undefined,
enterDraw: async () => ({} as DrawEntry),
claim: async () => undefined,
};
const mockValues_NotRegistered: TTestAndEarnContext = {
...methodDefaults,
};
export const MockTestAndEarnProvider_NotRegistered = ({ children }: { children: React.ReactNode }) => (
<TestAndEarnContext.Provider value={mockValues_NotRegistered}>{children}</TestAndEarnContext.Provider>
);
export const testMarkdown = `**Create a sentence including "Nym" and one or more of the following words** *(in any language)*:
- Privacy
- Pleasure
- Pineapple
- Mix
`;
const mockValues_Registered: TTestAndEarnContext = {
...methodDefaults,
registration: {
id: '1234',
client_id_signature: 'signature',
client_id: '5678',
timestamp: '2022-12-12T18:17:37.840Z',
},
};
export const MockTestAndEarnProvider_Registered = ({ children }: { children: React.ReactNode }) => (
<TestAndEarnContext.Provider value={mockValues_Registered}>{children}</TestAndEarnContext.Provider>
);
const allDraws: DrawEntry[] = [
{
draw_id: '1111',
timestamp: DateTime.now().toISO(),
id: 'AAAA',
status: DrawEntryStatus.pending,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.noWin,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.claimed,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.winner,
},
];
const draws: DrawEntry[] = [
{
draw_id: '1111',
timestamp: DateTime.now().toISO(),
id: 'AAAA',
status: DrawEntryStatus.pending,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.noWin,
},
];
const drawsWithWin: DrawEntry[] = [
{
draw_id: '1111',
timestamp: DateTime.now().toISO(),
id: 'AAAA',
status: DrawEntryStatus.winner,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.noWin,
},
];
const drawsWithClaim: DrawEntry[] = [
{
draw_id: '1111',
timestamp: DateTime.now().toISO(),
id: 'AAAA',
status: DrawEntryStatus.claimed,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.noWin,
},
];
const current: DrawWithWordOfTheDay = {
id: '1111',
start_utc: DateTime.now().toISO(),
end_utc: DateTime.now().plus({ day: 1 }).minus({ second: 25 }).toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: testMarkdown,
};
const mockValues_RegisteredWithAllDrawsAndEntry: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
current: {
...current,
},
draws: allDraws,
},
};
export const MockTestAndEarnProvider_RegisteredWithAllDraws = ({ children }: { children: React.ReactNode }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithAllDrawsAndEntry}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsNoCurrent: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
draws: drawsWithClaim,
},
};
export const MockTestAndEarnProvider_RegisteredWithDrawsNoCurrent = ({ children }: { children: React.ReactNode }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsNoCurrent}>{children}</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDraws: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
current,
draws,
},
};
export const MockTestAndEarnProvider_RegisteredWithDraws = ({ children }: { children: React.ReactNode }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDraws}>{children}</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntry: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
current: {
...current,
entry: mockValues_RegisteredWithDraws.draws!.draws[0],
},
draws,
},
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntry = ({ children }: { children: React.ReactNode }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntry}>{children}</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntryAndWinner: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
current: {
...current,
entry: drawsWithWin[0],
},
draws: drawsWithWin,
},
isWinnerWithUnclaimedPrize: true,
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner = ({
children,
}: {
children: React.ReactNode;
}) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntryAndWinner}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntryAndNoWinner: TTestAndEarnContext = {
...mockValues_RegisteredWithDrawsAndEntry,
isWinnerWithUnclaimedPrize: false,
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndNoWinner = ({
children,
}: {
children: React.ReactNode;
}) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntryAndNoWinner}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntryAndWinnerCollectWallet: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
draws: drawsWithWin,
},
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerCollectWallet = ({
children,
}: {
children: React.ReactNode;
}) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntryAndWinnerCollectWallet}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntryAndWinnerClaimed: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
draws: drawsWithClaim,
},
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerClaimed = ({
children,
}: {
children: React.ReactNode;
}) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntryAndWinnerClaimed}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredAndError: TTestAndEarnContext = {
...mockValues_Registered,
error: 'Error message text will go here',
};
export const MockTestAndEarnProvider_RegisteredAndError = ({ children }: { children: React.ReactNode }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredAndError}>{children}</TestAndEarnContext.Provider>
);
@@ -0,0 +1,66 @@
import { DateTime } from 'luxon';
export interface ClientId {
client_id: string;
client_id_signature: string;
}
export interface Registration {
id: string;
client_id: string;
client_id_signature: string;
timestamp: string;
}
export interface DrawEntryPartial {
draw_id: string;
client_id: string;
client_id_signature: string;
}
export enum DrawEntryStatus {
pending = 'Pending',
winner = 'Winner',
noWin = 'NoWin',
claimed = 'Claimed',
}
export interface DrawEntry {
id: string;
draw_id: string;
timestamp: string;
status: DrawEntryStatus;
}
export interface DrawWithWordOfTheDay {
id: string;
start_utc: string;
end_utc: string;
word_of_the_day?: string;
last_modified: string;
entry?: DrawEntry;
}
export interface ClaimPartial {
draw_id: string;
registration_id: string;
client_id: string;
client_id_signature: string;
wallet_address: string;
}
export interface Winner {
id: string;
client_id: string;
draw_id: string;
timestamp: string;
winner_reg_id: string;
winner_wallet_address?: string;
winner_claim_timestamp?: string;
}
export interface Draws {
current?: DrawWithWordOfTheDay;
next?: DrawWithWordOfTheDay;
draws: DrawEntry[];
}
@@ -0,0 +1,97 @@
import React, { FC, useEffect, useRef, useState } from 'react';
import type { UnlistenFn } from '@tauri-apps/api/event';
import { listen } from '@tauri-apps/api/event';
import { Box, Paper, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
// see https://github.com/tauri-apps/tauri-plugin-log/blob/dev/webview-src/index.ts#L4
enum LogLevel {
Trace = 1,
Debug,
Info,
Warn,
Error,
}
const getLogLevelName = (value: LogLevel) => {
switch (value) {
case LogLevel.Trace:
return 'Trace';
case LogLevel.Debug:
return 'Debug';
case LogLevel.Info:
return 'Info';
case LogLevel.Warn:
return 'Warn';
case LogLevel.Error:
return 'Error';
default:
return 'Unknown';
}
};
// see https://github.com/tauri-apps/tauri-plugin-log/blob/dev/webview-src/index.ts#L147
interface RecordPayload {
level: LogLevel;
message: string;
}
export const LogViewer: FC = () => {
const unlisten = useRef<UnlistenFn>();
const messages = useRef<RecordPayload[]>([]);
const [messageCount, setMessageCount] = useState(0);
useEffect(() => {
listen('log://log', (event) => {
console.log(event.payload);
const payload = event.payload as RecordPayload;
messages.current.unshift(payload);
setMessageCount((prev) => prev + 1);
}).then((fn) => {
unlisten.current = fn;
});
return () => {
if (unlisten.current) {
unlisten.current();
}
};
}, []);
return (
<Box sx={{ height: '100vh', width: '100vw', display: 'grid', gridTemplateRows: '1fr auto' }}>
<Box sx={{ overflowY: 'hidden', p: 2 }}>
<TableContainer component={Paper} sx={{ maxHeight: '100%' }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Severity</TableCell>
<TableCell>Log message</TableCell>
</TableRow>
</TableHead>
<TableBody>
{messages.current.map((m) => (
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell sx={{ padding: 1 }}>
<Chip label={getLogLevelName(m.level)} variant="outlined" size="small" />
</TableCell>
<TableCell sx={{ padding: 1, fontFamily: 'Monospace' }}>{m.message}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
<Box
sx={{
p: 1,
textAlign: 'right',
fontSize: 'small',
borderTop: '2px solid',
borderTopColor: (theme) => theme.palette.divider,
}}
>
{messageCount} log entries since opening this window
</Box>
</Box>
);
};
@@ -0,0 +1,37 @@
import React from 'react';
import { Autocomplete, Box, Chip, Dialog, DialogProps, TextField, Typography } from '@mui/material';
import { ServiceProvider, Service, Services } from '../types/directory';
export const ServiceProviderPopup: React.FC<
DialogProps & { services: Services; onServiceProviderChanged: (sp?: ServiceProvider, s?: Service) => void }
> = ({ services, onServiceProviderChanged, ...dialogProps }) => {
const options = services.flatMap((s) =>
s.items.map((sp) => ({ id: `${s.id}-${sp.id}`, title: sp.description, service: s, sp })),
);
return (
<Dialog {...dialogProps} fullWidth PaperProps={{ sx: { p: 0, m: 0, width: '100%' } }}>
<Autocomplete
fullWidth
openOnFocus
sx={{ p: 1 }}
// filterOptions={(filterOptions, { inputValue }) =>
// filterOptions.filter((o) => o.title.toLowerCase().includes(inputValue.toLowerCase()))
// }
options={options}
groupBy={(option) => option.service.description}
getOptionLabel={(option) => option.title}
onChange={(event, value) => onServiceProviderChanged(value?.sp, value?.service)}
isOptionEqualToValue={(option, value) => option.id.toLowerCase() === value?.id.toLowerCase()}
renderOption={(props, option) => (
<Box key={option.id} component="li" sx={{ '& > img': { mr: 2, flexShrink: 0 } }} {...props} fontSize="small">
<Typography component="p" sx={{ opacity: 0.5, mr: 2, fontSize: 'inherit' }}>
{option.id}
</Typography>
<p>{option.title}</p>
</Box>
)}
renderInput={(params) => <TextField autoFocus {...params} label="Select a service provider" />}
/>
</Dialog>
);
};
@@ -0,0 +1,342 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { Box } from '@mui/material';
import { ServiceProviderSelector } from './ServiceProviderSelector';
import { Services } from '../types/directory';
export default {
title: 'Components/Service Provider Selector',
component: ServiceProviderSelector,
} as ComponentMeta<typeof ServiceProviderSelector>;
const width = 240;
export const Loading = () => (
<Box width={width}>
<ServiceProviderSelector />
</Box>
);
const services: Services = JSON.parse(`[
{
"id": "keybase",
"description": "Keybase",
"items": [
{
"id": "nym-keybase",
"description": "Nym Keybase Service Provider",
"address": "Entztfv6Uaz2hpYHQJ6JKoaCTpDL5dja18SuQWVJAmmx.Cvhn9rBJw5Ay9wgHcbgCnVg89MPSV5s2muPV2YF1BXYu@Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf",
"gateway": "Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf"
},
{
"id": "shipyard-keybase-1",
"description": "Nym Keybase Service Provider",
"address": "D55ksecHzY6vAeqk8MCTzCfj2pqwJeKCKZCUUGnwGnn3.FS42vXS5a6GNTb1qk3aVk5mjSiJCAuawbBVyQZZVfhvt@DfNMqQRy6pPkU8Z5rBsxRwzDUzAMXHPFwMhjF16ScZqn",
"gateway": "DfNMqQRy6pPkU8Z5rBsxRwzDUzAMXHPFwMhjF16ScZqn"
},
{
"id": "shipyard-keybase-2",
"description": "Nym Keybase Service Provider",
"address": "DFdDtW7LNBATxQ4ef3jNbqs3cRE8b9wDZTCctHCQRULa.4AbKiTNVUwYFWHhy98o5pT9dELiUrkXoJQ9wHqPgf6GV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN",
"gateway": "GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN"
},
{
"id": "shipyard-keybase-3",
"description": "Nym Keybase Service Provider",
"address": "6Y1HE1jJ92P9yoHer11TR4A2NdZePrLGaBNFf65MnYGe.FwXoh217odQDWNmViqzNX28fauYrjB3PYLrVvpqnQrX4@5vC8spDvw5VDQ8Zvd9fVvBhbUDv9jABR4cXzd4Kh5vz",
"gateway": "5vC8spDvw5VDQ8Zvd9fVvBhbUDv9jABR4cXzd4Kh5vz"
},
{
"id": "shipyard-keybase-4",
"description": "Nym Keybase Service Provider",
"address": "3zzhLtWvaJgn755MkRckG5aRnoTZich8ASn395iSsTgj.J1R5VuxXbh2eNHiaRbrwbKGXrrEQcHKLdzf8eg9HTB6q@3B7PsbXFuqq6rerYFLw5HPbQb4UmBqAhfWURRovMmWoj",
"gateway": "3B7PsbXFuqq6rerYFLw5HPbQb4UmBqAhfWURRovMmWoj"
},
{
"id": "shipyard-keybase-5",
"description": "Nym Keybase Service Provider",
"address": "CHuXdZJYQ8xH7ekgN9gAuVtQ7ZikjjHEZY5BSN7yc5mN.29dFvqicKQQQvoX1vup44mspmc249RH5xgLibWMwTYGT@CfWcDJq8QBz6cVAPCYSaLbaJEhVTmHEmyYgQ6C5GdDW9",
"gateway": "CfWcDJq8QBz6cVAPCYSaLbaJEhVTmHEmyYgQ6C5GdDW9"
}
]
},
{
"id": "electrum",
"description": "Electrum Wallet",
"items": [
{
"id": "nym-electrum",
"description": "Nym Electrum Service Provider",
"address": "DpB3cHAchJiNBQi5FrZx2csXb1mrHkpYh9Wzf8Rjsuko.ANNWrvHqMYuertHGHUrZdBntQhpzfbWekB39qez9U2Vx@2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh",
"gateway": "2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh"
},
{
"id": "shipyard-electrum-1",
"description": "Nym Electrum Service Provider",
"address": "8Tb73cFQpXCLpgxEA2VSDru2hHrcZ3KQcyMsGbxcTjBp.4x5tu66k8YkHk4tYac1qwEFbNq5WsKiX5kR51q5KKH88@4WgKhJdmUffz4e1o1ftVAGS3HnG56LiNAxA9dmaekrVd",
"gateway": "4WgKhJdmUffz4e1o1ftVAGS3HnG56LiNAxA9dmaekrVd"
},
{
"id": "shipyard-electrum-2",
"description": "Nym Electrum Service Provider",
"address": "GR6z31MwCsvxHrnvvVN1Cpasd8aQ1giwQqPTZM9dN7VH.5rEiqakSPDrBtKmvpU8Shnhz6gRM85JLoB7AX4h7PJYr@5Ao1J38frnU9Rx5YVeF5BWExcnDTcW8etNe9W2sRASXD",
"gateway": "5Ao1J38frnU9Rx5YVeF5BWExcnDTcW8etNe9W2sRASXD"
}
]
},
{
"id": "telegram",
"description": "Telegram",
"items": [
{
"id": "shipyard-telegram-2",
"description": "Nym Telegram Service Provider",
"address": "C4w6ewbQtoaZEeoaaNw1xVASChqo4WVjNfuYEUFjZxpc.8F1D7rQXf2jGoj1Ken7PiGDM8HS2Ug79wSoc9nZ1iqh1@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve",
"gateway": "62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve"
},
{
"id": "shipyard-telegram-3",
"description": "Nym Telegram Service Provider",
"address": "DStL3BEUZuQZfbij1KAY3BvJh8rC5jpr9mc6AQ6aTLUu.Ax9foYaKfFgX6g8y393GoNpKkKrnDGFGRZwxDv9R7X6M@FQon7UwF5knbUr2jf6jHhmNLbJnMreck1eUcVH59kxYE",
"gateway": "FQon7UwF5knbUr2jf6jHhmNLbJnMreck1eUcVH59kxYE"
},
{
"id": "shipyard-telegram-4",
"description": "Nym Telegram Service Provider",
"address": "8gRdGTzsDxYzpasRQhsRg59MCgNNhnfag2oFfwwZPXnB.DtDrGz7ScVm4o7sN4K3CYUJveYgz7fcXELBVLNDfMS9Y@3ojQD6V7skM1bSXJX7fVQvscjmcgptzdixQEaAha2ixh",
"gateway": "3ojQD6V7skM1bSXJX7fVQvscjmcgptzdixQEaAha2ixh"
},
{
"id": "shipyard-telegram-5",
"description": "Nym Telegram Service Provider",
"address": "AR3oEM6Uvmfs6fyddwSehoBUKCFxz7MdFi4z7aahuHuY.3ZKapg9A3Py1PXhyLbCJr8ZbJsEV6NZdN1WJaGGut5tj@EEyq16v63aotPBCepxUpCgAojrNasZ6Hk1PjpRyBAdEp",
"gateway": "EEyq16v63aotPBCepxUpCgAojrNasZ6Hk1PjpRyBAdEp"
},
{
"id": "shipyard-telegram-6",
"description": "Nym Telegram Service Provider",
"address": "7n1BYhsXSwcr8Qim8AqZTAodqFia4QG6T7CRc1ihQHpv.7o4trpGqu2LHMUiXc3dddgfGET1CFFcAK9gKYoHoSn5e@BTZNB3bkkEePsT14GN8ofVtM1SJae4YLWjpBerrKYfr",
"gateway": "BTZNB3bkkEePsT14GN8ofVtM1SJae4YLWjpBerrKYfr"
},
{
"id": "shipyard-telegram-7",
"description": "Nym Telegram Service Provider",
"address": "Gv4TWhUKrvJfqh1jBRPGEQrikNZvZse2kS3ZgN9Z2nAZ.7KGPaaqUEum2C59jLvw7f8Ug8a48YuZdjjZu3t4JES4U@C7J8SwZQqjWqhBryyjJxLt7FacVuPTwAmR2otGy53ayi",
"gateway": "C7J8SwZQqjWqhBryyjJxLt7FacVuPTwAmR2otGy53ayi"
},
{
"id": "shipyard-telegram-8",
"description": "Nym Telegram Service Provider",
"address": "8Mqgp12cpF6FSXMeqzxgFgQXvTSapyNqGAi5wy7ub4ge.7z7PDsiJGiGxGz4p77v5L5fZhXBJ5qNZ8CgJwYNr6H6J",
"gateway": "3zd3wrCK8Dz5TXrcvk5dG5s9EEdf4Ck1v9VgBPMMFVkR"
},
{
"id": "shipyard-telegram-9",
"description": "Nym Telegram Service Provider",
"address": "F3N5eiPDZcGFC985Go4Mpv8p9uxFD1L3jRUdrLCbrZLm.EyTxWwwTwYpPrJBmc97GLd1LpUAphjptS5y1ed182bGk@GAjhJcrd6f1edaqUkfWCff6zdHoqo756qYrc2TfPuCXJ",
"gateway": "GAjhJcrd6f1edaqUkfWCff6zdHoqo756qYrc2TfPuCXJ"
},
{
"id": "shipyard-telegram-10",
"description": "Nym Telegram Service Provider",
"address": "G7y7e1nVBr8fmQSzdeAxXnCmmmJb5k8N3E8LBV31KE5g.GRRUCj6t6cCUUjakmTWzidMLiYA7EdCedKnup8osaBC6@AJad2R9virYEYXEsTcicN5y5tyPoixrhhAGsxoESZVnc",
"gateway": "AJad2R9virYEYXEsTcicN5y5tyPoixrhhAGsxoESZVnc"
},
{
"id": "shipyard-telegram-11",
"description": "Nym Telegram Service Provider",
"address": "2kq9Z7RyDZtb8kxXjyP3ZT8VMWHg6JXFDChGuuNBk7Hw.F5XYbBaGSoF8qAo8faPcaNRPHEq3Y25BDcwESeabUS9S@HaLyPQrhBTq75dnGeBUdYWeFVA2BBn39MgkhEt3VTMMM",
"gateway": "HaLyPQrhBTq75dnGeBUdYWeFVA2BBn39MgkhEt3VTMMM"
},
{
"id": "shipyard-telegram-12",
"description": "Nym Telegram Service Provider",
"address": "GegdtpNzYj4QCgpih9Kxv7ZVZxmVdxYHsDkiPsbT71XG.E8xtE8mrapjzFtyuziZSrsScAKhwZMH5wNpKWtKfzJ5Y@9Byd9VAtyYMnbVAcqdoQxJnq76XEg2dbxbiF5Aa5Jj9J",
"gateway": "9Byd9VAtyYMnbVAcqdoQxJnq76XEg2dbxbiF5Aa5Jj9J"
},
{
"id": "shipyard-telegram-13",
"description": "Nym Telegram Service Provider",
"address": "4SsrDQeEtG3mpeD9nN5CDdGaCsxFvNeYMhoviDzNNB9f.GyqG6iK5rBvhe3HXLR11m6ULpf13ATgYvkkidLmteDLs@5EpkkrMFYAM3XcaztXnZwBWquURHSKsyc9JxUCengDFS",
"gateway": "5EpkkrMFYAM3XcaztXnZwBWquURHSKsyc9JxUCengDFS"
},
{
"id": "shipyard-telegram-14",
"description": "Nym Telegram Service Provider",
"address": "9JoHRu2RrSD1fjbj9NSTASgjv9Szep7Nhd9L2PywxbBi.AZhAUDNX6iH8BqXyR5c7TJuzpwMEvDXrabNLGuRukvVf@9xJM74FwwHhEKKJHihD21QSZnHM2QBRMoFx9Wst6qNBS",
"gateway": "9xJM74FwwHhEKKJHihD21QSZnHM2QBRMoFx9Wst6qNBS"
},
{
"id": "shipyard-telegram-15",
"description": "Nym Telegram Service Provider",
"address": "3K174ijjXqCkhMDT9xLcqjS4MXk2QsqZt4PdgNcuUrnn.BNnHnQmWoj6Uo6kkS1QkPqsdHaXrcwyR9F6MnnzDkZJL@C7J8SwZQqjWqhBryyjJxLt7FacVuPTwAmR2otGy53ayi",
"gateway": "C7J8SwZQqjWqhBryyjJxLt7FacVuPTwAmR2otGy53ayi"
},
{
"id": "shipyard-telegram-16",
"description": "Nym Telegram Service Provider",
"address": "BqX5Q3MEcbTnM39hUswQchLW68SrqbhL8K5ucrLmtP39.AWrVsFoVC9s6KjdpcasATmZPA3GtMsUxcfHpAkuNdtFG@Emswx6KXyjRfq1c2k4d4uD2e6nBSbH1biorCZUei8UNS",
"gateway": "Emswx6KXyjRfq1c2k4d4uD2e6nBSbH1biorCZUei8UNS"
},
{
"id": "shipyard-telegram-17",
"description": "Nym Telegram Service Provider",
"address": "2tQxccgcqdkuUvLqgiEkEN4rNRZ5QknygnKAFcS4gfoe.EVrY5q5sqDqBUbS3wHsRRZhk2MAw1S17hNoH1Bicyv7n@DAGQxdxwAkwjaLjTw1B9vndia4YyFD15qRgcTQxrmkom",
"gateway": "DAGQxdxwAkwjaLjTw1B9vndia4YyFD15qRgcTQxrmkom"
},
{
"id": "shipyard-telegram-18",
"description": "Nym Telegram Service Provider",
"address": "8YG1rcEauJA814Nd7hSxjNe2UrRwrGsrXTm1Cmd3gRrU.FxYaYqpNN8PciNsySs3zYPrTB1J8AYUu9DBsM2vVDDfF@7EfEESLo71GUvx3UEW79LgTegHUBPUocUzGyJVv6LHog",
"gateway": "7EfEESLo71GUvx3UEW79LgTegHUBPUocUzGyJVv6LHog"
},
{
"id": "shipyard-telegram-19",
"description": "Nym Telegram Service Provider",
"address": "HPiXADVFLwLQPNpPtyYefzvYntC6tp9UJ5fJZGfkqvDt.2EUUxmeT3AiaUzAcE5SyXRAk8a2JXBkRz4B8McSdkrST@9ACTkYraCqE9jMb6zb6ne8EDQGGhZw5ykNiq9YRUdHTD",
"gateway": "9ACTkYraCqE9jMb6zb6ne8EDQGGhZw5ykNiq9YRUdHTD"
},
{
"id": "shipyard-telegram-20",
"description": "Nym Telegram Service Provider",
"address": "2QLnEEnTmf2NRWtcQPWBeRcg7Hej5WSPWRWwtTpEEZWF.BheS78ozc8ngvhsXNNnshdJzpoYsmEvhfn3WKUYF5dRU@C2uyokSPoxhku9GexRxEo1e8KPZ7q6e8FXmK3gtY8kkF",
"gateway": "C2uyokSPoxhku9GexRxEo1e8KPZ7q6e8FXmK3gtY8kkF"
},
{
"id": "shipyard-telegram-21",
"description": "Nym Telegram Service Provider",
"address": "FuBbnwiANfaXZnn683jBapK5XVm5ttgZSykU3vqPSHoD.94MFGv1VcBLTkRwzBDQUkWjvqtZYVBrJg2Q8JGbizcib@CTqYPY8htdAQMXCzRW9SjZzZuqYwSt2iUh6HPaNgmTvK",
"gateway": "CTqYPY8htdAQMXCzRW9SjZzZuqYwSt2iUh6HPaNgmTvK"
},
{
"id": "shipyard-telegram-22",
"description": "Nym Telegram Service Provider",
"address": "9EbQx5jQznSVbftFom7sqUSHAACrsfvMhrzhaFt4A3SZ.D1FQCirL4YKwfcmtMGvB5Rugt5sAzGEhdSjJ3bHVQRZ@7Zh1Sz5dXpA6s53CbtcdqhQhLqwf4cLynL7KqHKcjrG4",
"gateway": "7Zh1Sz5dXpA6s53CbtcdqhQhLqwf4cLynL7KqHKcjrG4"
},
{
"id": "shipyard-telegram-24",
"description": "Nym Telegram Service Provider",
"address": "6Umawwvf551VyB3Ko46NgKLqJdTFJeToCM67mrTmM3G.3A4sesBac4KGuMTFjvYBwLpksMJvbMbteGJQgmm4PV4Y@AnnYnEtBjB2a5sHmeRCnBq43qxyHDf95Bqd7cwQyKNLR",
"gateway": "AnnYnEtBjB2a5sHmeRCnBq43qxyHDf95Bqd7cwQyKNLR"
},
{
"id": "shipyard-telegram-25",
"description": "Nym Telegram Service Provider",
"address": "CDtxTeoyqq83JpV9G8cR5HRHRdMMaVspQsCwH3Qnajt3.F5EHK9HFcdGrE2hqA7bK9AUmkbihujYDhtNNqHKxW765@BDkeNx7JQm5NsQakst9s8htogZXhpTQedFAgZpvsGCqH",
"gateway": "BDkeNx7JQm5NsQakst9s8htogZXhpTQedFAgZpvsGCqH"
},
{
"id": "shipyard-telegram-26",
"description": "Nym Telegram Service Provider",
"address": "HukZkLG2DoarQEqaoDLuqW1GFf2NSHDUMGBZiyJGRYJD.9GyU8wPsyzcvRjcyk8hiNpTJbXCmq5F3VoVhFBZYuHR3@GsGEZiDBz8SWfHGaK5SDmhfbTEM55v37WCYYcT9wTSxN",
"gateway": "GsGEZiDBz8SWfHGaK5SDmhfbTEM55v37WCYYcT9wTSxN"
},
{
"id": "shipyard-telegram-27",
"description": "Nym Telegram Service Provider",
"address": "773y8iMVJiRk4dRbjQzkJVbrei4TwkePNE5WTEttt77d.3Mw47C9XZj3oAzk1iSqC5Y36tbBsjtaTtdgaHM3Zsdma@7fiZtNL1RACQTwGrKLBT9nbY77bfwZnX9rqcWqc53qgv",
"gateway": "7fiZtNL1RACQTwGrKLBT9nbY77bfwZnX9rqcWqc53qgv"
},
{
"id": "shipyard-telegram-28",
"description": "Nym Telegram Service Provider",
"address": "6jQJEorCu7YiP9HdDaMeHxcNhxeNmZ1kpd836GnqLZX.HsJqEmNTszGecsKqFB37i84nBXxqf4ETgrKmKmBvMGHC@FYnDMQzT49ZGM23gVqpTxfih14V6wuedNXirekmt37zE",
"gateway": "FYnDMQzT49ZGM23gVqpTxfih14V6wuedNXirekmt37zE"
},
{
"id": "shipyard-telegram-29",
"description": "Nym Telegram Service Provider",
"address": "BiCSyovpFMuSnTvF2TdiuZwrytXDrd9AH47ZMcCxscVC.G9YpdicA9BBNoVHDgjWjgt17wv5WYKWcbE3vPJJVpSJD@GAjhJcrd6f1edaqUkfWCff6zdHoqo756qYrc2TfPuCXJ",
"gateway":"GAjhJcrd6f1edaqUkfWCff6zdHoqo756qYrc2TfPuCXJ"
},
{
"id": "shipyard-telegram-30",
"description": "Nym Telegram Service Provider",
"address": "AQRRAs9oc8QWXAFBs44YhCKUny7AyLsfLy91pwmGgxuf.CWUKoKA1afSKyw5BnFJJg19UDgnaVATupsFhQpyTEBHJ@EBT8jTD8o4tKng2NXrrcrzVhJiBnKpT1bJy5CMeArt2w",
"gateway": "EBT8jTD8o4tKng2NXrrcrzVhJiBnKpT1bJy5CMeArt2w"
},
{
"id": "shipyard-telegram-31",
"description": "Nym Telegram Service Provider",
"address": "6YqjAZK3Pr1ngiBLcDkotboB5WiN6k6NPpbXvShH4pR5.9Ss6VW3Xbyi8LuxduNNwnXEv9njHCQ2PLSP1UK6tsoa5@42XCK9dMS9m5XJLzQd2dBuwimk6ndZnczhZaV5VPFkQD",
"gateway": "42XCK9dMS9m5XJLzQd2dBuwimk6ndZnczhZaV5VPFkQD"
},
{
"id": "shipyard-telegram-32",
"description": "Nym Telegram Service Provider",
"address": "EmYWLeybmj86Vzr62vxuZ3T15jwMNHggzK7sQwid96yp.GyaF9WprSr56cxUdGf5TpcUvAjb2VbAr8CVBrmBUYAaw@GL5wESoz4oSbpBaTki9qB9213FGUQXCiRjbzDkhWwoBC",
"gateway": "GL5wESoz4oSbpBaTki9qB9213FGUQXCiRjbzDkhWwoBC"
},
{
"id": "shipyard-telegram-33",
"description": "Nym Telegram Service Provider",
"address": "4PDb96cck5btTj6G7rsomqwHJsp4qu8uPvFCbwHfjFUx.C5dKbaoakH7egsZvAueRbwLFbmxnQaVMeSr6QTMpuBAA@58ceEFaLJh6zAo3cirzT1BDQm7D3L5acnQrxGH1D6TAY",
"gateway": "58ceEFaLJh6zAo3cirzT1BDQm7D3L5acnQrxGH1D6TAY"
},
{
"id": "shipyard-telegram-34",
"description": "Nym Telegram Service Provider",
"address": "BeZbeMf9vcpUf368qDd85dtLwXLj4Ee5bsHMB2fUD8uX.HELVbppkwU1jmzUAUrCEbHeJfVciSeo8VGAkbJSpwxsb@ADdHkiTfkpsSt31zVToWW9j3KikH24aLAAwDKtCYE5jY",
"gateway":"ADdHkiTfkpsSt31zVToWW9j3KikH24aLAAwDKtCYE5jY"
},
{
"id": "shipyard-telegram-35",
"description": "Nym Telegram Service Provider",
"address": "Bp4JRFyf7GB9L9J95AqMPnz9zbGmPnViA5fDXKeNraLJ.D6CTdcjJVxDmH2UQvzXuPWg9Se9xXYe76uDMypXvhzd7@6UjGEeQZK14C5K2kenycTkqt7qRjEHGLgaQx3FWySo3N",
"gateway": "6UjGEeQZK14C5K2kenycTkqt7qRjEHGLgaQx3FWySo3N"
},
{
"id": "shipyard-telegram-36",
"description": "Nym Telegram Service Provider",
"address": "91h7io6BGhVjbtC7dbbRcScyTJcTfnMsTQZ6aWMVsrWR.Epb4hANXCp8cGEY3wSgawux991ti9Z5Y1FHTMzAKFa6E@DF4TE7V8kJkttMvnoSVGnRFFRt6WYGxxiC2w1XyPQnHe",
"gateway": "DF4TE7V8kJkttMvnoSVGnRFFRt6WYGxxiC2w1XyPQnHe"
},
{
"id": "shipyard-telegram-37",
"description": "Nym Telegram Service Provider",
"address": "Cy2wuwKpWZ3iWzKU3ZWL1qqcVfJ5Cq85dU7UHVWwv2gc.9AhC9b2zVcLnXLGriMdxjpsWJpq6iAdCavDi63udbL89@678qVUJ21uwxZBhp3r56z7GRf6gMh3NYDHruTegPtgMf",
"gateway": "678qVUJ21uwxZBhp3r56z7GRf6gMh3NYDHruTegPtgMf"
},
{
"id": "shipyard-telegram-38",
"description": "Nym Telegram Service Provider",
"address": "GgUeUWW1NRSuquZYeZf3WkppE92EQUHJrFjNZtYU1jow.CSEjwrRi4f4uyw7N6L2LPKw2tB8spcMbFu99wHZzFZSj@77TSuVU8d1oXKbPzjec2xh4i3Wj5WwUyy9Lr36sm8gZm",
"gateway": "77TSuVU8d1oXKbPzjec2xh4i3Wj5WwUyy9Lr36sm8gZm"
},
{
"id": "shipyard-telegram-39",
"description": "Nym Telegram Service Provider",
"address": "kz4zWwSkYiQxqxXFPNcGUByTPQWXascD9RfYsmSxY7n.ajp3SjbBVBjrU9nXpSQXAXzbb6EHJJyhbY6cc1ajayx@BTZNB3bkkEePsT14GN8ofVtM1SJae4YLWjpBerrKYf",
"gateway": "HyS2UZtZX3kQXdazbdE99DhCjBXjbG61LC9QsmXwbxrU"
}
]
},
{
"id": "blockstream",
"description": "Blockstream Green",
"items": [
{
"id": "nym-blockstream",
"description": "Nym Blockstream Green Service Provider",
"address": "GiRjFWrMxt58pEMuusm4yT3RxoMD1MMPrR9M2N4VWRJP.3CNZBPq4vg7v7qozjGjdPMXcvDmkbWPCgbGCjQVw9n6Z@2xU4CBE6QiiYt6EyBXSALwxkNvM7gqJfjHXaMkjiFmYW",
"gateway": "2xU4CBE6QiiYt6EyBXSALwxkNvM7gqJfjHXaMkjiFmYW"
}
]
}
]`);
export const Loaded = () => (
<Box width={width}>
<ServiceProviderSelector services={services} />
</Box>
);
export const ServiceAlreadySelected = () => (
<Box width={width}>
<ServiceProviderSelector
services={services}
currentSp={services[2].items[2]}
onChange={(serviceProvider) => console.log('New service provider selected: ', serviceProvider)}
/>
</Box>
);
@@ -1,6 +1,7 @@
import React, { useEffect, useMemo } from 'react';
import { Box, CircularProgress, Stack, TextField, Tooltip, Typography, MenuItem, ListItemIcon } from '@mui/material';
import { ServiceProvider, Service, Services } from '../types/directory';
import { useTauriEvents } from '../utils';
type ServiceWithRandomSp = {
id: string;
@@ -8,13 +9,31 @@ type ServiceWithRandomSp = {
sp: ServiceProvider;
};
const defaultServiceValue = { id: '', description: '', items: [] };
export const ServiceProviderSelector: React.FC<{
onChange?: (serviceProvider: ServiceProvider) => void;
onChange?: (serviceProvider?: ServiceProvider) => void;
services?: Services;
currentSp?: ServiceProvider;
}> = ({ services, currentSp, onChange }) => {
const [service, setService] = React.useState<Service>({ id: '', description: '', items: [] });
const [service, setService] = React.useState<Service>(defaultServiceValue);
const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>(currentSp);
const [resetTrigger, setResetTrigger] = React.useState(new Date().toISOString());
const handleSelectSp = (newServiceProvider?: ServiceProvider) => {
if (newServiceProvider && newServiceProvider !== currentSp) {
setServiceProvider(newServiceProvider);
onChange?.(newServiceProvider);
}
};
// when the user clears local storage, reset the selector
useTauriEvents('help://clear-storage', () => {
setService(defaultServiceValue);
setServiceProvider(undefined);
onChange?.(undefined);
setResetTrigger(new Date().toISOString());
});
useEffect(() => {
if (!serviceProvider && currentSp) {
@@ -39,13 +58,6 @@ export const ServiceProviderSelector: React.FC<{
}
}, [serviceProvider, services]);
const handleSelectSp = (newServiceProvider?: ServiceProvider) => {
if (newServiceProvider && newServiceProvider !== currentSp) {
setServiceProvider(newServiceProvider);
onChange?.(newServiceProvider);
}
};
if (!services) {
return (
<Box display="flex" alignItems="center" justifyContent="center" sx={{ my: 3 }}>
@@ -64,7 +76,7 @@ export const ServiceProviderSelector: React.FC<{
description,
sp: items[Math.floor(Math.random() * items.length)],
})),
[services],
[services, resetTrigger],
);
if (!service) return null;
@@ -0,0 +1,342 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { Box } from '@mui/material';
import { ServiceSelector } from './ServiceSelector';
import { Services } from '../types/directory';
export default {
title: 'Components/Service Selector',
component: ServiceSelector,
} as ComponentMeta<typeof ServiceSelector>;
const width = 240;
export const Loading = () => (
<Box width={width}>
<ServiceSelector />
</Box>
);
const services: Services = JSON.parse(`[
{
"id": "keybase",
"description": "Keybase",
"items": [
{
"id": "nym-keybase",
"description": "Nym Keybase Service Provider",
"address": "Entztfv6Uaz2hpYHQJ6JKoaCTpDL5dja18SuQWVJAmmx.Cvhn9rBJw5Ay9wgHcbgCnVg89MPSV5s2muPV2YF1BXYu@Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf",
"gateway": "Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf"
},
{
"id": "shipyard-keybase-1",
"description": "Nym Keybase Service Provider",
"address": "D55ksecHzY6vAeqk8MCTzCfj2pqwJeKCKZCUUGnwGnn3.FS42vXS5a6GNTb1qk3aVk5mjSiJCAuawbBVyQZZVfhvt@DfNMqQRy6pPkU8Z5rBsxRwzDUzAMXHPFwMhjF16ScZqn",
"gateway": "DfNMqQRy6pPkU8Z5rBsxRwzDUzAMXHPFwMhjF16ScZqn"
},
{
"id": "shipyard-keybase-2",
"description": "Nym Keybase Service Provider",
"address": "DFdDtW7LNBATxQ4ef3jNbqs3cRE8b9wDZTCctHCQRULa.4AbKiTNVUwYFWHhy98o5pT9dELiUrkXoJQ9wHqPgf6GV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN",
"gateway": "GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN"
},
{
"id": "shipyard-keybase-3",
"description": "Nym Keybase Service Provider",
"address": "6Y1HE1jJ92P9yoHer11TR4A2NdZePrLGaBNFf65MnYGe.FwXoh217odQDWNmViqzNX28fauYrjB3PYLrVvpqnQrX4@5vC8spDvw5VDQ8Zvd9fVvBhbUDv9jABR4cXzd4Kh5vz",
"gateway": "5vC8spDvw5VDQ8Zvd9fVvBhbUDv9jABR4cXzd4Kh5vz"
},
{
"id": "shipyard-keybase-4",
"description": "Nym Keybase Service Provider",
"address": "3zzhLtWvaJgn755MkRckG5aRnoTZich8ASn395iSsTgj.J1R5VuxXbh2eNHiaRbrwbKGXrrEQcHKLdzf8eg9HTB6q@3B7PsbXFuqq6rerYFLw5HPbQb4UmBqAhfWURRovMmWoj",
"gateway": "3B7PsbXFuqq6rerYFLw5HPbQb4UmBqAhfWURRovMmWoj"
},
{
"id": "shipyard-keybase-5",
"description": "Nym Keybase Service Provider",
"address": "CHuXdZJYQ8xH7ekgN9gAuVtQ7ZikjjHEZY5BSN7yc5mN.29dFvqicKQQQvoX1vup44mspmc249RH5xgLibWMwTYGT@CfWcDJq8QBz6cVAPCYSaLbaJEhVTmHEmyYgQ6C5GdDW9",
"gateway": "CfWcDJq8QBz6cVAPCYSaLbaJEhVTmHEmyYgQ6C5GdDW9"
}
]
},
{
"id": "electrum",
"description": "Electrum Wallet",
"items": [
{
"id": "nym-electrum",
"description": "Nym Electrum Service Provider",
"address": "DpB3cHAchJiNBQi5FrZx2csXb1mrHkpYh9Wzf8Rjsuko.ANNWrvHqMYuertHGHUrZdBntQhpzfbWekB39qez9U2Vx@2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh",
"gateway": "2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh"
},
{
"id": "shipyard-electrum-1",
"description": "Nym Electrum Service Provider",
"address": "8Tb73cFQpXCLpgxEA2VSDru2hHrcZ3KQcyMsGbxcTjBp.4x5tu66k8YkHk4tYac1qwEFbNq5WsKiX5kR51q5KKH88@4WgKhJdmUffz4e1o1ftVAGS3HnG56LiNAxA9dmaekrVd",
"gateway": "4WgKhJdmUffz4e1o1ftVAGS3HnG56LiNAxA9dmaekrVd"
},
{
"id": "shipyard-electrum-2",
"description": "Nym Electrum Service Provider",
"address": "GR6z31MwCsvxHrnvvVN1Cpasd8aQ1giwQqPTZM9dN7VH.5rEiqakSPDrBtKmvpU8Shnhz6gRM85JLoB7AX4h7PJYr@5Ao1J38frnU9Rx5YVeF5BWExcnDTcW8etNe9W2sRASXD",
"gateway": "5Ao1J38frnU9Rx5YVeF5BWExcnDTcW8etNe9W2sRASXD"
}
]
},
{
"id": "telegram",
"description": "Telegram",
"items": [
{
"id": "shipyard-telegram-2",
"description": "Nym Telegram Service Provider",
"address": "C4w6ewbQtoaZEeoaaNw1xVASChqo4WVjNfuYEUFjZxpc.8F1D7rQXf2jGoj1Ken7PiGDM8HS2Ug79wSoc9nZ1iqh1@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve",
"gateway": "62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve"
},
{
"id": "shipyard-telegram-3",
"description": "Nym Telegram Service Provider",
"address": "DStL3BEUZuQZfbij1KAY3BvJh8rC5jpr9mc6AQ6aTLUu.Ax9foYaKfFgX6g8y393GoNpKkKrnDGFGRZwxDv9R7X6M@FQon7UwF5knbUr2jf6jHhmNLbJnMreck1eUcVH59kxYE",
"gateway": "FQon7UwF5knbUr2jf6jHhmNLbJnMreck1eUcVH59kxYE"
},
{
"id": "shipyard-telegram-4",
"description": "Nym Telegram Service Provider",
"address": "8gRdGTzsDxYzpasRQhsRg59MCgNNhnfag2oFfwwZPXnB.DtDrGz7ScVm4o7sN4K3CYUJveYgz7fcXELBVLNDfMS9Y@3ojQD6V7skM1bSXJX7fVQvscjmcgptzdixQEaAha2ixh",
"gateway": "3ojQD6V7skM1bSXJX7fVQvscjmcgptzdixQEaAha2ixh"
},
{
"id": "shipyard-telegram-5",
"description": "Nym Telegram Service Provider",
"address": "AR3oEM6Uvmfs6fyddwSehoBUKCFxz7MdFi4z7aahuHuY.3ZKapg9A3Py1PXhyLbCJr8ZbJsEV6NZdN1WJaGGut5tj@EEyq16v63aotPBCepxUpCgAojrNasZ6Hk1PjpRyBAdEp",
"gateway": "EEyq16v63aotPBCepxUpCgAojrNasZ6Hk1PjpRyBAdEp"
},
{
"id": "shipyard-telegram-6",
"description": "Nym Telegram Service Provider",
"address": "7n1BYhsXSwcr8Qim8AqZTAodqFia4QG6T7CRc1ihQHpv.7o4trpGqu2LHMUiXc3dddgfGET1CFFcAK9gKYoHoSn5e@BTZNB3bkkEePsT14GN8ofVtM1SJae4YLWjpBerrKYfr",
"gateway": "BTZNB3bkkEePsT14GN8ofVtM1SJae4YLWjpBerrKYfr"
},
{
"id": "shipyard-telegram-7",
"description": "Nym Telegram Service Provider",
"address": "Gv4TWhUKrvJfqh1jBRPGEQrikNZvZse2kS3ZgN9Z2nAZ.7KGPaaqUEum2C59jLvw7f8Ug8a48YuZdjjZu3t4JES4U@C7J8SwZQqjWqhBryyjJxLt7FacVuPTwAmR2otGy53ayi",
"gateway": "C7J8SwZQqjWqhBryyjJxLt7FacVuPTwAmR2otGy53ayi"
},
{
"id": "shipyard-telegram-8",
"description": "Nym Telegram Service Provider",
"address": "8Mqgp12cpF6FSXMeqzxgFgQXvTSapyNqGAi5wy7ub4ge.7z7PDsiJGiGxGz4p77v5L5fZhXBJ5qNZ8CgJwYNr6H6J",
"gateway": "3zd3wrCK8Dz5TXrcvk5dG5s9EEdf4Ck1v9VgBPMMFVkR"
},
{
"id": "shipyard-telegram-9",
"description": "Nym Telegram Service Provider",
"address": "F3N5eiPDZcGFC985Go4Mpv8p9uxFD1L3jRUdrLCbrZLm.EyTxWwwTwYpPrJBmc97GLd1LpUAphjptS5y1ed182bGk@GAjhJcrd6f1edaqUkfWCff6zdHoqo756qYrc2TfPuCXJ",
"gateway": "GAjhJcrd6f1edaqUkfWCff6zdHoqo756qYrc2TfPuCXJ"
},
{
"id": "shipyard-telegram-10",
"description": "Nym Telegram Service Provider",
"address": "G7y7e1nVBr8fmQSzdeAxXnCmmmJb5k8N3E8LBV31KE5g.GRRUCj6t6cCUUjakmTWzidMLiYA7EdCedKnup8osaBC6@AJad2R9virYEYXEsTcicN5y5tyPoixrhhAGsxoESZVnc",
"gateway": "AJad2R9virYEYXEsTcicN5y5tyPoixrhhAGsxoESZVnc"
},
{
"id": "shipyard-telegram-11",
"description": "Nym Telegram Service Provider",
"address": "2kq9Z7RyDZtb8kxXjyP3ZT8VMWHg6JXFDChGuuNBk7Hw.F5XYbBaGSoF8qAo8faPcaNRPHEq3Y25BDcwESeabUS9S@HaLyPQrhBTq75dnGeBUdYWeFVA2BBn39MgkhEt3VTMMM",
"gateway": "HaLyPQrhBTq75dnGeBUdYWeFVA2BBn39MgkhEt3VTMMM"
},
{
"id": "shipyard-telegram-12",
"description": "Nym Telegram Service Provider",
"address": "GegdtpNzYj4QCgpih9Kxv7ZVZxmVdxYHsDkiPsbT71XG.E8xtE8mrapjzFtyuziZSrsScAKhwZMH5wNpKWtKfzJ5Y@9Byd9VAtyYMnbVAcqdoQxJnq76XEg2dbxbiF5Aa5Jj9J",
"gateway": "9Byd9VAtyYMnbVAcqdoQxJnq76XEg2dbxbiF5Aa5Jj9J"
},
{
"id": "shipyard-telegram-13",
"description": "Nym Telegram Service Provider",
"address": "4SsrDQeEtG3mpeD9nN5CDdGaCsxFvNeYMhoviDzNNB9f.GyqG6iK5rBvhe3HXLR11m6ULpf13ATgYvkkidLmteDLs@5EpkkrMFYAM3XcaztXnZwBWquURHSKsyc9JxUCengDFS",
"gateway": "5EpkkrMFYAM3XcaztXnZwBWquURHSKsyc9JxUCengDFS"
},
{
"id": "shipyard-telegram-14",
"description": "Nym Telegram Service Provider",
"address": "9JoHRu2RrSD1fjbj9NSTASgjv9Szep7Nhd9L2PywxbBi.AZhAUDNX6iH8BqXyR5c7TJuzpwMEvDXrabNLGuRukvVf@9xJM74FwwHhEKKJHihD21QSZnHM2QBRMoFx9Wst6qNBS",
"gateway": "9xJM74FwwHhEKKJHihD21QSZnHM2QBRMoFx9Wst6qNBS"
},
{
"id": "shipyard-telegram-15",
"description": "Nym Telegram Service Provider",
"address": "3K174ijjXqCkhMDT9xLcqjS4MXk2QsqZt4PdgNcuUrnn.BNnHnQmWoj6Uo6kkS1QkPqsdHaXrcwyR9F6MnnzDkZJL@C7J8SwZQqjWqhBryyjJxLt7FacVuPTwAmR2otGy53ayi",
"gateway": "C7J8SwZQqjWqhBryyjJxLt7FacVuPTwAmR2otGy53ayi"
},
{
"id": "shipyard-telegram-16",
"description": "Nym Telegram Service Provider",
"address": "BqX5Q3MEcbTnM39hUswQchLW68SrqbhL8K5ucrLmtP39.AWrVsFoVC9s6KjdpcasATmZPA3GtMsUxcfHpAkuNdtFG@Emswx6KXyjRfq1c2k4d4uD2e6nBSbH1biorCZUei8UNS",
"gateway": "Emswx6KXyjRfq1c2k4d4uD2e6nBSbH1biorCZUei8UNS"
},
{
"id": "shipyard-telegram-17",
"description": "Nym Telegram Service Provider",
"address": "2tQxccgcqdkuUvLqgiEkEN4rNRZ5QknygnKAFcS4gfoe.EVrY5q5sqDqBUbS3wHsRRZhk2MAw1S17hNoH1Bicyv7n@DAGQxdxwAkwjaLjTw1B9vndia4YyFD15qRgcTQxrmkom",
"gateway": "DAGQxdxwAkwjaLjTw1B9vndia4YyFD15qRgcTQxrmkom"
},
{
"id": "shipyard-telegram-18",
"description": "Nym Telegram Service Provider",
"address": "8YG1rcEauJA814Nd7hSxjNe2UrRwrGsrXTm1Cmd3gRrU.FxYaYqpNN8PciNsySs3zYPrTB1J8AYUu9DBsM2vVDDfF@7EfEESLo71GUvx3UEW79LgTegHUBPUocUzGyJVv6LHog",
"gateway": "7EfEESLo71GUvx3UEW79LgTegHUBPUocUzGyJVv6LHog"
},
{
"id": "shipyard-telegram-19",
"description": "Nym Telegram Service Provider",
"address": "HPiXADVFLwLQPNpPtyYefzvYntC6tp9UJ5fJZGfkqvDt.2EUUxmeT3AiaUzAcE5SyXRAk8a2JXBkRz4B8McSdkrST@9ACTkYraCqE9jMb6zb6ne8EDQGGhZw5ykNiq9YRUdHTD",
"gateway": "9ACTkYraCqE9jMb6zb6ne8EDQGGhZw5ykNiq9YRUdHTD"
},
{
"id": "shipyard-telegram-20",
"description": "Nym Telegram Service Provider",
"address": "2QLnEEnTmf2NRWtcQPWBeRcg7Hej5WSPWRWwtTpEEZWF.BheS78ozc8ngvhsXNNnshdJzpoYsmEvhfn3WKUYF5dRU@C2uyokSPoxhku9GexRxEo1e8KPZ7q6e8FXmK3gtY8kkF",
"gateway": "C2uyokSPoxhku9GexRxEo1e8KPZ7q6e8FXmK3gtY8kkF"
},
{
"id": "shipyard-telegram-21",
"description": "Nym Telegram Service Provider",
"address": "FuBbnwiANfaXZnn683jBapK5XVm5ttgZSykU3vqPSHoD.94MFGv1VcBLTkRwzBDQUkWjvqtZYVBrJg2Q8JGbizcib@CTqYPY8htdAQMXCzRW9SjZzZuqYwSt2iUh6HPaNgmTvK",
"gateway": "CTqYPY8htdAQMXCzRW9SjZzZuqYwSt2iUh6HPaNgmTvK"
},
{
"id": "shipyard-telegram-22",
"description": "Nym Telegram Service Provider",
"address": "9EbQx5jQznSVbftFom7sqUSHAACrsfvMhrzhaFt4A3SZ.D1FQCirL4YKwfcmtMGvB5Rugt5sAzGEhdSjJ3bHVQRZ@7Zh1Sz5dXpA6s53CbtcdqhQhLqwf4cLynL7KqHKcjrG4",
"gateway": "7Zh1Sz5dXpA6s53CbtcdqhQhLqwf4cLynL7KqHKcjrG4"
},
{
"id": "shipyard-telegram-24",
"description": "Nym Telegram Service Provider",
"address": "6Umawwvf551VyB3Ko46NgKLqJdTFJeToCM67mrTmM3G.3A4sesBac4KGuMTFjvYBwLpksMJvbMbteGJQgmm4PV4Y@AnnYnEtBjB2a5sHmeRCnBq43qxyHDf95Bqd7cwQyKNLR",
"gateway": "AnnYnEtBjB2a5sHmeRCnBq43qxyHDf95Bqd7cwQyKNLR"
},
{
"id": "shipyard-telegram-25",
"description": "Nym Telegram Service Provider",
"address": "CDtxTeoyqq83JpV9G8cR5HRHRdMMaVspQsCwH3Qnajt3.F5EHK9HFcdGrE2hqA7bK9AUmkbihujYDhtNNqHKxW765@BDkeNx7JQm5NsQakst9s8htogZXhpTQedFAgZpvsGCqH",
"gateway": "BDkeNx7JQm5NsQakst9s8htogZXhpTQedFAgZpvsGCqH"
},
{
"id": "shipyard-telegram-26",
"description": "Nym Telegram Service Provider",
"address": "HukZkLG2DoarQEqaoDLuqW1GFf2NSHDUMGBZiyJGRYJD.9GyU8wPsyzcvRjcyk8hiNpTJbXCmq5F3VoVhFBZYuHR3@GsGEZiDBz8SWfHGaK5SDmhfbTEM55v37WCYYcT9wTSxN",
"gateway": "GsGEZiDBz8SWfHGaK5SDmhfbTEM55v37WCYYcT9wTSxN"
},
{
"id": "shipyard-telegram-27",
"description": "Nym Telegram Service Provider",
"address": "773y8iMVJiRk4dRbjQzkJVbrei4TwkePNE5WTEttt77d.3Mw47C9XZj3oAzk1iSqC5Y36tbBsjtaTtdgaHM3Zsdma@7fiZtNL1RACQTwGrKLBT9nbY77bfwZnX9rqcWqc53qgv",
"gateway": "7fiZtNL1RACQTwGrKLBT9nbY77bfwZnX9rqcWqc53qgv"
},
{
"id": "shipyard-telegram-28",
"description": "Nym Telegram Service Provider",
"address": "6jQJEorCu7YiP9HdDaMeHxcNhxeNmZ1kpd836GnqLZX.HsJqEmNTszGecsKqFB37i84nBXxqf4ETgrKmKmBvMGHC@FYnDMQzT49ZGM23gVqpTxfih14V6wuedNXirekmt37zE",
"gateway": "FYnDMQzT49ZGM23gVqpTxfih14V6wuedNXirekmt37zE"
},
{
"id": "shipyard-telegram-29",
"description": "Nym Telegram Service Provider",
"address": "BiCSyovpFMuSnTvF2TdiuZwrytXDrd9AH47ZMcCxscVC.G9YpdicA9BBNoVHDgjWjgt17wv5WYKWcbE3vPJJVpSJD@GAjhJcrd6f1edaqUkfWCff6zdHoqo756qYrc2TfPuCXJ",
"gateway":"GAjhJcrd6f1edaqUkfWCff6zdHoqo756qYrc2TfPuCXJ"
},
{
"id": "shipyard-telegram-30",
"description": "Nym Telegram Service Provider",
"address": "AQRRAs9oc8QWXAFBs44YhCKUny7AyLsfLy91pwmGgxuf.CWUKoKA1afSKyw5BnFJJg19UDgnaVATupsFhQpyTEBHJ@EBT8jTD8o4tKng2NXrrcrzVhJiBnKpT1bJy5CMeArt2w",
"gateway": "EBT8jTD8o4tKng2NXrrcrzVhJiBnKpT1bJy5CMeArt2w"
},
{
"id": "shipyard-telegram-31",
"description": "Nym Telegram Service Provider",
"address": "6YqjAZK3Pr1ngiBLcDkotboB5WiN6k6NPpbXvShH4pR5.9Ss6VW3Xbyi8LuxduNNwnXEv9njHCQ2PLSP1UK6tsoa5@42XCK9dMS9m5XJLzQd2dBuwimk6ndZnczhZaV5VPFkQD",
"gateway": "42XCK9dMS9m5XJLzQd2dBuwimk6ndZnczhZaV5VPFkQD"
},
{
"id": "shipyard-telegram-32",
"description": "Nym Telegram Service Provider",
"address": "EmYWLeybmj86Vzr62vxuZ3T15jwMNHggzK7sQwid96yp.GyaF9WprSr56cxUdGf5TpcUvAjb2VbAr8CVBrmBUYAaw@GL5wESoz4oSbpBaTki9qB9213FGUQXCiRjbzDkhWwoBC",
"gateway": "GL5wESoz4oSbpBaTki9qB9213FGUQXCiRjbzDkhWwoBC"
},
{
"id": "shipyard-telegram-33",
"description": "Nym Telegram Service Provider",
"address": "4PDb96cck5btTj6G7rsomqwHJsp4qu8uPvFCbwHfjFUx.C5dKbaoakH7egsZvAueRbwLFbmxnQaVMeSr6QTMpuBAA@58ceEFaLJh6zAo3cirzT1BDQm7D3L5acnQrxGH1D6TAY",
"gateway": "58ceEFaLJh6zAo3cirzT1BDQm7D3L5acnQrxGH1D6TAY"
},
{
"id": "shipyard-telegram-34",
"description": "Nym Telegram Service Provider",
"address": "BeZbeMf9vcpUf368qDd85dtLwXLj4Ee5bsHMB2fUD8uX.HELVbppkwU1jmzUAUrCEbHeJfVciSeo8VGAkbJSpwxsb@ADdHkiTfkpsSt31zVToWW9j3KikH24aLAAwDKtCYE5jY",
"gateway":"ADdHkiTfkpsSt31zVToWW9j3KikH24aLAAwDKtCYE5jY"
},
{
"id": "shipyard-telegram-35",
"description": "Nym Telegram Service Provider",
"address": "Bp4JRFyf7GB9L9J95AqMPnz9zbGmPnViA5fDXKeNraLJ.D6CTdcjJVxDmH2UQvzXuPWg9Se9xXYe76uDMypXvhzd7@6UjGEeQZK14C5K2kenycTkqt7qRjEHGLgaQx3FWySo3N",
"gateway": "6UjGEeQZK14C5K2kenycTkqt7qRjEHGLgaQx3FWySo3N"
},
{
"id": "shipyard-telegram-36",
"description": "Nym Telegram Service Provider",
"address": "91h7io6BGhVjbtC7dbbRcScyTJcTfnMsTQZ6aWMVsrWR.Epb4hANXCp8cGEY3wSgawux991ti9Z5Y1FHTMzAKFa6E@DF4TE7V8kJkttMvnoSVGnRFFRt6WYGxxiC2w1XyPQnHe",
"gateway": "DF4TE7V8kJkttMvnoSVGnRFFRt6WYGxxiC2w1XyPQnHe"
},
{
"id": "shipyard-telegram-37",
"description": "Nym Telegram Service Provider",
"address": "Cy2wuwKpWZ3iWzKU3ZWL1qqcVfJ5Cq85dU7UHVWwv2gc.9AhC9b2zVcLnXLGriMdxjpsWJpq6iAdCavDi63udbL89@678qVUJ21uwxZBhp3r56z7GRf6gMh3NYDHruTegPtgMf",
"gateway": "678qVUJ21uwxZBhp3r56z7GRf6gMh3NYDHruTegPtgMf"
},
{
"id": "shipyard-telegram-38",
"description": "Nym Telegram Service Provider",
"address": "GgUeUWW1NRSuquZYeZf3WkppE92EQUHJrFjNZtYU1jow.CSEjwrRi4f4uyw7N6L2LPKw2tB8spcMbFu99wHZzFZSj@77TSuVU8d1oXKbPzjec2xh4i3Wj5WwUyy9Lr36sm8gZm",
"gateway": "77TSuVU8d1oXKbPzjec2xh4i3Wj5WwUyy9Lr36sm8gZm"
},
{
"id": "shipyard-telegram-39",
"description": "Nym Telegram Service Provider",
"address": "kz4zWwSkYiQxqxXFPNcGUByTPQWXascD9RfYsmSxY7n.ajp3SjbBVBjrU9nXpSQXAXzbb6EHJJyhbY6cc1ajayx@BTZNB3bkkEePsT14GN8ofVtM1SJae4YLWjpBerrKYf",
"gateway": "HyS2UZtZX3kQXdazbdE99DhCjBXjbG61LC9QsmXwbxrU"
}
]
},
{
"id": "blockstream",
"description": "Blockstream Green",
"items": [
{
"id": "nym-blockstream",
"description": "Nym Blockstream Green Service Provider",
"address": "GiRjFWrMxt58pEMuusm4yT3RxoMD1MMPrR9M2N4VWRJP.3CNZBPq4vg7v7qozjGjdPMXcvDmkbWPCgbGCjQVw9n6Z@2xU4CBE6QiiYt6EyBXSALwxkNvM7gqJfjHXaMkjiFmYW",
"gateway": "2xU4CBE6QiiYt6EyBXSALwxkNvM7gqJfjHXaMkjiFmYW"
}
]
}
]`);
export const Loaded = () => (
<Box width={width}>
<ServiceSelector services={services} />
</Box>
);
export const ServiceAlreadySelected = () => (
<Box width={width}>
<ServiceSelector
services={services}
currentSp={services[2].items[2]}
onChange={(serviceProvider) => console.log('New service provider selected: ', serviceProvider)}
/>
</Box>
);
@@ -0,0 +1,179 @@
import React, { useEffect } from 'react';
import {
Box,
CircularProgress,
Divider,
FormControl,
InputLabel,
MenuItem,
Select,
Stack,
Typography,
} from '@mui/material';
import { Service, ServiceProvider, Services } from '../types/directory';
import { useTauriEvents } from '../utils';
import { ServiceProviderPopup } from './ServiceProviderPopup';
export const ServiceSelector: React.FC<{
onChange?: (serviceProvider?: ServiceProvider) => void;
services?: Services;
currentSp?: ServiceProvider;
}> = ({ services, currentSp, onChange }) => {
const [service, setService] = React.useState<Service | undefined>();
const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>(currentSp);
const [isPopupVisible, setPopupVisible] = React.useState(false);
const getService = () => {
if (!services || !currentSp) {
return undefined;
}
return services.find((s) =>
s.items.some(
({ id, address, gateway }) =>
id === currentSp.id && address === currentSp.address && gateway === currentSp.gateway,
),
);
};
useEffect(() => {
if (!service && currentSp) {
setServiceProvider(currentSp);
setService(getService());
}
}, [currentSp, services]);
/**
* Gets a random service provider from the currently selected service.
*
* If there is no service selected, or it does not have items, `undefined` is returned.
*/
const getRandomServiceProviderForService = (serviceToUse?: Service): ServiceProvider | undefined => {
if (!serviceToUse?.items.length) {
return undefined;
}
return serviceToUse.items[Math.floor(Math.random() * serviceToUse.items.length)];
};
const handleServiceSelected = React.useCallback(
(newService?: Service) => {
console.log(newService?.id, service?.id);
// if the user has chosen a new service, then pick a random service provider
if (newService?.id !== service?.id) {
const newServiceProvider = getRandomServiceProviderForService(newService);
setServiceProvider(newServiceProvider);
onChange?.(newServiceProvider);
setService(newService);
}
},
[service],
);
// clears the display and fire on change (to trigger upstream storage clearance)
const clearServiceProviderAndFireOnChange = () => {
setService(undefined);
setServiceProvider(undefined);
onChange?.(undefined);
};
// when the user clears local storage, reset the selector
useTauriEvents('help://clear-storage', () => {
clearServiceProviderAndFireOnChange();
});
const handleAdvancedSpChange = (newServiceProvider?: ServiceProvider, newService?: Service) => {
setPopupVisible(false);
setService(newService);
setServiceProvider(newServiceProvider);
onChange?.(newServiceProvider);
};
const handleNewService = (newServiceId?: string) => {
const newService = (services || []).find((s) => s.id === newServiceId);
setService(newService);
};
if (!services) {
return (
<Box display="flex" alignItems="center" justifyContent="center" sx={{ my: 3 }}>
<Typography fontSize={14} fontWeight={700} color={(theme) => theme.palette.common.white}>
<CircularProgress size={14} sx={{ mr: 1 }} color="inherit" />
Loading services...
</Typography>
</Box>
);
}
return (
<Box display="flex" alignItems="center" justifyContent="space-between" sx={{ my: 3 }}>
<FormControl fullWidth>
<InputLabel
id="service-label"
sx={{
color: 'grey.500',
'&.MuiInputLabel-shrink': {
marginTop: '16px',
marginLeft: '-2px',
},
'&.Mui-focused': {
color: 'grey.500',
},
}}
>
Select a service
</InputLabel>
<Select
labelId="service-label"
id="service-id"
variant="filled"
value={service?.id || ''}
onChange={(event) => handleNewService(event.target.value)}
fullWidth
MenuProps={{
PaperProps: {
sx: {
background: '#383C41',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
'&& .Mui-selected': {
backgroundColor: '#FFFFFF33',
},
'&& .Mui-focusVisible': {
backgroundColor: '#FFFFFF33',
},
},
},
}}
>
{serviceProvider && (
<Box px={2} pb={1} sx={{ opacity: 0.5 }}>
<Stack direction="column" fontSize="small">
<Typography fontSize="inherit">{serviceProvider.description}</Typography>
<Typography fontSize="inherit">
<code>{serviceProvider.id}</code>
</Typography>
</Stack>
</Box>
)}
{serviceProvider && <Divider />}
{services.map((item) => (
<MenuItem key={item.id} value={item.id} onClick={() => handleServiceSelected(item)}>
<Typography>{item.description}</Typography>
</MenuItem>
))}
<Divider />
<Typography ml={2} variant="overline" display="block" sx={{ opacity: 0.5 }}>
Advanced
</Typography>
<MenuItem onClick={() => setPopupVisible(true)}>Choose service provider</MenuItem>
<MenuItem onClick={clearServiceProviderAndFireOnChange}>Clear settings</MenuItem>
</Select>
</FormControl>
<ServiceProviderPopup
open={isPopupVisible}
services={services}
onBackdropClick={() => setPopupVisible(false)}
onServiceProviderChanged={handleAdvancedSpChange}
/>
</Box>
);
};
+16 -5
View File
@@ -32,7 +32,7 @@ export type TClientContext = {
setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void;
setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void;
setConnectedSince: (connectedSince: DateTime | undefined) => void;
setServiceProvider: (serviceProvider: ServiceProvider) => void;
setServiceProvider: (serviceProvider?: ServiceProvider) => void;
startConnecting: () => Promise<void>;
startDisconnecting: () => Promise<void>;
@@ -58,6 +58,14 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
getAppVersion();
}, []);
useEffect(() => {
// when mounting, load the connection state (needed for the Growth window, that checks the connection state)
(async () => {
const currentStatus: ConnectionStatusKind = await invoke('get_connection_status');
setConnectionStatus(currentStatus);
})();
}, []);
useEffect(() => {
const unlisten: UnlistenFn[] = [];
@@ -107,10 +115,12 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
} as any)();
};
const setServiceProvider = useCallback(async (newServiceProvider: ServiceProvider) => {
await invoke('set_gateway', { gateway: newServiceProvider.gateway });
await invoke('set_service_provider', { serviceProvider: newServiceProvider.address });
await setSpInStorage(newServiceProvider);
const setServiceProvider = useCallback(async (newServiceProvider?: ServiceProvider) => {
await invoke('set_gateway', { gateway: newServiceProvider?.gateway });
await invoke('set_service_provider', { serviceProvider: newServiceProvider?.address });
if (newServiceProvider) {
await setSpInStorage(newServiceProvider);
}
setRawServiceProvider(newServiceProvider);
}, []);
@@ -177,6 +187,7 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
handleShowHelp,
}),
[
appVersion,
mode,
appVersion,
error,
+7 -4
View File
@@ -3,8 +3,8 @@ import { ConnectionStatusKind } from 'src/types';
import { ClientContext, TClientContext } from '../main';
const mockValues: TClientContext = {
appVersion: 'v1.x.x',
mode: 'dark',
appVersion: '1.1.1',
connectionStatus: ConnectionStatusKind.disconnected,
services: [],
showHelp: false,
@@ -20,6 +20,9 @@ const mockValues: TClientContext = {
startDisconnecting: async () => {},
};
export const MockProvider = ({ children }: { children: React.ReactNode }) => {
return <ClientContext.Provider value={mockValues}>{children}</ClientContext.Provider>;
};
export const MockProvider: React.FC<{
children?: React.ReactNode;
connectionStatus?: ConnectionStatusKind;
}> = ({ connectionStatus = ConnectionStatusKind.disconnected, children }) => (
<ClientContext.Provider value={{ ...mockValues, connectionStatus }}>{children}</ClientContext.Provider>
);
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientContextProvider } from './context/main';
import { ErrorFallback } from './components/Error';
import { NymShipyardTheme } from './theme';
import { TestAndEarnPopup } from './components/Growth/TestAndEarnPopup';
import { TestAndEarnContextProvider } from './components/Growth/context/TestAndEarnContext';
const root = document.getElementById('root-growth');
ReactDOM.render(
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ClientContextProvider>
<TestAndEarnContextProvider>
<NymShipyardTheme mode="dark">
<TestAndEarnPopup />
</NymShipyardTheme>
</TestAndEarnContextProvider>
</ClientContextProvider>
</ErrorBoundary>,
root,
);
+8 -5
View File
@@ -6,17 +6,20 @@ import { ErrorFallback } from './components/Error';
import { NymMixnetTheme } from './theme';
import { App } from './App';
import { AppWindowFrame } from './components/AppWindowFrame';
import { TestAndEarnContextProvider } from './components/Growth/context/TestAndEarnContext';
const root = document.getElementById('root');
ReactDOM.render(
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ClientContextProvider>
<NymMixnetTheme mode="dark">
<AppWindowFrame>
<App />
</AppWindowFrame>
</NymMixnetTheme>
<TestAndEarnContextProvider>
<NymMixnetTheme mode="dark">
<AppWindowFrame>
<App />
</AppWindowFrame>
</NymMixnetTheme>
</TestAndEarnContextProvider>
</ClientContextProvider>
</ErrorBoundary>,
root,
@@ -9,6 +9,7 @@ import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ConnectionButton } from '../components/ConnectionButton';
import { IpAddressAndPort } from '../components/IpAddressAndPort';
import { ServiceProvider } from '../types/directory';
import { TestAndEarnButtonArea } from '../components/Growth/TestAndEarnButtonArea';
export const ConnectedLayout: React.FC<{
status: ConnectionStatusKind;
@@ -44,5 +45,6 @@ export const ConnectedLayout: React.FC<{
{/* <ConnectionStats stats={stats} /> */}
<ConnectionTimer connectedSince={connectedSince} />
<ConnectionButton status={status} busy={busy} onClick={onConnectClick} isError={isError} />
<TestAndEarnButtonArea />
</>
);
+9 -13
View File
@@ -1,15 +1,15 @@
import React from 'react';
import { Typography } from '@mui/material';
import { Box } from '@mui/material';
import { Box, Typography } from '@mui/material';
import { ConnectionStatus } from 'src/components/ConnectionStatus';
import { ConnectionTimer } from 'src/components/ConntectionTimer';
import { InfoModal } from 'src/components/InfoModal';
import { Error } from 'src/types/error';
import { ConnectionButton } from '../components/ConnectionButton';
import { ServiceProviderSelector } from '../components/ServiceProviderSelector';
import { ServiceSelector } from '../components/ServiceSelector';
import { useClientContext } from '../context/main';
import { ConnectionStatusKind } from '../types';
import { ServiceProvider, Services } from '../types/directory';
import { Services } from '../types/directory';
import { TestAndEarnButtonArea } from '../components/Growth/TestAndEarnButtonArea';
export const DefaultLayout: React.FC<{
error?: Error;
@@ -19,13 +19,8 @@ export const DefaultLayout: React.FC<{
isError?: boolean;
clearError: () => void;
onConnectClick?: (status: ConnectionStatusKind) => void;
onServiceProviderChange?: (serviceProvider: ServiceProvider) => void;
}> = ({ status, error, services, busy, isError, onConnectClick, onServiceProviderChange, clearError }) => {
const handleServiceProviderChange = (newServiceProvider: ServiceProvider) => {
onServiceProviderChange?.(newServiceProvider);
};
const { serviceProvider: currentSp } = useClientContext();
}> = ({ status, error, services, busy, isError, onConnectClick, clearError }) => {
const context = useClientContext();
return (
<Box pt={1}>
@@ -39,15 +34,16 @@ export const DefaultLayout: React.FC<{
This is experimental software. Do not rely on it for strong anonymity (yet).
</Typography>
</Box>
<ServiceProviderSelector services={services} onChange={handleServiceProviderChange} currentSp={currentSp} />
<ServiceSelector services={services} onChange={context.setServiceProvider} currentSp={context.serviceProvider} />
<ConnectionTimer />
<ConnectionButton
status={status}
disabled={currentSp === undefined}
disabled={context.serviceProvider === undefined}
busy={busy}
isError={isError}
onClick={onConnectClick}
/>
<TestAndEarnButtonArea />
</Box>
);
};
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { LogViewer } from './components/LogViewer';
import { ErrorFallback } from './components/ErrorFallback';
import { NymMixnetTheme } from './theme';
const Log = () => (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<NymMixnetTheme mode="dark">
<LogViewer />
</NymMixnetTheme>
</ErrorBoundary>
);
const root = document.getElementById('root-log');
ReactDOM.render(<Log />, root);
+42 -36
View File
@@ -8,12 +8,16 @@ import { ConnectionStatusKind } from '../types';
import { DefaultLayout } from '../layouts/DefaultLayout';
import { ConnectedLayout } from '../layouts/ConnectedLayout';
import { Services } from '../types/directory';
import { TestAndEarnButtonArea } from '../components/Growth/TestAndEarnButtonArea';
export default {
title: 'App/Flow',
component: AppWindowFrame,
} as ComponentMeta<typeof AppWindowFrame>;
const width = 240;
const height = 575;
export const Mock: ComponentStory<typeof AppWindowFrame> = () => {
const context = useClientContext();
const [busy, setBusy] = React.useState<boolean>();
@@ -67,45 +71,47 @@ export const Mock: ComponentStory<typeof AppWindowFrame> = () => {
context.connectionStatus === ConnectionStatusKind.connecting
) {
return (
<AppWindowFrame>
<DefaultLayout
status={context.connectionStatus}
busy={busy}
onConnectClick={handleConnectClick}
services={services}
clearError={() => {}}
/>
</AppWindowFrame>
<Box width={width} height={height}>
<AppWindowFrame>
<DefaultLayout
status={context.connectionStatus}
busy={busy}
onConnectClick={handleConnectClick}
services={services}
clearError={() => {}}
/>
</AppWindowFrame>
</Box>
);
}
return (
<AppWindowFrame>
<ConnectedLayout
showInfoModal={false}
handleCloseInfoModal={() => {
return undefined;
}}
status={context.connectionStatus}
busy={busy}
onConnectClick={handleConnectClick}
ipAddress="127.0.0.1"
port={1080}
connectedSince={context.connectedSince}
serviceProvider={services[0].items[0]}
stats={[
{
label: 'in:',
totalBytes: 1024,
rateBytesPerSecond: 1024 * 1024 * 1024 + 10,
},
{
label: 'out:',
totalBytes: 1024 * 1024 * 1024 * 1024 * 20,
rateBytesPerSecond: 1024 * 1024 + 10,
},
]}
/>
</AppWindowFrame>
<Box width={width} height={height}>
<AppWindowFrame>
<ConnectedLayout
showInfoModal={false}
handleCloseInfoModal={() => undefined}
status={context.connectionStatus}
busy={busy}
onConnectClick={handleConnectClick}
ipAddress="127.0.0.1"
port={1080}
connectedSince={context.connectedSince}
serviceProvider={services[0].items[0]}
stats={[
{
label: 'in:',
totalBytes: 1024,
rateBytesPerSecond: 1024 * 1024 * 1024 + 10,
},
{
label: 'out:',
totalBytes: 1024 * 1024 * 1024 * 1024 * 20,
rateBytesPerSecond: 1024 * 1024 + 10,
},
]}
/>
</AppWindowFrame>
</Box>
);
};
+11 -2
View File
@@ -1,8 +1,7 @@
import React, { useContext } from 'react';
import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import { getDesignTokens } from './theme';
import { ClientContext } from '../context/main';
import '../../../assets/fonts/non-variable/fonts.css';
/**
@@ -17,3 +16,13 @@ export const NymMixnetTheme: React.FC<{ mode: 'light' | 'dark' }> = ({ children,
</ThemeProvider>
);
};
export const NymShipyardTheme: React.FC<{ mode?: 'light' | 'dark' }> = ({ children, mode = 'dark' }) => {
const theme = React.useMemo(() => createTheme(getDesignTokens(mode, true)), [mode]);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
);
};
+4
View File
@@ -38,6 +38,7 @@ declare module '@mui/material/styles' {
light: string;
dark: string;
};
shipyard: string;
}
interface NymPaletteVariant {
@@ -52,6 +53,9 @@ declare module '@mui/material/styles' {
topNav: {
background: string;
};
shipyard: {
main: string;
};
}
/**
+57 -3
View File
@@ -30,6 +30,7 @@ const nymPalette: NymPalette = {
light: '#F2F2F2',
dark: '#1D2125',
},
shipyard: '#817FFA',
};
const darkMode: NymPaletteVariant = {
@@ -44,6 +45,9 @@ const darkMode: NymPaletteVariant = {
topNav: {
background: '#111826',
},
shipyard: {
main: '#817FFA',
},
};
const lightMode: NymPaletteVariant = {
@@ -58,6 +62,9 @@ const lightMode: NymPaletteVariant = {
topNav: {
background: '#111826',
},
shipyard: {
main: '#817FFA',
},
};
/**
@@ -103,6 +110,35 @@ const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({
},
});
/**
* Map a Nym palette variant onto the MUI palette for Shipyard
*/
const variantShipyardToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({
text: {
primary: variant.text.main,
},
primary: {
main: nymPalette.shipyard,
contrastText: '#fff',
},
secondary: {
main: variant.mode === 'dark' ? nymPalette.background.light : nymPalette.background.dark,
},
success: {
main: nymPalette.success,
},
info: {
main: nymPalette.info,
},
warning: {
main: nymPalette.warning,
},
background: {
default: variant.background.main,
paper: variant.background.paper,
},
});
/**
* Returns the Network Explorer palette for light mode.
*/
@@ -125,6 +161,17 @@ const createDarkModePalette = (): PaletteOptions => ({
...variantToMUIPalette(darkMode),
});
/**
* Returns the Shipyard palette for dark mode.
*/
const createShipyardDarkModePalette = (): PaletteOptions => ({
nym: {
...nymPalette,
...nymMixnetPalette(darkMode),
},
...variantShipyardToMUIPalette(darkMode),
});
/**
* IMPORANT: if you need to get the default MUI theme, use the following
*
@@ -157,12 +204,19 @@ const createDarkModePalette = (): PaletteOptions => ({
*
* @param mode The theme mode: 'light' or 'dark'
*/
export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
// first, create the palette from user's choice of light or dark mode
export const getDesignTokens = (mode: PaletteMode, isShipyard: boolean = false): ThemeOptions => {
let overrides;
if (isShipyard) {
overrides = createShipyardDarkModePalette();
} else {
overrides = mode === 'light' ? createLightModePalette() : createDarkModePalette();
}
// create the palette from user's choice of light or dark mode
const { palette } = createTheme({
palette: {
mode,
...(mode === 'light' ? createLightModePalette() : createDarkModePalette()),
...overrides,
},
});
+4
View File
@@ -0,0 +1,4 @@
declare module '*.webp' {
const value: any;
export default value;
}
+9
View File
@@ -0,0 +1,9 @@
declare module '*.yml' {
const content: { [key: string]: any };
export default content;
}
declare module '*.yaml' {
const content: { [key: string]: any };
export default content;
}
+19 -1
View File
@@ -1 +1,19 @@
// TODO
import { useEffect, useRef } from 'react';
import { EventName, listen, UnlistenFn, EventCallback } from '@tauri-apps/api/event';
export const useTauriEvents = <T>(event: EventName, handler: EventCallback<T>) => {
const unlisten = useRef<UnlistenFn>();
// list for events to clear local storage
useEffect(() => {
listen(event, handler).then((fn) => {
unlisten.current = fn;
});
return () => {
if (unlisten.current) {
unlisten.current();
}
};
}, []);
};
+47 -6
View File
@@ -2,6 +2,12 @@ const path = require('path');
const { mergeWithRules } = require('webpack-merge');
const { webpackCommon } = require('@nymproject/webpack');
const entry = {
app: path.resolve(__dirname, 'src/index.tsx'),
growth: path.resolve(__dirname, 'src/growth.tsx'),
log: path.resolve(__dirname, 'src/log.tsx'),
};
module.exports = mergeWithRules({
module: {
rules: {
@@ -9,10 +15,45 @@ module.exports = mergeWithRules({
use: 'replace',
},
},
})(webpackCommon(__dirname, 'public/index.html'), {
entry: path.resolve(__dirname, 'src/index.tsx'),
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
})(
webpackCommon(__dirname, [
{ filename: 'index.html', chunks: ['app'], template: path.resolve(__dirname, 'public/index.html') },
{ filename: 'log.html', chunks: ['log'], template: path.resolve(__dirname, 'public/log.html') },
{ filename: 'growth.html', chunks: ['growth'], template: path.resolve(__dirname, 'public/growth.html') },
]),
{
module: {
rules: [
{
test: /\.mdx?$/,
use: [
{
loader: '@mdx-js/loader',
/** @type {import('@mdx-js/loader').Options} */
options: {},
},
],
},
{
test: /\.ya?ml$/,
type: 'asset/resource',
use: [
{
loader: 'yaml-loader',
options: {
asJSON: true,
},
},
],
},
],
},
entry,
output: {
clean: true,
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
publicPath: '/',
},
},
});
);
+7 -1
View File
@@ -2,10 +2,16 @@ const path = require('path');
const { default: merge } = require('webpack-merge');
const common = require('./webpack.common');
const entry = {
app: path.resolve(__dirname, 'src/index.tsx'),
growth: path.resolve(__dirname, 'src/growth.tsx'),
log: path.resolve(__dirname, 'src/log.tsx'),
};
module.exports = merge(common, {
mode: 'production',
node: {
__dirname: false,
},
entry: path.resolve(__dirname, './src/index'),
entry,
});
+158 -237
View File
@@ -14,12 +14,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aead"
version = "0.4.3"
@@ -153,20 +147,18 @@ dependencies = [
[[package]]
name = "attohttpc"
version = "0.19.1"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "262c3f7f5d61249d8c00e5546e2685cd15ebeeb1bc0f3cc5449350a1cb07319e"
checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7"
dependencies = [
"flate2",
"http",
"log",
"native-tls",
"openssl",
"serde",
"serde_json",
"serde_urlencoded",
"url",
"wildmatch",
]
[[package]]
@@ -438,12 +430,11 @@ dependencies = [
[[package]]
name = "cargo_toml"
version = "0.11.4"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e270ef0cd868745878982f7ce470aa898d0d4bb248af67f0cf66f54617913ef"
checksum = "aa0e3586af56b3bfa51fca452bd56e8dbbbd5d8d81cbf0b7e4e35b695b537eb8"
dependencies = [
"serde",
"serde_derive",
"toml",
]
@@ -1076,16 +1067,6 @@ dependencies = [
"syn",
]
[[package]]
name = "deflate"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
dependencies = [
"adler32",
"byteorder",
]
[[package]]
name = "der"
version = "0.5.1"
@@ -1226,6 +1207,12 @@ dependencies = [
"dtoa",
]
[[package]]
name = "dunce"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c"
[[package]]
name = "dyn-clone"
version = "1.0.4"
@@ -1304,19 +1291,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "embed-resource"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936c1354206a875581696369aef920e12396e93bbd251c43a7a3f3fa85023a7d"
dependencies = [
"cc",
"rustc_version 0.4.0",
"toml",
"vswhom",
"winreg 0.10.1",
]
[[package]]
name = "embed_plist"
version = "1.2.2"
@@ -1325,9 +1299,9 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]]
name = "encoding_rs"
version = "0.8.30"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
dependencies = [
"cfg-if",
]
@@ -1569,21 +1543,6 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
[[package]]
name = "futures-lite"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
name = "futures-macro"
version = "0.3.21"
@@ -2234,12 +2193,12 @@ dependencies = [
[[package]]
name = "ico"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a4b3331534254a9b64095ae60d3dc2a8225a7a70229cd5888be127cdc1f6804"
checksum = "031530fe562d8c8d71c0635013d6d155bbfe8ba0aa4b4d2d24ce8af6b71047bd"
dependencies = [
"byteorder",
"png 0.11.0",
"png",
]
[[package]]
@@ -2316,15 +2275,6 @@ dependencies = [
"cfb",
]
[[package]]
name = "inflate"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4"
dependencies = [
"adler32",
]
[[package]]
name = "instant"
version = "0.1.12"
@@ -2386,23 +2336,9 @@ dependencies = [
[[package]]
name = "jni"
version = "0.18.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf"
dependencies = [
"cesu8",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
]
[[package]]
name = "jni"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
dependencies = [
"cesu8",
"combine",
@@ -2803,17 +2739,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
dependencies = [
"autocfg 1.1.0",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
@@ -3061,9 +2986,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.14.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "opaque-debug"
@@ -3160,12 +3085,6 @@ dependencies = [
"system-deps 6.0.2",
]
[[package]]
name = "parking"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
[[package]]
name = "parking_lot"
version = "0.11.2"
@@ -3304,9 +3223,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
@@ -3512,18 +3431,6 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "png"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925"
dependencies = [
"bitflags",
"deflate",
"inflate",
"num-iter",
]
[[package]]
name = "png"
version = "0.17.6"
@@ -3612,11 +3519,11 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.36"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-xid",
"unicode-ident",
]
[[package]]
@@ -3868,9 +3775,9 @@ dependencies = [
[[package]]
name = "raw-window-handle"
version = "0.4.3"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41"
checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a"
dependencies = [
"cty",
]
@@ -3905,9 +3812,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [
"aho-corasick",
"memchr",
@@ -3971,7 +3878,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.7.0",
"winreg",
]
[[package]]
@@ -3987,13 +3894,12 @@ dependencies = [
[[package]]
name = "rfd"
version = "0.9.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f121348fd3b9035ed11be1f028e8944263c30641f8c5deacf57a4320782fb402"
checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea"
dependencies = [
"block",
"dispatch",
"embed-resource",
"glib-sys",
"gobject-sys",
"gtk-sys",
@@ -4007,7 +3913,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows",
"windows 0.37.0",
]
[[package]]
@@ -4248,9 +4154,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.136"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
checksum = "e326c9ec8042f1b5da33252c8a37e9ffbd2c9bef0155215b6e6c80c790e05f91"
dependencies = [
"serde_derive",
]
@@ -4275,9 +4181,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.136"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e"
dependencies = [
"proc-macro2",
"quote",
@@ -4644,13 +4550,13 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.86"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
"unicode-ident",
]
[[package]]
@@ -4693,9 +4599,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.12.2"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6fd7725dc1e593e9ecabd9fe49c112a204c8c8694db4182e78b2a5af490b1ae"
checksum = "a1fa15735311b4816d030ff54da58560b047daca0970e1031aed5502e84231a8"
dependencies = [
"bitflags",
"cairo-rs",
@@ -4715,7 +4621,7 @@ dependencies = [
"gtk",
"image",
"instant",
"jni 0.19.0",
"jni",
"lazy_static",
"libc",
"log",
@@ -4726,13 +4632,13 @@ dependencies = [
"once_cell",
"parking_lot 0.12.1",
"paste",
"png 0.17.6",
"png",
"raw-window-handle",
"scopeguard",
"serde",
"unicode-segmentation",
"uuid 1.1.2",
"windows",
"uuid 1.2.2",
"windows 0.39.0",
"windows-implement",
"x11-dl",
]
@@ -4756,9 +4662,9 @@ dependencies = [
[[package]]
name = "tauri"
version = "1.0.5"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1a56a8b125069c2682bd31610109b4436c050c74447bee1078217a0325c1add"
checksum = "d8ea1d785ab2164373703817bff144c4610a69ad3f659becaca0e1ea004b98d8"
dependencies = [
"anyhow",
"attohttpc",
@@ -4766,9 +4672,9 @@ dependencies = [
"cocoa",
"dirs-next",
"embed_plist",
"encoding_rs",
"flate2",
"futures",
"futures-lite",
"futures-util",
"glib",
"glob",
"gtk",
@@ -4800,18 +4706,18 @@ dependencies = [
"time 0.3.7",
"tokio",
"url",
"uuid 1.1.2",
"uuid 1.2.2",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.39.0",
"zip",
]
[[package]]
name = "tauri-build"
version = "1.0.4"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acafb1c515c5d14234a294461bd43c723639a84891a45f6a250fd3441ad2e8ed"
checksum = "8807c85d656b2b93927c19fe5a5f1f1f348f96c2de8b90763b3c2d561511f9b4"
dependencies = [
"anyhow",
"cargo_toml",
@@ -4825,16 +4731,16 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "1.0.4"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16d62a3c8790d6cba686cea6e3f7f569d12c662c3274c2d165a4fd33e3871b72"
checksum = "14388d484b6b1b5dc0f6a7d6cc6433b3b230bec85eaa576adcdf3f9fafa49251"
dependencies = [
"base64",
"brotli",
"ico",
"json-patch",
"plist",
"png 0.17.6",
"png",
"proc-macro2",
"quote",
"regex",
@@ -4845,15 +4751,15 @@ dependencies = [
"tauri-utils",
"thiserror",
"time 0.3.7",
"uuid 1.1.2",
"uuid 1.2.2",
"walkdir",
]
[[package]]
name = "tauri-macros"
version = "1.0.4"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7296fa17996629f43081e1c66d554703900187ed900c5bf46f97f0bcfb069278"
checksum = "069319e5ecbe653a799b94b0690d9f9bf5d00f7b1d3989aa331c524d4e354075"
dependencies = [
"heck 0.4.0",
"proc-macro2",
@@ -4865,29 +4771,29 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.10.2"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4cff3b4d9469727fa2107c4b3d2eda110df1ba45103fb420178e536362fae4"
checksum = "c507d954d08ac8705d235bc70ec6975b9054fb95ff7823af72dbb04186596f3b"
dependencies = [
"gtk",
"http",
"http-range",
"infer",
"rand 0.8.5",
"raw-window-handle",
"serde",
"serde_json",
"tauri-utils",
"thiserror",
"uuid 1.1.2",
"uuid 1.2.2",
"webview2-com",
"windows",
"windows 0.39.0",
]
[[package]]
name = "tauri-runtime-wry"
version = "0.10.2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fa8c4edaf01d8b556e7172c844b1b4dd3399adcd1a606bd520fc3e65f698546"
checksum = "36b1c5764a41a13176a4599b5b7bd0881bea7d94dfe45e1e755f789b98317e30"
dependencies = [
"cocoa",
"gtk",
@@ -4896,24 +4802,25 @@ dependencies = [
"raw-window-handle",
"tauri-runtime",
"tauri-utils",
"uuid 1.1.2",
"uuid 1.2.2",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.39.0",
"wry",
]
[[package]]
name = "tauri-utils"
version = "1.0.3"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ff4b68d9faeb57c9c727bf58c9c9768d2b67d8e84e62ce6146e7859a2e9c6b"
checksum = "5abbc109a6eb45127956ffcc26ef0e875d160150ac16cfa45d26a6b2871686f1"
dependencies = [
"brotli",
"ctor",
"glob",
"heck 0.4.0",
"html5ever",
"infer",
"json-patch",
"kuchiki",
"memchr",
@@ -4927,7 +4834,7 @@ dependencies = [
"thiserror",
"url",
"walkdir",
"windows",
"windows 0.39.0",
]
[[package]]
@@ -5340,6 +5247,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]]
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-normalization"
version = "0.1.9"
@@ -5351,9 +5264,9 @@ dependencies = [
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
[[package]]
name = "unicode-xid"
@@ -5379,13 +5292,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
version = "2.2.2"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
checksum = "22fe195a4f217c25b25cb5058ced57059824a678474874038dc88d211bf508d3"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
"serde",
]
@@ -5404,9 +5316,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "uuid"
version = "1.1.2"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
dependencies = [
"getrandom 0.2.5",
]
@@ -5521,32 +5433,6 @@ dependencies = [
"serde",
]
[[package]]
name = "vswhom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
dependencies = [
"libc",
"vswhom-sys",
]
[[package]]
name = "vswhom-sys"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
version = "2.3.2"
@@ -5664,9 +5550,9 @@ dependencies = [
[[package]]
name = "webkit2gtk"
version = "0.18.0"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29952969fb5e10fe834a52eb29ad0814ccdfd8387159b0933edf1344a1c9cdcc"
checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370"
dependencies = [
"bitflags",
"cairo-rs",
@@ -5730,13 +5616,13 @@ dependencies = [
[[package]]
name = "webview2-com"
version = "0.16.0"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a489a9420acabb3c2ed0434b6f71f6b56b9485ec32665a28dec1ee186d716e0f"
checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows 0.39.0",
"windows-implement",
]
@@ -5753,24 +5639,19 @@ dependencies = [
[[package]]
name = "webview2-com-sys"
version = "0.16.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0258c53ee9adc0a4f8ba1c8c317588f7a58c7048a55b621d469ba75ab3709ca1"
checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7"
dependencies = [
"regex",
"serde",
"serde_json",
"thiserror",
"windows",
"windows 0.39.0",
"windows-bindgen",
"windows-metadata",
]
[[package]]
name = "wildmatch"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -5808,7 +5689,6 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647"
dependencies = [
"windows-implement",
"windows_aarch64_msvc 0.37.0",
"windows_i686_gnu 0.37.0",
"windows_i686_msvc 0.37.0",
@@ -5817,10 +5697,24 @@ dependencies = [
]
[[package]]
name = "windows-bindgen"
version = "0.37.0"
name = "windows"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bed7be31ade0af08fec9b5343e9edcc005d22b1f11859b8a59b24797f5858e8"
checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a"
dependencies = [
"windows-implement",
"windows_aarch64_msvc 0.39.0",
"windows_i686_gnu 0.39.0",
"windows_i686_msvc 0.39.0",
"windows_x86_64_gnu 0.39.0",
"windows_x86_64_msvc 0.39.0",
]
[[package]]
name = "windows-bindgen"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41"
dependencies = [
"windows-metadata",
"windows-tokens",
@@ -5828,9 +5722,9 @@ dependencies = [
[[package]]
name = "windows-implement"
version = "0.37.0"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a1062e555f7d9d66fd1130ed4f7c6ec41a47529ee0850cd0e926d95b26bb14"
checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7"
dependencies = [
"syn",
"windows-tokens",
@@ -5838,9 +5732,9 @@ dependencies = [
[[package]]
name = "windows-metadata"
version = "0.37.0"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f33f2b90a6664e369c41ab5ff262d06f048fc9685d9bf8a0e99a47750bb0463"
checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278"
[[package]]
name = "windows-sys"
@@ -5857,9 +5751,9 @@ dependencies = [
[[package]]
name = "windows-tokens"
version = "0.37.0"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169"
checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597"
[[package]]
name = "windows_aarch64_msvc"
@@ -5873,6 +5767,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a"
[[package]]
name = "windows_aarch64_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
@@ -5885,6 +5785,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1"
[[package]]
name = "windows_i686_gnu"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
@@ -5897,6 +5803,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c"
[[package]]
name = "windows_i686_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
@@ -5909,6 +5821,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
@@ -5921,6 +5839,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
[[package]]
name = "winreg"
version = "0.7.0"
@@ -5930,15 +5854,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "winres"
version = "0.1.12"
@@ -5950,19 +5865,23 @@ dependencies = [
[[package]]
name = "wry"
version = "0.19.0"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce19dddbd3ce01dc8f14eb6d4c8f914123bf8379aaa838f6da4f981ff7104a3f"
checksum = "4c1ad8e2424f554cc5bdebe8aa374ef5b433feff817aebabca0389961fc7ef98"
dependencies = [
"base64",
"block",
"cocoa",
"core-graphics",
"crossbeam-channel",
"dunce",
"gdk",
"gio",
"glib",
"gtk",
"html5ever",
"http",
"jni 0.18.0",
"kuchiki",
"libc",
"log",
"objc",
@@ -5970,13 +5889,15 @@ dependencies = [
"once_cell",
"serde",
"serde_json",
"sha2 0.10.2",
"soup2",
"tao",
"thiserror",
"url",
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows 0.39.0",
"windows-implement",
]
@@ -6001,9 +5922,9 @@ dependencies = [
[[package]]
name = "x11-dl"
version = "2.19.1"
version = "2.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59"
checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0"
dependencies = [
"lazy_static",
"libc",
+1 -1
View File
@@ -32,7 +32,7 @@
"@nymproject/react": "^1.0.0",
"@nymproject/types": "^1.0.0",
"@storybook/react": "^6.5.8",
"@tauri-apps/api": "^1.0.2",
"@tauri-apps/api": "^1.2.0",
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
"big.js": "^6.2.1",
"bs58": "^4.0.1",
+4 -4
View File
@@ -13,10 +13,10 @@ rust-version = "1.58"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "=1.0.4", features = [] }
tauri-build = { version = "=1.2.1", features = [] }
tauri-codegen = "=1.0.4"
tauri-macros = "=1.0.4"
tauri-codegen = "=1.2.1"
tauri-macros = "=1.2.1"
[dependencies]
bip39 = "1.0"
@@ -38,7 +38,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
strum = { version = "0.23", features = ["derive"] }
tauri = { version = "=1.0.5", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] }
tauri = { version = "=1.2.2", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] }
tendermint-rpc = "0.23.0"
thiserror = "1.0"
tokio = { version = "1.10", features = ["full"] }
+2 -1
View File
@@ -28,7 +28,8 @@
"react-dom": "17",
"@nymproject/nym-validator-client": "^0.18.0",
"@nymproject/types": "1",
"base58": "4"
"base58": "4",
"bech32": "^1.1.4"
},
"dependencies": {
"flat": "^5.0.2",
@@ -0,0 +1,42 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { Box } from '@mui/material';
import { WalletAddressFormField } from './WalletAddressFormField';
export default {
title: 'Accounts/Wallet Address',
component: WalletAddressFormField,
} as ComponentMeta<typeof WalletAddressFormField>;
export const Empty = () => <WalletAddressFormField />;
export const ErrorValue = () => <WalletAddressFormField initialValue="this is a bad value" />;
export const ValidValue = () => <WalletAddressFormField initialValue="n1xr4w0kddak8d8zlfmu8sl6dk2r4p9uhhzzlaec" />;
export const ReadOnlyValidValue = () => (
<WalletAddressFormField readOnly initialValue="n1xr4w0kddak8d8zlfmu8sl6dk2r4p9uhhzzlaec" />
);
export const ReadOnlyErrorValue = () => <WalletAddressFormField readOnly initialValue="this is a bad value" />;
export const WithLabel = () => (
<Box p={2}>
<WalletAddressFormField
initialValue="n1xr4w0kddak8d8zlfmu8sl6dk2r4p9uhhzzlaec"
textFieldProps={{ label: 'Identity Key' }}
/>
</Box>
);
export const WithPlaceholder = () => (
<WalletAddressFormField textFieldProps={{ placeholder: 'Please enter an wallet address' }} />
);
export const FullWidth = () => (
<WalletAddressFormField fullWidth initialValue="n1xr4w0kddak8d8zlfmu8sl6dk2r4p9uhhzzlaec" />
);
export const HideValidTick = () => (
<WalletAddressFormField showTickOnValid={false} fullWidth initialValue="n1xr4w0kddak8d8zlfmu8sl6dk2r4p9uhhzzlaec" />
);
@@ -0,0 +1,105 @@
import * as React from 'react';
import { ChangeEvent } from 'react';
import { InputAdornment, TextField } from '@mui/material';
import { TextFieldProps } from '@mui/material/TextField/TextField';
import { validateWalletAddress } from '@nymproject/types';
import DoneIcon from '@mui/icons-material/Done';
import { SxProps } from '@mui/system';
export const WalletAddressFormField: React.FC<{
showTickOnValid?: boolean;
fullWidth?: boolean;
required?: boolean;
readOnly?: boolean;
initialValue?: string;
placeholder?: string;
label?: string;
helperText?: string;
onChanged?: (newValue: string) => void;
onValidate?: (isValid: boolean, error?: string) => void;
textFieldProps?: TextFieldProps;
errorText?: string;
sx?: SxProps;
}> = ({
required,
fullWidth,
placeholder,
label,
readOnly,
initialValue,
errorText,
sx,
onChanged,
onValidate,
textFieldProps,
showTickOnValid = true,
}) => {
const [value, setValue] = React.useState<string | undefined>(initialValue);
const [validationError, setValidationError] = React.useState<string | undefined>();
const doValidation = (newValue?: string): boolean => {
if (validateWalletAddress(newValue)) {
setValidationError(undefined);
if (onValidate) {
onValidate(true);
}
return true;
}
const newValidationError = 'Account address is not valid';
setValidationError(newValidationError);
if (onValidate) {
onValidate(false, newValidationError);
}
return false;
};
React.useEffect(() => {
// validate initial value (only if set), so that validation error UI hints are set without the user typing
if (initialValue) {
doValidation(initialValue);
}
if (errorText) {
setValidationError(errorText);
}
}, [initialValue, errorText]);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
if (doValidation(newValue)) {
setValue(newValue);
}
if (onChanged) {
onChanged(newValue);
}
};
return (
<TextField
fullWidth={fullWidth}
InputProps={{
readOnly,
required,
endAdornment: showTickOnValid && value && validationError === undefined && (
<InputAdornment position="end">
<DoneIcon color="success" />
</InputAdornment>
),
}}
placeholder={placeholder}
label={label}
sx={sx}
{...textFieldProps}
aria-readonly={readOnly}
error={validationError !== undefined}
helperText={validationError}
defaultValue={initialValue}
onChange={handleChange}
InputLabelProps={{ shrink: true }}
/>
);
};
+2 -1
View File
@@ -6,7 +6,8 @@
"types": "dist/index.d.ts",
"peerDependencies": {
"@cosmjs/math": "^0.27.1",
"bs58": "4"
"bs58": "4",
"bech32": "^1.1.4"
},
"devDependencies": {
"@nymproject/eslint-config-react-typescript": "^1.0.0",
@@ -0,0 +1,28 @@
import { decode } from 'bech32';
export const validateWalletAddress = (address?: string, prefix: string = 'n', logErrorToConsole = false): boolean => {
if (!address) {
return false;
}
if (address.length < 1) {
return false;
}
if (!address.startsWith(prefix)) {
return false;
}
try {
// try to decode the address
decode(address);
} catch (e) {
if (logErrorToConsole) {
// eslint-disable-next-line no-console
console.error('Failed to decode address', e);
}
return false;
}
return true;
};
+1
View File
@@ -1,3 +1,4 @@
export * from './coin';
export * from './keys';
export * from './decimal';
export * from './accounts';
+1 -1
View File
@@ -35,7 +35,7 @@ module.exports = (baseDir, htmlPath) => ({
use: ['@svgr/webpack'],
},
{
test: /\.(png|jpe?g|gif|md)$/i,
test: /\.(png|jpe?g|gif|md|webp)$/i,
// More information here https://webpack.js.org/guides/asset-modules/
type: 'asset',
},
+4892 -124
View File
File diff suppressed because it is too large Load Diff