use crate::errors::Error::{ClientError, ServerError}; use crate::errors::Result; use chrono::{NaiveDateTime, Utc}; use log::{debug, info, warn}; use serde::{Deserialize, Serialize}; use std::ops::Add; use std::time::Duration; use tauri::{is_dev, AppHandle, Emitter, Manager, Runtime, WebviewWindow}; use ts_rs::TS; use yaak_models::queries::UpdateSource; const KV_NAMESPACE: &str = "license"; const KV_ACTIVATION_ID_KEY: &str = "activation_id"; const TRIAL_SECONDS: u64 = 3600 * 24 * 30; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "gen_models.ts")] pub struct CheckActivationRequestPayload { pub app_version: String, pub app_platform: String, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "license.ts")] pub struct CheckActivationResponsePayload { pub active: bool, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "license.ts")] pub struct ActivateLicenseRequestPayload { pub license_key: String, pub app_version: String, pub app_platform: String, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "license.ts")] pub struct DeactivateLicenseRequestPayload { pub app_version: String, pub app_platform: String, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "license.ts")] pub struct ActivateLicenseResponsePayload { pub activation_id: String, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "license.ts")] pub struct APIErrorResponsePayload { pub error: String, pub message: String, } pub async fn activate_license( window: &WebviewWindow, p: ActivateLicenseRequestPayload, ) -> Result<()> { let client = reqwest::Client::new(); let response = client.post(build_url("/licenses/activate")).json(&p).send().await?; if response.status().is_client_error() { let body: APIErrorResponsePayload = response.json().await?; return Err(ClientError { message: body.message, error: body.error, }); } if response.status().is_server_error() { return Err(ServerError); } let body: ActivateLicenseResponsePayload = response.json().await?; yaak_models::queries::set_key_value_string( window, KV_ACTIVATION_ID_KEY, KV_NAMESPACE, body.activation_id.as_str(), &UpdateSource::Window, ) .await; if let Err(e) = window.emit("license-activated", true) { warn!("Failed to emit check-license event: {}", e); } Ok(()) } pub async fn deactivate_license( window: &WebviewWindow, p: DeactivateLicenseRequestPayload, ) -> Result<()> { let activation_id = get_activation_id(window).await; let client = reqwest::Client::new(); let path = format!("/licenses/activations/{}/deactivate", activation_id); let response = client.post(build_url(&path)).json(&p).send().await?; if response.status().is_client_error() { let body: APIErrorResponsePayload = response.json().await?; return Err(ClientError { message: body.message, error: body.error, }); } if response.status().is_server_error() { return Err(ServerError); } yaak_models::queries::delete_key_value( window, KV_ACTIVATION_ID_KEY, KV_NAMESPACE, &UpdateSource::Window, ) .await; if let Err(e) = window.emit("license-deactivated", true) { warn!("Failed to emit deactivate-license event: {}", e); } Ok(()) } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "snake_case", tag = "type")] #[ts(export, export_to = "license.ts")] pub enum LicenseCheckStatus { PersonalUse { trial_ended: NaiveDateTime }, CommercialUse, InvalidLicense, Trialing { end: NaiveDateTime }, } pub async fn check_license(app_handle: &AppHandle, payload: CheckActivationRequestPayload) -> Result { let activation_id = get_activation_id(app_handle).await; let settings = yaak_models::queries::get_or_create_settings(app_handle).await; let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS)); debug!("Trial ending at {trial_end:?}"); let has_activation_id = !activation_id.is_empty(); let trial_period_active = Utc::now().naive_utc() < trial_end; match (has_activation_id, trial_period_active) { (false, true) => Ok(LicenseCheckStatus::Trialing { end: trial_end }), (false, false) => Ok(LicenseCheckStatus::PersonalUse { trial_ended: trial_end, }), (true, _) => { info!("Checking license activation"); // A license has been activated, so let's check the license server let client = reqwest::Client::new(); let path = format!("/licenses/activations/{activation_id}/check"); let response = client.post(build_url(&path)).json(&payload).send().await?; if response.status().is_client_error() { let body: APIErrorResponsePayload = response.json().await?; return Err(ClientError { message: body.message, error: body.error, }); } if response.status().is_server_error() { return Err(ServerError); } let body: CheckActivationResponsePayload = response.json().await?; if !body.active { return Ok(LicenseCheckStatus::InvalidLicense); } Ok(LicenseCheckStatus::CommercialUse) } } } fn build_url(path: &str) -> String { if is_dev() { format!("http://localhost:9444{path}") } else { format!("https://license.yaak.app{path}") } } pub async fn get_activation_id(mgr: &impl Manager) -> String { yaak_models::queries::get_key_value_string(mgr, KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "") .await }