From 032b652675cfadb26581bfd8391a7664f643e608 Mon Sep 17 00:00:00 2001 From: Vincent van der Wal Date: Fri, 7 Aug 2026 10:58:30 +0200 Subject: [PATCH] park --- src/lib/components/navigation/header.svelte | 9 ++- .../navigation/settings-menu.svelte | 56 ++++++++++--------- .../SupporterBadge.svelte | 2 +- .../SupporterGate.svelte} | 2 +- .../SupporterIcon.svelte | 2 +- .../UnlockDialog.svelte | 8 +-- src/lib/{paywall => supporter}/config.ts | 29 +++++++--- .../supporter.ts => supporter/store.ts} | 16 +++++- .../historical/[location]/+page.svelte | 10 ++-- .../weather/seasonal/[location]/+page.svelte | 12 ++-- 10 files changed, 85 insertions(+), 61 deletions(-) rename src/lib/{paywall => supporter}/SupporterBadge.svelte (94%) rename src/lib/{paywall/PaywallGate.svelte => supporter/SupporterGate.svelte} (99%) rename src/lib/{paywall => supporter}/SupporterIcon.svelte (93%) rename src/lib/{paywall => supporter}/UnlockDialog.svelte (97%) rename src/lib/{paywall => supporter}/config.ts (69%) rename src/lib/{paywall/supporter.ts => supporter/store.ts} (88%) diff --git a/src/lib/components/navigation/header.svelte b/src/lib/components/navigation/header.svelte index 32c9cca..811629b 100644 --- a/src/lib/components/navigation/header.svelte +++ b/src/lib/components/navigation/header.svelte @@ -14,7 +14,8 @@ import { href, routePath } from '$lib/i18n'; import * as m from '$lib/paraglide/messages'; - import SupporterBadge from '$lib/paywall/SupporterBadge.svelte'; + import SupporterBadge from '$lib/supporter/SupporterBadge.svelte'; + import { SUPPORTER_ENABLED } from '$lib/supporter/config'; import SettingsMenu from './settings-menu.svelte'; import ThemeIcon from './theme-icon.svelte'; @@ -158,8 +159,10 @@ + {/if} diff --git a/src/lib/paywall/SupporterBadge.svelte b/src/lib/supporter/SupporterBadge.svelte similarity index 94% rename from src/lib/paywall/SupporterBadge.svelte rename to src/lib/supporter/SupporterBadge.svelte index 8275329..1110bca 100644 --- a/src/lib/paywall/SupporterBadge.svelte +++ b/src/lib/supporter/SupporterBadge.svelte @@ -5,7 +5,7 @@ import SupporterIcon from './SupporterIcon.svelte'; import UnlockDialog from './UnlockDialog.svelte'; - import { isSupporter, refreshSupporter } from './supporter'; + import { isSupporter, refreshSupporter } from './store'; let open = $state(false); diff --git a/src/lib/paywall/PaywallGate.svelte b/src/lib/supporter/SupporterGate.svelte similarity index 99% rename from src/lib/paywall/PaywallGate.svelte rename to src/lib/supporter/SupporterGate.svelte index 190cdb6..0d9701c 100644 --- a/src/lib/paywall/PaywallGate.svelte +++ b/src/lib/supporter/SupporterGate.svelte @@ -6,7 +6,7 @@ import SupporterIcon from './SupporterIcon.svelte'; import UnlockDialog from './UnlockDialog.svelte'; import { SIGNUP_URL, SUPPORTER_PERKS, getSupporterPrice } from './config'; - import { isSupporter, refreshSupporter, supporterState } from './supporter'; + import { isSupporter, refreshSupporter, supporterState } from './store'; interface Props { /** Short feature name shown in the locked panel headline. */ diff --git a/src/lib/paywall/SupporterIcon.svelte b/src/lib/supporter/SupporterIcon.svelte similarity index 93% rename from src/lib/paywall/SupporterIcon.svelte rename to src/lib/supporter/SupporterIcon.svelte index 9d14f1c..3a9f4ba 100644 --- a/src/lib/paywall/SupporterIcon.svelte +++ b/src/lib/supporter/SupporterIcon.svelte @@ -2,7 +2,7 @@ /** * The supporter mark. A heart rather than a padlock: nothing here is locked * away as a punishment - the extras are a thank-you for chipping in, and a - * padlock framed it as a paywall. Filled once someone is supporting, + * padlock framed it as a hard wall. Filled once someone is supporting, * outlined as an invitation before that. */ interface Props { diff --git a/src/lib/paywall/UnlockDialog.svelte b/src/lib/supporter/UnlockDialog.svelte similarity index 97% rename from src/lib/paywall/UnlockDialog.svelte rename to src/lib/supporter/UnlockDialog.svelte index e3a2a75..600803f 100644 --- a/src/lib/paywall/UnlockDialog.svelte +++ b/src/lib/supporter/UnlockDialog.svelte @@ -9,13 +9,7 @@ import * as m from '$lib/paraglide/messages'; import { SIGNUP_URL, getSupporterPrice } from './config'; - import { - clearLicense, - isSupporter, - storedLicenseKey, - supporterState, - verifyKey - } from './supporter'; + import { clearLicense, isSupporter, storedLicenseKey, supporterState, verifyKey } from './store'; interface Props { open?: boolean; diff --git a/src/lib/paywall/config.ts b/src/lib/supporter/config.ts similarity index 69% rename from src/lib/paywall/config.ts rename to src/lib/supporter/config.ts index 7f2e398..dfd640e 100644 --- a/src/lib/paywall/config.ts +++ b/src/lib/supporter/config.ts @@ -1,29 +1,40 @@ import * as m from '$lib/paraglide/messages'; /** - * Paywall configuration. + * Supporter configuration. * * The frontend stays fully static and open source; the only server piece is the * tiny `drizzli-paywall` verify API (a separate, self-hosted repo). Point the - * build at your deployment with the `VITE_PAYWALL_*` env vars (e.g. in a - * `.env` file), otherwise the sensible drizz.li defaults are used. + * build at your deployment with the `VITE_SUPPORTER_*` env vars (e.g. in a + * `.env` file), otherwise the sensible drizz.li defaults are used. The + * pre-rename `VITE_PAYWALL_*` names still work. */ const env = import.meta.env as Record; const stripTrailingSlash = (url: string): string => url.replace(/\/+$/, ''); +/** + * Master switch for the supporter features. While this is `false` everything + * supporter-related is parked: the nav entry points are hidden, no key is + * verified, and every gated page renders as if the visitor were a supporter. + * The logic stays in place — set `VITE_SUPPORTER_ENABLED=true` (or default this + * to `true`) to bring it back. + */ +export const SUPPORTER_ENABLED = env.VITE_SUPPORTER_ENABLED === 'true'; + /** Base URL of the self-hosted verify API (drizzli-paywall). */ -export const PAYWALL_API_BASE = stripTrailingSlash( - env.VITE_PAYWALL_API_BASE ?? 'https://support.drizz.li' +export const SUPPORTER_API_BASE = stripTrailingSlash( + env.VITE_SUPPORTER_API_BASE ?? env.VITE_PAYWALL_API_BASE ?? 'https://support.drizz.li' ); -/** Where prospective subscribers go to sign up (the paywall repo's signup form). */ -export const SIGNUP_URL = env.VITE_PAYWALL_SIGNUP_URL ?? `${PAYWALL_API_BASE}/`; +/** Where prospective supporters go to sign up (the verify API's signup form). */ +export const SIGNUP_URL = + env.VITE_SUPPORTER_SIGNUP_URL ?? env.VITE_PAYWALL_SIGNUP_URL ?? `${SUPPORTER_API_BASE}/`; // ─── Location-based pricing ────────────────────────────────────────────────── // The signup form charges 3 in the visitor's currency (EUR / USD / CHF), picked -// from their location. Mirror that here so the paywall copy matches. Detection +// from their location. Mirror that here so the supporter copy matches. Detection // is timezone/locale based (no network geo lookup) and guarded for SSR. const CURRENCY_SYMBOL: Record = { EUR: '€', USD: '$', CHF: 'CHF' }; @@ -54,7 +65,7 @@ export function detectCurrency(): SupporterCurrency { return 'EUR'; } -/** Display price for the paywall panel, e.g. "€3 / month" or "CHF 3 / Monat". */ +/** Display price for the locked panel, e.g. "€3 / month" or "CHF 3 / Monat". */ export function getSupporterPrice(): string { // VITE_PREMIUM_PRICE is the pre-rename name, still honoured so an existing // deployment's .env keeps working. diff --git a/src/lib/paywall/supporter.ts b/src/lib/supporter/store.ts similarity index 88% rename from src/lib/paywall/supporter.ts rename to src/lib/supporter/store.ts index 416d9ba..3939349 100644 --- a/src/lib/paywall/supporter.ts +++ b/src/lib/supporter/store.ts @@ -8,7 +8,7 @@ * * This gate is a convenience/honor-system gate: the frontend is open source and * static, so it can be bypassed. Keeping the subscriber list server-side (in the - * paywall repo) is what makes it meaningful in practice. + * verify API's repo) is what makes it meaningful in practice. */ import { derived, get, writable } from 'svelte/store'; @@ -16,7 +16,7 @@ import { persisted } from 'svelte-persisted-store'; import * as m from '$lib/paraglide/messages'; -import { PAYWALL_API_BASE } from './config'; +import { SUPPORTER_API_BASE, SUPPORTER_ENABLED } from './config'; /** The supporter's access key, e.g. "DRZ-7Q2K-9F4M-XW3P". Empty when signed out. */ export const storedLicenseKey = persisted('license_key', ''); @@ -75,10 +75,14 @@ function notExpired(expires: string | null | undefined): boolean { * Whether supporter content should be shown. A live "valid"/"invalid" result * wins; otherwise we fall back to the cached result (so a reload or a brief * network blip doesn't lock a paying user out). + * + * While supporter features are parked (`SUPPORTER_ENABLED === false`) this is always + * true, so every gated page renders unlocked for everyone. */ export const isSupporter = derived( [supporterState, storedSupporterCache], ([$state, $cache]): boolean => { + if (!SUPPORTER_ENABLED) return true; if ($state.status === 'valid') return true; if ($state.status === 'invalid') return false; return !!($cache && $cache.valid && notExpired($cache.expires)); @@ -98,6 +102,10 @@ export interface VerifyResult { * subscription can still show a "renew" state) but the cache is marked invalid. */ export async function verifyKey(key: string): Promise { + // Parked: never touch the verify API (and report success, since everything + // is unlocked anyway). + if (!SUPPORTER_ENABLED) return { valid: true, expires: null }; + const trimmed = key.trim(); if (!trimmed) { supporterState.set({ status: 'invalid' }); @@ -106,7 +114,7 @@ export async function verifyKey(key: string): Promise { supporterState.set({ status: 'checking' }); try { - const res = await fetch(`${PAYWALL_API_BASE}/verify?key=${encodeURIComponent(trimmed)}`, { + const res = await fetch(`${SUPPORTER_API_BASE}/verify?key=${encodeURIComponent(trimmed)}`, { headers: { accept: 'application/json' } }); const data = (await res.json()) as VerifyResult; @@ -135,6 +143,8 @@ export async function verifyKey(key: string): Promise { /** Re-verify the stored key (call on app / gated-page load). No-op if signed out. */ export async function refreshSupporter(): Promise { + if (!SUPPORTER_ENABLED) return; + const key = get(storedLicenseKey); if (!key) { supporterState.set({ status: 'idle' }); diff --git a/src/routes/weather/historical/[location]/+page.svelte b/src/routes/weather/historical/[location]/+page.svelte index aaeee1c..a6e626a 100644 --- a/src/routes/weather/historical/[location]/+page.svelte +++ b/src/routes/weather/historical/[location]/+page.svelte @@ -18,14 +18,14 @@ import { ChartContainer } from '$lib/components/charts'; import * as m from '$lib/paraglide/messages'; - import PaywallGate from '$lib/paywall/PaywallGate.svelte'; - import { isSupporter } from '$lib/paywall/supporter'; import { type ClimateNormals, type HistoricalForecastResult, fetchClimateNormals, fetchHistoricalWeather } from '$lib/services/weather'; + import SupporterGate from '$lib/supporter/SupporterGate.svelte'; + import { isSupporter } from '$lib/supporter/store'; import { useHeroActions } from '../../hero.svelte'; import { archiveModelGroups, defaultParameters } from '../../options'; @@ -43,7 +43,7 @@ // The page cross-fade waits for this before revealing the new page - and so // does the loading overlay, so a visitor without a key has to count as ready: - // the paywall is the finished page here, nothing is on its way. + // the locked panel is the finished page here, nothing is on its way. reportPageReady(() => result != null || loadError != null || !$isSupporter); useHeroActions(heroActions); @@ -243,7 +243,7 @@ - + {/if} - + diff --git a/src/routes/weather/seasonal/[location]/+page.svelte b/src/routes/weather/seasonal/[location]/+page.svelte index 1916ebe..0554a1a 100644 --- a/src/routes/weather/seasonal/[location]/+page.svelte +++ b/src/routes/weather/seasonal/[location]/+page.svelte @@ -13,14 +13,14 @@ import { Switch } from '$lib/components/ui/switch'; import * as m from '$lib/paraglide/messages'; - import PaywallGate from '$lib/paywall/PaywallGate.svelte'; - import { isSupporter } from '$lib/paywall/supporter'; import { type ClimateNormals, type SeasonalForecastResult, fetchClimateNormals, fetchSeasonalForecast } from '$lib/services/weather'; + import SupporterGate from '$lib/supporter/SupporterGate.svelte'; + import { isSupporter } from '$lib/supporter/store'; import { useHeroActions } from '../../hero.svelte'; import { defaultParameters, seasonalModelGroups } from '../../options'; @@ -37,7 +37,7 @@ // The page cross-fade waits for this before revealing the new page - and so // does the loading overlay, so a visitor without a key has to count as ready: - // the paywall is the finished page here, nothing is on its way. + // the locked panel is the finished page here, nothing is on its way. reportPageReady(() => result != null || loadError != null || !$isSupporter); useHeroActions(heroActions); @@ -202,7 +202,7 @@ mounting them on arrival re-flowed the row and nudged the heading. Two exceptions, both of which would reserve a gap for something that is never coming: below sm the group is a full-width row of its own, - and behind the paywall there is no forecast on its way at all. --> + and behind the supporter gate there is no forecast on its way at all. --> {#if $isSupporter}
{/if}
- +