Skip to main content

keylight/
state.rs

1//! License state, trial/keyless status, lifecycle events, and the pure state resolver.
2
3/// The high-level licensing status an app reacts to, resolved from the cached lease,
4/// trial, and free-tier configuration.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum LicenseState {
7    Trial { days_left: i64 },
8    Licensed,
9    Limited,
10    FreeTier,
11    Expired,
12    Invalid,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum TrialStatus {
17    NotStarted,
18    Active { days_left: i64 },
19    Expired,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum KeylessState {
24    Trial,
25    FreeTier,
26    Expired,
27}
28impl KeylessState {
29    pub fn wire(&self) -> &'static str {
30        match self {
31            Self::Trial => "trial",
32            Self::FreeTier => "free_tier",
33            Self::Expired => "expired",
34        }
35    }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum LicenseLifecycleEvent {
40    Renewed,
41    Cancelled,
42    Expired,
43    Restored,
44}
45
46/// Resolve the high-level state from inputs (pure; mirrors Swift LicenseManager).
47/// `lease_status`: Some("active"|"fallback"|"expired") if a *signature-valid* cached
48/// lease exists; `lease_current`: whether it is within skew. `had_license`: a key is stored.
49pub fn resolve_state(
50    lease_status: Option<&str>,
51    lease_current: bool,
52    had_license: bool,
53    trial: &TrialStatus,
54    free_tier_enabled: bool,
55) -> LicenseState {
56    if let Some(status) = lease_status {
57        match (status, lease_current) {
58            ("active", true) => return LicenseState::Licensed,
59            ("fallback", _) => return LicenseState::Limited,
60            ("expired", _) => return LicenseState::Expired,
61            // stale active lease (or anything else) falls through to offline/expired handling
62            _ => {}
63        }
64    }
65    if had_license {
66        return LicenseState::Expired;
67    }
68    match trial {
69        TrialStatus::Active { days_left } => LicenseState::Trial {
70            days_left: *days_left,
71        },
72        _ if free_tier_enabled => LicenseState::FreeTier,
73        _ => LicenseState::Invalid,
74    }
75}
76
77pub fn lifecycle_event(
78    prev: &LicenseState,
79    next: &LicenseState,
80    expiry_moved_later: bool,
81) -> Option<LicenseLifecycleEvent> {
82    use LicenseState::*;
83    match (prev, next) {
84        (Licensed, Licensed) if expiry_moved_later => Some(LicenseLifecycleEvent::Renewed),
85        (Licensed, Expired) | (Licensed, Limited) => Some(LicenseLifecycleEvent::Cancelled),
86        (Expired, Licensed) | (Limited, Licensed) | (Invalid, Licensed) => {
87            Some(LicenseLifecycleEvent::Restored)
88        }
89        (_, Expired) if !matches!(prev, Expired) => Some(LicenseLifecycleEvent::Expired),
90        _ => None,
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    #[test]
98    fn active_current_lease_is_licensed() {
99        assert_eq!(
100            resolve_state(Some("active"), true, true, &TrialStatus::NotStarted, false),
101            LicenseState::Licensed
102        );
103    }
104    #[test]
105    fn fallback_is_limited() {
106        assert_eq!(
107            resolve_state(
108                Some("fallback"),
109                true,
110                true,
111                &TrialStatus::NotStarted,
112                false
113            ),
114            LicenseState::Limited
115        );
116    }
117    #[test]
118    fn no_license_trial_active_is_trial() {
119        assert_eq!(
120            resolve_state(
121                None,
122                false,
123                false,
124                &TrialStatus::Active { days_left: 5 },
125                false
126            ),
127            LicenseState::Trial { days_left: 5 }
128        );
129    }
130    #[test]
131    fn no_license_free_tier_is_free_tier() {
132        assert_eq!(
133            resolve_state(None, false, false, &TrialStatus::NotStarted, true),
134            LicenseState::FreeTier
135        );
136    }
137    #[test]
138    fn nothing_is_invalid() {
139        assert_eq!(
140            resolve_state(None, false, false, &TrialStatus::NotStarted, false),
141            LicenseState::Invalid
142        );
143    }
144    #[test]
145    fn keyless_wire_strings() {
146        assert_eq!(KeylessState::FreeTier.wire(), "free_tier");
147    }
148
149    use LicenseLifecycleEvent as E;
150    use LicenseState as S;
151    #[test]
152    fn renewed_when_licensed_and_expiry_later() {
153        assert_eq!(
154            lifecycle_event(&S::Licensed, &S::Licensed, true),
155            Some(E::Renewed)
156        );
157        assert_eq!(lifecycle_event(&S::Licensed, &S::Licensed, false), None);
158    }
159    #[test]
160    fn cancelled_on_licensed_to_expired_or_limited() {
161        assert_eq!(
162            lifecycle_event(&S::Licensed, &S::Expired, false),
163            Some(E::Cancelled)
164        );
165        assert_eq!(
166            lifecycle_event(&S::Licensed, &S::Limited, false),
167            Some(E::Cancelled)
168        );
169    }
170    #[test]
171    fn restored_on_recovery_to_licensed() {
172        assert_eq!(
173            lifecycle_event(&S::Expired, &S::Licensed, false),
174            Some(E::Restored)
175        );
176        assert_eq!(
177            lifecycle_event(&S::Limited, &S::Licensed, false),
178            Some(E::Restored)
179        );
180        assert_eq!(
181            lifecycle_event(&S::Invalid, &S::Licensed, false),
182            Some(E::Restored)
183        );
184    }
185    #[test]
186    fn expired_when_crossing_into_expired_from_non_expired() {
187        assert_eq!(
188            lifecycle_event(&S::Trial { days_left: 1 }, &S::Expired, false),
189            Some(E::Expired)
190        );
191    }
192    #[test]
193    fn no_event_on_noop_transitions() {
194        assert_eq!(
195            lifecycle_event(
196                &S::Trial { days_left: 3 },
197                &S::Trial { days_left: 2 },
198                false
199            ),
200            None
201        );
202        assert_eq!(lifecycle_event(&S::Expired, &S::Expired, false), None);
203    }
204}