feat(FN-266): write post-P0 subscription UX audit deliverable
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged: - feat(FN-266): write post-P0 subscription UX audit deliverable Files changed: docs/design-specs/post-p0-subscription-audit.md | 351 ++++++++++++++++++++++++ 1 file changed, 351 insertions(+) Fusion-Task-Id: FN-266
This commit is contained in:
351
docs/design-specs/post-p0-subscription-audit.md
Normal file
351
docs/design-specs/post-p0-subscription-audit.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# Post-P0 Subscription Page UX Audit
|
||||
|
||||
**Date:** 2026-05-13
|
||||
**Auditor:** Designer (agent-08d09be5)
|
||||
**Source file:** `apps/web/src/routes/dashboard/subscription/index.tsx` (1233 lines)
|
||||
**Context:** P0 CRO overhaul across FN-203 (P0-1–6) and FN-199 (P0-7–10), shipped without pre-merge QA review.
|
||||
|
||||
---
|
||||
|
||||
## Audit Summary
|
||||
|
||||
| # | Area | Severity | Status |
|
||||
|---|------|----------|--------|
|
||||
| 1 | Pricing cards | — | ✅ PASS |
|
||||
| 2 | CTA progression | — | ✅ PASS |
|
||||
| 3 | Order summary | — | ✅ PASS |
|
||||
| 4 | Trust copy | P1 | ⚠️ Hardcoded aria-labels (not i18n) |
|
||||
| 5 | Payment trust badges | P1 | ⚠️ Text badges instead of card brand SVGs |
|
||||
| 6 | Skeleton loading | **P0** | ❌ Layout shift: md:grid-cols-2 vs lg:grid-cols-4 |
|
||||
| 7 | Feature matrix | — | ✅ PASS |
|
||||
| 8 | Trial urgency banner | — | ✅ PASS |
|
||||
| 9 | Cancel-save flow | P1 | ⚠️ max-w-md tight on narrow mobile |
|
||||
| 10 | Sticky checkout bar | P2 | ⚠️ Missing safe-area padding |
|
||||
|
||||
**Result: CONDITIONAL-PASS — 1 P0 regression, 3 P1 polish issues, 1 P2 observation.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Pricing Cards — ✅ PASS
|
||||
|
||||
**Lines:** 884–930 (plan comparison grid)
|
||||
|
||||
- **Grid layout:** `sm:grid-cols-2 lg:grid-cols-4` — correct 4-column layout on large screens.
|
||||
- **Hover state:** `hover:shadow-md` transition on each `Card` — correct micro-interaction.
|
||||
- **Selected state:** `border-primary ring-2 ring-primary/20 dark:ring-primary/25` — clear visual feedback.
|
||||
- **"Popüler" badge:** Absolute positioned `-top-3 left-1/2 -translate-x-1/2` — centered above card edge.
|
||||
- **"Mevcut Plan" badge:** Absolute positioned `-top-3 right-4` with green colorway (`border-green-500 bg-green-50 text-green-700`) — visually distinct from "Popüler".
|
||||
- **Popular card styling:** `border-primary/40`, `ring-2 ring-primary/25`, `shadow-brand`, `bg-primary/[0.07]` — elevated visual treatment.
|
||||
- **Current plan card styling:** `border-green-500/50 bg-green-50/50` with muted CTA (non-button `<div>` with `bg-muted`) — correctly prevents re-selecting current plan.
|
||||
- **Button progression:** "Plan Seç" → "Devam Et" on selection. Button variant transitions: unselected non-popular gets `outline`, selected/popular gets `default`.
|
||||
|
||||
**Verdict:** All states, badges, and interactions render per design intent. No issues found.
|
||||
|
||||
---
|
||||
|
||||
## 2. CTA Progression — ✅ PASS
|
||||
|
||||
**Lines:** 924–929 (card footer button), 984–1026 (proceed button), 1036–1042 (order summary conditional)
|
||||
|
||||
- **Button text transition:** `t("subscription.choosePlan")` → `t("subscription.proceed")` (`"Plan Seç"` → `"Devam Et"`).
|
||||
- **Button variant transition:** Outline → filled (default) on selection for non-popular plans.
|
||||
- **aria-pressed:** Set to `isSelected` — screen readers get pressed-state feedback.
|
||||
- **Order summary visibility:** Conditionally renders only when `selectedPlanKey` is truthy (line ~990).
|
||||
- **Order summary content:** Plan name, billing period, brand count, total price with period label — all using i18n keys.
|
||||
- **Proceed to payment:** `handleProceedToPayment()` validates brand selection for non-full plans, fires PostHog `checkout_started`, navigates to `/dashboard/subscription/pay` with plan/period/brands in search params.
|
||||
|
||||
**Verdict:** CTA flow is correct end-to-end. No issues found.
|
||||
|
||||
---
|
||||
|
||||
## 3. Order Summary — ✅ PASS
|
||||
|
||||
**Lines:** 990–1033
|
||||
|
||||
- **Card structure:** `Card > CardHeader (title) + CardContent (line items)`. Correct visual hierarchy.
|
||||
- **Line items:** Plan name, billing period, number of brands selected, separated by `Separator`, then total price.
|
||||
- **Price display:** `formatTRY(summaryPrice)` with period label suffix.
|
||||
- **Conditional brand line:** Only renders when `selectedBrandIds.length > 0`.
|
||||
- **Preceded by:** `Separator` before the order summary card — clean visual separation from plan cards.
|
||||
|
||||
**Verdict:** Order summary renders correctly with all required data points. No issues found.
|
||||
|
||||
---
|
||||
|
||||
## 4. Trust Copy — ⚠️ P1 (Hardcoded aria-labels)
|
||||
|
||||
**Lines:** 1068–1112
|
||||
|
||||
### What works:
|
||||
- Three trust items rendered with correct i18n keys:
|
||||
- `subscription.paymentTrustSSL` → "256-bit SSL" with `🔒` emoji
|
||||
- `subscription.paymentTrustProvider` → "Iyzico altyapısı" with `·` separator
|
||||
- `subscription.paymentTrustKVKK` → "KVKK uyumlu" with link to `/kvkk`
|
||||
- Semantic `<section>` wrapper with descriptive `aria-label`.
|
||||
- KVKK link has `underline underline-offset-2 hover:text-foreground` for affordance and hover feedback.
|
||||
|
||||
### Issues:
|
||||
1. **Line 1068 — Hardcoded `aria-label`:** `aria-label="Ödeme güvencesi"` is a Turkish string literal. Should use `t("subscription.paymentTrustAriaLabel")` or equivalent i18n key.
|
||||
|
||||
```tsx
|
||||
// Current (line 1068):
|
||||
<section aria-label="Ödeme güvencesi" ...>
|
||||
|
||||
// Recommended:
|
||||
<section aria-label={t("subscription.paymentTrustAriaLabel")} ...>
|
||||
```
|
||||
|
||||
**Severity:** P1 — Missed i18n; English-speaking screen reader users get Turkish label.
|
||||
|
||||
**Recommended fix:**
|
||||
- Add `"paymentTrustAriaLabel": "Ödeme güvencesi"` to `tr.json` and `"paymentTrustAriaLabel": "Payment security"` to `en.json` under `subscription`.
|
||||
- Replace the hardcoded string with `t("subscription.paymentTrustAriaLabel")`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Payment Trust Badges — ⚠️ P1 (Text instead of card brand SVGs)
|
||||
|
||||
**Lines:** 1081–1112
|
||||
|
||||
### What works:
|
||||
- Four card brands rendered: Visa, Mastercard, Troy, American Express.
|
||||
- Each inside `<Badge variant="outline">` with consistent styling.
|
||||
- `role="img"` on parent with descriptive `aria-label`.
|
||||
- Individual badges marked `aria-hidden="true"` — correct accessibility pattern.
|
||||
|
||||
### Issues:
|
||||
1. **Lines 1085–1112 — Text badges instead of card brand logos/SVGs.** Industry standard for payment trust sections is to use recognizable card brand logos (SVGs or icon font), not plain text. Text badges lack visual authority and brand recognition.
|
||||
|
||||
```tsx
|
||||
// Current (lines 1085–1112):
|
||||
<Badge variant="outline" className="px-1.5 py-0 text-xs font-normal" aria-hidden="true">
|
||||
Visa
|
||||
</Badge>
|
||||
// ... Mastercard, Troy, AmEx
|
||||
|
||||
// Recommended:
|
||||
<CardBrandIcon brand="visa" className="h-5 w-auto opacity-70" />
|
||||
// Using lucide-react or simple SVGs for each card brand
|
||||
```
|
||||
|
||||
2. **Line 1081 — Hardcoded `aria-label`:** `"Kabul edilen kartlar: Visa, Mastercard, Troy, American Express"` is a Turkish string literal. Should use an i18n key.
|
||||
|
||||
```tsx
|
||||
// Current (line 1081):
|
||||
aria-label="Kabul edilen kartlar: Visa, Mastercard, Troy, American Express"
|
||||
|
||||
// Recommended:
|
||||
aria-label={t("subscription.paymentTrustAcceptedCards")}
|
||||
```
|
||||
|
||||
**Severity:** P1 — Visual downgrade from established design patterns. Missed i18n for screen reader label.
|
||||
|
||||
**Recommended fix:**
|
||||
- Add card brand SVG icons (Visa, Mastercard, Troy, AmEx) to `@sase/ui` or inline as simple SVGs.
|
||||
- Replace text `Badge` components with icon components.
|
||||
- Add `"paymentTrustAcceptedCards"` i18n key to both `tr.json` and `en.json`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Skeleton Loading — ❌ P0 (Layout shift)
|
||||
|
||||
**Lines:** 396–406 (skeleton) vs 884 (real grid)
|
||||
|
||||
### Issue:
|
||||
The skeleton loading state does not match the real layout, causing a visible layout shift on load.
|
||||
|
||||
```tsx
|
||||
// Skeleton (lines 400–402):
|
||||
<div className="grid gap-4 md:grid-cols-2"> // ← md breakpoint, 2 cols
|
||||
<Skeleton className="h-48 w-full" /> // ← only 2 placeholders
|
||||
<Skeleton className="h-48 w-full" />
|
||||
</div>
|
||||
|
||||
// Real grid (line 884):
|
||||
<div ref={planCardsRef} className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
// ← sm breakpoint (2 cols), lg breakpoint (4 cols)
|
||||
// ← 4 plan cards rendered via plans.map()
|
||||
</div>
|
||||
```
|
||||
|
||||
**Three mismatches:**
|
||||
1. **Breakpoint:** Skeleton uses `md:` (768px), real grid uses `sm:` (640px) → skeleton stays 1-col between 640–767px while real grid would be 2-col.
|
||||
2. **Column count:** Skeleton is always 2-col on md+, real grid is 4-col on lg+ → visible expansion on lg screens.
|
||||
3. **Item count:** Skeleton renders 2 placeholders, real grid renders 4 cards.
|
||||
|
||||
**Impact:** On desktop (lg+), the page loads showing 2 skeleton cards in a 2-column grid, then abruptly shifts to 4 cards in a 4-column grid. This is a Cumulative Layout Shift (CLS) issue.
|
||||
|
||||
**Severity:** P0 — Core Web Vitals regression (CLS). Degrades perceived performance.
|
||||
|
||||
**Recommended fix:**
|
||||
```tsx
|
||||
// Recommended skeleton:
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-48 w-full" />
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
Also consider adding skeleton placeholders for the title, billing toggle, Separator, and FeatureMatrix to avoid a multi-phase load appearance.
|
||||
|
||||
---
|
||||
|
||||
## 7. Feature Matrix — ✅ PASS
|
||||
|
||||
**Lines:** 1184–1233
|
||||
|
||||
- **Table structure:** Proper `<table>` with `<thead>` and `<tbody>` — semantically correct.
|
||||
- **Column headers:** `aria-label` on each `<th>` with plan name i18n key — accessible.
|
||||
- **Popular column:** Gets `text-primary` color styling — visual hierarchy.
|
||||
- **Feature rows:** Alternating `bg-background` / `bg-muted/25` for zebra striping.
|
||||
- **Check/Minus icons:** `Check` (text-primary) for included features, `Minus` (muted/40) for excluded — clear visual distinction.
|
||||
- **Icon aria-labels:** `t("common.yes")` / `t("common.no")` — screen reader friendly.
|
||||
- **Horizontal scroll:** `overflow-x-auto` wrapper — works on narrow viewports.
|
||||
- **Band positioning:** Placed between plan cards and order summary — logical flow.
|
||||
|
||||
**Verdict:** Feature comparison table renders correctly with proper accessibility. No issues found.
|
||||
|
||||
---
|
||||
|
||||
## 8. Trial Urgency Banner — ✅ PASS
|
||||
|
||||
**Component:** `apps/web/src/components/trial-urgency-banner.tsx`
|
||||
**Integration:** `apps/web/src/routes/dashboard.tsx:386`
|
||||
|
||||
### What was verified:
|
||||
1. **Component exists and is implemented:** Full component at `components/trial-urgency-banner.tsx` with proper React patterns.
|
||||
2. **Rendered in dashboard layout:** `<TrialUrgencyBanner />` at top of `<main>` in `dashboard.tsx` — visible on all dashboard pages including subscription.
|
||||
3. **Display logic:** Shows only when `status === "trial"`, `endDate` exists, and `≤ 3 days` remain.
|
||||
4. **Dismiss mechanism:** Stores dismissal in `localStorage` keyed by endDate (`trialBannerDismissed-{endDate}`) — persists across sessions until trial end date changes.
|
||||
5. **i18n keys:** Uses `subscription.trialBanner.title`, `.description`, `.cta` — all present in both `tr.json` and `en.json`.
|
||||
6. **Visual design:** Amber colorway (`bg-amber-50`, `border-amber-200`, `text-amber-900`) with `AlertTriangle` icon — consistent urgency signaling.
|
||||
7. **CTA:** Links to `/dashboard/subscription` with "Plana Yükselt" button — correct navigation target.
|
||||
|
||||
**Verdict:** Trial urgency banner is fully implemented and correctly integrated. **The previous audit's claim of "component never implemented" is incorrect.** The component was likely built as part of FN-209 and integrated into the dashboard layout (not the subscription page directly, which is correct since it's a site-wide banner).
|
||||
|
||||
---
|
||||
|
||||
## 9. Cancel-Save Flow — ⚠️ P1 (Dialog width on mobile)
|
||||
|
||||
**Lines:** 636–751 (downgrade offer dialog), 754–789 (cancel confirm dialog)
|
||||
|
||||
### What works:
|
||||
- **Downgrade dialog:** Shows when subscription is "active" and a lower-tier plan exists.
|
||||
- Displays target plan with savings calculation (`formatTRY`).
|
||||
- Brand selector for non-full plans with proper maxBrands constraint.
|
||||
- "Yine de İptal Et" → opens cancel confirm dialog. "Bu plana geç" → calls downgrade API.
|
||||
- PostHog tracking: `downgrade_offer_shown`, `downgrade_offer_declined`, `downgrade_offer_accepted`, `cancel_save_clicked`.
|
||||
- **Cancel confirm dialog:** Standard confirmation with destructive button.
|
||||
- PostHog tracking: `cancel_flow_viewed`, `subscription_cancelled`.
|
||||
- **Fallback:** When no downgrade path exists (`currentPlanKey` is "brand1"), cancel dialog opens directly.
|
||||
|
||||
### Issues:
|
||||
1. **Line 637 — `max-w-md` may overflow on narrow mobile screens.** `max-w-md` = 28rem (448px). On a 320px-wide device, the dialog exceeds viewport width.
|
||||
|
||||
```tsx
|
||||
// Current (line 637):
|
||||
<DialogContent className="max-w-md">
|
||||
|
||||
// Recommended:
|
||||
<DialogContent className="max-w-[calc(100vw-2rem)] sm:max-w-md">
|
||||
```
|
||||
|
||||
**Severity:** P1 — Dialog content may be cut off on iPhone SE-class devices.
|
||||
|
||||
**Recommended fix:** Add `max-w-[calc(100vw-2rem)]` as a mobile-safe fallback, with `sm:max-w-md` for larger screens.
|
||||
|
||||
---
|
||||
|
||||
## 10. Sticky Checkout Bar — ⚠️ P2 (Missing safe-area padding)
|
||||
|
||||
**Lines:** 1119–1146
|
||||
|
||||
### What works:
|
||||
- **IntersectionObserver:** Watches `planCardsRef` with `threshold: 0, rootMargin: "-80px 0px 0px 0px"` — bar appears when plan cards scroll past 80px below viewport top.
|
||||
- **Appearance condition:** Only when `stickyBarVisible && selectedPlanKey` — correct guard.
|
||||
- **Animation:** `animate-fade-in-up` (0.5s cubic-bezier) — smooth entrance.
|
||||
- **Backdrop blur:** `bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80` — frosted glass effect.
|
||||
- **Content:** Plan name, price with period label, "Devam Et" button → mirrors order summary.
|
||||
- **Width:** `max-w-5xl` inner container — matches page content width.
|
||||
|
||||
### Issues:
|
||||
1. **Line 1131 — No safe-area padding for notched devices.** The bar uses `fixed inset-x-0 bottom-0` but does not account for the iOS home indicator area.
|
||||
|
||||
```tsx
|
||||
// Current (line 1131):
|
||||
<div className="fixed inset-x-0 bottom-0 z-50 animate-fade-in-up ...">
|
||||
|
||||
// Recommended:
|
||||
<div className="fixed inset-x-0 bottom-0 z-50 animate-fade-in-up pb-safe ...">
|
||||
```
|
||||
|
||||
Without `pb-safe` (or `pb-[env(safe-area-inset-bottom)]`), the bar's content may be partially obscured by the home indicator on iPhone X and newer.
|
||||
|
||||
**Severity:** P2 — Cosmetic issue affecting only notched iPhone users. Content remains visible and interactive; only the bottom padding is suboptimal.
|
||||
|
||||
**Recommended fix:** Add `pb-safe` utility class (requires Tailwind safe-area plugin) or inline `pb-[env(safe-area-inset-bottom,0px)]`.
|
||||
|
||||
---
|
||||
|
||||
## Additional Observations
|
||||
|
||||
### A. planNameToKey indirection (P2)
|
||||
|
||||
**Lines:** 120–131 (definition), 231 (usage)
|
||||
|
||||
```tsx
|
||||
// Line 231:
|
||||
const currentPlanKey = subscription?.plan?.name
|
||||
? planNameToKey(subscription.plan.name)
|
||||
: null;
|
||||
```
|
||||
|
||||
The `Subscription` interface includes `plan?: { name: string; key: string }`, so `subscription.plan.key` is available but unused. The `planNameToKey()` function maps Turkish plan names to internal keys, adding an unnecessary failure point if plan names change or the user's locale differs.
|
||||
|
||||
```tsx
|
||||
// Recommended:
|
||||
const currentPlanKey = subscription?.plan?.key ?? null;
|
||||
```
|
||||
|
||||
**Severity:** P2 — Functionally correct for current data but fragile. Not a visual design issue.
|
||||
|
||||
### B. Dashboard footer hardcoded copy (P2)
|
||||
|
||||
**File:** `apps/web/src/routes/dashboard.tsx`, line ~393
|
||||
|
||||
```tsx
|
||||
<p className="text-center text-xs text-muted-foreground/60">
|
||||
© {new Date().getFullYear()} Sase.tr | Gizlilik Politikası, Kullanım Koşulları
|
||||
</p>
|
||||
```
|
||||
|
||||
Hardcoded Turkish text in the dashboard footer. Should use i18n keys for "Privacy Policy" and "Terms of Use".
|
||||
|
||||
**Severity:** P2 — Outside direct audit scope but visible on subscription page.
|
||||
|
||||
---
|
||||
|
||||
## Follow-up Tasks
|
||||
|
||||
The following tasks should be created to address P0 and P1 findings:
|
||||
|
||||
| Priority | Issue | Recommended Task Title |
|
||||
|----------|-------|----------------------|
|
||||
| **P0** | Skeleton layout shift (Section 6, lines 400-402) | Fix subscription page skeleton grid to match real 4-column layout |
|
||||
| **P1** | Trust copy hardcoded aria-label (Section 4, line 1068) | Add i18n keys for subscription trust section aria-labels |
|
||||
| **P1** | Payment trust badges: text → SVGs + i18n aria-label (Section 5, lines 1081-1112) | Replace text-based payment trust badges with card brand SVGs |
|
||||
| **P1** | Cancel-save dialog mobile overflow (Section 9, line 637) | Add mobile-safe max-width to downgrade offer dialog |
|
||||
|
||||
---
|
||||
|
||||
## Verification Notes
|
||||
|
||||
- **Code reviewed:** Full 1233-line `apps/web/src/routes/dashboard/subscription/index.tsx`
|
||||
- **Supporting files checked:**
|
||||
- `apps/web/src/components/trial-urgency-banner.tsx` (TrialUrgencyBanner implementation)
|
||||
- `apps/web/src/routes/dashboard.tsx` (dashboard layout — banner integration)
|
||||
- `apps/web/src/messages/tr.json` (Turkish i18n keys)
|
||||
- `apps/web/src/messages/en.json` (English i18n keys)
|
||||
- `apps/web/src/globals.css` (animate-fade-in-up definition)
|
||||
- **Methodology:** Static code analysis against design intent described in FN-203 (P0-1–6) and FN-199 (P0-7–10). No Playwright visual tests were possible in this execution environment (missing system libraries — see MEMORY.md Playwright limitation).
|
||||
Reference in New Issue
Block a user