Skip to main content

tauri_plugin_moonbase/
models.rs

1//! What the plugin hands the frontend. Every type serializes to camelCase JSON.
2
3use std::time::SystemTime;
4
5use moonbase::{
6    ActivationMethod, DeviceIdDescription, ErrorType, License, Product, ReleaseAccess, SubProduct,
7    SubProductCatalogEntry, User, serde_iso8601,
8};
9use serde::Serialize;
10use serde_json::Value;
11
12/// Where the license stands. Drive your UI off this.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub enum Status {
16    /// Validating the stored license at startup.
17    Loading,
18    /// No license on this device. [`LicenseState::lock_reason`] says why, when
19    /// there was one.
20    Unlicensed,
21    /// A browser activation is in progress. A license held from before (a trial
22    /// being unlocked) stays in force until the new one lands.
23    Activating,
24    /// A full license is loaded.
25    Licensed,
26    /// A trial license is loaded.
27    Trial,
28    /// The trial has ended. [`LicenseState::expired_trial`] has it, for display.
29    TrialExpired,
30    /// The license is valid, but doesn't cover this version of the app, such as
31    /// one whose updates ended before it came out. The app is locked, and the
32    /// license stays stored: [`LicenseState::not_covered`] has it, what it covers,
33    /// and where to upgrade.
34    NotCovered,
35}
36
37/// Why the app lost its license, so the UI can say so.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
39#[serde(rename_all = "camelCase")]
40pub enum LockReason {
41    /// Deactivated or forgotten on this device, here or in another instance.
42    Deactivated,
43    /// The license reached its end date.
44    Expired,
45    /// Moonbase no longer accepts it: revoked, or bound to another device.
46    Invalid,
47    /// It could not be checked online within the offline grace period.
48    Unverified,
49    /// It doesn't cover this version of the app. It stays stored; see
50    /// [`LicenseState::not_covered`].
51    NotCovered,
52}
53
54/// The browser activation in progress.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
56#[serde(rename_all = "camelCase")]
57pub struct PendingActivation {
58    /// The link the customer finishes activation at. `None` until Moonbase has
59    /// created the request; show it (or a copy button) for when no browser opened.
60    pub browser_url: Option<String>,
61    pub method: ActivationMethod,
62    /// The link is here but the browser couldn't be opened for it (no default
63    /// browser, say): show it, or a copy button, front and centre.
64    pub browser_open_failed: bool,
65}
66
67/// A license that is valid, but doesn't cover the version of the app that runs.
68#[derive(Debug, Clone, PartialEq, Serialize)]
69#[serde(rename_all = "camelCase")]
70pub struct NotCovered {
71    /// The license, as [`LicenseState::license`] shows one: still stored, and good
72    /// for the versions it covers.
73    pub license: LicenseView,
74    /// The version it doesn't cover, as `MAJOR.MINOR.PATCH`; empty when unknown.
75    pub app_version: String,
76    /// What it covers.
77    pub release_access: Option<ReleaseAccess>,
78    /// Where the customer buys the upgrade or renewal. Only Moonbase knows it, so
79    /// it is `None` when the license was judged on this device (offline, or
80    /// checked online moments ago), and when the store sells none. Without it,
81    /// `upgradeLicense` starts an online activation, whose page offers the upgrade.
82    pub upgrade_url: Option<String>,
83}
84
85/// Where the add-on list is.
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub enum AddOnsPhase {
89    /// Not loaded.
90    #[default]
91    Idle,
92    /// Listing; `addOns` keeps what an earlier load found.
93    Loading,
94    /// Listed, or failed (`error` set).
95    Ready,
96}
97
98/// The product's add-ons the customer can do something with, as Moonbase lists
99/// them for this device, and the add-on an activation or trial is running for.
100/// Loaded by `loadAddOns`.
101#[derive(Debug, Clone, Default, PartialEq, Serialize)]
102#[serde(rename_all = "camelCase")]
103pub struct AddOnsState {
104    pub phase: AddOnsPhase,
105    /// The currency to show prices in.
106    pub currency: Option<String>,
107    pub add_ons: Vec<SubProductCatalogEntry>,
108    /// Why listing failed, for the customer.
109    pub error: Option<String>,
110    /// The add-on an activation or trial is running for.
111    pub busy_id: Option<String>,
112    /// Why the last activation or trial failed, for the customer.
113    pub action_error: Option<String>,
114}
115
116/// The last operation that failed, worded for the customer, with the SDK's own
117/// message for logs and support.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
119#[serde(rename_all = "camelCase")]
120pub struct Failure {
121    pub message: String,
122    pub detail: String,
123    pub kind: Option<ErrorType>,
124}
125
126/// A newer release than the running app, from the license's `p:rel` claim. No
127/// network involved; call `checkUpdate` for the notes.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
129#[serde(rename_all = "camelCase")]
130pub struct UpdateHint {
131    pub current_version: String,
132    pub latest_version: String,
133}
134
135/// A snapshot of everything the UI needs, sent with every `moonbase://state-changed`
136/// event and returned by every command that changes it.
137#[derive(Debug, Clone, PartialEq, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct LicenseState {
140    pub status: Status,
141    /// Is there a valid license right now? Gate features on this.
142    pub licensed: bool,
143    /// The license in force.
144    pub license: Option<LicenseView>,
145    /// The trial that ended, with [`Status::TrialExpired`].
146    pub expired_trial: Option<LicenseView>,
147    pub not_covered: Option<NotCovered>,
148    pub lock_reason: Option<LockReason>,
149    pub activation: Option<PendingActivation>,
150    /// A deactivation is in flight.
151    pub busy: bool,
152    /// A re-check of the license is waiting for Moonbase.
153    pub refreshing: bool,
154    pub error: Option<Failure>,
155    pub update: Option<UpdateHint>,
156    /// The add-ons to unlock on this device right now, by their own part of their
157    /// product ID: the license lists them as active and they haven't expired.
158    pub active_sub_products: Vec<String>,
159    pub add_ons: AddOnsState,
160}
161
162/// This device, for an about box or a "copy diagnostics" button.
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
164#[serde(rename_all = "camelCase")]
165pub struct DeviceInfo {
166    pub name: String,
167    /// `"Mac"`, `"Windows"`, `"Linux"` or `"Unknown"`.
168    pub platform: String,
169    /// How the device id was derived: the id, the spec version and the *names*
170    /// of the parameters that went into it, never their values.
171    pub description: Option<DeviceIdDescription>,
172    /// Why there is no description: this machine has no readable identity.
173    pub error: Option<String>,
174}
175
176/// The release behind an available update.
177#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
178#[serde(rename_all = "camelCase")]
179pub struct UpdateInfo {
180    pub available: bool,
181    pub current_version: String,
182    pub latest_version: Option<String>,
183    pub release_notes: String,
184    pub has_downloads: bool,
185    /// Whether this license may download the installer, per the product's
186    /// release access control.
187    pub can_download: bool,
188    /// False when Moonbase says the customer's licenses don't cover the new
189    /// version (`can_download` is false then too): it needs an upgrade.
190    pub covered: bool,
191    /// What the customer's licenses cover, when that isn't every release.
192    pub release_access: Option<ReleaseAccess>,
193    /// Where to buy the upgrade or renewal, while the store sells one.
194    pub upgrade_url: Option<String>,
195}
196
197/// A license as the frontend sees it: everything the SDK's [`License`] carries but
198/// the raw token, which the webview has no use for and is better off never
199/// holding, plus `days_remaining`. Serializes as the SDK's license JSON does.
200#[derive(Debug, Clone, PartialEq, Serialize)]
201#[serde(rename_all = "camelCase")]
202pub struct LicenseView {
203    pub id: String,
204    pub activation_id: String,
205    pub trial: bool,
206    #[serde(rename = "activationMethod")]
207    pub method: ActivationMethod,
208    #[serde(rename = "product")]
209    pub licensed_product: Product,
210    pub issued_to: User,
211    #[serde(with = "serde_iso8601")]
212    pub issued_at: SystemTime,
213    #[serde(with = "serde_iso8601::option", skip_serializing_if = "Option::is_none")]
214    pub expires_at: Option<SystemTime>,
215    #[serde(with = "serde_iso8601")]
216    pub validated_at: SystemTime,
217    pub owned_sub_product_ids: Vec<String>,
218    pub sub_products: Vec<SubProduct>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub subscription_id: Option<String>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub seat_count: Option<i64>,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub seats_used: Option<i64>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub release_access: Option<ReleaseAccess>,
227    pub properties: Value,
228    /// Whole days until `expires_at`, rounded up, and 0 once it has passed. `None`
229    /// when the license never expires.
230    pub days_remaining: Option<u64>,
231}
232
233impl From<&License> for LicenseView {
234    fn from(license: &License) -> Self {
235        // Spelled out rather than spread, so a field added to `License` is a
236        // decision here, not a leak.
237        let License {
238            id,
239            activation_id,
240            trial,
241            method,
242            licensed_product,
243            issued_to,
244            issued_at,
245            expires_at,
246            validated_at,
247            owned_sub_product_ids,
248            sub_products,
249            subscription_id,
250            seat_count,
251            seats_used,
252            release_access,
253            properties,
254            token: _,
255        } = license.clone();
256        let days_remaining = expires_at.map(|expires| match expires.duration_since(SystemTime::now()) {
257            Ok(left) => left.as_secs().div_ceil(86_400),
258            Err(_) => 0,
259        });
260        Self {
261            id,
262            activation_id,
263            trial,
264            method,
265            licensed_product,
266            issued_to,
267            issued_at,
268            expires_at,
269            validated_at,
270            owned_sub_product_ids,
271            sub_products,
272            subscription_id,
273            seat_count,
274            seats_used,
275            release_access,
276            properties,
277            days_remaining,
278        }
279    }
280}