From 5498cfa258321738c3549aa01fd076659a973188 Mon Sep 17 00:00:00 2001 From: Mehfuz Hossain Date: Mon, 9 Nov 2026 20:35:56 -0800 Subject: [PATCH] Agent panel and Export follow each other's license changes Buying Pro from Export saved the key but the agent panel kept the plan it had fetched, so it stayed locked until a restart. Saving or removing the key now bumps sf_license::license_generation; the agent panel fetches the plan again when it changed (keeping the chat), and Export re-reads the key. --- apps/studio/src/agent_panel.rs | 19 +++++++++++++++++++ apps/studio/src/export.rs | 12 ++++++++++-- crates/sf-license/src/lib.rs | 23 +++++++++++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) 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)]