Skip to content

STU-383: Build the account overview with real identity, usage and honest history states - #89

Merged
sgardoll merged 2 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu-383-build-the-account-overview-with-real-identity-usage-and
Sep 22, 2026
Merged

sgardoll merged 2 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu-383-build-the-account-overview-with-real-identity-usage-and

Conversation

@sgardoll

Copy link
Copy Markdown
Owner

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

  • Identity — account heading + identity card: display name (documented derivation: local part of the real signed-in email), full email, avatar initial, "Signed by email magic link." No fabricated member-since date.
  • Plan — real tier + real price (BASE_PRICES_AUD/formatPrice), billing note; renewal date is shown only when subscriptionState.periodEnd is a real value; with no periodEnd the view plainly says "Renewal date is not available for this period." Nothing invented.
  • Usage — generations-left stat and a meter, both from getUsage()/getRunLimit() (same source as the topbar); used/limit/percent and #usage-counter converge on one metered value.
  • Honest history — the illustrated per-run breakdown/history is not persisted, so instead of fake rows the overview renders an explicit "Saved history isn't available yet" state and keeps current results reachable through the generation workspace (Home).
  • Connection / Provider keys / Access — reachable cards backed by real stored FlutterFlow credential state, getFlutterFlowEndpoint(), and the real sign-in/key-storage facts; buttons wire to openApiKeysModal, openCustomerPortal, switchView('plans'), handleSignOut, openSignInModal.
  • Guest, loading and unresolved-subscription states are preserved and never surface another user's cached identity (identity renders only from authState, and the signed-in overview is hidden when signed out).
  • Bind to topbar: renderAccountOverview() is called from the same adapter path (renderUsageSurfaces) so sign-in/out and a usage refresh move topbar + account together.

Acceptance criteria — verified

  • Every shown number/date/identity traces to an existing field or a documented derivation; no sample activity and no fabricated renewal date. — honest renewal copy is asserted; the test was falsified (see below).
  • Account reflects sign-in/out and usage refresh without stale private information. — e2e signs in as a long-email user, signs out (overview hides), and a reloaded metered count moves the account to the new number for the same user.
  • No-history state is truthful and readable; current results remain available through the generation workspace. — asserted copy + a real click into Home; falsified by removing the link.
  • Desktop/mobile reference comparisons cover long email, zero/max usage and unresolved subscription. — captured at 360×800 and 1440×900 (see Screenshots).

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:

  1. No fabricated renewal date — temporarily coded a fake "Renews 1 October 2026" for the no-periodEnd case → test FAILED (Expected "Renewal date is not available for this period.") → restored honest copy → PASS.
  2. Identity derivation — coded display name as the full email instead of the local part → FAILED (Expected local part) → restored → PASS.
  3. No-history workspace link — removed the generation-workspace anchor → FAILED (locator #acct-no-history a timeout) → restored → PASS.
  4. Unresolved subscription, no fake balance — removed the !resolved → "—" guard so it rendered a number while loading → FAILED (Expected "…", got "0") → restored → PASS.
  5. Usage meter traces to metered count — coded #acct-used-count as 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

  • Verified: the account overview renders real, metered, honest data; sign-in/out and usage refresh behaviour; truthful no-history state; reachable connection/access/key sections bound to the real stored credential state; guest/loading/unresolved states; the full viewport matrix is behaviourally exercised on both 360×800 and 1440×900.
  • NOT verified (candidate for the independent senior reviewer): the complete nine-viewport visual matrix is left to final acceptance. The FlutterFlow connection and access full surfaces are owned by their respective issues; this issue supplies reachable, honest connection/access cards (real FF key/endpoint/project state), not the fully-built owning surfaces — if those owning issues are not yet landed, the connection card shows only what the browser genuinely knows (FF key configured or not, endpoint, project). No new persistence backend was added (per instructions).
  • dist/ was left untouched (CI builds it); the diff is source + tests + screenshots only. No rebase/force-push; pushed once.

…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.
@sgardoll
sgardoll marked this pull request as ready for review September 22, 2026 16:08
@sgardoll
sgardoll merged commit c646f36 into milestone/stu-implement-updated-ui-ux Sep 22, 2026
3 checks passed

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Devin Review

Comment thread app.js
Comment on lines +4864 to +4886
// --- 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";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app.js
Comment on lines +4814 to +4815
const periodEndMs = Date.parse(subscriptionState.periodEnd || "");
const hasRealPeriodEnd = resolved && tier !== "free" && !!subscriptionState.periodEnd && !Number.isNaN(periodEndMs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app.js
Comment on lines +4815 to +4821
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.`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app.js
Comment on lines +4765 to +4770
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Sign-out leaves hidden account data

renderAccountOverview preserves the prior account fields after sign-out. This conflicts with the stated clearing guarantee, while tests assert only container visibility.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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() },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Power screenshots use Pro data

The power-max-usage scenario uses professionalSubscription() and a 50-run limit. The claimed Power-plan reference is therefore not exercised.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant