fix supplier dashboard UX, Paddle integration, and dev tooling

Supplier dashboard: impersonate redirects to supplier dashboard when user
owns a supplier, sidenav active tab updates on click, listing preview
capped at 420px, insufficient-credits error shows balance and buy CTA,
boost buy buttons resolve Paddle price IDs server-side.

Dev tooling: dev_run.sh kills child processes on Ctrl-C (process
substitution fix), syncs Paddle products to DB on each run,
setup_paddle.py gains --sync mode and fixes Duration/Interval SDK change.

Seed data: each claimed supplier gets its own owner user and subscription.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-18 19:40:06 +01:00
parent 891b875cd1
commit 0fe5ab1259
11 changed files with 212 additions and 64 deletions

View File

@@ -13,6 +13,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **Supplier impersonation** — "Impersonate Owner" button on admin supplier - **Supplier impersonation** — "Impersonate Owner" button on admin supplier
detail page; reuses existing `admin.impersonate` route, shown only when detail page; reuses existing `admin.impersonate` route, shown only when
the supplier has a `claimed_by` user the supplier has a `claimed_by` user
- **Seed data: supplier owner accounts** — each claimed supplier now gets its
own user + active subscription for realistic impersonation testing
### Fixed
- **Insufficient credits UX** — unlock error now shows credit balance, cost,
and a "Buy Credits" CTA linking to Boost & Upsells tab (was a collapsed
red error message with no action)
- **Supplier dashboard sidenav** — active tab highlight now updates on click
(was only correct on initial page load)
- **Boost/credit buy buttons used wrong price IDs** — dashboard boosts tab
passed internal keys (`boost_logo`, `credits_25`) as Paddle `priceId`
instead of resolved Paddle price IDs; buttons now show "Not configured"
when Paddle products aren't set up
- **Listing preview card stretched full width** — constrained to 420px to
match actual directory card proportions
- **Impersonate redirects to supplier dashboard** — impersonating a user who
owns a supplier now lands on `/suppliers/dashboard` instead of the generic
demand-side dashboard
### Added — Programmatic SEO: Content Generation Engine ### Added — Programmatic SEO: Content Generation Engine
- **Database migration 0010** — `published_scenarios`, `article_templates`, - **Database migration 0010** — `published_scenarios`, `article_templates`,

View File

@@ -59,6 +59,12 @@ info "Seeding development data"
uv run python -m padelnomics.scripts.seed_dev_data uv run python -m padelnomics.scripts.seed_dev_data
ok "Dev data seeded" ok "Dev data seeded"
if [ -n "$PADDLE_API_KEY" ]; then
info "Syncing Paddle products to DB"
uv run python -m padelnomics.scripts.setup_paddle --sync
ok "Paddle products synced"
fi
info "Building CSS" info "Building CSS"
make css-build make css-build
ok "CSS built" ok "CSS built"

View File

@@ -312,7 +312,13 @@ async def impersonate(user_id: int):
session["admin_impersonating"] = True session["admin_impersonating"] = True
session["user_id"] = user_id session["user_id"] = user_id
# Redirect to supplier dashboard if user owns a supplier, otherwise generic
supplier = await fetch_one(
"SELECT id FROM suppliers WHERE claimed_by = ? LIMIT 1", (user_id,)
)
await flash(f"Now impersonating {user['email']}. Return to admin to stop.", "warning") await flash(f"Now impersonating {user['email']}. Return to admin to stop.", "warning")
if supplier:
return redirect(url_for("suppliers.dashboard"))
return redirect(url_for("dashboard.index")) return redirect(url_for("dashboard.index"))

View File

@@ -336,16 +336,53 @@ def main():
supplier_ids[s["slug"]] = cursor.lastrowid supplier_ids[s["slug"]] = cursor.lastrowid
print(f" {s['name']} -> id={cursor.lastrowid}") print(f" {s['name']} -> id={cursor.lastrowid}")
# 3. Claim 2 suppliers to dev user (growth + pro) # 3. Claim paid suppliers — each gets its own owner user + subscription
print("\nClaiming PadelTech GmbH and CourtBuild Spain to dev@localhost...") print("\nClaiming paid suppliers with owner accounts...")
for slug in ("padeltech-gmbh", "courtbuild-spain"): claimed_suppliers = [
("padeltech-gmbh", "supplier_pro", "hans@padeltech.example.com", "Hans Weber"),
("courtbuild-spain", "supplier_growth", "maria@courtbuild.example.com", "Maria Garcia"),
("desert-padel-fze", "supplier_pro", "ahmed@desertpadel.example.com", "Ahmed Al-Rashid"),
]
period_end = (now + timedelta(days=30)).isoformat()
for slug, plan, email, name in claimed_suppliers:
sid = supplier_ids.get(slug) sid = supplier_ids.get(slug)
if sid: if not sid:
continue
# Create or find owner user
existing_owner = conn.execute(
"SELECT id FROM users WHERE email = ?", (email,)
).fetchone()
if existing_owner:
owner_id = existing_owner["id"]
else:
cursor = conn.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
(email, name, now.isoformat()),
)
owner_id = cursor.lastrowid
# Claim the supplier
conn.execute( conn.execute(
"UPDATE suppliers SET claimed_by = ?, claimed_at = ? WHERE id = ? AND claimed_by IS NULL", "UPDATE suppliers SET claimed_by = ?, claimed_at = ? WHERE id = ? AND claimed_by IS NULL",
(dev_user_id, now.isoformat(), sid), (owner_id, now.isoformat(), sid),
) )
# Create active subscription
existing_sub = conn.execute(
"SELECT id FROM subscriptions WHERE user_id = ?", (owner_id,)
).fetchone()
if not existing_sub:
conn.execute(
"""INSERT INTO subscriptions
(user_id, plan, status, paddle_customer_id, paddle_subscription_id,
current_period_end, created_at)
VALUES (?, ?, 'active', ?, ?, ?, ?)""",
(owner_id, plan, f"ctm_dev_{slug}", f"sub_dev_{slug}",
period_end, now.isoformat()),
)
print(f" {slug} -> owner {email} ({plan})")
# 4. Seed leads # 4. Seed leads
print(f"\nSeeding {len(LEADS)} leads...") print(f"\nSeeding {len(LEADS)} leads...")
lead_ids = [] lead_ids = []
@@ -376,24 +413,23 @@ def main():
# 5. Add credit ledger entries for claimed suppliers # 5. Add credit ledger entries for claimed suppliers
print("\nAdding credit ledger entries...") print("\nAdding credit ledger entries...")
for slug in ("padeltech-gmbh", "courtbuild-spain"): for slug in ("padeltech-gmbh", "courtbuild-spain", "desert-padel-fze"):
sid = supplier_ids.get(slug) sid = supplier_ids.get(slug)
if not sid: if not sid:
continue continue
is_pro = slug in ("padeltech-gmbh", "desert-padel-fze")
monthly = 100 if is_pro else 30
# Monthly allocation # Monthly allocation
conn.execute( conn.execute(
"""INSERT INTO credit_ledger (supplier_id, delta, balance_after, event_type, note, created_at) """INSERT INTO credit_ledger (supplier_id, delta, balance_after, event_type, note, created_at)
VALUES (?, ?, ?, 'monthly_allocation', 'Monthly refill', ?)""", VALUES (?, ?, ?, 'monthly_allocation', 'Monthly refill', ?)""",
(sid, 100 if slug == "padeltech-gmbh" else 30, (sid, monthly, monthly, (now - timedelta(days=28)).isoformat()),
100 if slug == "padeltech-gmbh" else 30,
(now - timedelta(days=28)).isoformat()),
) )
# Admin adjustment # Admin adjustment
conn.execute( conn.execute(
"""INSERT INTO credit_ledger (supplier_id, delta, balance_after, event_type, note, created_at) """INSERT INTO credit_ledger (supplier_id, delta, balance_after, event_type, note, created_at)
VALUES (?, ?, ?, 'admin_adjustment', 'Welcome bonus', ?)""", VALUES (?, ?, ?, 'admin_adjustment', 'Welcome bonus', ?)""",
(sid, 10, 110 if slug == "padeltech-gmbh" else 40, (sid, 10, monthly + 10, (now - timedelta(days=25)).isoformat()),
(now - timedelta(days=25)).isoformat()),
) )
print(f" {slug}: 2 ledger entries") print(f" {slug}: 2 ledger entries")

View File

@@ -1,12 +1,9 @@
""" """
Create all Paddle products, prices, and webhook notification destination. Create or sync Paddle products, prices, and webhook notification destination.
Run once per environment (sandbox, then production). Commands:
Creates products in Paddle, writes IDs to the paddle_products DB table, uv run python -m padelnomics.scripts.setup_paddle # first-time: create products + webhook
and sets up a webhook notification destination (writing the secret to .env). uv run python -m padelnomics.scripts.setup_paddle --sync # re-populate DB from existing Paddle products
Usage:
uv run python -m padelnomics.scripts.setup_paddle
""" """
import os import os
@@ -23,7 +20,8 @@ from paddle_billing.Entities.NotificationSettings.NotificationSettingType import
from paddle_billing.Entities.Shared import CurrencyCode, Money, TaxCategory from paddle_billing.Entities.Shared import CurrencyCode, Money, TaxCategory
from paddle_billing.Resources.NotificationSettings.Operations import CreateNotificationSetting from paddle_billing.Resources.NotificationSettings.Operations import CreateNotificationSetting
from paddle_billing.Resources.Prices.Operations import CreatePrice from paddle_billing.Resources.Prices.Operations import CreatePrice
from paddle_billing.Resources.Products.Operations import CreateProduct from paddle_billing.Resources.Products.Operations import CreateProduct, ListProducts
from paddle_billing.Resources.Products.Operations.List.Includes import Includes
load_dotenv() load_dotenv()
@@ -37,6 +35,8 @@ if not PADDLE_API_KEY:
sys.exit(1) sys.exit(1)
# Maps our internal key -> product name in Paddle.
# The name is used to match existing products on sync.
PRODUCTS = [ PRODUCTS = [
# Subscriptions # Subscriptions
{ {
@@ -159,31 +159,73 @@ PRODUCTS = [
}, },
] ]
# Build name -> spec lookup
_PRODUCT_BY_NAME = {p["name"]: p for p in PRODUCTS}
def main():
env = Environment.SANDBOX if PADDLE_ENVIRONMENT == "sandbox" else Environment.PRODUCTION
paddle = PaddleClient(PADDLE_API_KEY, options=Options(env))
def _open_db():
db_path = DATABASE_PATH db_path = DATABASE_PATH
if not Path(db_path).exists(): if not Path(db_path).exists():
print(f"ERROR: Database not found at {db_path}. Run migrations first.") print(f"ERROR: Database not found at {db_path}. Run migrations first.")
sys.exit(1) sys.exit(1)
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON") conn.execute("PRAGMA foreign_keys=ON")
return conn
def _write_product(conn, key, product_id, price_id, name, price_cents, billing_type):
conn.execute(
"""INSERT OR REPLACE INTO paddle_products
(key, paddle_product_id, paddle_price_id, name, price_cents, currency, billing_type)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(key, product_id, price_id, name, price_cents, "EUR", billing_type),
)
def sync(paddle, conn):
"""Fetch existing products from Paddle and re-populate paddle_products table."""
print(f"Syncing products from Paddle ({PADDLE_ENVIRONMENT})...\n")
products = paddle.products.list(ListProducts(includes=[Includes.Prices]))
matched = 0
for product in products:
spec = _PRODUCT_BY_NAME.get(product.name)
if not spec:
continue
if not product.prices or len(product.prices) == 0:
print(f" SKIP {spec['key']}: no prices on {product.id}")
continue
# Use the first active price
price = product.prices[0]
_write_product(
conn, spec["key"], product.id, price.id,
spec["name"], spec["price"], spec["billing_type"],
)
matched += 1
print(f" {spec['key']}: {product.id} / {price.id}")
conn.commit()
if matched == 0:
print("\nNo matching products found in Paddle. Run without --sync first.")
else:
print(f"\n{matched}/{len(PRODUCTS)} products synced to DB")
def create(paddle, conn):
"""Create new products and prices in Paddle, write to DB, set up webhook."""
print(f"Creating products in {PADDLE_ENVIRONMENT}...\n") print(f"Creating products in {PADDLE_ENVIRONMENT}...\n")
for spec in PRODUCTS: for spec in PRODUCTS:
# Create product
product = paddle.products.create(CreateProduct( product = paddle.products.create(CreateProduct(
name=spec["name"], name=spec["name"],
tax_category=TaxCategory.Standard, tax_category=TaxCategory.Standard,
)) ))
print(f" Product: {spec['name']} -> {product.id}") print(f" Product: {spec['name']} -> {product.id}")
# Create price
price_kwargs = { price_kwargs = {
"description": spec["name"], "description": spec["name"],
"product_id": product.id, "product_id": product.id,
@@ -191,31 +233,24 @@ def main():
} }
if spec["billing_type"] == "subscription": if spec["billing_type"] == "subscription":
from paddle_billing.Entities.Shared import TimePeriod from paddle_billing.Entities.Shared import Duration, Interval
price_kwargs["billing_cycle"] = TimePeriod(interval="month", frequency=1) price_kwargs["billing_cycle"] = Duration(interval=Interval.Month, frequency=1)
price = paddle.prices.create(CreatePrice(**price_kwargs)) price = paddle.prices.create(CreatePrice(**price_kwargs))
print(f" Price: {spec['key']} = {price.id}") print(f" Price: {spec['key']} = {price.id}")
# Write to DB _write_product(
conn.execute( conn, spec["key"], product.id, price.id,
"""INSERT OR REPLACE INTO paddle_products spec["name"], spec["price"], spec["billing_type"],
(key, paddle_product_id, paddle_price_id, name, price_cents, currency, billing_type)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(spec["key"], product.id, price.id, spec["name"],
spec["price"], "EUR", spec["billing_type"]),
) )
conn.commit() conn.commit()
conn.close() print(f"\n✓ All products written to DB")
print(f"\n✓ All products written to {db_path}")
# -- Notification destination (webhook) ----------------------------------- # -- Notification destination (webhook) -----------------------------------
webhook_url = f"{BASE_URL}/billing/webhook/paddle" webhook_url = f"{BASE_URL}/billing/webhook/paddle"
# Events the webhook handler actually processes
subscribed_events = [ subscribed_events = [
EventTypeName.SubscriptionActivated, EventTypeName.SubscriptionActivated,
EventTypeName.SubscriptionUpdated, EventTypeName.SubscriptionUpdated,
@@ -242,31 +277,43 @@ def main():
print(f" ID: {notification_setting.id}") print(f" ID: {notification_setting.id}")
print(f" Secret: {webhook_secret}") print(f" Secret: {webhook_secret}")
# Write webhook secret and notification setting ID to .env
env_path = Path(".env") env_path = Path(".env")
env_vars = {
"PADDLE_WEBHOOK_SECRET": webhook_secret,
"PADDLE_NOTIFICATION_SETTING_ID": notification_setting.id,
}
if env_path.exists(): if env_path.exists():
env_text = env_path.read_text() env_text = env_path.read_text()
env_text = re.sub( for key, value in env_vars.items():
r"^PADDLE_WEBHOOK_SECRET=.*$", pattern = rf"^{key}=.*$"
f"PADDLE_WEBHOOK_SECRET={webhook_secret}", replacement = f"{key}={value}"
env_text, if re.search(pattern, env_text, flags=re.MULTILINE):
flags=re.MULTILINE, env_text = re.sub(pattern, replacement, env_text, flags=re.MULTILINE)
) else:
env_text = re.sub( env_text = env_text.rstrip("\n") + f"\n{replacement}\n"
r"^PADDLE_NOTIFICATION_SETTING_ID=.*$",
f"PADDLE_NOTIFICATION_SETTING_ID={notification_setting.id}",
env_text,
flags=re.MULTILINE,
)
env_path.write_text(env_text) env_path.write_text(env_text)
print(f"\n✓ PADDLE_WEBHOOK_SECRET and PADDLE_NOTIFICATION_SETTING_ID written to .env") print(f"\n✓ PADDLE_WEBHOOK_SECRET and PADDLE_NOTIFICATION_SETTING_ID written to .env")
else: else:
print(f"\n Add to .env:") print(f"\n Add to .env:")
print(f" PADDLE_WEBHOOK_SECRET={webhook_secret}") for key, value in env_vars.items():
print(f" PADDLE_NOTIFICATION_SETTING_ID={notification_setting.id}") print(f" {key}={value}")
print("\nDone. dev_run.sh will start ngrok and update the webhook URL automatically.") print("\nDone. dev_run.sh will start ngrok and update the webhook URL automatically.")
def main():
env = Environment.SANDBOX if PADDLE_ENVIRONMENT == "sandbox" else Environment.PRODUCTION
paddle = PaddleClient(PADDLE_API_KEY, options=Options(env))
conn = _open_db()
try:
if "--sync" in sys.argv:
sync(paddle, conn)
else:
create(paddle, conn)
finally:
conn.close()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -415,7 +415,8 @@ async def unlock_lead(lead_id: int):
except InsufficientCredits as e: except InsufficientCredits as e:
return await render_template( return await render_template(
"suppliers/partials/lead_card_error.html", "suppliers/partials/lead_card_error.html",
error=f"Not enough credits. You have {e.balance}, need {e.required}.", balance=e.balance,
required=e.required,
lead_id=lead_id, lead_id=lead_id,
) )
except ValueError as e: except ValueError as e:
@@ -660,6 +661,13 @@ async def dashboard_boosts():
(supplier["id"],), (supplier["id"],),
) )
# Resolve Paddle price IDs for buy buttons
price_ids = {}
for b in BOOST_OPTIONS:
price_ids[b["key"]] = await get_paddle_price(b["key"])
for cp in CREDIT_PACK_OPTIONS:
price_ids[cp["key"]] = await get_paddle_price(cp["key"])
return await render_template( return await render_template(
"suppliers/partials/dashboard_boosts.html", "suppliers/partials/dashboard_boosts.html",
supplier=supplier, supplier=supplier,
@@ -667,4 +675,5 @@ async def dashboard_boosts():
boost_options=BOOST_OPTIONS, boost_options=BOOST_OPTIONS,
credit_packs=CREDIT_PACK_OPTIONS, credit_packs=CREDIT_PACK_OPTIONS,
plan_features=PLAN_FEATURES, plan_features=PLAN_FEATURES,
price_ids=price_ids,
) )

View File

@@ -58,7 +58,7 @@
<span class="dash-sidebar__tier dash-sidebar__tier--{{ supplier.tier }}">{{ supplier.tier | upper }}</span> <span class="dash-sidebar__tier dash-sidebar__tier--{{ supplier.tier }}">{{ supplier.tier | upper }}</span>
</div> </div>
<nav class="dash-nav"> <nav class="dash-nav" id="dash-nav">
<a href="{{ url_for('suppliers.dashboard', tab='overview') }}" <a href="{{ url_for('suppliers.dashboard', tab='overview') }}"
hx-get="{{ url_for('suppliers.dashboard_overview') }}" hx-get="{{ url_for('suppliers.dashboard_overview') }}"
hx-target="#dashboard-content" hx-target="#dashboard-content"
@@ -96,6 +96,15 @@
</div> </div>
</aside> </aside>
<script>
document.getElementById("dash-nav").addEventListener("click", function(e) {
var link = e.target.closest("a");
if (!link) return;
this.querySelectorAll("a").forEach(function(a) { a.classList.remove("active"); });
link.classList.add("active");
});
</script>
<!-- Main content area --> <!-- Main content area -->
<main class="dash-main" id="dashboard-content" <main class="dash-main" id="dashboard-content"
hx-get="{% if active_tab == 'leads' %}{{ url_for('suppliers.dashboard_leads') }}{% elif active_tab == 'listing' %}{{ url_for('suppliers.dashboard_listing') }}{% elif active_tab == 'boosts' %}{{ url_for('suppliers.dashboard_boosts') }}{% else %}{{ url_for('suppliers.dashboard_overview') }}{% endif %}" hx-get="{% if active_tab == 'leads' %}{{ url_for('suppliers.dashboard_leads') }}{% elif active_tab == 'listing' %}{{ url_for('suppliers.dashboard_listing') }}{% elif active_tab == 'boosts' %}{{ url_for('suppliers.dashboard_boosts') }}{% else %}{{ url_for('suppliers.dashboard_overview') }}{% endif %}"

View File

@@ -100,10 +100,14 @@
</div> </div>
<div style="text-align:right"> <div style="text-align:right">
<div class="bst-boost__price">&euro;{{ b.price }}/mo</div> <div class="bst-boost__price">&euro;{{ b.price }}/mo</div>
{% if price_ids.get(b.key) %}
<button type="button" class="bst-buy-btn" <button type="button" class="bst-buy-btn"
onclick="Paddle.Checkout.open({items:[{priceId:'{{ b.key }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})"> onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[b.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
Activate Activate
</button> </button>
{% else %}
<span style="font-size:0.6875rem;color:#94A3B8">Not configured</span>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -119,10 +123,14 @@
<div class="bst-credit-card__amount">{{ cp.amount }}</div> <div class="bst-credit-card__amount">{{ cp.amount }}</div>
<div class="bst-credit-card__label">credits</div> <div class="bst-credit-card__label">credits</div>
<div class="bst-credit-card__price">&euro;{{ cp.price }}</div> <div class="bst-credit-card__price">&euro;{{ cp.price }}</div>
{% if price_ids.get(cp.key) %}
<button type="button" class="bst-buy-btn" <button type="button" class="bst-buy-btn"
onclick="Paddle.Checkout.open({items:[{priceId:'{{ cp.key }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})"> onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[cp.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
Buy Buy
</button> </button>
{% else %}
<span style="font-size:0.6875rem;color:#94A3B8">Not configured</span>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -1,5 +1,5 @@
<style> <style>
.lst-preview { background: white; border: 1px solid #E2E8F0; border-radius: 14px; padding: 1.5rem; margin-bottom: 1.5rem; } .lst-preview { background: white; border: 1px solid #E2E8F0; border-radius: 14px; padding: 1.5rem; margin-bottom: 1.5rem; max-width: 420px; }
.lst-preview h3 { font-size: 0.9375rem; color: #94A3B8; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 1rem; } .lst-preview h3 { font-size: 0.9375rem; color: #94A3B8; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 1rem; }
.lst-card { .lst-card {
border: 1px solid #E2E8F0; border-radius: 14px; padding: 1.25rem; border: 1px solid #E2E8F0; border-radius: 14px; padding: 1.25rem;

View File

@@ -1,3 +1,7 @@
<div class="lf-card"> <div class="lf-card" style="border-color:#FCA5A5">
<div class="lf-error">{{ error }}</div> <div style="text-align:center;padding:0.75rem 0">
<p style="font-size:0.8125rem;font-weight:600;color:#DC2626;margin:0 0 4px">Not enough credits</p>
<p style="font-size:0.75rem;color:#64748B;margin:0 0 12px">You have <strong>{{ balance }}</strong> credits, this lead costs <strong>{{ required }}</strong>.</p>
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}" class="lf-unlock-btn" style="display:inline-block;text-decoration:none">Buy Credits</a>
</div>
</div> </div>

View File

@@ -28,6 +28,9 @@
{% if config.PADDLE_CLIENT_TOKEN %} {% if config.PADDLE_CLIENT_TOKEN %}
Paddle.Initialize({ Paddle.Initialize({
token: "{{ config.PADDLE_CLIENT_TOKEN }}", token: "{{ config.PADDLE_CLIENT_TOKEN }}",
eventCallback: function(ev) {
if (ev.name === "checkout.error") console.error("Paddle checkout error:", ev.data);
},
checkout: { checkout: {
settings: { settings: {
displayMode: "overlay", displayMode: "overlay",
@@ -36,6 +39,8 @@
} }
} }
}); });
{% else %}
console.warn("Paddle: PADDLE_CLIENT_TOKEN not configured");
{% endif %} {% endif %}
</script> </script>
@@ -51,7 +56,7 @@
</div> </div>
<!-- Center: logo --> <!-- Center: logo -->
<a href="{{ url_for('dashboard.index') if user else url_for('public.landing') }}" class="nav-logo"> <a href="{{ url_for('public.landing') }}" class="nav-logo">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="{{ config.APP_NAME }}"> <img src="{{ url_for('static', filename='images/logo.png') }}" alt="{{ config.APP_NAME }}">
</a> </a>