Repository navigation
STU-383: Build the account overview with real identity, usage and honest history states - #89
Conversation
…est history states Rebuild the account view into a single-source-overview bound to the same session/subscription/metered state as the topbar (renderAccountOverview feeds from renderUsageSurfaces): identity from the signed-in email, real plan/price, a metered usage meter, and honest period information. A renewal/reset date is shown only from a real subscriptionState.periodEnd — never fabricated. The per-run breakdown/history the redesign illustrated is not persisted, so the overview renders a truthful no-history state and keeps current results reachable via the generation workspace. Guest/loading/unresolved states are preserved and never surface stale private identity. Verified: npm test (290), npm run build, and a targeted Playwright spec whose load-bearing assertions were falsified (break -> FAIL -> restore -> PASS).
…the screenshot test on layout
Finding 1 (BLOCKING): the account view title was a second <h1>, breaking the
smoke spec's page.locator('h1') strict-mode assertion and turning CI red.
The account view's title is now an <h2> so the document keeps a single
top-level heading and the existing smoke selector resolves to one element
(duplicate h1 was the defect, not the assertion; verified 1/6 FAIL -> 6/6 PASS).
Finding 2: the connected branch of the FlutterFlow connection card was never
exercised (every test ran keyless). Added a test that seeds real encrypted-at-
rest FlutterFlow credentials (AES-GCM under a session-migrated JWK, decrypted
through the app's genuine getApiKey path) and asserts 'Connected to
FlutterFlow', the ok dot, the default-project row and the endpoint label.
Falsified: breaking the connected branch fails the test; restoring passes.
Finding 3: the screenshot test captured PNGs without asserting anything,
so any visual regression passed unnamed. Relabelled it explicitly as a
human-review artifact (pixels not compared) and added a genuine falsifiable
layout gate (no horizontal overflow, card inside the viewport, non-empty).
That gate caught a real pre-existing mobile overflow (the connection row's
Configure button pushed past 360px by the long status text), which is fixed
with a mobile-only flex-wrap. Screenshots regenerated to reflect the fix.
Falsified with an injected 8000px wide element (FAIL at 8389px) then restore.
Full CI sequence green: npm test (290), npm run build, 57 browser tests,
accessibility (7 shells), deploy-guard (9/9), proxies (30/30), leak scan.
c646f36
into
milestone/stu-implement-updated-ui-ux
| // --- FlutterFlow connection (real stored credential state) --- | ||
| const connKeyConfigured = hasStoredKey("flutterflow"); | ||
| const connProjectConfigured = hasStoredKey("flutterflow_project_id"); | ||
| const ffDot = document.getElementById("acct-ff-dot"); | ||
| const ffStatus = document.getElementById("acct-ff-status"); | ||
| const ffEndpoint = document.getElementById("acct-ff-endpoint"); | ||
| const ffProject = document.getElementById("acct-ff-project"); | ||
| if (ffStatus) { | ||
| if (connKeyConfigured && connProjectConfigured) { | ||
| ffStatus.textContent = "Connected to FlutterFlow"; | ||
| if (ffDot) { ffDot.className = "acct-dot ok"; } | ||
| } else if (connKeyConfigured) { | ||
| ffStatus.textContent = "API key set — pick a project"; | ||
| if (ffDot) { ffDot.className = "acct-dot warn"; } | ||
| } else { | ||
| ffStatus.textContent = "Not connected — add your FlutterFlow API key"; | ||
| if (ffDot) { ffDot.className = "acct-dot"; } | ||
| } | ||
| } | ||
| if (ffEndpoint) { | ||
| const endpoint = getFlutterFlowEndpoint(); | ||
| ffEndpoint.textContent = endpoint && endpoint.includes("staging") ? "Staging" : "Production"; | ||
| } |
There was a problem hiding this comment.
🟡 Connection card stays stale
After credential or endpoint changes, renderAccountOverview does not rerun. The account card keeps its old connection, project, and endpoint values.
Learn more
The account card reads decrypted credentials and the endpoint only when a usage render invokes renderAccountOverview. Credential saves and clears call initializeApiKeys, while the endpoint selector calls setFlutterFlowEndpoint; neither path refreshes this card. The pending identity request can hide the issue during initial page load, but it does not help after a user changes settings.
Example: A user opens Account while disconnected, then configures an API key and project. Closing the modal leaves “Not connected” and “—” visible even though deployment now uses the saved values.
Recommended fix: Refresh the account overview after initializeApiKeys updates flutterflowApiKey and flutterflowProjectId, and after setFlutterFlowEndpoint persists a new endpoint. Keep the refresh independent of usage reconciliation.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const periodEndMs = Date.parse(subscriptionState.periodEnd || ""); | ||
| const hasRealPeriodEnd = resolved && tier !== "free" && !!subscriptionState.periodEnd && !Number.isNaN(periodEndMs); |
There was a problem hiding this comment.
🟡 Stripe timestamps hide renewal dates
When periodEnd is a Unix timestamp, Date.parse does not interpret its seconds value correctly. Paid accounts then report no renewal date.
Learn more
The subscription normalizer accepts subscription.current_period_end, the raw Stripe field, in normalizeSubscriptionResponse. Stripe-shaped values use Unix seconds, while this renderer only handles date strings accepted by Date.parse.
Example: A response containing current_period_end: 1790812800 reaches subscriptionState.periodEnd. The account prints “Renewal date is not available for this period” instead of the corresponding date.
Recommended fix: Normalize period-end values into milliseconds in normalizeSubscriptionResponse, or branch here for finite numeric values and multiply Unix seconds by 1,000. Validate the resulting Date before rendering.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const hasRealPeriodEnd = resolved && tier !== "free" && !!subscriptionState.periodEnd && !Number.isNaN(periodEndMs); | ||
| if (loading) renewal.textContent = ""; | ||
| else if (!resolved) renewal.textContent = ""; | ||
| else if (tier === "free") renewal.textContent = ""; | ||
| else if (hasRealPeriodEnd) { | ||
| const date = new Date(periodEndMs).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" }); | ||
| renewal.textContent = `Renews ${date} · reset at the start of each month.`; |
There was a problem hiding this comment.
🟡 Canceled plans appear to renew
A canceled non-free plan with periodEnd is labeled “Renews” because subscriptionState.status is ignored. The account presents an ended subscription as recurring.
Learn more
The normalized subscription retains its status in normalizeSubscriptionResponse, but the account view bases renewal copy only on tier and period end. A period end can mark the end of access rather than the next renewal for canceled or non-renewing subscriptions.
Example: A response with tier: "professional", status: "canceled", and a future periodEnd displays “billed monthly through Stripe” and “Renews …”. The date actually marks when access ends.
Recommended fix: Derive renewal copy from the normalized status and any cancellation flag supplied by the subscription API. Use renewal language only for recurring statuses; otherwise label the date as the access or subscription end.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const signedIn = authState.isVerified && !!authState.email; | ||
| if (!signedIn) { | ||
| // Signed-out/guest state has its own container; leave the signed-in | ||
| // overview untouched in the hidden subtree so no previous identity lingers | ||
| // anywhere a later sign-in could briefly flash it. | ||
| return; |
There was a problem hiding this comment.
| sub: () => professionalSubscription(), | ||
| }, | ||
| { name: "free-zero-usage", email: "zero@example.com", count: 0, sub: () => freeSubscription() }, | ||
| { name: "power-max-usage", email: "max@example.com", count: 50, sub: () => professionalSubscription() }, |
STU-383 — Build the account overview with real identity, usage and honest history states
Implements the account overview bound to the same session/subscription/metered state the topbar reads. Linear: https://linear.app/stuart-gardoll/issue/STU-383
What changed
BASE_PRICES_AUD/formatPrice), billing note; renewal date is shown only whensubscriptionState.periodEndis a real value; with no periodEnd the view plainly says "Renewal date is not available for this period." Nothing invented.getUsage()/getRunLimit()(same source as the topbar); used/limit/percent and#usage-counterconverge on one metered value.getFlutterFlowEndpoint(), and the real sign-in/key-storage facts; buttons wire toopenApiKeysModal,openCustomerPortal,switchView('plans'),handleSignOut,openSignInModal.authState, and the signed-in overview is hidden when signed out).renderAccountOverview()is called from the same adapter path (renderUsageSurfaces) so sign-in/out and a usage refresh move topbar + account together.Acceptance criteria — verified
Test commands + results
npm test→ 290 passed, 0 failed (source untouched-master count).npm run build→ success.CCC_TEST_PORT=3093 npx playwright test e2e/stu383-account-overview.spec.js→ 11 passed.CCC_TEST_PORT=3093 npx playwright test e2e/stu381-monthly-usage.spec.js(sibling touching#usage-counter) → 14 passed, no regression.Falsification evidence (break → FAIL → restore → PASS)
Per repo house rule, each load-bearing assertion was proven to genuinely fail:
#acct-no-history atimeout) → restored → PASS.!resolved → "—"guard so it rendered a number while loading → FAILED (Expected "…", got "0") → restored → PASS.#acct-used-countas hardcoded "0" → FAILED (Expected "31") → restored → PASS.Screenshots (reference comparisons)
screenshots/stu-383/account-professional-31-50-long-email-desktop.png/-mobile.png(long email)screenshots/stu-383/account-free-zero-usage-desktop.png/-mobile.png(zero usage)screenshots/stu-383/account-power-max-usage-desktop.png/-mobile.png(max usage)screenshots/stu-383/account-unresolved-desktop.png/-mobile.png(unresolved subscription)What is verified and what is NOT
dist/was left untouched (CI builds it); the diff is source + tests + screenshots only. No rebase/force-push; pushed once.