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}