import CodexBarCoreimport CryptoKitimport Foundation @MainActorstruct CodexResetCreditExpiryNotifier { static let expiryWindow: TimeInterval = 3 * 24 * 60 * 60 static let notificationPrefix = "codex-reset-credit-expiry" static let summaryFingerprintsKey = "codexResetCreditExpirySummaryFingerprints" static let maximumRememberedSummaries = 64 var userDefaults: UserDefaults = .standard var notificationPoster: (String, String, String) -> Void = { prefix, title, body in AppNotifications.shared.post(idPrefix: prefix, title: title, body: body) } func postExpiringCreditsIfNeeded( snapshot: CodexRateLimitResetCreditsSnapshot, resetStyle: ResetTimeDisplayStyle, now: Date = Date()) { let expiringCredits = snapshot.availableInventory(at: now).credits.filter { credit in guard let expiresAt = credit.expiresAt else { return false } return expiresAt.timeIntervalSince(now) <= Self.expiryWindow } guard !expiringCredits.isEmpty else { return } let fingerprint = Self.summaryFingerprint(expiringCredits) // Account-scoped refreshes can alternate inventories, so remember more than the latest summary. var notifiedFingerprints = self.userDefaults.stringArray(forKey: Self.summaryFingerprintsKey) ?? [] guard !notifiedFingerprints.contains(fingerprint) else { return } notifiedFingerprints.append(fingerprint) if notifiedFingerprints.count > Self.maximumRememberedSummaries { notifiedFingerprints.removeFirst(notifiedFingerprints.count - Self.maximumRememberedSummaries) } self.userDefaults.set(notifiedFingerprints, forKey: Self.summaryFingerprintsKey) let expiringSnapshot = CodexRateLimitResetCreditsSnapshot( credits: expiringCredits, availableCount: expiringCredits.count, updatedAt: now) guard let presentation = CodexResetCreditsPresentation.make( snapshot: expiringSnapshot, resetStyle: resetStyle, now: now) else { return } self.notificationPoster( Self.notificationPrefix, L("Limit Reset Credits"), presentation.helpText) } private static func summaryFingerprint(_ credits: [CodexRateLimitResetCredit]) -> String { let material = credits.map { credit in "\(credit.id)\u{1f}\(credit.expiresAt?.timeIntervalSince1970 ?? 0)" }.joined(separator: "\u{1e}") return SHA256.hash(data: Data(material.utf8)) .map { String(format: "%02x", $0) } .joined() }}
import CodexBarCore
import CryptoKit
import Foundation
@MainActor
struct CodexResetCreditExpiryNotifier {
static let expiryWindow: TimeInterval = 3 * 24 * 60 * 60
static let notificationPrefix = "codex-reset-credit-expiry"
static let summaryFingerprintsKey = "codexResetCreditExpirySummaryFingerprints"
static let maximumRememberedSummaries = 64
var userDefaults: UserDefaults = .standard
var notificationPoster: (String, String, String) -> Void = { prefix, title, body in
AppNotifications.shared.post(idPrefix: prefix, title: title, body: body)
}
func postExpiringCreditsIfNeeded(
snapshot: CodexRateLimitResetCreditsSnapshot,
resetStyle: ResetTimeDisplayStyle,
now: Date = Date())
{
let expiringCredits = snapshot.availableInventory(at: now).credits.filter { credit in
guard let expiresAt = credit.expiresAt else { return false }
return expiresAt.timeIntervalSince(now) <= Self.expiryWindow
}
guard !expiringCredits.isEmpty else { return }
let fingerprint = Self.summaryFingerprint(expiringCredits)
// Account-scoped refreshes can alternate inventories, so remember more than the latest summary.
var notifiedFingerprints = self.userDefaults.stringArray(forKey: Self.summaryFingerprintsKey) ?? []
guard !notifiedFingerprints.contains(fingerprint) else { return }
notifiedFingerprints.append(fingerprint)
if notifiedFingerprints.count > Self.maximumRememberedSummaries {
notifiedFingerprints.removeFirst(notifiedFingerprints.count - Self.maximumRememberedSummaries)
}
self.userDefaults.set(notifiedFingerprints, forKey: Self.summaryFingerprintsKey)
let expiringSnapshot = CodexRateLimitResetCreditsSnapshot(
credits: expiringCredits,
availableCount: expiringCredits.count,
updatedAt: now)
guard let presentation = CodexResetCreditsPresentation.make(
snapshot: expiringSnapshot,
resetStyle: resetStyle,
now: now)
else {
return
}
self.notificationPoster(
Self.notificationPrefix,
L("Limit Reset Credits"),
presentation.helpText)
}
private static func summaryFingerprint(_ credits: [CodexRateLimitResetCredit]) -> String {
let material = credits.map { credit in
"\(credit.id)\u{1f}\(credit.expiresAt?.timeIntervalSince1970 ?? 0)"
}.joined(separator: "\u{1e}")
return SHA256.hash(data: Data(material.utf8))
.map { String(format: "%02x", $0) }
.joined()
}
}