feat(billing): Polar.sh Pro subscription integration
Some checks failed
CI / UI (push) Successful in 1m36s
CI / Backend (push) Successful in 59s
Release / Test backend (push) Successful in 39s
Release / Check ui (push) Successful in 33s
CI / Backend (pull_request) Successful in 44s
CI / UI (pull_request) Successful in 34s
Release / Docker / runner (push) Successful in 2m44s
Release / Docker / ui (push) Successful in 2m45s
Release / Docker / backend (push) Successful in 3m35s
Release / Docker / caddy (push) Successful in 1m8s
Release / Gitea Release (push) Failing after 2s

- Webhook handler verifies HMAC-SHA256 sig and updates user role on
  subscription.created / subscription.updated / subscription.revoked
- Audio endpoint gated: free users limited to 3 chapters/day via Valkey
  counter; returns 402 {error:'pro_required'} when limit reached
- Translation proxy endpoint enforces 402 for non-pro users
- AudioPlayer.svelte surfaces 402 via onProRequired callback + upgrade banner
- Chapter page shows lock icon + upgrade prompts for gated translation langs
- Profile page: subscription section shows Pro badge + manage link (active)
  or monthly/annual checkout buttons (free); isPro resolved fresh from DB
- i18n: 13 new profile_subscription_* keys across all 5 locales
This commit is contained in:
Admin
2026-03-29 13:21:23 +05:00
parent 83b3dccc41
commit 98631df47a
19 changed files with 539 additions and 13 deletions

View File

@@ -67,6 +67,8 @@
chapters?: { number: number; title: string }[];
/** List of available voices from the backend. */
voices?: Voice[];
/** Called when the server returns 402 (free daily limit reached). */
onProRequired?: () => void;
}
let {
@@ -77,7 +79,8 @@
cover = '',
nextChapter = null,
chapters = [],
voices = []
voices = [],
onProRequired = undefined
}: Props = $props();
// ── Derived: voices grouped by engine ──────────────────────────────────
@@ -563,6 +566,15 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ voice })
});
if (res.status === 402) {
// Free daily limit reached — surface upgrade CTA
audioStore.status = 'idle';
stopProgress();
onProRequired?.();
return;
}
if (!res.ok) throw new Error(`Generation failed: HTTP ${res.status}`);
if (res.status === 200) {