Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4744,6 +4744,147 @@ function renderUsageSurfaces() {
guestEl.textContent = `${gCount} / ${TIER_LIMITS.free} generations used`;
}
}

renderAccountOverview();
}

/**
* Renders the account overview: identity, plan, usage meter and available
* period information. This is the single presentation adapter for the account
* view and reads the same session, subscription and metered state as the
* topbar (renderUsageSurfaces), so sign-in/out and a usage refresh move all
* surfaces together.
*
* Honesty contract, enforced by render: every number/date/identity traces to a
* real field or a documented derivation (email-derived display name/avatar
* initial, metered count, tier limit). A renewal/reset date is shown ONLY when
* subscriptionState.periodEnd is a real value — never fabricated — and an
* unresolved plan surfaces checking/unavailable copy, never a stale balance.
*/
function renderAccountOverview() {
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;
Comment on lines +4765 to +4770

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.

}

const loading = isSubscriptionLoading();
const resolved = isSubscriptionResolved();
const usage = getUsage();
const count = resolved ? usage.count : 0;
const limit = resolved ? getRunLimit() : 0;
const remaining = Math.max(0, limit - count);
const tier = resolved ? (subscriptionState.tier || "free") : null;
const tierLabel = { free: "Free", professional: "Pro", power: "Power" }[tier] || "Free";

// --- identity: real email, documented derivations only, no member-since ---
const email = authState.email || "";
const localPart = email.split("@")[0] || email;
const avatar = document.getElementById("acct-avatar");
if (avatar) avatar.textContent = (email[0] || "?").toUpperCase();
const name = document.getElementById("acct-name");
if (name) name.textContent = localPart || "—";

// --- plan ---
const planTier = document.getElementById("acct-plan-tier");
const planPrice = document.getElementById("acct-plan-price");
const planNote = document.getElementById("acct-plan-note");
const renewal = document.getElementById("acct-renewal");
if (planTier) {
planTier.textContent = loading ? "Checking…" : resolved ? tierLabel : "Plan unavailable";
}
if (planPrice) {
if (loading) planPrice.textContent = "—";
else if (!resolved) planPrice.textContent = "—";
else if (tier === "free") planPrice.textContent = "$0 / month";
else {
const money = formatPrice(BASE_PRICES_AUD[tier] ?? 0, detectUserCurrency());
planPrice.textContent = `${money} / month`;
}
}
if (planNote) {
if (loading) planNote.textContent = "Verifying your subscription…";
else if (!resolved) planNote.textContent = "Could not verify your subscription. Check billing or sign out.";
else if (tier === "free") planNote.textContent = "No subscription. Upgrade for more generations.";
else planNote.textContent = "billed monthly through Stripe";
}
if (renewal) {
const periodEndMs = Date.parse(subscriptionState.periodEnd || "");
const hasRealPeriodEnd = resolved && tier !== "free" && !!subscriptionState.periodEnd && !Number.isNaN(periodEndMs);
Comment on lines +4814 to +4815

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.

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

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.

} else {
// No fabricated date: if the plan has no real period end we say so plainly.
renewal.textContent = "Renewal date is not available for this period.";
}
}

// --- usage: generations left + meter ---
const leftCount = document.getElementById("acct-left-count");
const leftLimit = document.getElementById("acct-left-limit");
const resetNote = document.getElementById("acct-reset-note");
if (leftCount) leftCount.textContent = loading ? "…" : !resolved ? "—" : String(remaining);
if (leftLimit) leftLimit.textContent = loading ? "…" : !resolved ? "—" : String(limit);
if (resetNote) {
if (loading) resetNote.textContent = "Checking plan…";
else if (!resolved) resetNote.textContent = "Plan check failed. Refresh or manage billing.";
else resetNote.textContent = "Runs reset at the start of each month.";
}

const usedCount = document.getElementById("acct-used-count");
const meterFill = document.getElementById("acct-meter-fill");
const meterMeta = document.getElementById("acct-meter-meta");
const meterTrack = document.getElementById("acct-meter-track");
if (usedCount) usedCount.textContent = loading ? "…" : !resolved ? "—" : String(count);
if (meterFill) {
const pct = resolved && limit > 0 ? Math.min(100, Math.round((count / limit) * 100)) : 0;
meterFill.style.width = `${pct}%`;
}
if (meterMeta) {
if (loading) meterMeta.textContent = "Checking…";
else if (!resolved) meterMeta.textContent = "Plan check failed";
else if (limit === 0) meterMeta.textContent = `${count} of ${limit}`;
else {
const pct = Math.round((count / limit) * 100);
meterMeta.textContent = `${count} of ${limit} · ${pct}%`;
}
}
if (meterTrack) {
if (loading) meterTrack.setAttribute("aria-label", "Checking usage…");
else if (!resolved) meterTrack.setAttribute("aria-label", "Usage unavailable");
else meterTrack.setAttribute("aria-label", `${count} of ${limit} generations used`);
}

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

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.

if (ffProject) ffProject.textContent = flutterflowProjectId || "—";
}

function updateUsageDisplay() {
Expand Down
Loading
Loading