From 3023dc14e06824cbc34ee816ac187607a971a28f Mon Sep 17 00:00:00 2001 From: SaladDay Date: Wed, 1 Jul 2026 16:56:01 +0000 Subject: [PATCH] fix(subscription): display Codex free-plan 30-day quota window (#3651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex free accounts are metered on a rolling 30-day secondary window (limit_window_seconds = 2,592,000) instead of the weekly window used by paid plans. The backend already maps this correctly to the tier name "30_day", but the frontend TIER_I18N_KEYS whitelist had no entry for it, so SubscriptionQuotaView filtered the tier out. When that was the only surviving tier (as happens for free accounts), the whole quota footer rendered nothing — free accounts showed no remaining quota or reset time while paid accounts worked. - Add "30_day" -> subscription.thirtyDay to TIER_I18N_KEYS - Add the thirtyDay label to all four locales (zh/en/ja/zh-TW) - Add a unit test locking in window_seconds_to_tier_name mappings, including the 30-day case Fixes #3651 --- src-tauri/src/services/subscription.rs | 18 ++++++++++++++++++ src/components/SubscriptionQuotaFooter.tsx | 2 ++ src/i18n/locales/en.json | 1 + src/i18n/locales/ja.json | 1 + src/i18n/locales/zh-TW.json | 1 + src/i18n/locales/zh.json | 1 + 6 files changed, 24 insertions(+) diff --git a/src-tauri/src/services/subscription.rs b/src-tauri/src/services/subscription.rs index 5528455e6..5d6cbf58d 100644 --- a/src-tauri/src/services/subscription.rs +++ b/src-tauri/src/services/subscription.rs @@ -1340,3 +1340,21 @@ fn now_millis() -> i64 { .unwrap_or_default() .as_millis() as i64 } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn window_seconds_map_to_expected_tier_names() { + // 官方特例窗口 + assert_eq!(window_seconds_to_tier_name(18000), "five_hour"); + assert_eq!(window_seconds_to_tier_name(604800), "seven_day"); + // Codex 免费方案的次要窗口是 30 天(30 * 24 * 3600 = 2_592_000 秒)。 + // 前端 TIER_I18N_KEYS 需要有对应的 "30_day" 才能展示,见 #3651。 + assert_eq!(window_seconds_to_tier_name(2_592_000), "30_day"); + // 其他窗口按小时/天回退命名 + assert_eq!(window_seconds_to_tier_name(3600), "1_hour"); + assert_eq!(window_seconds_to_tier_name(86400), "1_day"); + } +} diff --git a/src/components/SubscriptionQuotaFooter.tsx b/src/components/SubscriptionQuotaFooter.tsx index 7d46bc376..e5ce8ecaf 100644 --- a/src/components/SubscriptionQuotaFooter.tsx +++ b/src/components/SubscriptionQuotaFooter.tsx @@ -27,6 +27,8 @@ export const TIER_I18N_KEYS: Record = { seven_day: "subscription.sevenDay", seven_day_opus: "subscription.sevenDayOpus", seven_day_sonnet: "subscription.sevenDaySonnet", + // Codex 免费方案的次要窗口是 30 天(付费方案为 7 天) + "30_day": "subscription.thirtyDay", // Gemini 模型分类 gemini_pro: "subscription.geminiPro", gemini_flash: "subscription.geminiFlash", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index fcad23b3f..1bb0c7314 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -2903,6 +2903,7 @@ "sevenDay": "7-Day", "sevenDayOpus": "7-Day (Opus)", "sevenDaySonnet": "7-Day (Sonnet)", + "thirtyDay": "30-Day", "geminiPro": "Pro", "geminiFlash": "Flash", "geminiFlashLite": "Flash Lite", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 46c3b51fc..1638fde00 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -2903,6 +2903,7 @@ "sevenDay": "7日間", "sevenDayOpus": "7日間(Opus)", "sevenDaySonnet": "7日間(Sonnet)", + "thirtyDay": "30日間", "geminiPro": "Pro", "geminiFlash": "Flash", "geminiFlashLite": "Flash Lite", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index feaddbbb0..ee07ad703 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -2875,6 +2875,7 @@ "sevenDay": "7 天", "sevenDayOpus": "7 天 (Opus)", "sevenDaySonnet": "7 天 (Sonnet)", + "thirtyDay": "30 天", "geminiPro": "Pro", "geminiFlash": "Flash", "geminiFlashLite": "Flash Lite", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index c58366488..c31ccd930 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -2903,6 +2903,7 @@ "sevenDay": "7天", "sevenDayOpus": "7天(Opus)", "sevenDaySonnet": "7天(Sonnet)", + "thirtyDay": "30天", "geminiPro": "Pro", "geminiFlash": "Flash", "geminiFlashLite": "Flash Lite",