diff --git a/apps/studio/src/agent_panel.rs b/apps/studio/src/agent_panel.rs index a6865ed..d3fd4e5 100644 --- a/apps/studio/src/agent_panel.rs +++ b/apps/studio/src/agent_panel.rs @@ -338,6 +338,9 @@ pub struct AgentPanel { /// here (the app's data was reset): the sign-in being restored, tried once per session. restore_rx: Option>>, restore_tried: bool, + /// `sf_license::license_generation` when the plan was last fetched: a key saved or removed + /// elsewhere (Export activated Pro, a renewal) fetches it again. + license_seen: Option, /// "Signed in." and the like: shown above the suggestions until the first message. notice: Option, } @@ -683,6 +686,8 @@ impl AgentPanel { self.log.clear(); self.notice = note; self.license_changed = true; + // this panel's own sign-in: no second fetch for the key it just saved + self.license_seen = Some(sf_license::license_generation()); self.fetch_credits(); } @@ -1043,6 +1048,20 @@ impl AgentPanel { if std::mem::take(&mut self.license_changed) { ed.export.license = sf_license::load_saved().map(|k| sf_license::verify(&k)).unwrap_or_else(|| sf_license::Status::Invalid("no key".into())); } + // the license changed outside this panel (bought or signed in from Export): fetch the + // plan again, keeping the chat + // (a fetch already running is left to finish; the next frame asks again) + let generation = sf_license::license_generation(); + match self.license_seen { + None => self.license_seen = Some(generation), + Some(seen) if seen != generation && self.credits_rx.is_none() => { + self.license_seen = Some(generation); + self.credits = None; + self.credits_error = None; + self.fetch_credits(); + } + _ => {} + } if self.credits.is_none() && self.credits_error.is_none() { self.fetch_credits(); } diff --git a/apps/studio/src/export.rs b/apps/studio/src/export.rs index fe3fb90..1ffe444 100644 --- a/apps/studio/src/export.rs +++ b/apps/studio/src/export.rs @@ -67,6 +67,9 @@ pub struct ExportDialog { free_rx: Option>>, /// The dialog was open last frame (opening it refreshes the free exports). was_open: bool, + /// `sf_license::license_generation` the license was last read at: a key saved or removed + /// elsewhere (the agent panel's upgrade) reads it again. + license_seen: u64, /// The export running now is a free one: counted once it finishes. free_job: bool, progress: Option, @@ -79,7 +82,7 @@ pub struct ExportDialog { /// that loads the license and talks to the backend. impl Default for ExportDialog { fn default() -> Self { - ExportDialog { open: false, license: sf_license::Status::Invalid("no key".into()), email: String::new(), activation: None, check_in: None, activation_msg: None, code_sent_to: None, code: String::new(), signing_in: false, promo: String::new(), promo_open: false, checkout: None, claim_until: None, next_claim: None, claim: None, free_left: None, free_rx: None, was_open: false, free_job: false, format: Format::Mp4, fps: 60.0, height: 1080, quality: QualityPreset::SocialMedia, codec: VideoCodec::H264, audio: true, job: None, progress: None, result: None, started: None } + ExportDialog { open: false, license: sf_license::Status::Invalid("no key".into()), email: String::new(), activation: None, check_in: None, activation_msg: None, code_sent_to: None, code: String::new(), signing_in: false, promo: String::new(), promo_open: false, checkout: None, claim_until: None, next_claim: None, claim: None, free_left: None, free_rx: None, was_open: false, license_seen: 0, free_job: false, format: Format::Mp4, fps: 60.0, height: 1080, quality: QualityPreset::SocialMedia, codec: VideoCodec::H264, audio: true, job: None, progress: None, result: None, started: None } } } @@ -103,7 +106,7 @@ impl ExportDialog { None }; // a checkout from an earlier session, paid after quitting: look for it a while - let mut d = ExportDialog { license, email, check_in, ..Default::default() }; + let mut d = ExportDialog { license, email, check_in, license_seen: sf_license::license_generation(), ..Default::default() }; // known before the dialog first opens if !d.license.is_valid() { d.fetch_free_exports(); @@ -256,6 +259,11 @@ impl ExportDialog { /// Drain background messages (export progress, activation) and handle Escape. pub fn poll(&mut self, ctx: &egui::Context) { + let generation = sf_license::license_generation(); + if generation != self.license_seen { + self.license_seen = generation; + self.license = sf_license::load_saved().map(|k| sf_license::verify(&k)).unwrap_or_else(|| sf_license::Status::Invalid("no key".into())); + } self.poll_checkout(ctx); self.poll_free_exports(ctx); if let Some(rx) = &self.check_in { diff --git a/crates/sf-license/src/lib.rs b/crates/sf-license/src/lib.rs index 658f15a..312dc7c 100644 --- a/crates/sf-license/src/lib.rs +++ b/crates/sf-license/src/lib.rs @@ -488,10 +488,27 @@ pub fn billing_portal_url() -> Result { v.get("url").and_then(|u| u.as_str()).map(String::from).ok_or_else(|| "no portal url".to_string()) } +/// Bumped whenever the key on this computer is saved or removed (activated, renewed, signed +/// out, moved away), so every part of the app that shows the plan can refresh: the agent +/// panel and Export each check [`license_generation`] and re-read when it changed. +static LICENSE_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +pub fn license_generation() -> u64 { + LICENSE_GENERATION.load(std::sync::atomic::Ordering::Relaxed) +} + +fn license_changed() { + LICENSE_GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed); +} + /// Remove the saved key (the license moved to another computer). pub fn forget() -> std::io::Result<()> { match key_file() { - Some(p) if p.exists() => std::fs::remove_file(p), + Some(p) if p.exists() => { + std::fs::remove_file(p)?; + license_changed(); + Ok(()) + } _ => Ok(()), } } @@ -499,7 +516,9 @@ pub fn forget() -> std::io::Result<()> { pub fn save(key: &str) -> std::io::Result<()> { let p = key_file().ok_or_else(|| std::io::Error::other("no home dir"))?; std::fs::create_dir_all(p.parent().unwrap())?; - std::fs::write(p, key.trim()) + std::fs::write(p, key.trim())?; + license_changed(); + Ok(()) } #[cfg(test)]