2026-04-01 Session Log
Full-day sprint on Nativ: billing lifecycle, legal pages, brand overhaul, modal animations, translation proxy, landing redesign
Billing & Payments
Fixed subscription cancel lifecycle, added refund policy page, introduced yearly plan, and set up Paddle pricing
What I Did
- Subscription cancel fix — When a user cancels mid-billing-cycle, they now keep Plus access until the period ends. Previously, the webhook handler immediately downgraded them to Free. The fix lives in two layers: the webhook sets
tier=plusifperiod_end > now, and the DB query (get_tier) also checkscanceled + period_end > NOW()as a safety net. - Refund Policy page (
/refund) — All purchases are non-refundable. Exceptions only for verified technical outages or duplicate charges. This was a Paddle domain approval blocker (they require a visible refund policy). - Monthly/Yearly toggle — Added a plan interval selector to both the billing page and the upgrade modal. Monthly: $6.99/mo, Yearly: $53.88/year ($4.49/mo, 36% off). Default selection is yearly for better conversion.
- Legal page cleanup — Fixed light mode visibility (
text-white→text-ink-bright), replaced Stripe references with Paddle, fixed a leftover “LINGURARAG” reference, increased font sizes, updated all dates. - Contact email — Changed from
hello@nativ.totosupport@nativ.to. Set up Namecheap Catch All email forwarding to Gmail. - Paddle pricing — Changed base price from KRW ₩9,900 to USD $6.99 with KRW as a country-specific override. Tax inclusive.
- 20 billing webhook tests — Covers the full lifecycle: create → renew → cancel mid-cycle (keeps Plus) → period expire (downgrades to Free). Also tests signature verification, transaction completion, and helper functions.
Key Decisions
- No-refund policy (ChatPDF pattern) — The Free plan already acts as a trial. A 7-day refund window would be abusable (subscribe, use, refund, repeat).
- DB query guards + webhook guards — Defensive on both sides. If the webhook misses an event, the DB query still calculates the correct tier from
status + period_end. - Tax inclusive pricing — What users see is what they pay. Paddle handles per-country tax calculations automatically.
Brand & Logo Overhaul
Replaced all placeholder book icons with the new N mark logo, set up dark/light mode variants
What I Did
- Logo replacement — Every book SVG icon in the app was replaced with the new N mark PNG:
- Sidebar header:
logotitle-yellow.png(dark) /logotitle-black.png(light) - Landing hero & CTA:
logo-no-bg.png(dark) /logo-dark.png(light) - Footer:
logotitle-white.png(dark) /logotitle-black.png(light) - Chat AI avatar: N mark replaces the old book icon
- Sidebar header:
- Favicon regeneration — All sizes (32px ico, 180px Apple Touch, 192px Android, 512px PWA) generated from the new logo. Added
iconsmetadata tolayout.tsx. - Cleanup — Removed 4 unused files from
public/(old logo variants, duplicate screenshots). - Dark/light switching — Used Tailwind’s
hidden dark:block/dark:hiddenpattern. Zero JavaScript, purely CSS-driven.
Key Decisions
- PNG over inline SVG — The logo SVGs are auto-traced (complex paths, 180KB+). Not suitable for inline use. Next.js
<Image>gives automatic WebP conversion and size optimization anyway. - No logo+title combo image yet — Tried generating one with ImageMagick but the text rendering quality was poor. Needs to be done in Figma.
Modal Animation System
Built a CSS-only animation wrapper and migrated all 14+ modals
What I Did
AnimatedModalcomponent +useAnimatedMounthook — CSS-only enter/exit animations (fade + scale, 200ms). No dependencies (saves 30KB vs framer-motion).- Full migration — All modals in the app now use
<Modal isOpen={show} />instead of{show && <Modal />}. This enables exit animations (previously impossible because the component unmounted immediately). - Light mode accent color — Changed from indigo to blue (
blue-600/blue-700). Better contrast on white backgrounds. - SelectionPopup dual theme — Light mode gets white background with darker semantic colors; dark mode keeps zinc-800 with lighter colors.
- Spinner unification — All 11 loading spinners now use
border-t-accentinstead of hardcodedblue-500oramber-400. - Quality toggle fix — The paywall message incorrectly said “free trial messages” when toggling the quality model. Now correctly says “High-quality AI model is available for Plus members.”
Key Decisions
- CSS-only over framer-motion — A 50-line hook handles
setTimeout(20)for paint guarantee, managingmounted → visible → hidden → unmountedlifecycle. Simpler and lighter.
Translation Proxy
Added Google Translate as primary translator with GPT fallback, redesigned quota system
What I Did
- Google Translate proxy (
/api/translate-proxy) — Uses the unofficialclient=gtxendpoint. Single words return up to 3 alternative meanings. Backend handles the Google → GPT fallback in one endpoint (no frontend retry logic). - Quota redesign — Extracted shared
check_translation_limit/record_translationintotranslate_limit.py. Count-on-success pattern: quota is consumed only after a successful translation. Daily limits: Guest 200, Free 500, Plus unlimited. - Guest support —
/api/guest/translate-proxywith IP-based rate limiting.
Key Decisions
- Google
client=gtx— 10-year track record, no known legal issues, sufficient for current scale. If it gets blocked, GPT fallback kicks in automatically. - Backend-side fallback — Prevents double-counting and keeps quota enforcement in one place.
Landing Page Redesign
Complete visual overhaul with zigzag layout, new copy, and scoped drag-drop
What I Did
- Zigzag showcase — 5 feature sections (Listen, Speak, Translate, Save, Organize) with alternating image-text layout and product screenshots.
- New brand copy — “Be Nativ in Any Language.” / “Turn any PDF into your personal language tutor.”
- Pricing section — Monthly/Annual toggle with “Save 36%” badge and 8-item plan comparison.
- CTA section — Gradient background, entire card accepts click + file drop.
- Tier limit fixes — Plus users were incorrectly getting login-tier limits (200 pages instead of 2000). Fixed in ChatContext, backend account endpoint, and translation endpoints.
- OCR fix —
google-cloud-documentaiwas missing fromrequirements.txt, causingModuleNotFoundErroron scanned PDF uploads.
Key Decisions
- USD as display currency — Paddle handles local currency conversion at checkout. Avoids maintaining 10 localized price strings.
- Zigzag over card grid — Larger screenshots, natural scroll rhythm, follows Linear/Vercel conventions.
Selection Popup Fixes
Scoped amber highlight to popup-enabled areas, fixed NoteViewer popup positioning
What I Did
- Scoped selection color — Created
.selection-amberCSS class and applied it only to containers that have a SelectionPopup (MessageList, SlidePanel, NoteViewer). Previously, chat messages had the amber highlight but note modals and side panels used browser-default blue. - NoteViewer popup position — Changed from
range.getBoundingClientRect()toe.clientX/Yto match PDF/Chat/SlidePanel behavior. Also captured mouse coordinates beforesetTimeoutto avoid React event pooling edge cases.
Sidebar & UI Polish
Various fixes for sidebar behavior, vocabularypage, and i18n
What I Did
- Added 16 vocabulary i18n keys to all 10 language files
- Replaced native
<select>with custom dropdowns (macOS overlay issue) - New chat button now matches the account button pattern
- Vocab sidebar refreshes live when words are added/deleted (
vocab-changedevent) - Fixed ghost popup on external click, dark mode highlight icon, PDF rename input height jump
- Contact modal in footer with Help/Feedback and Partnerships cards
What’s Next
- Deploy all commits and verify Paddle domain approval
- End-to-end payment test (monthly + yearly checkout flow)
- Test subscription cancel → verify Plus access maintained until period end
- Hero section screenshot for landing page
- Google Translate proxy monitoring for potential blocks