Skip to main content

keylight/
state.rs

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