Commit Graph

62 Commits

Author SHA1 Message Date
e5ed6b9f36 feat(web): /catalog — recently-used brands strip
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Adds a horizontal scrollable "Son ziyaret ettiklerin" strip above the main
brand grid. Renders only when the user has actually opened at least one
brand detail page before — silent in cold-start state.

Why localStorage, not a backend endpoint
- This is a behavioural shortcut, not authoritative state. Adding a
  user_recent_brands table for data we don't have yet is premature.
- Keying by `sase-recent-brands-${userId}` mirrors the trial-banner
  scoping pattern; a second user on the same browser doesn't inherit the
  first user's list.
- localStorage failures (private mode, quota) silently degrade — the
  strip just stays hidden, never throws.

How a brand gets added
- Tracked at the destination (`/catalog/:brandName` visit), not on the
  link click. A click that never resolves into a real visit (auth gate,
  slow nav cancel) shouldn't be a "recently used" signal.
- 12 entries stored, 8 surfaced. Headroom for future ranking (e.g.
  weight by frequency × recency) without re-recording history.

Plan-lock awareness
- The strip cross-references the `/catalog/brands` access map, so a
  brand the user opened while on Full and then lost on a downgrade
  shows the same lock chip + amber upgrade route used by the main grid.
- New PostHog event: `catalog_recent_brand_clicked`. Locked recent
  chips reuse `catalog_locked_brand_upgrade_clicked` with
  `surface: "recents"` for funnel distinction.

i18n: `catalog.recentSection` (TR: "Son ziyaret ettiklerin" / EN:
"Recently visited").
2026-06-01 00:36:50 +03:00
05de73dd41 feat(web): catalog shared primitives — ViewModeToggle, CatalogHeader, useViewMode
Three drift-prone catalog surfaces (main /catalog, /catalog/$brandName, and
the legacy CategoryViewToggle) each carried their own inline view-mode
toggle with subtly different aria labels, sizing, icon paths and even a
"Sutun" typo. New surfaces will inherit a single primitive.

- ViewModeToggle: grid/tree/columns radio-group with i18n labels driven by
  `catalog.view.*` keys; fan-out by passing `groupLabelKey`.
- useViewMode(key): centralises read/write of `brandViewMode`/`modelViewMode`
  in user-settings — page just owns its current value.
- CatalogHeader: title + subtitle + optional breadcrumb trail + back button +
  trailing actions slot, so every catalog drill page renders the same shell.

i18n: adds catalog.subtitle, catalog.view.{groupLabel,grid,list,columns},
catalog.{brandSearch,modelSearch}{Placeholder,NoMatch}, catalog.{hideLocked,
showLocked,lockedSection,lockedSectionHint,inPlanSection,goToModels,
selectBrandHint,columnsBrowseHint,sortLabel}, catalog.sort.{newest,oldest,
alphabetical}.

These primitives stand alone in this commit — the main /catalog and
brand-detail pages migrate to them in 10B and 10C.
2026-06-01 00:23:05 +03:00
adbf23945e feat(web): history production refactor — search, sort, delete, plan-lock, i18n, analytics
The old 66-line history page was a raw list with `any[]` typing, no search,
no sort, no delete, no dates, no i18n and no analytics — opening a vehicle
detail was the only action available.

Replaces it with a real management surface:
- Search-by-text (brand, model, VIN, year, engine) — client-side over the
  paginated payload, Turkish-locale case-folding
- Sort dropdown (newest, oldest, brand A→Z, year DESC)
- Per-item Remove with a confirmation dialog; mutation does optimistic
  setQueryData so the card disappears immediately, then invalidates the
  search-page recent-history query
- Relative-time "decoded {when}" using Intl.RelativeTimeFormat with the active
  locale; absolute timestamp lives in title/aria for power users
- Plan-lock badge: when the user's plan no longer covers a brand in their
  history, that card surfaces a Lock chip linking to /subscription
- Brand logo, VIN with copy-to-clipboard, focus-visible rings, Link.preload
  intent so opening a vehicle is instant
- Empty state (no history) and no-results state (filter wiped everything)
  are now distinct, both with the right CTA
- PostHog events: history_viewed, history_search_used, history_sort_changed,
  history_load_more, history_item_clicked, history_vin_copied,
  history_item_deleted — full funnel coverage from open to action
- Full i18n via new `history.*` block in tr.json/en.json, plus the page now
  fetches paginated (PAGE_SIZE × 8 first chunk, "show more" client-side)
2026-06-01 00:10:50 +03:00
229666e333 i18n(web): full landing page TR↔EN — language switcher actually works now
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Root cause: landing was almost entirely hard-coded Turkish (5 t() calls in
1889 lines), and initLocale() — which reads the persisted locale from
localStorage on boot — was never called from main.tsx, so a reload always
reverted to the TR default.

This commit fixes both.

apps/web/src/main.tsx
- Call initLocale() before first render so the persisted choice survives reload.

apps/web/src/routes/__root.tsx
- Translate the skip-to-content link via a new `a11y.skipToContent` key.

apps/web/src/routes/index.tsx
- Internationalize ~145 user-visible strings across hero, brand strip, features,
  comparison table, bento grid, use cases, stats, dashboard showcase, ecommerce
  block, testimonials, pricing tiers, FAQ, final CTA, referral banner and ARIA
  labels. Module-scope data arrays moved inside the component so re-renders
  pick up the active locale.
- Wrap the three Remotion players (Schema / Dashboard / Ecommerce) so they read
  the current locale and pass translated labels into their inputProps.

Remotion demos (DashboardDemo, EcommerceDemo, SchemaDemo)
- Each now accepts a `labels` prop (DashboardDemoLabels / EcommerceDemoLabels /
  partLabel). All hard-coded TR sidebar items, placeholders, category names,
  product names, "Add to Cart" buttons, the cart-added toast, overlays and
  stat labels are now driven by labels with a TR fallback (keeps Remotion
  Studio preview working standalone).

messages/{tr,en}.json
- New blocks: `a11y`, `landing.*` (huge tree covering every section above),
  `landing.dashboardDemo`, `landing.ecommerceDemo`.

Intentionally preserved as data, not translated: brand names (Volkswagen,
Mitsubishi …), VIN literals, OEM codes (1J0 820 803F …), numeric stat values
(1.2sn, 27, 1M+, %99.9 …), prices (200/350/500/999), and the testimonial
person name "Özge D." (proper noun).

Verified: typecheck clean, 17/17 test files / 57/57 tests pass, biome clean,
Playwright TR↔EN round-trip succeeds, only the testimonial name remains as
Turkish on the EN page.
2026-05-31 23:50:52 +03:00
931c0cfd85 feat(web): pricing — by-the-numbers stat strip, verifiable facts only
Adds a four-stat band below the hero CTAs — 27 brand catalogs, 1M+ OEM/alt
parts, 30-day free trial, 7-day no-questions refund. Big tabular-nums numerals
with small labels; divides on sm+. Every number is product or policy fact we
can defend — no fabricated customer counts, no testimonial copy.

Side effect: FAQPage JSON-LD now injected into <head> via useEffect (was an
inline <script dangerouslySetInnerHTML>). Same SEO payload, less attack
surface, tagged with data-sase-faq so it's diff-friendly.
2026-05-31 23:18:14 +03:00
1ea53920b0 refactor(web): brand selector controlled state + search; trial banner urgency tiers + progress
BrandSelector
- Now fully controlled — drops the internal `selected` state that could drift
  from the prop source of truth
- Full-plan auto-select effect compares against the live selection and only
  fires when out of sync, removing the infinite-loop risk if parents passed an
  unstable onSelectionChange (now also ref-stashed)
- Search input appears when there are more than 12 brands; Turkish-locale
  case-folding so "Volkswagen"/"volkswagen" both match
- No-match state for empty search results

TrialUrgencyBanner
- Three urgency tiers (soft 7d, urgent ≤3d, critical ≤1d) — colors and
  tighter copy escalate as the trial winds down
- Adds an elapsed-time progress bar so "3 days left" reads as 90% used at a
  glance (number alone undersells urgency at the tail)
- LS dismissal key now userId-scoped (mirrors subscription page)
- Banner emits `tier` alongside days for funnel segmentation
2026-05-31 23:10:08 +03:00
35871403c4 feat(web): billing & Stripe — empty state CTA, failed-payment hint, inline retry, card-brand trust
- Billing empty state now has a ReceiptText avatar and (when no subscription)
  a "choose plan" CTA — beats a bare "no payments yet" line
- Receipt buttons promoted from ghost to outline so users actually find them
- Failed payments show a help icon with a hint (card declined → bank / retry)
- Stripe button: Clock → Loader2 (Clock-as-spinner was awkward)
- Stripe init failure now also surfaces inline with a retry button — a toast
  vanishing in 4s left users with no recovery affordance
- Adds an accepted-cards trust strip (VISA / Mastercard / Troy / 3D Secure)
  below the pay button to ground the secure-checkout copy
2026-05-31 23:06:46 +03:00
611b41c793 feat(web): subscription — radiogroup plan picker, trial stuck guard, stale-brand cleanup, scoped LS
- PlanGrid now wraps cards as a real radiogroup with role=radio + aria-checked +
  focus-visible ring (was aria-pressed on plain buttons)
- Adds ProvisioningStuckHint: at 30s in trial provisioning we surface a
  contact-support + retry escape so users aren't trapped on a spinner
- PaymentMethodSection strips stale brand IDs against the live /brands query
  before Stripe checkout (would have 4xx'd silently)
- trial-banner dismissal LS key now scoped to userId + endDate (was endDate
  only — two users on the same browser could leak state)
- Sticky CTA disabled state gains a hint title + aria-disabled + opacity
- Brand-selector Suspense gets an 8s slow-load fallback with reload affordance
- OnboardingCompletedView shows the actual subscription plan name (was hard-
  coded "Full Paket")
- Picks up sase-pending-period stashed by the public pricing toggle
2026-05-31 23:04:20 +03:00
eb8d8c2dee feat(web): pricing — hero CTA, monthly/yearly toggle, numeric savings, popular elevation
- Hero gets a primary "start trial" CTA + secondary "see plans" scroll-to anchor
- Adds monthly/yearly billing toggle that switches displayed amounts and surfaces
  effective per-month yearly cost
- Yearly savings shown as both percent and TL/year (no more vague "save")
- Full Paket card visually elevated (ring + tinted bg + slight vertical offset on lg)
- Plan CTA carries plan=key into /register; chosen period stashed in localStorage
- New PostHog events: pricing_hero_cta_clicked, pricing_period_toggled,
  pricing_plan_cta_clicked
2026-05-31 22:58:18 +03:00
eb7b75fbe8 fix(web): onboarding modal — escape hatch + trial_failed analytics
When the trial mutation either fails repeatedly or just hangs past 30s,
new users were trapped staring at the spinner with no way out. Add:

- A `trial_failed` capture on every mutation error (Onboarding funnel can
  finally measure the activation failure rate).
- After 2 failures OR 30s in provisioning, surface a "still trying"
  panel with "Destek ile konuş" (opens Chatwoot) and "Yine de kapat"
  options, and let interact-outside / Esc dismiss the modal again.
- An openChatwoot() helper + typed `toggle` on the ChatwootApi so other
  components can open the support widget without poking the global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:24:20 +03:00
209e98cb41 fix(web): stop leaking /vehicles/undefined and the raw uuid parse error
When the landing decodes a VIN for a logged-in visitor and the API
returns a multi-candidate response (no single id), the code was
navigating to /dashboard/vehicles/$id with id=undefined. The catalog
page then asked the API for /vehicles/undefined and the backend
"invalid input syntax for type uuid: 'undefined'" message landed in
front of the user.

Two-sided fix:
- On the landing, if the decode reply has no id, hand the VIN off to
  /dashboard/search?vin=… instead — that page already renders the
  candidate-picker and the unrecognised-VIN flow.
- On the vehicle detail page, gate both queries on a UUID check and
  render a friendly "Geçersiz araç bağlantısı / Aramaya git" panel for
  any malformed id. Also stop showing raw backend error messages in the
  generic error state — surface the localised hint instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:04:21 +03:00
b9dd154bfd i18n(web): translate the vehicle catalog detail surface
Move every hard-coded Turkish string on /dashboard/vehicles/:id and the
drill-down /categories/:categoryId page into a vehicle.* message block
(tr + en) — page header, error/404 panels with retry, "Araç Bilgileri"
accordion, "Yedek Parça Kategorileri" + the new empty state, equipment
title and show-more/less toggle, summary labels — plus the leaf-category
catalog-error panel and the drill-down's loading/retry copy.

Also translate the CatalogSearch component (placeholder, arias,
searching/treeIncomplete/noMatch hints, section headings, parts-count
badge) under a new catalogSearch.* block, since it lives inside the same
surface and the rest of the page already speaks two languages.

PCAT_LABELS / EMEX_LABELS / VINAPI_FIELDS / LABEL_OVERRIDES are
source-specific data tables, not UI chrome — left as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 21:42:48 +03:00
a6ce215c47 i18n(web): translate the search page
Move every hard-coded Turkish string on /dashboard/search into a
search.* message block (tr + en) and resolve them via t() — title,
subtitle, input label/placeholder, counter, example button, submit, the
error banner with the subscription nudge, the report-VIN flow + toasts,
preview loading + identified badge + hint, recent-searches header /
see-all / open hint / row aria-label, validation/generic/toast errors,
auto-correction toast, decoding skeleton sr-only text, and the candidate
selection failure toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 17:28:45 +03:00
efd42ade4a fix(web): align remaining "50+ marka" claims to the real count (27)
Follow-up to the landing fix: the credibility strip (subscription/checkout),
meta description, auth-layout badge and blog copy still claimed "50+
marka" while the catalog supports 27. Align all to 27 for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:59:46 +03:00
d35a009653 i18n(web): relabel billing receipt action to "Makbuzunuz"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:58:01 +03:00
d77face831 fix(web): billing — correct cancelled status label and gate receipt button
- Subscription status is "cancelled" (and "pending") in the API; the i18n
  keys/variant map used "canceled", so the badge showed the raw
  billing.subStatus.cancelled key. Fix the spelling and add pending.
- Only show "View receipt" for Stripe payments that actually have a
  stripePaymentIntentId. Seeded/legacy completed payments have none, so
  the button used to appear and then toast "no receipt available".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:28:39 +03:00
7a8fe5e98e feat(payments): Stripe receipt link on billing rows
Add GET /payments/:id/receipt — resolves the Stripe-hosted receipt URL
from the payment intent's latest charge (ownership-scoped; returns EFT
receipt directly when present, null otherwise). Wire a "View receipt"
action on completed Stripe rows that fetches the URL on demand and opens
it, with a toast when none is available.

Closes the last billing-audit item (#8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:14:28 +03:00
0e61a2fcc7 feat(web): billing loading skeleton shape + show-more pagination
Make the payments loading state mirror the table card (header + rows)
instead of generic bars, and paginate the list client-side (10 at a time
with a "Show more" button) so long payment histories don't render all at
once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:07:50 +03:00
36bd4b500b feat(web): add subscription summary card to billing page
Billing was payment history only. Add a summary card at the top showing
the current plan, status badge, and next renewal / trial-end / end date
(from /subscriptions/me), with a "Manage subscription" link — or a
"Choose a plan" CTA when there's no active subscription.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:04:58 +03:00
acada22e71 fix(web): billing — distinguish empty vs filtered, localize date, tabular nums
Show "no payments match this filter" with a clear-filters button when
filters hide everything (was the misleading "no payments yet"). Follow
the active locale for the payment date instead of hard-coding tr-TR, and
use tabular figures for date and amount columns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:03:26 +03:00
10cd67911d refactor(web): move changelog out of settings into its own route
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Product changelog isn't an account setting. Give it a dedicated
/dashboard/changelog route and a "What's New" sidebar link (Support
section), and drop the changelog tab from settings. The header page
title now resolves support-section routes too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:37:28 +03:00
239791290e feat(web): add Preferences tab (theme + language) to settings
New "Preferences" settings tab giving the theme a real home in settings
(light/dark/system selector wired to userSettings + applyTheme) alongside
the language switcher, instead of theme living only in the header toggle.
Theme group uses a fieldset/legend for accessible grouping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:32:15 +03:00
9633888170 feat(web): settings form states — password reveal, inline validation, loading
- Add a PasswordField with show/hide toggle, hint text and inline error;
  the new-password hint and live "passwords do not match" message now
  render under the fields (submit disabled on mismatch) instead of only
  firing a toast on submit.
- Show skeletons while the connections and referral-stats queries load,
  instead of flashing "Not linked"/0.
- De-nest the referral stat cards (cards-inside-cards) into plain muted
  panels and use tabular-nums for the figures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:27:05 +03:00
397aef8c35 feat(web): add language switcher to site footer
TR/EN toggle in the footer bottom bar, wired to the i18n store's
setLocale (persists to localStorage, updates <html lang>). Buttons use
aria-pressed within a labelled group; active locale is highlighted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:33:55 +03:00
d482d5db6c fix(web): footer content — real anchors, single email, drop fake status
- Point the three product links to distinct, real feature anchors
  (#feature-vin / #feature-oem / #feature-schema); add matching ids and
  scroll-mt to the feature cards in index.tsx (were all -> #features).
- De-duplicate the email: brand-column address is now the single
  mailto link; removed the redundant bottom-bar copy.
- Remove the static "all services operational" indicator that never
  reflected real status (misleading trust signal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:54:39 +03:00
6cefd2d560 i18n(web): translate SiteFooter strings
Move all hard-coded Turkish footer copy (column titles, links, tagline,
copyright, status) into a footer.* message block in tr/en and resolve
them via t(), so the footer follows the selected locale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:51:22 +03:00
fa4e08a0a7 feat(web): add "upgrade account" CTA above sidebar profile for free users
Shows a brand-tinted "Hesabını Yükselt" badge above the username in the
sidebar footer, linking to the subscription page. Hidden for users with
an active paid plan. Renders in desktop (expanded + collapsed icon
variant), and the mobile drawer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:09:12 +03:00
71242a72be feat(web): dashboard sidebar UX/UI + a11y overhaul
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Replace the click-to-logout profile trap with a proper DropdownMenu
(settings / subscription / logout), swap the hand-rolled mobile overlay
for a Radix-Dialog Sheet (focus-trap, Esc, scroll-lock), and fix
accessibility gaps: aria-labels on icon buttons, aria-current + labelled
nav landmarks, and effect-based auth redirect instead of navigating
during render.

Also: reorder nav (Main → Account → Support → Admin), full i18n of
labels/sections, conditional "upgrade plan" button, header page title,
collapsed-mode active indicator + tooltips, contrast/touch-target
tweaks, and a typed NavItem to drop the `as any` exact cast.

Adds DropdownMenu, Tooltip and Sheet primitives to @sase/ui.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:54:53 +03:00
1685ee4b44 feat(web): add "Nasıl çalışır" 3-step band to pricing page
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Cold visitors landing straight on /pricing had no product context. Added a
compact 3-step band between the credibility strip and FAQ: enter the chassis
number -> see the parts (OEM codes, diagrams, compatible parts) -> pick the
right part by comparing prices. Grounded in the actual product, no ordering
claims.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:16:46 +03:00
b8c2e7568a feat(web): add FAQ section to pricing page
The pricing page had no objection handling and a half-empty page below the
cards. Added a PricingFaq disclosure section (native <details>) covering the
six recurring questions: how the free trial works, whether a card is needed,
cancellation, refunds, changing plan/brands, and payment security — all
grounded in actual product behaviour (30-day Full trial, no card, period-end
access on cancel, downgrade, Stripe PCI-DSS / KVKK).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:15:03 +03:00
34bbb01fed feat(web): align pricing cards with subscription page
Bring the public pricing cards in line with the in-app subscription page and
strengthen the CTA:
- Move the "Popüler" badge from 2 Marka to Full Paket (one consistent story
  across both surfaces).
- CTA "Başla" -> "Ücretsiz Başla" (leans on the free trial the flow grants).
- Yearly line now uses a consistent ₺ format ("veya ₺2.000,00/yıl") plus a
  "· 2 ay bedava" savings cue instead of the bare, differently-formatted
  "veya 2.000,00 TL/yıl".
- Plan-decision helper above the grid ("Hangi planı seçmeliyim? …").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:10:43 +03:00
d49964aac1 feat(web): value-focused pricing hero with trial + risk-reversal trust line
The pricing hero was purely procedural ("Fiyatlandırma" / "İhtiyacınıza
uygun planı seçin") with no value prop and no mention of the free trial —
the strongest conversion lever, present in the meta description and header
CTA but absent from the page body. Reworked into an eyebrow + outcome
headline ("Doğru parçayı saniyede bul") + supporting line, plus a trust
row: 30 gün ücretsiz · kredi kartı gerekmez · istediğin zaman iptal ·
7 gün iade garantisi (reusing existing subscription trust keys).

Also sorted pricing.tsx imports to satisfy biome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:08:35 +03:00
8fe42d729a feat(web): relabel Stripe pay button to "Kredi Kartı ile Öde"
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The checkout button read "{amount} ile öde"; per request it now reads
"Kredi Kartı ile Öde" (EN: "Pay with Credit Card"), clarifying the payment
method. The amount stays visible in the order summary directly above the
button, so no pricing context is lost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:53:47 +03:00
85f4252f7e feat(web): default to yearly billing, make Full Paket the popular plan
Per request: shift the "Popüler" emphasis from the monthly-anchored 2 Marka
to Full Paket, default the billing toggle to yearly, and put a "+2 ay hediye"
gift badge on the yearly tab (replacing the abstract -17%). Removed the
now-redundant "2 ay bedava" note under the toggle since the badge carries
that message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:49:56 +03:00
0bae337c69 feat(web): show persistent trial-progress banner for days 8-30
Trial users previously got no reminder of remaining days until the urgent
(<=7d) amber banner appeared, leaving most of the 30-day window with no
conversion nudge. Added a calm, brand-toned TrialProgressBanner shown when
more than 7 days remain: days left, a progress bar (elapsed share of the
30-day trial), and a "Planını seç" CTA that scrolls to checkout and fires
trial_progress_cta_clicked. The urgent banner still owns the final 7 days.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:29:42 +03:00
2912dc6eb2 feat(web): add credibility strip to subscription page
The page had no credibility signal at the point of conversion. Added a
CredibilityStrip under the header (shown only to converting users:
no-sub / trial / expired / pending) surfacing real, verifiable product
facts — 50+ marka, 1M+ parça, OEM + alternatif kodlar, saniyede sonuç,
KVKK uyumlu · SSL. Deliberately no fabricated customer counts or
testimonials.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:27:43 +03:00
daaf616335 feat(web): add plan decision helper + trial value hook
The brand-count plan model leaves users unsure whether they need 1/2/3
brands. Added a PlanHelperHint above the grid ("Hangi planı seçmeliyim? …
4+ marka için Full Paket marka başına daha avantajlı"). Also added an
outcome-focused hook line to the trial card ("Şase numarasını gir, doğru
parçayı saniyede bul …") above its feature list, selling the result rather
than only listing features.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:25:55 +03:00
ef4ebec025 feat(web): reinforce value on subscription plan selection
Three CRO additions at the point of decision:
- Outcome-focused page subtitle ("Doğru parçayı saniyede bul …") replacing
  the purely procedural copy.
- Per-day price anchor on every plan card (≈ ₺X/gün), reframing the cost as
  a small daily number. Computed from the displayed monthly/yearly price.
- "2 ay bedava" note under the billing toggle when yearly is selected,
  making the otherwise-abstract -17% concrete (yearly = 10× monthly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:23:21 +03:00
b32c777951 fix(web): render plan names + price in subscription feature matrix headers
The comparison table's column headers were empty <th/> cells (aria-label
only), so sighted users could not tell which column belonged to which plan.
Headers now show plan name + monthly price + the Popüler badge. Added a
"Marka sayısı" row exposing the brand limit (1/2/3/Tümü) — the attribute
that actually differentiates the brand1/brand2/brand3 tiers, which the
matrix previously omitted entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:16:57 +03:00
Fusion
65daf99b2e chore(payments): remove EFT/bank-transfer method, keep Stripe only
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Removes the EFT (havale) payment surface across API, web, i18n, and docs.
Card payment via Stripe is now the only checkout path.

API (apps/api/src):
- Delete payments/bank-accounts/ module (controller + service + module)
- payments.service: drop EFT methods (createEftPayment, uploadEftReceipt,
  approveEft, rejectEft, getActiveBankInfo, getPendingEftPayments) and the
  BankAccountsService dependency
- payments.controller: drop /payments/eft, /payments/eft/:id/{receipt,approve,reject},
  /payments/bank-info, /payments/pending; keep /payments/me
- payments.module: drop BankAccountsModule import
- admin.service: drop pending EFT counter from getDashboardStats; drop
  getPendingPayments (admin EFT approval list)
- admin.controller: drop /admin/payments/pending endpoint
- admin.service.spec: drop the getPendingPayments + pendingPayments assertions

Web (apps/web/src):
- Delete components/payment/bank-transfer-card.tsx
- Delete routes/dashboard/admin/payments.tsx (EFT approval page); regenerate
  routeTree.gen.ts
- subscription/index.tsx: drop EFT tab from PaymentMethodSection, drop the
  paymentMethod state + handleEftCompleted, simplify ConfirmationCard to the
  Stripe-only path, drop unused imports (Tabs, BankTransferCard, Building2)
- billing.tsx: drop "eft" from method filter chips; keep methodLabels.eft and
  the legacy receipt download so historical EFT records still display
- dashboard.tsx + admin/index.tsx: drop /dashboard/admin/payments from the
  sidebar + admin quick-link grid; trim unused icon imports; KEYS_6 → KEYS_5
  for the now-5-card admin stats skeleton
- messages/{tr,en}.json: strip every EFT-only key under payment.* (eftTransfer,
  eftConfirmationDescription, eftPaid, eftStatus, bank.*, uploadReceipt* etc.)

Schema (apps/api/src/database/schema):
- bankAccounts table + payments.bankAccountId column are kept as-is: legacy
  EFT payments remain visible on the billing page and the schema preserves
  historical records.

Docs:
- README.md: "Stripe (kart) + EFT" → "Stripe (kart)"
- CLAUDE.md: stack table + PaymentsModule row + payments table note + route
  list updated; admin EFT approval route removed

Verification:
- pnpm typecheck: green (api + web + shared + config + ui)
- pnpm lint: green
- pnpm test: 20 web tests pass, 173 api tests pass
2026-05-16 11:03:30 +03:00
Fusion
6c28688283 feat(FN-373): grid view drill-down for parent categories (+1 more)
Commits merged:
- chore(FN-373): verify grid drill-down lint + typecheck clean
- fix(FN-373): grid view drill-down for parent categories

Files changed:
.../src/components/categories/category-grid.tsx    | 175 ++++++++++++++++-----
 apps/web/src/messages/en.json                      |   3 +
 apps/web/src/messages/tr.json                      |   3 +
 3 files changed, 138 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-373
2026-05-14 18:56:08 +00:00
Fusion
561b02b3d2 feat(FN-343): remove lingering iyzico references after Stripe migration (+1 more)
Commits merged:
- chore(FN-343): remove lingering iyzico references from docs, config, and scripts
- feat(FN-343): remove lingering iyzico references after Stripe migration

Files changed:
CLAUDE.md                                    | 10 +++++-----
 README.md                                    |  2 +-
 apps/api/src/database/schema/core.ts         |  1 +
 apps/web/src/messages/en.json                |  1 -
 apps/web/src/messages/tr.json                |  1 -
 apps/web/src/routes/dashboard/billing.tsx    |  6 +++---
 docker-compose.coolify.yml                   |  5 ++---
 docs/INDEX.md                                | 27 ++++++++++++-------------
 knowledge.md                                 | 30 ++++++++++++++--------------
 packages/shared/src/constants/error-codes.ts |  1 -
 packages/shared/src/index.ts                 |  1 -
 packages/shared/src/types/payment.ts         | 12 +----------
 scripts/fn342-pw-verify.mjs                  |  2 +-
 scripts/validate-env.sh                      |  3 +--
 14 files changed, 43 insertions(+), 59 deletions(-)

Fusion-Task-Id: FN-343
2026-05-14 02:31:42 +00:00
Sase Dev
02bf7342a1 fix(subscription): recover stepper when a pending checkout is abandoned
Some checks failed
Deploy / Deploy to Production (push) Has been cancelled
A user who picked a plan + brands, clicked the checkout button to
create the pending subscription, then closed the tab without paying
came back to a frozen "Mevcut Plan · Bekliyor" card with no payment
button and no way to edit. Status was "pending", which my conditional
treated as an active subscription and routed away from the stepper.

Frontend
- hasActiveSub narrowed to active|cancelled only. Pending users now
  see the stepper again.
- New effect pre-fills selectedPlanKey, selectedBrandIds,
  billingPeriod from the pending row on first render and jumps
  straight to the payment step.
- New PendingPaymentBanner at the top of the stepper with
  "Odemeye devam et" (scrolls + sets step=payment) and "Vazgec"
  (calls cancel-pending then resets selection).

Backend
- subscriptions.service.create() auto-expires existing pending
  rows for the user before inserting a new one — abandoned checkouts
  no longer accumulate and a fresh attempt with a different plan
  doesn't 409.
- New PATCH /subscriptions/cancel-pending + service method for
  the banner's explicit cancel action.

i18n: subscription.pendingBanner.* (tr/en).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:57:13 +00:00
Sase Dev
6a0b36a7a0 feat: replace Iyzico with Stripe + single-page subscription stepper
Subscription/checkout flow rewritten end-to-end. The plan-card "Devam Et"
button silently wiped brand selection on re-click and the actual proceed
button lived offscreen — fixed by collapsing plan/brands/payment/confirm
into a single vertical stepper with one sticky CTA.

Backend
- Stripe Hosted Checkout (`/payments/stripe/checkout`) + webhook
  (`/payments/stripe/webhook`, raw body) replacing the stubbed Iyzico
  module. Webhook activates subscription on `checkout.session.completed`,
  expires the pending subscription on cancel/expire so users can retry.
- New `bank_accounts` table — multiple rows, single `is_active` enforced
  by a partial unique index. Admin CRUD under `/admin/bank-accounts`
  with multipart QR upload to MinIO; atomic `/activate` swap in a
  transaction; `GET /payments/bank-info` returns the active row.
- `payments` gains `stripe_session_id`, `stripe_payment_intent_id`,
  `bank_account_id`. EFT flow now reads the active bank account at
  payment time and stores the FK for reconciliation.
- Env: `IYZICO_*` removed, `STRIPE_*` added (validated by zod schema).
- `main.ts` `rawBody: true` for Stripe signature verification.
- Drizzle 0003 snapshot id collision fixed (VIEW-only migration shared
  prevId with 0002, blocking new generates).

Frontend
- `/dashboard/subscription` rewritten as a 4-step vertical stepper with
  step-aware sticky bottom CTA; plan re-selection is idempotent and
  preserves brand state. `/dashboard/subscription/pay` deleted; Stripe
  returns to the same page via `?stripe=success|cancelled` and the UI
  polls `/subscriptions/me` until the webhook activates the row.
- New components: `bank-transfer-card.tsx` (DB-driven IBAN + Kolay Adres
  + uploaded QR image + receipt upload) and `stripe-checkout-button.tsx`.
- Active subscription view, trial onboarding/urgency banner, downgrade
  and cancel dialogs preserved.
- TR/EN i18n: new `subscription.steps.*`, `subscription.stickyCta.*`,
  `payment.stripe.*`, `payment.bank.*`; provider label updated.

PostHog: `method: "iyzico"` → `"stripe"`; new events `iban_copied`,
`kolay_adres_copied`, `qr_viewed`, `eft_initiated`,
`stripe_redirect_returned`.

Deploy runs `db:migrate` which applies 0004_hot_quicksilver
(additive: new table + nullable columns; safe to apply on prod).
Operator must add `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`,
`STRIPE_WEBHOOK_SECRET` to env and create the first
`bank_accounts` row via the admin endpoint before the bank tab works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:42:28 +00:00
Fusion
0b49c164dc feat(FN-209): implement trial urgency banner
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-209): implement trial urgency banner

Files changed:
apps/web/src/components/trial-urgency-banner.tsx | 111 +++++++++++++++++++++++
 apps/web/src/messages/en.json                    |   7 +-
 apps/web/src/messages/tr.json                    |   7 +-
 apps/web/src/routes/dashboard.tsx                |   8 +-
 4 files changed, 129 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-209
2026-05-12 21:47:26 +00:00
Fusion
405c0067dc feat(FN-207): Step 1 — add downgradeOffer i18n keys (tr + en) (+4 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- style(FN-207): apply Biome formatting
- feat(FN-207): Steps 4-9 — add downgrade save-flow UI (helper, dialogs, plans query, handler)
- feat(FN-207): Step 3 — add PATCH /subscriptions/downgrade and GET /subscriptions/plans endpoints
- feat(FN-207): Step 2 — add downgrade() and getPlans() service methods
- feat(FN-207): Step 1 — add downgradeOffer i18n keys (tr + en)

Files changed:
.../src/subscriptions/subscriptions.controller.ts  |  13 ++
 .../api/src/subscriptions/subscriptions.service.ts |  75 ++++++
 apps/web/src/messages/en.json                      |  10 +
 apps/web/src/messages/tr.json                      |  10 +
 apps/web/src/routes/dashboard/index.tsx            |  16 +-
 .../src/routes/dashboard/subscription/index.tsx    | 255 ++++++++++++++++++---
 6 files changed, 340 insertions(+), 39 deletions(-)

Fusion-Task-Id: FN-207
2026-05-12 21:29:12 +00:00
Fusion
1eefd931c9 feat(FN-206): add FeatureMatrix component, ALL_FEATURES constant, and i18n keys (+1 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-206): add FeatureMatrix tests and export SubscriptionPage for testing
- feat(FN-206): add FeatureMatrix component, ALL_FEATURES constant, and i18n keys

Files changed:
apps/web/src/messages/en.json                      |   1 +
 apps/web/src/messages/tr.json                      |   1 +
 .../routes/dashboard/subscription/index.test.tsx   | 177 +++++++++++++++++++++
 .../src/routes/dashboard/subscription/index.tsx    |  73 +++++++++
 package.json                                       |   1 +
 5 files changed, 253 insertions(+)

Fusion-Task-Id: FN-206
2026-05-12 18:42:41 +00:00
Fusion
ba43df45bc feat(FN-204): i18n pricing/subscription/payment surfaces
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-204): i18n pricing/subscription/payment surfaces

Files changed:
.../web/src/components/payment/payment-content.tsx |  11 +-
 apps/web/src/lib/i18n.ts                           |  17 ++-
 apps/web/src/messages/en.json                      |  27 ++++-
 apps/web/src/messages/tr.json                      |  27 ++++-
 .../src/routes/dashboard/subscription/index.tsx    |   4 +-
 apps/web/src/routes/pricing.tsx                    | 127 +++++++++++----------
 6 files changed, 135 insertions(+), 78 deletions(-)

Fusion-Task-Id: FN-204
2026-05-12 18:11:33 +00:00
Fusion
688db7c990 feat(FN-199): add i18n keys for trust copy, payment trust, daysLeft (+2 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-199): complete Step 3 — add Clock and CreditCard to lucide-react imports
- feat(FN-199): complete Step 2 — replace hardcoded  gün kaldı with i18n key
- feat(FN-199): complete Step 1 — add i18n keys for trust copy, payment trust, daysLeft

Files changed:
apps/web/src/messages/en.json                        | 9 ++++++++-
 apps/web/src/messages/tr.json                        | 9 ++++++++-
 apps/web/src/routes/dashboard/subscription/index.tsx | 4 +++-
 3 files changed, 19 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-199
2026-05-12 16:50:29 +00:00
Fusion
9c8054d6be feat(FN-203): implement P0-1 through P0-6 subscription page fixes
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-203): complete Step 1 — implement P0-1 through P0-6 subscription page fixes

Files changed:
apps/web/src/messages/en.json                      |   7 +
 apps/web/src/messages/tr.json                      |   7 +
 .../src/routes/dashboard/subscription/index.tsx    | 188 +++++++++++++++------
 apps/web/vitest.config.ts                          |   3 +
 4 files changed, 153 insertions(+), 52 deletions(-)

Fusion-Task-Id: FN-203
2026-05-12 16:07:52 +00:00