Add NOK pricing catalog, credit ledger, success-based charging, and tier-gated model routing
- PricingCatalog.php: single source of truth for plans (free/plus/pro), top-ups, Stripe price env keys, tool costs (0–6 credits), STT variable billing, feature limits - FreeTier.php: monthly-first credit deduction, ledger (user_tool_credit_ledger), STT reservation/settle/release, monthly reset, trial logic - StripeClient.php: canonical SKUs (plus/pro/topup_100/300/1000), legacy aliases kept - stripe-checkout.php: subscription vs payment mode, trial gating, catalog metadata - stripe-webhook.php: idempotent via stripe_events, handles subscription lifecycle + invoice.paid renewal + one-time topup credit grants - All API tools: success-based credit deduction (check before, charge after) - transcribe.php: file-size heuristic reservation, settle from actual provider duration - ask.php + LegalTools.php: ToolModels engine resolution — Pro gets gpt-4o - KorrespondAgent.php + korrespond.php: tier-gated draft deployment — Free/Plus gets gpt-4o-mini, Pro gets gpt-4o - pricing.php: NOK-only, plan cards, top-up packs, Organisation contact card, tool cost table, separate monthly/prepaid balance display - 003_pricing_credit_catalog.sql: ledger and STT reservation tables Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+284
-321
@@ -3,8 +3,10 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
require_once __DIR__ . '/includes/FreeTier.php';
|
||||
require_once __DIR__ . '/includes/PricingCatalog.php';
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$isNorwegian = $uiLang === 'no';
|
||||
$isAuthed = dbnToolsIsAuthenticated();
|
||||
$currentTier = $isAuthed ? dbnToolsCurrentTier() : 'free';
|
||||
$surveyDone = false;
|
||||
@@ -15,167 +17,114 @@ if ($isAuthed && dbnToolsIsFreeTier()) {
|
||||
$status = (string)($_GET['status'] ?? '');
|
||||
$loginUrl = 'https://dobetternorge.no/tools-login.php?return=' . urlencode('/pricing.php');
|
||||
$surveyUrl = 'https://dobetternorge.no/survey.php';
|
||||
$orgUrl = 'mailto:support@dobetternorge.no?subject=DBN%20Tools%20Organisasjon';
|
||||
|
||||
function pt(string $key, string $lang): string {
|
||||
return htmlspecialchars(dbnToolsT($key, $lang));
|
||||
function h(mixed $value): string
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$creditsPerMonth = $uiLang === 'no' ? 'kreditter / mnd' : ($uiLang === 'uk' ? 'кредитів / міс' : ($uiLang === 'pl' ? 'kredytów / mies' : 'credits / mo'));
|
||||
$perMonth = $uiLang === 'no' ? '/ måned' : ($uiLang === 'uk' ? '/ міс' : ($uiLang === 'pl' ? '/ mies' : '/ month'));
|
||||
$capSuffix = $uiLang === 'no' ? '/ time' : ($uiLang === 'uk' ? '/ год' : ($uiLang === 'pl' ? '/ godz' : '/ hour'));
|
||||
$freeName = $uiLang === 'no' ? 'Gratis' : ($uiLang === 'uk' ? 'Безкоштовно' : ($uiLang === 'pl' ? 'Bezpłatnie' : 'Free'));
|
||||
$unlimited = $uiLang === 'no' ? 'Ubegrenset' : ($uiLang === 'uk' ? 'Необмежено' : ($uiLang === 'pl' ? 'Nieograniczone' : 'Unlimited'));
|
||||
$noStorage = $uiLang === 'no' ? 'Ingen saksoppbevaring' : ($uiLang === 'uk' ? 'Без сховища справ' : ($uiLang === 'pl' ? 'Brak przechowywania spraw' : 'No case storage'));
|
||||
function nok(int $amount): string
|
||||
{
|
||||
return PricingCatalog::formatNok($amount);
|
||||
}
|
||||
|
||||
$tiers = [
|
||||
[
|
||||
'sku' => 'free',
|
||||
'name' => $freeName,
|
||||
'price' => '€0',
|
||||
'period' => $uiLang === 'no' ? 'alltid' : ($uiLang === 'uk' ? 'завжди' : ($uiLang === 'pl' ? 'zawsze' : 'always')),
|
||||
'credits' => '30 ' . $creditsPerMonth,
|
||||
'storage' => $noStorage,
|
||||
'seats' => $uiLang === 'no' ? '1 bruker' : ($uiLang === 'uk' ? '1 користувач' : ($uiLang === 'pl' ? '1 użytkownik' : '1 user')),
|
||||
'cap' => '10 ' . $capSuffix,
|
||||
'features' => $uiLang === 'no' ? [
|
||||
'Alle 13 verktøy på innlimt tekst',
|
||||
'Norsk juridisk korpus (~220K passasjer)',
|
||||
'EU-vert (Tyskland / Finland / Norge)',
|
||||
] : ($uiLang === 'uk' ? [
|
||||
'Усі 13 інструментів на вставленому тексті',
|
||||
'Норвезький правовий корпус (~220K уривків)',
|
||||
'Розміщено в ЄС (Німеччина / Фінляндія / Норвегія)',
|
||||
] : ($uiLang === 'pl' ? [
|
||||
'Wszystkie 13 narzędzi na wklejonym tekście',
|
||||
'Norweski korpus prawny (~220K fragmentów)',
|
||||
'Hostowane w UE (Niemcy / Finlandia / Norwegia)',
|
||||
] : [
|
||||
'All 13 tools on pasted text',
|
||||
'Norwegian legal corpus (~220K passages)',
|
||||
'EU-hosted (Germany / Finland / Norway)',
|
||||
])),
|
||||
'cta' => $isAuthed ? null : dbnToolsT('pricing_cta_login', $uiLang),
|
||||
'highlight' => false,
|
||||
],
|
||||
[
|
||||
'sku' => 'light',
|
||||
'name' => 'Light',
|
||||
'price' => '€9',
|
||||
'period' => $perMonth,
|
||||
'credits' => '100 ' . $creditsPerMonth,
|
||||
'storage' => '500 MB',
|
||||
'seats' => $uiLang === 'no' ? '1 bruker' : ($uiLang === 'uk' ? '1 користувач' : ($uiLang === 'pl' ? '1 użytkownik' : '1 user')),
|
||||
'cap' => '20 ' . $capSuffix,
|
||||
'features' => $uiLang === 'no' ? [
|
||||
'Min Sak — last opp dokumenter med OCR',
|
||||
'Bruk min sak som kontekst i alle verktøy',
|
||||
'Lagrede analyser — alle resultater samlet',
|
||||
'14 dagers gratis prøveperiode (kort kreves)',
|
||||
] : ($uiLang === 'uk' ? [
|
||||
'Моя справа — завантаження документів з OCR',
|
||||
'Використання справи як контексту в усіх інструментах',
|
||||
'Збережені аналізи — всі результати в одному місці',
|
||||
'14-денна безкоштовна пробна версія (потрібна картка)',
|
||||
] : ($uiLang === 'pl' ? [
|
||||
'Moja sprawa — przesyłanie dokumentów z OCR',
|
||||
'Używanie sprawy jako kontekstu w każdym narzędziu',
|
||||
'Zapisane analizy — wszystkie wyniki w jednym miejscu',
|
||||
'14-dniowy bezpłatny okres próbny (wymagana karta)',
|
||||
] : [
|
||||
'My Case — upload documents with OCR',
|
||||
'Use my case as context in every tool',
|
||||
'Saved analyses — every run kept and searchable',
|
||||
'14-day free trial (card required)',
|
||||
])),
|
||||
'highlight' => false,
|
||||
'badge' => null,
|
||||
],
|
||||
[
|
||||
'sku' => 'pro',
|
||||
'name' => 'Pro',
|
||||
'price' => '€29',
|
||||
'period' => $perMonth,
|
||||
'credits' => '500 ' . $creditsPerMonth,
|
||||
'storage' => '5 GB',
|
||||
'seats' => $uiLang === 'no' ? '3 brukere · delt sak' : ($uiLang === 'uk' ? '3 користувачі · спільна справа' : ($uiLang === 'pl' ? '3 użytkowników · wspólna sprawa' : '3 users · shared case')),
|
||||
'cap' => '40 ' . $capSuffix,
|
||||
'features' => $uiLang === 'no' ? [
|
||||
'Alt i Light, med mer plass og raskere modeller',
|
||||
'Familie-sete: 3 innlogginger på samme sak',
|
||||
'Prioritert GPT-4o for kompleks analyse',
|
||||
'Audit-logg for hvem som kjørte hva',
|
||||
] : ($uiLang === 'uk' ? [
|
||||
'Все з Light, більше місця та швидші моделі',
|
||||
'Сімейні місця: 3 входи до однієї справи',
|
||||
'Пріоритетний GPT-4o для складного аналізу',
|
||||
'Журнал аудиту — хто що запускав',
|
||||
] : ($uiLang === 'pl' ? [
|
||||
'Wszystko z Light, więcej miejsca i szybsze modele',
|
||||
'Miejsca rodzinne: 3 logowania do jednej sprawy',
|
||||
'Priorytetowy GPT-4o do złożonych analiz',
|
||||
'Dziennik audytu — kto co uruchamiał',
|
||||
] : [
|
||||
'Everything in Light, with more space and faster models',
|
||||
'Family seats: 3 logins sharing one case',
|
||||
'Priority GPT-4o for complex analysis',
|
||||
'Audit log of who ran what',
|
||||
])),
|
||||
'highlight' => true,
|
||||
'badge' => $uiLang === 'no' ? 'Mest populær' : ($uiLang === 'uk' ? 'Найпопулярніший' : ($uiLang === 'pl' ? 'Najpopularniejszy' : 'Most popular')),
|
||||
],
|
||||
[
|
||||
'sku' => 'pro-plus',
|
||||
'name' => 'Pro+',
|
||||
'price' => '€79',
|
||||
'period' => $perMonth,
|
||||
'credits' => $unlimited,
|
||||
'storage' => '25 GB',
|
||||
'seats' => $uiLang === 'no' ? '10 brukere · delt sak' : ($uiLang === 'uk' ? '10 користувачів · спільна справа' : ($uiLang === 'pl' ? '10 użytkowników · wspólna sprawa' : '10 users · shared case')),
|
||||
'cap' => '80 ' . $capSuffix,
|
||||
'features' => $uiLang === 'no' ? [
|
||||
'Alt i Pro, ubegrenset kreditter',
|
||||
'25 GB delt sakslagring',
|
||||
'Dedikert prosesserings-kø (ingen ventetid)',
|
||||
'Personlig onboarding-samtale',
|
||||
] : ($uiLang === 'uk' ? [
|
||||
'Все з Pro, необмежені кредити',
|
||||
'25 GB спільного сховища справ',
|
||||
'Виділена черга обробки (без очікування)',
|
||||
'Особистий вступний дзвінок',
|
||||
] : ($uiLang === 'pl' ? [
|
||||
'Wszystko z Pro, nieograniczone kredyty',
|
||||
'25 GB wspólnego przechowywania spraw',
|
||||
'Dedykowana kolejka przetwarzania (bez czekania)',
|
||||
'Osobista rozmowa onboardingowa',
|
||||
] : [
|
||||
'Everything in Pro, unlimited credits',
|
||||
'25 GB shared case storage',
|
||||
'Dedicated processing queue (no waiting)',
|
||||
'Personal onboarding call',
|
||||
])),
|
||||
'highlight' => false,
|
||||
'badge' => $uiLang === 'no' ? 'For organisasjoner' : ($uiLang === 'uk' ? 'Для організацій' : ($uiLang === 'pl' ? 'Dla organizacji' : 'For organisations')),
|
||||
],
|
||||
function credits(int $amount): string
|
||||
{
|
||||
return PricingCatalog::formatCredits($amount);
|
||||
}
|
||||
|
||||
$copy = $isNorwegian ? [
|
||||
'title' => 'Priser - DBN Tools',
|
||||
'description' => 'NOK-priser, kreditter og abonnement for tools.dobetternorge.no.',
|
||||
'eyebrow' => 'NOK-priser for DBN Tools',
|
||||
'headline' => 'Kreditter som gir mening',
|
||||
'subhead' => 'Månedlige kreditter brukes først. Forhåndsbetalte kreditter legges på toppen og utløper ikke.',
|
||||
'trial' => 'Pluss har 14 dagers prøveperiode. Kort kreves, og du kan kansellere når som helst.',
|
||||
'survey_title' => 'Få 25 ekstra kreditter',
|
||||
'survey_text' => 'Svar på fem korte spørsmål om hvordan du bruker verktøyene.',
|
||||
'survey_cta' => 'Ta undersøkelsen',
|
||||
'current' => 'Din plan',
|
||||
'choose' => 'Velg',
|
||||
'login' => 'Logg inn for å velge',
|
||||
'available' => 'Tilgjengelig',
|
||||
'topups_title' => 'Ekstra kreditter',
|
||||
'topups_lead' => 'Top-ups er engangskjøp. De utløper ikke og brukes etter månedlige kreditter.',
|
||||
'buy' => 'Kjøp',
|
||||
'login_buy' => 'Logg inn for å kjøpe',
|
||||
'tool_costs' => 'Verktøykostnader',
|
||||
'tool_costs_lead' => 'Kreditter trekkes bare når verktøyet fullfører med et gyldig resultat.',
|
||||
'organisation' => 'Organisasjon',
|
||||
'organisation_price' => 'Kontakt',
|
||||
'organisation_text' => 'For rådgivere, frivillige miljøer og større familieteam som trenger flere brukere, særskilte avtaler eller onboarding.',
|
||||
'contact' => 'Snakk med oss',
|
||||
'billing_note' => 'Stripe brukes for kortbetaling, abonnement og kvitteringer. Lokale DBN-kreditter er fasiten for tilgang.',
|
||||
'status_success' => 'Betalingen er bekreftet. Kontoen oppdateres når Stripe-webhooken er behandlet.',
|
||||
'status_canceled' => 'Betalingen ble avbrutt. Ingen endringer er gjort.',
|
||||
'connecting' => 'Kobler til Stripe...',
|
||||
'checkout_error' => 'Kunne ikke starte betaling. Prøv igjen.',
|
||||
] : [
|
||||
'title' => 'Pricing - DBN Tools',
|
||||
'description' => 'NOK pricing, credits, and subscriptions for tools.dobetternorge.no.',
|
||||
'eyebrow' => 'NOK pricing for DBN Tools',
|
||||
'headline' => 'Credits that make sense',
|
||||
'subhead' => 'Monthly credits are spent first. Prepaid credits sit on top and never expire.',
|
||||
'trial' => 'Plus includes a 14-day trial. Card required, cancel anytime.',
|
||||
'survey_title' => 'Get 25 extra credits',
|
||||
'survey_text' => 'Answer five short questions about how you use the tools.',
|
||||
'survey_cta' => 'Take the survey',
|
||||
'current' => 'Current plan',
|
||||
'choose' => 'Choose',
|
||||
'login' => 'Log in to choose',
|
||||
'available' => 'Available',
|
||||
'topups_title' => 'Extra credits',
|
||||
'topups_lead' => 'Top-ups are one-time purchases. They never expire and are spent after monthly credits.',
|
||||
'buy' => 'Buy',
|
||||
'login_buy' => 'Log in to buy',
|
||||
'tool_costs' => 'Tool costs',
|
||||
'tool_costs_lead' => 'Credits are charged only when a tool completes with a valid result.',
|
||||
'organisation' => 'Organisation',
|
||||
'organisation_price' => 'Contact',
|
||||
'organisation_text' => 'For advisers, volunteer groups, and larger family teams that need more users, custom terms, or onboarding.',
|
||||
'contact' => 'Talk to us',
|
||||
'billing_note' => 'Stripe handles cards, subscriptions, and receipts. Local DBN credits remain authoritative for access.',
|
||||
'status_success' => 'Payment confirmed. Your account updates when the Stripe webhook is processed.',
|
||||
'status_canceled' => 'Payment was canceled. No changes were made.',
|
||||
'connecting' => 'Connecting to Stripe...',
|
||||
'checkout_error' => 'Could not start checkout. Please try again.',
|
||||
];
|
||||
|
||||
$topupNotes = [
|
||||
'topup_s' => dbnToolsT('pricing_topup_s_note', $uiLang),
|
||||
'topup_m' => dbnToolsT('pricing_topup_m_note', $uiLang),
|
||||
'topup_l' => dbnToolsT('pricing_topup_l_note', $uiLang),
|
||||
$plans = PricingCatalog::plans();
|
||||
$topups = PricingCatalog::topups();
|
||||
$planFeaturesNo = [
|
||||
'free' => ['30 kreditter per måned', 'Verktøy på innlimt tekst', 'Juridisk korpussøk', 'Ingen Min Sak-lagring'],
|
||||
'plus' => ['250 kreditter per måned', '500 MB Min Sak-lagring', '1 bruker', '14 dagers prøveperiode'],
|
||||
'pro' => ['900 kreditter per måned', '5 GB Min Sak-lagring', '3 brukere', 'Full Azure-modellrute'],
|
||||
];
|
||||
$topups = [
|
||||
['sku' => 'topup_s', 'price' => 'NOK 49', 'credits' => 30, 'note' => $topupNotes['topup_s']],
|
||||
['sku' => 'topup_m', 'price' => 'NOK 149', 'credits' => 100, 'note' => $topupNotes['topup_m']],
|
||||
['sku' => 'topup_l', 'price' => 'NOK 399', 'credits' => 300, 'note' => $topupNotes['topup_l']],
|
||||
$planFeaturesEn = [
|
||||
'free' => ['30 credits per month', 'Tools on pasted text', 'Legal corpus search', 'No My Case storage'],
|
||||
'plus' => ['250 credits per month', '500 MB My Case storage', '1 user', '14-day trial'],
|
||||
'pro' => ['900 credits per month', '5 GB My Case storage', '3 users', 'Full Azure model route'],
|
||||
];
|
||||
$planFeatures = $isNorwegian ? $planFeaturesNo : $planFeaturesEn;
|
||||
|
||||
$toolCostRows = [
|
||||
['0', 'search, corpus-search, clarify-only gates'],
|
||||
['1', 'ask, extract, summarize, translate, korrespond_refine'],
|
||||
['2', 'timeline, redact'],
|
||||
['3', 'barnevernet, advocate, korrespond, legal-analysis'],
|
||||
['4', 'discrepancy'],
|
||||
['6', 'deep-research'],
|
||||
[$isNorwegian ? 'variabel' : 'variable', $isNorwegian ? 'transcribe: 1 kreditt per startet lydminutt, minst 5' : 'transcribe: 1 credit per started audio minute, minimum 5'],
|
||||
];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
||||
<html lang="<?= h($uiLang) ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= pt('pricing_title_meta', $uiLang) ?></title>
|
||||
<meta name="description" content="<?= pt('pricing_desc_meta', $uiLang) ?>">
|
||||
<title><?= h($copy['title']) ?></title>
|
||||
<meta name="description" content="<?= h($copy['description']) ?>">
|
||||
<link rel="canonical" href="https://tools.dobetternorge.no/pricing.php">
|
||||
<meta name="theme-color" content="#00205B">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
@@ -184,218 +133,232 @@ $topups = [
|
||||
<link rel="stylesheet" href="assets/css/tools.css">
|
||||
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.css">
|
||||
<style>
|
||||
.pricing-shell { max-width: 1200px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
|
||||
.pricing-hero { text-align: center; margin-bottom: 3rem; }
|
||||
.pricing-hero h1 { font-family: 'Crimson Pro', serif; font-size: 2.5rem; margin: 0 0 0.75rem; }
|
||||
.pricing-hero p { color: #4b5563; font-size: 1.1rem; max-width: 640px; margin: 0 auto; }
|
||||
.pricing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.25rem; margin-bottom: 3rem; }
|
||||
.pricing-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 1.75rem 1.5rem; display: flex; flex-direction: column; position: relative; }
|
||||
.pricing-card.is-highlight { border-color: #00205B; border-width: 2px; box-shadow: 0 8px 24px rgba(0,32,91,0.08); }
|
||||
.pricing-card .pricing-badge { position: absolute; top: -10px; right: 14px; background: #00205B; color: #fff; padding: 4px 10px; font-size: 0.72rem; border-radius: 999px; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 600; }
|
||||
.pricing-card h2 { margin: 0 0 0.25rem; font-size: 1.4rem; font-family: 'Crimson Pro', serif; }
|
||||
.pricing-price { display: flex; align-items: baseline; gap: 0.4rem; margin: 0.5rem 0 1rem; }
|
||||
.pricing-price .amount { font-size: 2.2rem; font-weight: 700; color: #00205B; }
|
||||
.pricing-price .period { color: #6b7280; font-size: 0.95rem; }
|
||||
.pricing-meta { margin: 0 0 1.25rem; padding: 0; list-style: none; font-size: 0.92rem; color: #374151; }
|
||||
.pricing-meta li { padding: 6px 0; border-bottom: 1px dashed #f3f4f6; }
|
||||
.pricing-meta li:last-child { border-bottom: none; }
|
||||
.pricing-features { list-style: none; padding: 0; margin: 0 0 1.5rem; flex: 1; }
|
||||
.pricing-features li { padding: 5px 0 5px 1.4rem; position: relative; font-size: 0.92rem; color: #1f2937; }
|
||||
.pricing-features li::before { content: "✓"; position: absolute; left: 0; color: #059669; font-weight: 700; }
|
||||
.pricing-cta { display: block; text-align: center; padding: 0.75rem 1rem; border-radius: 8px; font-weight: 600; text-decoration: none; transition: all 0.15s; cursor: pointer; border: none; font-size: 0.95rem; }
|
||||
.pricing-cta.primary { background: #00205B; color: #fff; }
|
||||
.pricing-cta.primary:hover { background: #001740; }
|
||||
.pricing-cta.secondary { background: #f3f4f6; color: #1f2937; }
|
||||
.pricing-cta.secondary:hover { background: #e5e7eb; }
|
||||
.pricing-cta.current { background: #d1fae5; color: #065f46; cursor: default; }
|
||||
.pricing-topups { margin-top: 2rem; padding: 2rem 1.5rem; background: #f9fafb; border-radius: 12px; }
|
||||
.pricing-topups h2 { font-family: 'Crimson Pro', serif; margin: 0 0 0.5rem; font-size: 1.6rem; }
|
||||
.pricing-topups p.lead { color: #6b7280; margin: 0 0 1.5rem; }
|
||||
.topup-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
|
||||
.topup-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 1.25rem; text-align: center; }
|
||||
.topup-card .price { font-size: 1.6rem; font-weight: 700; color: #00205B; }
|
||||
.topup-card .credits { color: #374151; font-size: 0.95rem; margin: 0.25rem 0 0.5rem; }
|
||||
.topup-card .note { color: #6b7280; font-size: 0.82rem; margin-bottom: 0.75rem; }
|
||||
.survey-banner { background: linear-gradient(135deg, #00205B, #003478); color: #fff; padding: 1.75rem 1.5rem; border-radius: 12px; margin-bottom: 2rem; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; }
|
||||
.survey-banner .copy { flex: 1; min-width: 260px; }
|
||||
.survey-banner h3 { margin: 0 0 0.35rem; font-size: 1.3rem; font-family: 'Crimson Pro', serif; }
|
||||
.survey-banner p { margin: 0; opacity: 0.9; font-size: 0.95rem; }
|
||||
.survey-banner a { background: #ffd166; color: #00205B; padding: 0.7rem 1.4rem; border-radius: 8px; font-weight: 700; text-decoration: none; white-space: nowrap; }
|
||||
.pricing-faq { margin-top: 3rem; }
|
||||
.pricing-faq details { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 0.6rem; }
|
||||
.pricing-faq summary { font-weight: 600; cursor: pointer; }
|
||||
.pricing-faq p { color: #4b5563; margin: 0.75rem 0 0; font-size: 0.92rem; }
|
||||
.status-pill-info { display: inline-block; margin-bottom: 1.5rem; padding: 6px 12px; background: #fef3c7; color: #92400e; border-radius: 6px; font-size: 0.9rem; }
|
||||
.status-pill-success { background: #d1fae5; color: #065f46; }
|
||||
.status-pill-error { background: #fee2e2; color: #991b1b; }
|
||||
.lang-bar { text-align: right; margin-bottom: 1rem; font-size: 0.85rem; }
|
||||
.lang-bar a { margin-left: 0.5rem; color: #6b7280; text-decoration: none; padding: 2px 6px; border-radius: 4px; }
|
||||
.lang-bar a.is-active { background: #00205B; color: #fff; }
|
||||
:root { --dbn-navy:#00205B; --dbn-red:#BA0C2F; --dbn-ink:#111827; --dbn-muted:#5b6472; --dbn-line:#d9dee8; --dbn-soft:#f7f9fc; --dbn-green:#0f766e; }
|
||||
body { margin:0; background:#fbfcfe; color:var(--dbn-ink); font-family:'IBM Plex Sans', system-ui, sans-serif; }
|
||||
.pricing-shell { max-width:1180px; margin:0 auto; padding:28px 20px 64px; }
|
||||
.lang-bar { display:flex; justify-content:flex-end; gap:8px; font-size:0.86rem; margin-bottom:18px; }
|
||||
.lang-bar a { color:var(--dbn-muted); text-decoration:none; padding:4px 8px; border-radius:6px; }
|
||||
.lang-bar a.is-active { background:var(--dbn-navy); color:#fff; }
|
||||
.pricing-hero { display:grid; grid-template-columns:minmax(0, 1.2fr) minmax(280px, .8fr); gap:28px; align-items:end; padding:28px 0 34px; border-bottom:1px solid var(--dbn-line); }
|
||||
.eyebrow { margin:0 0 8px; color:var(--dbn-red); font-size:.82rem; font-weight:700; text-transform:uppercase; letter-spacing:.08em; }
|
||||
h1 { margin:0; font-family:'Crimson Pro', serif; font-size:clamp(2.2rem, 4vw, 4rem); line-height:1; letter-spacing:0; }
|
||||
.hero-copy { margin:14px 0 0; color:var(--dbn-muted); font-size:1.05rem; max-width:680px; }
|
||||
.hero-note { background:#fff; border:1px solid var(--dbn-line); border-left:5px solid var(--dbn-red); border-radius:8px; padding:18px 18px; color:#263244; }
|
||||
.status-pill { display:inline-flex; margin:22px 0 0; padding:9px 13px; border-radius:8px; font-size:.92rem; background:#fff7ed; color:#9a3412; border:1px solid #fed7aa; }
|
||||
.status-pill.success { background:#ecfdf5; color:#065f46; border-color:#a7f3d0; }
|
||||
.survey-banner { margin:26px 0 0; display:flex; align-items:center; justify-content:space-between; gap:16px; background:var(--dbn-navy); color:#fff; border-radius:8px; padding:18px 20px; }
|
||||
.survey-banner h2 { margin:0 0 4px; font-size:1.1rem; }
|
||||
.survey-banner p { margin:0; color:rgba(255,255,255,.82); }
|
||||
.btn, .pricing-cta { border:0; display:inline-flex; align-items:center; justify-content:center; min-height:42px; padding:0 15px; border-radius:8px; font-weight:700; text-decoration:none; cursor:pointer; line-height:1.1; }
|
||||
.btn-primary { background:var(--dbn-navy); color:#fff; }
|
||||
.btn-primary:hover { background:#001740; }
|
||||
.btn-light { background:#fff; color:var(--dbn-navy); }
|
||||
.btn-muted { background:#edf1f7; color:#263244; }
|
||||
.btn-current { background:#dcfce7; color:#166534; cursor:default; }
|
||||
.plans-grid { display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:16px; margin:28px 0; }
|
||||
.plan-card, .topup-card, .cost-panel { background:#fff; border:1px solid var(--dbn-line); border-radius:8px; padding:20px; }
|
||||
.plan-card { display:flex; flex-direction:column; min-height:390px; position:relative; }
|
||||
.plan-card.highlight { border-color:var(--dbn-navy); box-shadow:0 10px 30px rgba(0,32,91,.10); }
|
||||
.plan-badge { position:absolute; top:14px; right:14px; background:#e8eef8; color:var(--dbn-navy); border-radius:999px; padding:4px 9px; font-size:.74rem; font-weight:800; }
|
||||
.plan-name { margin:0; font-size:1.35rem; font-family:'Crimson Pro', serif; }
|
||||
.plan-price { margin:16px 0 4px; display:flex; align-items:baseline; gap:6px; }
|
||||
.plan-price strong { font-size:2rem; color:var(--dbn-navy); }
|
||||
.plan-meta { margin:0 0 16px; color:var(--dbn-muted); font-size:.92rem; }
|
||||
.plan-list { margin:0 0 22px; padding:0; list-style:none; flex:1; }
|
||||
.plan-list li { padding:8px 0; border-bottom:1px dashed #eef1f6; font-size:.94rem; }
|
||||
.fine-print { margin:10px 0 0; color:var(--dbn-muted); font-size:.84rem; }
|
||||
.section-head { display:flex; align-items:flex-end; justify-content:space-between; gap:16px; margin:34px 0 14px; }
|
||||
.section-head h2 { margin:0; font-family:'Crimson Pro', serif; font-size:1.9rem; }
|
||||
.section-head p { margin:0; color:var(--dbn-muted); max-width:640px; }
|
||||
.topup-grid { display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:16px; }
|
||||
.topup-card { display:grid; gap:10px; }
|
||||
.topup-price { color:var(--dbn-navy); font-size:1.8rem; font-weight:800; }
|
||||
.topup-credits { font-weight:700; }
|
||||
.topup-rate { color:var(--dbn-muted); font-size:.9rem; }
|
||||
.cost-panel { margin-top:16px; overflow:auto; }
|
||||
.cost-table { width:100%; border-collapse:collapse; min-width:620px; }
|
||||
.cost-table th, .cost-table td { text-align:left; padding:11px 10px; border-bottom:1px solid #edf1f7; }
|
||||
.cost-table th { color:var(--dbn-muted); font-size:.84rem; text-transform:uppercase; letter-spacing:.06em; }
|
||||
.billing-note { margin-top:18px; color:var(--dbn-muted); font-size:.92rem; }
|
||||
@media (max-width:980px) { .pricing-hero { grid-template-columns:1fr; } .plans-grid { grid-template-columns:repeat(2, minmax(0,1fr)); } }
|
||||
@media (max-width:680px) { .pricing-shell { padding-inline:14px; } .plans-grid, .topup-grid { grid-template-columns:1fr; } .survey-banner, .section-head { align-items:flex-start; flex-direction:column; } .plan-card { min-height:0; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="pricing-shell">
|
||||
<div class="lang-bar">
|
||||
<?php foreach (['no', 'en', 'uk', 'pl'] as $lc): ?>
|
||||
<a href="?lang=<?= $lc ?>" class="<?= $lc === $uiLang ? 'is-active' : '' ?>"><?= htmlspecialchars(dbnToolsLanguageLabel($lc)) ?></a>
|
||||
<nav class="lang-bar" aria-label="Language">
|
||||
<?php foreach (['no', 'en'] as $lc): ?>
|
||||
<a href="?lang=<?= h($lc) ?>" class="<?= $lc === $uiLang ? 'is-active' : '' ?>"><?= h(dbnToolsLanguageLabel($lc)) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="pricing-hero">
|
||||
<p style="margin:0 0 0.5rem; text-transform:uppercase; letter-spacing:0.08em; color:#6b7280; font-size:0.85rem;"><?= pt('pricing_eyebrow', $uiLang) ?></p>
|
||||
<h1><?= pt('pricing_hero_title', $uiLang) ?></h1>
|
||||
<p><?= pt('pricing_hero_sub', $uiLang) ?></p>
|
||||
<div>
|
||||
<p class="eyebrow"><?= h($copy['eyebrow']) ?></p>
|
||||
<h1><?= h($copy['headline']) ?></h1>
|
||||
<p class="hero-copy"><?= h($copy['subhead']) ?></p>
|
||||
</div>
|
||||
<aside class="hero-note">
|
||||
<?= h($copy['trial']) ?>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<?php if ($status === 'success'): ?>
|
||||
<p class="status-pill-info status-pill-success"><?= pt('pricing_status_success', $uiLang) ?></p>
|
||||
<p class="status-pill success"><?= h($copy['status_success']) ?></p>
|
||||
<?php elseif ($status === 'canceled'): ?>
|
||||
<p class="status-pill-info"><?= pt('pricing_status_canceled', $uiLang) ?></p>
|
||||
<p class="status-pill"><?= h($copy['status_canceled']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="background:linear-gradient(135deg,#fef3c7,#fcd34d);color:#78350f;padding:1rem 1.5rem;border-radius:10px;margin-bottom:2rem;text-align:center;font-weight:600;">
|
||||
<?= match($uiLang) {
|
||||
'no' => '🎉 Prøv Light gratis i 14 dager — kort kreves, kanseller når som helst, ingen belastning før dag 15.',
|
||||
'uk' => '🎉 Спробуйте Light безкоштовно 14 днів — потрібна картка, скасуйте будь-коли, списання лише з 15 дня.',
|
||||
'pl' => '🎉 Wypróbuj Light za darmo przez 14 dni — wymagana karta, anuluj w dowolnym momencie, brak opłat przed 15 dniem.',
|
||||
default => '🎉 Try Light free for 14 days — card required, cancel anytime, no charge until day 15.',
|
||||
} ?>
|
||||
</div>
|
||||
|
||||
<?php if ($isAuthed && !$surveyDone): ?>
|
||||
<div class="survey-banner">
|
||||
<div class="copy">
|
||||
<h3><?= pt('pricing_survey_title', $uiLang) ?></h3>
|
||||
<p><?= pt('pricing_survey_text', $uiLang) ?></p>
|
||||
</div>
|
||||
<a href="<?= htmlspecialchars($surveyUrl) ?>"><?= pt('pricing_survey_cta', $uiLang) ?></a>
|
||||
</div>
|
||||
<section class="survey-banner">
|
||||
<div>
|
||||
<h2><?= h($copy['survey_title']) ?></h2>
|
||||
<p><?= h($copy['survey_text']) ?></p>
|
||||
</div>
|
||||
<a class="btn btn-light" href="<?= h($surveyUrl) ?>"><?= h($copy['survey_cta']) ?></a>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="pricing-grid" aria-label="<?= pt('pricing_faq_title', $uiLang) ?>">
|
||||
<?php foreach ($tiers as $tier): ?>
|
||||
<article class="pricing-card<?= !empty($tier['highlight']) ? ' is-highlight' : '' ?>">
|
||||
<?php if (!empty($tier['badge'])): ?>
|
||||
<span class="pricing-badge"><?= htmlspecialchars($tier['badge']) ?></span>
|
||||
<?php endif; ?>
|
||||
<h2><?= htmlspecialchars($tier['name']) ?></h2>
|
||||
<div class="pricing-price">
|
||||
<span class="amount"><?= htmlspecialchars($tier['price']) ?></span>
|
||||
<span class="period"><?= htmlspecialchars($tier['period']) ?></span>
|
||||
</div>
|
||||
<ul class="pricing-meta">
|
||||
<li><?= htmlspecialchars($tier['credits']) ?></li>
|
||||
<li><?= htmlspecialchars($tier['storage']) ?></li>
|
||||
<li><?= htmlspecialchars($tier['seats']) ?></li>
|
||||
<li><?= htmlspecialchars($tier['cap']) ?></li>
|
||||
</ul>
|
||||
<ul class="pricing-features">
|
||||
<?php foreach ($tier['features'] as $feature): ?>
|
||||
<li><?= htmlspecialchars($feature) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php if ($tier['sku'] === 'free'): ?>
|
||||
<?php if (!$isAuthed): ?>
|
||||
<a class="pricing-cta primary" href="<?= htmlspecialchars($loginUrl) ?>"><?= htmlspecialchars($tier['cta'] ?? dbnToolsT('pricing_cta_login', $uiLang)) ?></a>
|
||||
<?php elseif ($currentTier === 'free'): ?>
|
||||
<span class="pricing-cta current"><?= pt('pricing_cta_current', $uiLang) ?></span>
|
||||
<section class="plans-grid" aria-label="Plans">
|
||||
<?php foreach (['free', 'plus', 'pro'] as $sku): ?>
|
||||
<?php $plan = $plans[$sku]; ?>
|
||||
<article class="plan-card<?= $sku === 'pro' ? ' highlight' : '' ?>">
|
||||
<?php if ($sku === 'pro'): ?><span class="plan-badge">Pro</span><?php endif; ?>
|
||||
<h2 class="plan-name"><?= h($plan['name']) ?></h2>
|
||||
<p class="plan-price">
|
||||
<strong><?= h(nok((int)$plan['price_nok'])) ?></strong>
|
||||
<span><?= $sku === 'free' ? '' : ($isNorwegian ? '/ mnd' : '/ mo') ?></span>
|
||||
</p>
|
||||
<p class="plan-meta">
|
||||
<?php if ($sku === 'free'): ?>
|
||||
<?= $isNorwegian ? 'Startnivå' : 'Starter tier' ?>
|
||||
<?php else: ?>
|
||||
<?= h(sprintf('%.2f', (float)$plan['effective_credit_cost'])) ?> kr / <?= $isNorwegian ? 'kreditt' : 'credit' ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<ul class="plan-list">
|
||||
<?php foreach ($planFeatures[$sku] as $feature): ?>
|
||||
<li><?= h($feature) ?></li>
|
||||
<?php endforeach; ?>
|
||||
<li><?= (int)$plan['hourly_cap'] ?> <?= $isNorwegian ? 'betalte kjøringer per time' : 'paid runs per hour' ?></li>
|
||||
</ul>
|
||||
<?php if ($sku === 'free'): ?>
|
||||
<?php if (!$isAuthed): ?>
|
||||
<a class="pricing-cta btn-primary" href="<?= h($loginUrl) ?>"><?= h($copy['login']) ?></a>
|
||||
<?php elseif ($currentTier === 'free'): ?>
|
||||
<span class="pricing-cta btn-current"><?= h($copy['current']) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="pricing-cta btn-muted"><?= h($copy['available']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span class="pricing-cta secondary"><?= pt('pricing_cta_available', $uiLang) ?></span>
|
||||
<?php if (!$isAuthed): ?>
|
||||
<a class="pricing-cta btn-primary" href="<?= h($loginUrl) ?>"><?= h($copy['login']) ?></a>
|
||||
<?php elseif ($currentTier === $sku): ?>
|
||||
<span class="pricing-cta btn-current"><?= h($copy['current']) ?></span>
|
||||
<?php else: ?>
|
||||
<button type="button" class="pricing-cta btn-primary" data-sku="<?= h($sku) ?>"><?= h($copy['choose'] . ' ' . $plan['name']) ?></button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<?php if (!$isAuthed): ?>
|
||||
<a class="pricing-cta primary" href="<?= htmlspecialchars($loginUrl) ?>"><?= pt('pricing_cta_subscribe', $uiLang) ?></a>
|
||||
<?php elseif ($currentTier === $tier['sku']): ?>
|
||||
<span class="pricing-cta current"><?= pt('pricing_cta_current', $uiLang) ?></span>
|
||||
<?php else: ?>
|
||||
<button type="button" class="pricing-cta primary" data-sku="<?= htmlspecialchars($tier['sku']) ?>" data-checkout="subscription">
|
||||
<?= pt('pricing_cta_choose', $uiLang) ?> <?= htmlspecialchars($tier['name']) ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<article class="plan-card">
|
||||
<span class="plan-badge"><?= h($copy['organisation']) ?></span>
|
||||
<h2 class="plan-name"><?= h($copy['organisation']) ?></h2>
|
||||
<p class="plan-price"><strong><?= h($copy['organisation_price']) ?></strong></p>
|
||||
<p class="plan-meta"><?= $isNorwegian ? 'Tilpasset avtale' : 'Custom terms' ?></p>
|
||||
<ul class="plan-list">
|
||||
<li><?= $isNorwegian ? 'Flere brukere' : 'More users' ?></li>
|
||||
<li><?= $isNorwegian ? 'Tilpassede kreditter' : 'Custom credits' ?></li>
|
||||
<li><?= $isNorwegian ? 'Onboarding og støtte' : 'Onboarding and support' ?></li>
|
||||
<li><?= $isNorwegian ? 'Avtales direkte' : 'Agreed directly' ?></li>
|
||||
</ul>
|
||||
<a class="pricing-cta btn-muted" href="<?= h($orgUrl) ?>"><?= h($copy['contact']) ?></a>
|
||||
<p class="fine-print"><?= h($copy['organisation_text']) ?></p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="pricing-topups" aria-label="<?= pt('pricing_topup_title', $uiLang) ?>">
|
||||
<h2><?= pt('pricing_topup_title', $uiLang) ?></h2>
|
||||
<p class="lead"><?= pt('pricing_topup_lead', $uiLang) ?></p>
|
||||
<div class="topup-grid">
|
||||
<?php foreach ($topups as $topup): ?>
|
||||
<div class="topup-card">
|
||||
<div class="price"><?= htmlspecialchars($topup['price']) ?></div>
|
||||
<div class="credits"><?= (int)$topup['credits'] ?> <?= pt('pricing_credits_label', $uiLang) ?></div>
|
||||
<div class="note"><?= htmlspecialchars($topup['note']) ?></div>
|
||||
<?php if ($isAuthed): ?>
|
||||
<button type="button" class="pricing-cta primary" data-sku="<?= htmlspecialchars($topup['sku']) ?>" data-checkout="topup"><?= pt('pricing_topup_buy', $uiLang) ?></button>
|
||||
<?php else: ?>
|
||||
<a class="pricing-cta primary" href="<?= htmlspecialchars($loginUrl) ?>"><?= pt('pricing_login_first', $uiLang) ?></a>
|
||||
<?php endif; ?>
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2><?= h($copy['topups_title']) ?></h2>
|
||||
<p><?= h($copy['topups_lead']) ?></p>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="topup-grid">
|
||||
<?php foreach ($topups as $topup): ?>
|
||||
<article class="topup-card">
|
||||
<h3 class="plan-name"><?= h($topup['name']) ?></h3>
|
||||
<div class="topup-price"><?= h(nok((int)$topup['price_nok'])) ?></div>
|
||||
<div class="topup-credits"><?= h(credits((int)$topup['credits'])) ?> <?= $isNorwegian ? 'kreditter' : 'credits' ?></div>
|
||||
<div class="topup-rate"><?= h(sprintf('%.2f', (float)$topup['cost_per_credit'])) ?> kr / <?= $isNorwegian ? 'kreditt' : 'credit' ?></div>
|
||||
<?php if ($isAuthed): ?>
|
||||
<button type="button" class="pricing-cta btn-primary" data-sku="<?= h($topup['sku']) ?>"><?= h($copy['buy']) ?></button>
|
||||
<?php else: ?>
|
||||
<a class="pricing-cta btn-primary" href="<?= h($loginUrl) ?>"><?= h($copy['login_buy']) ?></a>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pricing-faq" aria-label="<?= pt('pricing_faq_title', $uiLang) ?>">
|
||||
<h2 style="font-family:'Crimson Pro', serif; margin-bottom:1rem;"><?= pt('pricing_faq_title', $uiLang) ?></h2>
|
||||
<details>
|
||||
<summary><?= pt('pricing_faq1_q', $uiLang) ?></summary>
|
||||
<p><?= pt('pricing_faq1_a', $uiLang) ?></p>
|
||||
</details>
|
||||
<details>
|
||||
<summary><?= pt('pricing_faq2_q', $uiLang) ?></summary>
|
||||
<p><?= pt('pricing_faq2_a', $uiLang) ?></p>
|
||||
</details>
|
||||
<details>
|
||||
<summary><?= pt('pricing_faq3_q', $uiLang) ?></summary>
|
||||
<p><?= pt('pricing_faq3_a', $uiLang) ?></p>
|
||||
</details>
|
||||
<details>
|
||||
<summary><?= pt('pricing_faq4_q', $uiLang) ?></summary>
|
||||
<p><?= pt('pricing_faq4_a', $uiLang) ?></p>
|
||||
</details>
|
||||
<details>
|
||||
<summary><?= pt('pricing_faq5_q', $uiLang) ?></summary>
|
||||
<p><?= pt('pricing_faq5_a', $uiLang) ?></p>
|
||||
</details>
|
||||
<details>
|
||||
<summary><?= pt('pricing_faq6_q', $uiLang) ?></summary>
|
||||
<p><?= pt('pricing_faq6_a', $uiLang) ?></p>
|
||||
</details>
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2><?= h($copy['tool_costs']) ?></h2>
|
||||
<p><?= h($copy['tool_costs_lead']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cost-panel">
|
||||
<table class="cost-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $isNorwegian ? 'Kostnad' : 'Cost' ?></th>
|
||||
<th><?= $isNorwegian ? 'Verktøy' : 'Tools' ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($toolCostRows as $row): ?>
|
||||
<tr>
|
||||
<td><?= h($row[0]) ?></td>
|
||||
<td><?= h($row[1]) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="billing-note"><?= h($copy['billing_note']) ?></p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const connecting = <?= json_encode(dbnToolsT('pricing_connecting', $uiLang)) ?>;
|
||||
const errorRetry = <?= json_encode(dbnToolsT('pricing_error_retry', $uiLang)) ?>;
|
||||
const errorMsg = <?= json_encode(dbnToolsT('pricing_error_checkout', $uiLang)) ?>;
|
||||
|
||||
const buttons = document.querySelectorAll('button[data-checkout]');
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const sku = btn.getAttribute('data-sku');
|
||||
btn.disabled = true;
|
||||
const original = btn.textContent;
|
||||
btn.textContent = connecting;
|
||||
const connecting = <?= json_encode($copy['connecting'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const checkoutError = <?= json_encode($copy['checkout_error'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
document.querySelectorAll('button[data-sku]').forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
const sku = button.getAttribute('data-sku');
|
||||
const original = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = connecting;
|
||||
try {
|
||||
const res = await fetch('/api/stripe-checkout.php', {
|
||||
const response = await fetch('/api/stripe-checkout.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ sku })
|
||||
});
|
||||
const data = await res.json();
|
||||
const data = await response.json();
|
||||
if (data.ok && data.url) {
|
||||
window.location.href = data.url;
|
||||
} else {
|
||||
btn.textContent = errorRetry;
|
||||
alert(data.error?.message || errorMsg);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
btn.textContent = original;
|
||||
alert(e.message);
|
||||
alert(data.error?.message || checkoutError);
|
||||
} catch (error) {
|
||||
alert(error.message || checkoutError);
|
||||
} finally {
|
||||
setTimeout(() => { btn.disabled = false; btn.textContent = original; }, 1500);
|
||||
button.disabled = false;
|
||||
button.textContent = original;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user