build(nc-android): prepare for apk release (#2943)

* chore(nc-android): prepare for production build

* refactor(nc-android): remove dead code

* feat(nc-android): update native color theme

* feat(nc-android): update native color theme

* build(nc-android): fix rfd version issue

* build(nc-android): fix dist dir no such file error

* fix(nc-android): post rebase changes
This commit is contained in:
Pierre Dommerc
2023-02-02 15:18:58 +01:00
committed by Jon Häggblad
parent 3bc7f281b4
commit eeba17a01f
49 changed files with 215 additions and 2573 deletions
@@ -1,11 +0,0 @@
<!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>
-11
View File
@@ -1,11 +0,0 @@
<!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>
+8 -2
View File
@@ -41,8 +41,8 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
tap = "1.0.1"
tauri = { git = "https://github.com/tauri-apps/tauri", branch = "next", features = ["clipboard-write-text", "native-tls-vendored", "notification-all", "shell-open", "system-tray", "window-close", "window-minimize", "window-start-dragging"] }
# tauri = { version = "2.0.0-alpha.0", features = ["clipboard-write-text", "native-tls-vendored", "notification-all", "shell-open", "system-tray", "window-close", "window-minimize", "window-start-dragging"] }
# TODO swithing to `rfd101` temporarily, untill https://github.com/tauri-apps/tauri/pull/6174 is merged
tauri = { git = "https://github.com/tauri-apps/tauri", branch = "rfd101", features = ["clipboard-write-text", "native-tls-vendored", "notification-all", "shell-open", "system-tray", "window-close", "window-minimize", "window-start-dragging"] }
tendermint-rpc = "0.23.0"
thiserror = "1.0"
tokio = { version = "1.24.1", features = ["sync", "time"] }
@@ -64,3 +64,9 @@ tempfile = "3.3.0"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
# [profile.dev]
# strip = true
# opt-level = "s"
# lto = true
@@ -1,111 +1,112 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rustPlugin")
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rustPlugin")
}
android {
compileSdk = 33
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "net.nymtech.nym_connect_android"
minSdk = 24
targetSdk = 33
versionCode = 1
versionName = "1.0"
compileSdk = 33
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "net.nymtech.nym_connect_android"
minSdk = 24
targetSdk = 33
versionCode = 1
versionName = "1.0"
}
sourceSets.getByName("main") {
// Vulkan validation layers
val ndkHome = System.getenv("NDK_HOME")
jniLibs.srcDir("${ndkHome}/sources/third_party/vulkan/src/build-android/jniLibs")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packagingOptions {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
sourceSets.getByName("main") {
// Vulkan validation layers
val ndkHome = System.getenv("NDK_HOME")
jniLibs.srcDir("${ndkHome}/sources/third_party/vulkan/src/build-android/jniLibs")
getByName("release") {
isMinifyEnabled = false
// proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packagingOptions {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
}
flavorDimensions.add("abi")
productFlavors {
create("universal") {
val abiList = findProperty("abiList") as? String
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
dimension = "abi"
ndk {
abiFilters += abiList?.split(",")?.map { it.trim() } ?: listOf(
"arm64-v8a", "armeabi-v7a", "x86", "x86_64",
)
}
}
flavorDimensions.add("abi")
productFlavors {
create("universal") {
val abiList = findProperty("abiList") as? String
dimension = "abi"
ndk {
abiFilters += abiList?.split(",")?.map { it.trim() } ?: listOf( "arm64-v8a", "armeabi-v7a", "x86", "x86_64",
)
}
}
create("arm64") {
dimension = "abi"
ndk {
abiFilters += listOf("arm64-v8a")
}
}
create("arm") {
dimension = "abi"
ndk {
abiFilters += listOf("armeabi-v7a")
}
}
create("x86") {
dimension = "abi"
ndk {
abiFilters += listOf("x86")
}
}
create("x86_64") {
dimension = "abi"
ndk {
abiFilters += listOf("x86_64")
}
}
create("arm64") {
dimension = "abi"
ndk {
abiFilters += listOf("arm64-v8a")
}
}
assetPacks += mutableSetOf()
namespace = "net.nymtech.nym_connect_android"
create("arm") {
dimension = "abi"
ndk {
abiFilters += listOf("armeabi-v7a")
}
}
create("x86") {
dimension = "abi"
ndk {
abiFilters += listOf("x86")
}
}
create("x86_64") {
dimension = "abi"
ndk {
abiFilters += listOf("x86_64")
}
}
}
assetPacks += mutableSetOf()
namespace = "net.nymtech.nym_connect_android"
}
rust {
rootDirRel = "../../../../"
targets = listOf("aarch64", "armv7", "i686", "x86_64")
arches = listOf("arm64", "arm", "x86", "x86_64")
rootDirRel = "../../../../"
targets = listOf("aarch64", "armv7", "i686", "x86_64")
arches = listOf("arm64", "arm", "x86", "x86_64")
}
dependencies {
implementation("androidx.webkit:webkit:1.5.0")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
implementation("androidx.webkit:webkit:1.5.0")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
afterEvaluate {
android.applicationVariants.all {
tasks["mergeUniversalReleaseJniLibFolders"].dependsOn(tasks["rustBuildRelease"])
tasks["mergeUniversalDebugJniLibFolders"].dependsOn(tasks["rustBuildDebug"])
productFlavors.filter{ it.name != "universal" }.forEach { _ ->
val archAndBuildType = name.capitalize()
tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"])
}
android.applicationVariants.all {
tasks["mergeUniversalReleaseJniLibFolders"].dependsOn(tasks["rustBuildRelease"])
tasks["mergeUniversalDebugJniLibFolders"].dependsOn(tasks["rustBuildDebug"])
productFlavors.filter { it.name != "universal" }.forEach { _ ->
val archAndBuildType = name.capitalize()
tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"])
}
}
}
@@ -0,0 +1 @@
/home/pierre/Documents/nym/nym/nym-connect-android/src-tauri/target/aarch64-linux-android/debug/libnym_connect_android.so
@@ -0,0 +1 @@
/home/pierre/Documents/nym/nym/nym-connect-android/src-tauri/target/armv7-linux-androideabi/debug/libnym_connect_android.so
@@ -0,0 +1 @@
/home/pierre/Documents/nym/nym/nym-connect-android/src-tauri/target/i686-linux-android/debug/libnym_connect_android.so
@@ -2,15 +2,15 @@
<!-- Base application theme. -->
<style name="Theme.nym_connect_android" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<item name="colorPrimary">@color/grey_800</item>
<item name="colorPrimaryVariant">@color/grey_900</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorSecondary">@color/green_500</item>
<item name="colorSecondaryVariant">@color/green_900</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
</resources>
@@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="grey_900">#212121</color>
<color name="grey_800">#424242</color>
<color name="green_500">#4caf50</color>
<color name="green_900">#1b5e20</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -1,3 +1,3 @@
<resources>
<string name="app_name">nym-connect-android</string>
<string name="app_name">Nym Connect</string>
</resources>
@@ -2,12 +2,12 @@
<!-- Base application theme. -->
<style name="Theme.nym_connect_android" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorPrimary">@color/grey_800</item>
<item name="colorPrimaryVariant">@color/grey_900</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorSecondary">@color/green_500</item>
<item name="colorSecondaryVariant">@color/green_900</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
@@ -40,11 +40,6 @@ pub enum BackendError {
#[from]
source: ClientCoreError,
},
#[error("{source}")]
ApiClientError {
#[from]
source: crate::operations::growth::api_client::ApiClientError,
},
#[error("could not send disconnect signal to the SOCKS5 client")]
CoundNotSendDisconnectSignal,
@@ -1,270 +0,0 @@
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())
}
}
}
// TODO: use the method in `operations::http` instead
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
}
}
@@ -1,57 +0,0 @@
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"],
),
},
}
}
}
@@ -1,3 +0,0 @@
pub mod api_client;
pub mod assets;
pub mod test_and_earn;
@@ -1,159 +0,0 @@
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;
#[cfg(desktop)]
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);
#[cfg(desktop)]
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?)
}
#[cfg(desktop)]
#[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)
}
}
}
@@ -1,7 +1,6 @@
pub mod connection;
pub mod directory;
pub mod export;
pub mod growth;
pub mod help;
pub mod http;
#[cfg(desktop)]
+1 -1
View File
@@ -161,7 +161,7 @@ impl State {
pub fn load_socks5_config(&self) -> Result<Socks5Config> {
let id = self.get_config_id()?;
let config = Socks5Config::load_from_file(Some(&id))
let config = Socks5Config::load_from_file(&id)
.tap_err(|_| log::warn!("Failed to load configuration file"))?;
Ok(config)
}
@@ -1,84 +0,0 @@
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: FCWithChildren<{ 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>
);
}
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
};
export const TestAndEarnButtonArea: FCWithChildren = () => {
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>
);
};
@@ -1,94 +0,0 @@
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>
);
@@ -1,192 +0,0 @@
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: FCWithChildren<{ 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: FCWithChildren<{ 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: FCWithChildren<{ 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: FCWithChildren<{
draw?: DrawWithWordOfTheDay;
sx?: SxProps;
}> = ({ draw }) => {
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 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: FCWithChildren<{
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} />;
};
@@ -1,19 +0,0 @@
/* eslint-disable react/jsx-pascal-case */
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>
);
@@ -1,196 +0,0 @@
/* eslint-disable react/jsx-no-useless-fragment */
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: FCWithChildren<{ 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: FCWithChildren<{
sx?: SxProps;
}> = () => {
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: FCWithChildren<{
sx?: SxProps;
}> = ({ sx }) => {
const context = useTestAndEarnContext();
const drawCount = context.draws?.draws?.length || 0;
if (drawCount < 1) {
return null;
}
return <TestAndEarnDraws sx={sx} />;
};
@@ -1,41 +0,0 @@
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>
);
@@ -1,37 +0,0 @@
import React from 'react';
import { WalletAddressFormField } from '@nymproject/react/account/WalletAddressFormField';
import { SxProps } from '@mui/system';
import { Box, Button, Paper, Stack } from '@mui/material';
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
export const TestAndEarnEnterWalletAddress: FCWithChildren<{
initialValue?: string;
placeholder?: string;
onSubmit?: () => Promise<void> | void;
sx?: SxProps;
}> = ({ initialValue, placeholder, onSubmit }) => {
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>
);
};
@@ -1,13 +0,0 @@
import React from 'react';
import { Box, Button } from '@mui/material';
export const TestAndEarnError: FCWithChildren<{ 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>
);
@@ -1,162 +0,0 @@
/* 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 {
MockTestAndEarnProvider_NotRegistered,
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: FCWithChildren<{
width?: string | number;
height?: string | number;
title?: string;
children: React.ReactNode;
}> = ({ 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: FCWithChildren<{ 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>
);
@@ -1,118 +0,0 @@
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: FCWithChildren<{
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: FCWithChildren = () => {
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>} */}
</>
);
};
@@ -1,75 +0,0 @@
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 { Registration } from './context/types';
export const TestAndEarnTakePart: FCWithChildren<{
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={{ ...(Array.isArray(sx) ? 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>
)}
</>
);
};
@@ -1,19 +0,0 @@
/* eslint-disable react/jsx-pascal-case */
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>
);
@@ -1,114 +0,0 @@
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: FCWithChildren<{
sx?: SxProps;
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('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: FCWithChildren<{
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;
};
@@ -1,12 +0,0 @@
import React from 'react';
import { Box } from '@mui/material';
import { SxProps } from '@mui/system';
import Content from './content/TestAndEarn/WinnerEntersWalletAddress.mdx';
export const TestAndEarnWinnerWalletAddress: FCWithChildren<{
sx?: SxProps;
}> = () => (
<Box>
<Content />
</Box>
);
@@ -1,17 +0,0 @@
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>.
@@ -1,25 +0,0 @@
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>
@@ -1,9 +0,0 @@
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.
@@ -1,3 +0,0 @@
## 😕 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.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

@@ -1,43 +0,0 @@
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!
@@ -1,272 +0,0 @@
/* 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: FCWithChildren = ({ children }) => {
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);
@@ -1,262 +0,0 @@
/* 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: FCWithChildren = ({ children }) => (
<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: FCWithChildren = ({ children }) => (
<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: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithAllDrawsAndEntry}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsNoCurrent: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
draws: drawsWithClaim,
},
};
export const MockTestAndEarnProvider_RegisteredWithDrawsNoCurrent: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsNoCurrent}>{children}</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDraws: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
current,
draws,
},
};
export const MockTestAndEarnProvider_RegisteredWithDraws: FCWithChildren = ({ children }) => (
<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: FCWithChildren = ({ children }) => (
<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: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredAndError}>{children}</TestAndEarnContext.Provider>
);
@@ -1,64 +0,0 @@
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[];
}
-23
View File
@@ -1,23 +0,0 @@
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,
);
-18
View File
@@ -1,18 +0,0 @@
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);
+2 -2
View File
@@ -13,11 +13,11 @@
"resolveJsonModule": true,
"isolatedModules": false,
"jsx": "react-jsx",
"sourceMap": false,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@assets/*": ["../assets/*"]
}
},
"exclude": ["node_modules", "dist", "jest.config.js", "webpack.config.js", "webpack.prod.js", "webpack.common.js", "target"]
"exclude": ["node_modules", "dist", "jest.config.js", "webpack.config.js", "webpack.prod.js", "webpack.common.js", "target", "src-tauri"]
}
+10 -14
View File
@@ -4,8 +4,6 @@ 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({
@@ -18,22 +16,20 @@ module.exports = mergeWithRules({
})(
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: /\.mdx?$/,
use: [
{
loader: '@mdx-js/loader',
/** @type {import('@mdx-js/loader').Options} */
options: {},
},
],
},
{
test: /\.ya?ml$/,
type: 'asset/resource',
+14 -13
View File
@@ -1,7 +1,7 @@
const { mergeWithRules } = require('webpack-merge');
const webpack = require('webpack');
// const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// const ReactRefreshTypeScript = require('react-refresh-typescript');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript');
const commonConfig = require('./webpack.common');
module.exports = mergeWithRules({
@@ -13,7 +13,7 @@ module.exports = mergeWithRules({
},
})(commonConfig, {
mode: 'development',
devtool: false,
devtool: 'inline-source-map',
module: {
rules: [
{
@@ -21,9 +21,9 @@ module.exports = mergeWithRules({
use: 'ts-loader',
exclude: /node_modules/,
options: {
// getCustomTransformers: () => ({
// before: [ReactRefreshTypeScript()],
// }),
getCustomTransformers: () => ({
before: [ReactRefreshTypeScript()],
}),
// `ts-loader` does not work with HMR unless `transpileOnly` is used.
// If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative.
transpileOnly: true,
@@ -32,14 +32,15 @@ module.exports = mergeWithRules({
],
},
plugins: [
// new ReactRefreshWebpackPlugin(),
new ReactRefreshWebpackPlugin(),
// this can be included automatically by the dev server, however build mode fails if missing
// new webpack.HotModuleReplacementPlugin(),
new webpack.HotModuleReplacementPlugin(),
],
// recommended for faster rebuild
optimization: {
// runtimeChunk: true,
runtimeChunk: true,
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
@@ -55,14 +56,14 @@ module.exports = mergeWithRules({
devServer: {
port: 9000,
// compress: true,
compress: true,
historyApiFallback: true,
// disable this because on android it makes reloading infinity loop
hot: false,
host: 'local-ipv4',
allowedHosts: 'all',
// client: {
// overlay: false,
// },
client: {
overlay: false,
},
},
});
-2
View File
@@ -4,8 +4,6 @@ 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, {
+71 -4
View File
@@ -10715,6 +10715,18 @@ hex-rgb@^4.1.0:
resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776"
integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==
history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -10724,7 +10736,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -11685,6 +11697,11 @@ is-wsl@^2.1.1, is-wsl@^2.2.0:
dependencies:
is-docker "^2.0.0"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -12952,7 +12969,7 @@ longest-streak@^3.0.0:
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4"
integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==
loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -15061,6 +15078,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -16036,7 +16060,7 @@ react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0:
react-is@^16.10.2, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -16112,6 +16136,34 @@ react-router-dom@6, react-router-dom@^6.7.0:
"@remix-run/router" "1.3.0"
react-router "6.7.0"
react-router-dom@^5.2.0:
version "5.3.4"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6"
integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==
dependencies:
"@babel/runtime" "^7.12.13"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.3.4"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.3.4:
version "5.3.4"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5"
integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==
dependencies:
"@babel/runtime" "^7.12.13"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.7.0.tgz#db262684c13b5c2970694084ae9e8531718a0681"
@@ -16690,6 +16742,11 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -18123,7 +18180,12 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
tiny-warning@^1.0.2:
tiny-invariant@^1.0.2:
version "1.3.1"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
tiny-warning@^1.0.0, tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
@@ -18923,6 +18985,11 @@ validate-npm-package-name@^3.0.0:
dependencies:
builtins "^1.0.3"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"