# fix\(mobile\): subscription widget no longer expires on a window that already reset · PR \#22990 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22990)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: pingdotgg/t3code

Source branch: fix/issue-22973

Target branch: main

Head commit: 02932550995a8d55c28a336c343c56a557721c43

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22990)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22990/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22990/diffs?format=markdown)

## Description

---

## Problem

A freshly observed subscription snapshot can include a session reset timestamp that has already passed. The widget treated that timestamp as the whole provider’s expiry, so publishing the iOS timeline cleared every quota row, including a valid weekly row. This addresses the source reproduction in [#22973](https://github.com/pingdotgg/t3code/issues/22973); the phone’s actual provider timestamps have not been collected.

## Change

Only reset timestamps after their own account observation time now bound expiry. The 15-minute freshness cap and earliest future reset still expire the provider together. Past resets display “Reset” and retain the provider’s reported percentage. Both iOS and Android use this snapshot builder.

## Scope and approval

Fixes #22973
GitCafe: https://git.cafe/pingdotgg/t3code/issues/22973
Triaged bug via the automated issue flow.

## Verification

Synthetic before/after, fixed UTC times: observed October 21 at 16:45; session 0% used, reset 16:30; weekly 30% used, reset October 28. Before: expiry 16:30 and the 16:45 timeline entry says “Open T3 to refresh” with no rows. After: session 100% remaining and weekly 70% remaining at 16:45, then both rows expire at 17:00. Changing only the session reset to 17:30 produces the same 17:00 deadline.

Validation:
- Regression baseline: 3 failed, 7 passed. The pooled-account follow-up regression also failed before correction.
- `vp test run apps/mobile/src/widgets/subscriptionUsageSnapshot.test.ts`: 14 passed, including iOS/Android shared snapshot inputs, past/equal-observation resets, 15-minute expiry, an earlier future reset, and pooled accounts with different observation times.
- Targeted mobile lint and typecheck passed.
- `vp run check:changed`: format, lint, knip, affected typechecks, and focused tests passed.
- Final independent code reviews: GPT-6.1-Sol/Codex MERGE; Claude Opus 5.5/Claude Agent MERGE.
- Final evidence audit: Claude Opus 5.5/Claude Agent SUFFICIENT for the synthetic expiry bug; all 14 tests passed independently.
- cannot-verify ios live (Linux VM; no simulator or native build attempted).
- User documentation remains accurate and was not changed.

Model/harness: GPT-6.1-Sol through the Codex harness in T3 Code; independent code and evidence review delegated to GPT-6.1-Sol/Codex and Claude Opus 5.5/Claude Agent.

## Evidence

Real Android app, home-screen widget on the launcher. Before = `8d3b061121` (merge-base on main); after = `0293255099` (head, `02932550995a8d55c28a336c343c56a557721c43`). Both sides were seeded with the same fixture by the committed script `apps/mobile/scripts/seed-widget-fixture-22973.mts` (`apply <row> --anchor <iso>` / `revert`). It drives the real build → `publishSubscriptionUsage.android.ts` → `widget.updateSnapshot` path. Before swaps in `subscriptionUsageSnapshot.ts` from `8d3b061121`; nothing else differs. Fixture matrix: [matrix.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/cfab1765d727856b/matrix.md).

| Row | Before (main) | After (head) |
|---|---|---|
| F1 not yet reset | both windows "Next reset" | identical |
| F2 already reset (https://git.cafe/pingdotgg/t3code/issues/22973) | "Open T3 to refresh" immediately | Weekly "Next reset …", 5 hours "Reset …" |
| F3 all reset | expired immediately | both windows "Reset …" |
| F8 long window label, >2 windows | expired immediately | long red label on one line, Weekly 50%, "1 more in T3" |
| F7a pooled, newer account reset | expired immediately | 5 hours "Reset …", "As of" oldest reading |

| Row | Theme | Before | After | Diff0 | DiffT |
|---|---|---|---|---|---|
| F1 Window not yet reset | light | ![F1-light-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1ed02ef8d3698782/F1-light-before.png) | ![F1-light-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/a702653433e6e94f/F1-light-after.png) | ![F1-light-diff0](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5bca130af8e02182/F1-light-diff0.png) | ![F1-light-diffT](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/bc42dfbf9fbd70a5/F1-light-diffT.png) |
| F1 Window not yet reset | dark | ![F1-dark-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6fe1b988be01e3d6/F1-dark-before.png) | ![F1-dark-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8c2c53f9a1c28194/F1-dark-after.png) | ![F1-dark-diff0](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/de6e31c72109bf5e/F1-dark-diff0.png) | ![F1-dark-diffT](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/53811e52dc376d17/F1-dark-diffT.png) |
| F2 Window already reset (https://git.cafe/pingdotgg/t3code/issues/22973) | light | ![F2-light-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/fa03ed1c64ffc8da/F2-light-before.png) | ![F2-light-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2fc3a66aa72870d4/F2-light-after.png) | ![F2-light-diff0](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/95b2dab9e0776820/F2-light-diff0.png) | ![F2-light-diffT](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/832ba1b38d639d8a/F2-light-diffT.png) |
| F2 Window already reset (https://git.cafe/pingdotgg/t3code/issues/22973) | dark | ![F2-dark-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ae39c6207f4b95f0/F2-dark-before.png) | ![F2-dark-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0e817a4fc0b41d2f/F2-dark-after.png) | ![F2-dark-diff0](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d814845809aa7e3b/F2-dark-diff0.png) | ![F2-dark-diffT](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e477e0391fdca8d4/F2-dark-diffT.png) |
| F3 All windows reset | light | ![F3-light-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/37028ce70faad76c/F3-light-before.png) | ![F3-light-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/929951752eabef68/F3-light-after.png) | ![F3-light-diff0](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1e3640794ba9023e/F3-light-diff0.png) | ![F3-light-diffT](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/020ee6571b44334b/F3-light-diffT.png) |
| F3 All windows reset | dark | ![F3-dark-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/489d351e24277b4b/F3-dark-before.png) | ![F3-dark-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1ad576e282eb4605/F3-dark-after.png) | ![F3-dark-diff0](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d7041c5266e49ab9/F3-dark-diff0.png) | ![F3-dark-diffT](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/32b4c9255502d35f/F3-dark-diffT.png) |
| F8 Long window labels, >2 windows | light | ![F8-light-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/48abca4818c305f6/F8-light-before.png) | ![F8-light-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/885db88fa7f6d39f/F8-light-after.png) | ![F8-light-diff0](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8d8f57c7c9b5d66b/F8-light-diff0.png) | ![F8-light-diffT](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7a20622971f433af/F8-light-diffT.png) |
| F8 Long window labels, >2 windows | dark | ![F8-dark-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b80d6665a5bc10d2/F8-dark-before.png) | ![F8-dark-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/35a986424e2f774b/F8-dark-after.png) | ![F8-dark-diff0](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/aef206ff664d96a5/F8-dark-diff0.png) | ![F8-dark-diffT](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9063d60996b43a05/F8-dark-diffT.png) |
| F7a Pooled, newer account reset | dark | ![F7a-dark-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/492dc03b1a5fb15f/F7a-dark-before.png) | ![F7a-dark-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b36b13aab4f655b8/F7a-dark-after.png) | ![F7a-dark-diff0](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/642407027f1ecf01/F7a-dark-diff0.png) | ![F7a-dark-diffT](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9ccac8645de60950/F7a-dark-diffT.png) |

**Environment (every capture):** EAS cloud Android emulator `sdk_gphone64_x86_64`, Android 15 (API 35), 720×1600, density 300 (from EAS docs; `wm density` not readable, no adb shell), font scale 1.0, Pixel Launcher, device theme light/dark as labelled, app appearance default; EAS preview:dev APK build 67d8faf8 (fingerprint matches head), JS from Metro. The clock could not be frozen (agent-device has no adb shell). Each pair shares one anchor (device time rounded down to the minute), with fixtures relative to that anchor and the after shot taken within 15 min of it.

**What else changed:** the status bar clock in every pair (the two shots were taken in different minutes). F3-dark, F7a-dark and F8-dark after: dock and search bar sit lower because the screenshot caught the launcher's go-home animation; this is not a widget change. F3-light: a small diff near the gesture handle, from shots taken 21 min apart. Otherwise none: the fuzzed diff outside the status bar is confined to the widget, or is 0 px for F1.

<details><summary>Verification log</summary>

- Main merged into the branch by a merge commit (`d05935e9f9`, no rebase, no force-push); the seed script was added in `21250b531f` and its lint fixed in `0293255099`.
- Tests: `vp test run src/widgets/subscriptionUsageSnapshot.test.ts` passes 14/14 at `d05935e9f9` and at `0293255099`. `vp fmt --check` and `vp lint` are clean on all three changed files at head.
- Proof audit pass 1 (Claude Opus 5.5): fixture matrix F1–F9 plus light/dark. Long provider names can't occur, because the widget hard-codes "Codex"/"Claude", so F8 covers long window labels.
- Proof audit pass 2 (Claude Opus 5.5): **SUFFICIENT** for not-yet-reset, already-reset, all-reset and long labels, light and dark, on Android. SHAs were checked against the capture log. The auditor rebuilt F2 from the committed script in a scratch worktree: before expired at 11:09Z ("Open T3 to refresh"); after fresh until 11:39Z with "Next reset Oct 29, 11:24 AM" and "Reset Oct 22, 11:09 AM", matching the screenshot text for text.
- Caveats from audit 2:
  - F3-light-before was shot 28 min after its anchor, so it doesn't separate the two versions; F3-dark is the F3 proof.
  - F8-light-before has no capture-log line.
  - F8's past window is hidden by the 2-window limit, so a long label next to "Reset" is covered only by unit tests.
  - The matrix wrongly predicted a "2 accounts · pooled" line for F7a; Android shows the detail line only when no windows are shown.
- CI at the time of writing: 28 successful, 2 failing (`Lint`, `Check`). The status doesn't say which head they ran on; the earlier push `21250b531f` failed lint, and lint and format are clean locally at `0293255099`.
- Unverified rows:
  - iOS, all rows: the widget is native WidgetKit via expo-widgets, the repo has no static fixture mode, and this Linux VM has no simulator.
  - Android: F7a light and F9 (an invalid capture, then out of time); F4, F5, F6 and F7b not attempted (budget).
- Model/harness: Claude Opus 5.5 through the Claude Agent harness in T3 Code (evidence lead, proof auditor ×2, evidence capturer).

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

