feat: Tink open banking demo — 6-step API walkthrough

Demonstrates the full Tink integration flow for open banking:
  Step 1 — Client credentials auth (app token)
  Step 2 — Create Tink user with external_user_id
  Step 3 — Connect bank via Tink Link OAuth redirect
  Step 4 — List accounts (v2 endpoint)
  Step 5 — List transactions (v2 endpoint, cursor pagination)
  Step 6 — Webhooks (register endpoint, receive events)

Built with Python / FastAPI + Jinja2 templates.
Each step shows live JSON responses, cURL examples and API version badges.
Includes server-side token store (prevents session cookie overflow),
asyncio lock on OAuth callback, and demo mode with realistic mock data.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Henrik Jess Nielsen
2026-05-23 02:08:27 +02:00
commit ab591be464
23 changed files with 2477 additions and 0 deletions

119
src/templates/base.html Normal file
View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="da" class="h-full">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Tink API Demo {% block title %}{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.json-key { color: #7dd3fc; }
.json-str { color: #86efac; }
.json-num { color: #fcd34d; }
.json-bool { color: #f9a8d4; }
.json-null { color: #94a3b8; }
pre.json-block {
background: #0f172a;
border-radius: 0.5rem;
padding: 1.25rem;
overflow-x: auto;
font-size: 0.8rem;
line-height: 1.5;
font-family: 'Fira Code', 'Cascadia Code', monospace;
}
.step-badge-v2 {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
.step-badge-v1 {
background: #334155;
}
</style>
</head>
<body class="bg-slate-950 text-slate-100 min-h-full flex flex-col">
<!-- Top nav -->
<nav class="border-b border-slate-800 bg-slate-900/80 backdrop-blur sticky top-0 z-10">
<div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
<a href="/" class="flex items-center gap-3 group">
<div class="w-8 h-8 rounded-lg bg-violet-600 flex items-center justify-center text-white font-bold text-sm">T</div>
<div>
<span class="font-semibold text-white">Tink API Demo</span>
<span class="text-slate-400 text-sm ml-2">MoneyCapp × Tink</span>
</div>
</a>
<div class="flex items-center gap-3">
{% if session_customer %}
<div class="flex items-center gap-2 bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
<span class="text-xs text-slate-400">Kunde:</span>
<code class="text-xs text-emerald-300 font-mono font-semibold">{{ session_customer }}</code>
</div>
{% endif %}
<a href="/demo/log"
class="text-sm text-slate-400 hover:text-violet-300 transition flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
API Log
</a>
<a href="/demo/reset" class="text-sm text-slate-400 hover:text-white transition">↺ Reset</a>
</div>
</div>
</nav>
<!-- Step progress bar (only if step is defined in template) -->
{% block stepper %}{% endblock %}
<!-- Main content -->
<main class="flex-1 max-w-6xl w-full mx-auto px-4 py-8">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="border-t border-slate-800 text-center text-slate-500 text-xs py-4">
Tink API Demo &mdash; MoneyCapp sales prototype &mdash; i80.dk
</footer>
<script>
function syntaxHighlight(json) {
if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(match) {
let cls = 'json-num';
if (/^"/.test(match)) {
cls = /:$/.test(match) ? 'json-key' : 'json-str';
} else if (/true|false/.test(match)) {
cls = 'json-bool';
} else if (/null/.test(match)) {
cls = 'json-null';
}
return `<span class="${cls}">${match}</span>`;
});
}
document.querySelectorAll('.raw-json').forEach(el => {
try {
const data = JSON.parse(el.textContent);
el.innerHTML = syntaxHighlight(data);
} catch(e) {
el.innerHTML = syntaxHighlight(el.textContent);
}
el.classList.add('json-block');
el.classList.remove('raw-json');
});
function copyToClipboard(id) {
const el = document.getElementById(id);
navigator.clipboard.writeText(el.innerText).then(() => {
const btn = el.nextElementSibling;
if (btn) { btn.textContent = '✓ Kopieret'; setTimeout(() => btn.textContent = 'Kopier', 2000); }
});
}
function copyText(id, btn) {
const el = document.getElementById(id);
navigator.clipboard.writeText(el.innerText).then(() => {
const orig = btn.textContent;
btn.textContent = '✓';
setTimeout(() => btn.textContent = orig, 2000);
});
}
</script>
</body>
</html>

57
src/templates/index.html Normal file
View File

@@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %} — Start{% endblock %}
{% block content %}
<div class="flex flex-col items-center justify-center min-h-[60vh] text-center gap-8">
<!-- Hero -->
<div class="max-w-2xl">
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-violet-900/40 border border-violet-700/50 text-violet-300 text-sm mb-6">
<span class="w-2 h-2 rounded-full bg-violet-400 animate-pulse"></span>
Live demo · Tink Sandbox
</div>
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
Tink Open Banking<br/>
<span class="text-violet-400">API Demo</span>
</h1>
<p class="text-slate-400 text-lg leading-relaxed mb-2">
Step-for-step gennemgang af hele Tink integrationsflowet —
fra brugeroprettelse til live transaktioner og events.
</p>
<p class="text-slate-500 text-sm">
Bruger Tink <strong class="text-violet-400">v2 endpoints</strong> for accounts, transactions og events.
</p>
</div>
<!-- Flow overview -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 w-full max-w-4xl">
{% set steps = [
("1", "Auth", "Client Credentials", "v1"),
("2", "Opret Bruger", "POST /user/create", "v1"),
("3", "Tilslut Bank", "Tink Link", "Link"),
("4", "Konti", "GET /data/v2/accounts", "v2"),
("5", "Transaktioner", "GET /data/v2/transactions", "v2"),
("6", "Events", "GET /events/v2/…", "v2"),
] %}
{% for num, name, endpoint, version in steps %}
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="w-7 h-7 rounded-full bg-slate-800 text-slate-300 text-xs font-bold flex items-center justify-center">{{ num }}</span>
<span class="text-xs px-2 py-0.5 rounded-full font-mono {% if version == 'v2' %}bg-violet-900/50 text-violet-300 border border-violet-700/40{% else %}bg-slate-800 text-slate-400{% endif %}">{{ version }}</span>
</div>
<div class="text-sm font-semibold text-white">{{ name }}</div>
<div class="text-xs text-slate-500 font-mono leading-tight">{{ endpoint }}</div>
</div>
{% endfor %}
</div>
<!-- CTA -->
<a href="/demo/step/1"
class="inline-flex items-center gap-3 px-8 py-4 bg-violet-600 hover:bg-violet-500 text-white font-semibold rounded-xl text-lg transition-all hover:scale-105 shadow-lg shadow-violet-900/50">
Start Demo
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
</a>
</div>
{% endblock %}

99
src/templates/log.html Normal file
View File

@@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block title %}API Log — Tink Demo{% endblock %}
{% block content %}
<div class="max-w-5xl mx-auto px-4 py-8">
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-white">📋 API Request Log</h1>
<p class="text-slate-400 text-sm mt-1">Alle Tink API-kald i denne session</p>
</div>
<div class="flex items-center gap-3">
<span class="text-slate-400 text-sm">{{ log_count }} kald registreret</span>
{% if log_count > 0 %}
<form method="post" action="/demo/log/clear">
<button type="submit"
class="text-xs px-3 py-1.5 rounded bg-slate-700 hover:bg-slate-600 text-slate-300 transition">
Ryd log
</button>
</form>
{% endif %}
<a href="/demo/step/1"
class="text-xs px-3 py-1.5 rounded bg-violet-600 hover:bg-violet-500 text-white transition">
← Tilbage til demo
</a>
</div>
</div>
{% if log_count == 0 %}
<div class="rounded-xl border border-slate-700 bg-slate-800/50 p-12 text-center">
<div class="text-4xl mb-4">📭</div>
<p class="text-slate-400">Ingen API-kald endnu.</p>
<p class="text-slate-500 text-sm mt-1">Gå igennem demo-steppene for at se kaldene her.</p>
<a href="/demo/step/1" class="inline-block mt-4 px-4 py-2 rounded-lg bg-violet-600 hover:bg-violet-500 text-white text-sm transition">
Start fra Step 1
</a>
</div>
{% else %}
<div class="space-y-3">
{% for entry in log %}
<div class="rounded-xl border {% if entry.ok %}border-emerald-700/40 bg-emerald-900/10{% else %}border-red-700/40 bg-red-900/10{% endif %} overflow-hidden">
{# Header row #}
<button onclick="this.nextElementSibling.classList.toggle('hidden')"
class="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-white/5 transition">
{# Method badge #}
<span class="text-xs font-mono font-bold px-2 py-0.5 rounded
{% if entry.method == 'GET' %}bg-blue-500/20 text-blue-300 border border-blue-500/30
{% else %}bg-violet-500/20 text-violet-300 border border-violet-500/30{% endif %}">
{{ entry.method }}
</span>
{# Status badge #}
<span class="text-xs font-mono px-2 py-0.5 rounded
{% if entry.ok %}bg-emerald-500/20 text-emerald-300 border border-emerald-500/30
{% else %}bg-red-500/20 text-red-300 border border-red-500/30{% endif %}">
{{ entry.status }}
</span>
{# URL — strip domain for cleanliness #}
<span class="font-mono text-sm text-slate-200 flex-1 truncate">
{{ entry.url | replace("https://api.tink.com", "") }}
</span>
{# Time + duration #}
<span class="text-xs text-slate-500 font-mono">{{ entry.ts }}</span>
<span class="text-xs text-slate-500 font-mono">{{ entry.duration_ms }}ms</span>
<svg class="w-4 h-4 text-slate-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
{# Collapsible body — hidden by default #}
<div class="hidden border-t {% if entry.ok %}border-emerald-700/20{% else %}border-red-700/20{% endif %} px-4 py-3 grid grid-cols-1 md:grid-cols-2 gap-4">
{% if entry.req_body %}
<div>
<div class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">Request</div>
<pre class="text-xs font-mono text-slate-300 bg-slate-900/60 rounded p-3 overflow-auto max-h-48 whitespace-pre-wrap">{{ entry.req_body | tojson(indent=2) }}</pre>
</div>
{% endif %}
<div {% if not entry.req_body %}class="md:col-span-2"{% endif %}>
<div class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">Response</div>
<pre class="text-xs font-mono text-slate-300 bg-slate-900/60 rounded p-3 overflow-auto max-h-48 whitespace-pre-wrap">{{ entry.resp_body | tojson(indent=2) }}</pre>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endblock %}

335
src/templates/step.html Normal file
View File

@@ -0,0 +1,335 @@
{% extends "base.html" %}
{% block title %} — Step {{ step }}{% endblock %}
{% block stepper %}
<div class="bg-slate-900/60 border-b border-slate-800">
<div class="max-w-6xl mx-auto px-4 py-3">
<div class="flex items-center gap-1 overflow-x-auto">
{% set step_names = ["Auth", "Opret Bruger", "Tilslut Bank", "Konti", "Transaktioner", "Events"] %}
{% for i in range(1, 7) %}
<a href="/demo/step/{{ i }}"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm whitespace-nowrap transition
{% if i == step %}bg-violet-600 text-white font-semibold
{% elif i < step %}text-slate-300 hover:text-white
{% else %}text-slate-600 hover:text-slate-400{% endif %}">
<span class="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center
{% if i < step %}bg-slate-700 text-slate-300
{% elif i == step %}bg-violet-500 text-white
{% else %}bg-slate-800 text-slate-600{% endif %}">{{ i }}</span>
{{ step_names[i-1] }}
</a>
{% if i < 6 %}
<span class="text-slate-700"></span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="grid grid-cols-1 lg:grid-cols-5 gap-6">
<!-- Left: info panel -->
<div class="lg:col-span-2 space-y-4">
<!-- Step header -->
<div>
<div class="flex items-center gap-2 mb-2">
<span class="w-8 h-8 rounded-full bg-violet-600 text-white text-sm font-bold flex items-center justify-center">{{ step }}</span>
<div>
<h2 class="text-xl font-bold text-white">{{ title }}</h2>
<p class="text-slate-400 text-sm">{{ subtitle }}</p>
</div>
</div>
</div>
<!-- Endpoint badge -->
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-slate-500 uppercase tracking-wider">Endpoint</span>
<span class="text-xs px-2 py-0.5 rounded-full font-mono font-semibold
{% if 'v2' in api_version %}bg-violet-900/50 text-violet-300 border border-violet-700/40
{% else %}bg-slate-800 text-slate-400{% endif %}">
{{ api_version }}
</span>
</div>
<code class="text-sm text-emerald-400 font-mono break-all">{{ endpoint }}</code>
</div>
<!-- Description -->
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4">
<p class="text-slate-300 text-sm leading-relaxed">{{ description | safe }}</p>
</div>
<!-- curl example -->
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4">
<div class="flex items-center justify-between mb-3">
<span class="text-xs text-slate-500 uppercase tracking-wider">cURL eksempel</span>
<button onclick="copyToClipboard('curl-{{ step }}')"
class="text-xs text-slate-400 hover:text-white transition px-2 py-1 rounded bg-slate-800 hover:bg-slate-700">
Kopier
</button>
</div>
<pre id="curl-{{ step }}" class="text-xs text-amber-300 font-mono whitespace-pre-wrap leading-relaxed">{{ curl_example }}</pre>
</div>
<!-- Navigation -->
<div class="flex gap-3">
{% if prev_step %}
<a href="/demo/step/{{ prev_step }}"
class="flex-1 text-center px-4 py-2.5 border border-slate-700 text-slate-300 hover:text-white hover:border-slate-500 rounded-xl text-sm transition">
← Step {{ prev_step }}
</a>
{% endif %}
{% if next_step %}
<a href="/demo/step/{{ next_step }}"
class="flex-1 text-center px-4 py-2.5 bg-violet-600 hover:bg-violet-500 text-white rounded-xl text-sm font-semibold transition">
Step {{ next_step }} →
</a>
{% endif %}
</div>
</div>
<!-- Right: response panel -->
<div class="lg:col-span-3 space-y-4">
<!-- Tink Link special button (step 3) -->
{% if tink_link_url %}
{% if cb_success %}
<!-- Already connected — show success, hide connection UI -->
<div class="bg-emerald-950/60 border border-emerald-700/50 rounded-xl p-5 flex items-start gap-4">
<div class="w-10 h-10 rounded-full bg-emerald-900/60 flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
</div>
<div class="flex-1">
<p class="text-emerald-300 font-semibold text-base">Bank forbundet!</p>
<p class="text-emerald-400/70 text-sm mt-0.5">User token gemt i session. Trin 46 er klar.</p>
<a href="/demo/reset"
class="inline-flex items-center gap-1.5 mt-3 text-xs text-slate-500 hover:text-slate-300 transition">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Start forfra
</a>
</div>
</div>
{% else %}
<!-- Not yet connected — show connection UI -->
<!-- PRIMARY: direct callback flow -->
<div class="bg-slate-900 border border-emerald-700/40 rounded-xl p-5 space-y-4">
<div class="flex items-start gap-3">
<div class="w-8 h-8 rounded-full bg-emerald-900/50 flex items-center justify-center flex-shrink-0">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
</div>
<div>
<h3 class="text-white font-semibold mb-1">Tilslut testbank</h3>
<p class="text-slate-400 text-sm">Klik knappen, vælg Demo Bank og log ind — du redirectes automatisk tilbage.</p>
</div>
</div>
<!-- Instructions -->
<div class="bg-slate-800/60 border border-slate-700/50 rounded-lg p-4 text-sm space-y-2">
<p class="text-slate-300 font-semibold flex items-center gap-1.5">
<svg class="w-4 h-4 text-amber-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Trin i Tink Link
</p>
<ol class="text-slate-400 space-y-1.5 list-decimal list-inside leading-relaxed">
<li>Vælg <span class="text-slate-300 font-medium">Tink Demo Bank</span></li>
<li>Vælg <span class="text-slate-300">Open Banking</span><span class="text-slate-300">Password And OTP</span></li>
<li>Hent credentials: <span class="font-mono text-amber-300 text-xs bg-slate-900 px-1.5 py-0.5 rounded">Console → Demo Bank → Transactions → DK</span></li>
<li>Indtast username + password → OTP vises på siden → Continue</li>
<li>Vælg en konto → du redirectes automatisk tilbage her ✓</li>
</ol>
</div>
<!-- Demo Bank credentials hint -->
<div class="bg-slate-800/70 border border-amber-700/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-3">
<svg class="w-4 h-4 text-amber-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/></svg>
<span class="text-amber-300 text-sm font-semibold">Demo Bank Credentials</span>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<div class="text-xs text-slate-500 mb-1">Username</div>
<div class="flex items-center gap-2 bg-slate-900 border border-slate-700 rounded px-3 py-2">
<code id="demo-user" class="text-sm font-mono text-emerald-300 flex-1">u04877810</code>
<button onclick="copyText('demo-user', this)"
class="text-xs text-slate-500 hover:text-slate-300 transition flex-shrink-0">Kopier</button>
</div>
</div>
<div>
<div class="text-xs text-slate-500 mb-1">Password</div>
<div class="flex items-center gap-2 bg-slate-900 border border-slate-700 rounded px-3 py-2">
<code id="demo-pass" class="text-sm font-mono text-emerald-300 flex-1">vxw774</code>
<button onclick="copyText('demo-pass', this)"
class="text-xs text-slate-500 hover:text-slate-300 transition flex-shrink-0">Kopier</button>
</div>
</div>
</div>
<p class="text-xs text-slate-500 mt-2">Vælg <span class="text-slate-400">Tink Demo Bank → Open Banking → Password And OTP</span></p>
</div>
<div class="flex items-center gap-3 flex-wrap">
<a href="{{ tink_link_url }}"
class="inline-flex items-center gap-2 px-5 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded-lg transition">
Åbn Tink Link
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg>
</a>
{% if demo_bank_users %}
<button onclick="document.getElementById('demo-users-modal').classList.remove('hidden')"
class="inline-flex items-center gap-1.5 px-4 py-2.5 border border-violet-700 text-violet-400 hover:text-violet-200 hover:border-violet-500 rounded-lg text-sm transition">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Vis testbrugere
</button>
{% endif %}
<a href="/demo/reset"
class="inline-flex items-center gap-1.5 px-4 py-2.5 border border-slate-700 text-slate-400 hover:text-white hover:border-slate-500 rounded-lg text-sm transition">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Start forfra
</a>
</div>
{% if demo_bank_users %}
<!-- Demo Bank users modal -->
<div id="demo-users-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm" onclick="document.getElementById('demo-users-modal').classList.add('hidden')"></div>
<div class="relative bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-800">
<div>
<h3 class="text-white font-semibold text-base">Demo Bank — Testbrugere</h3>
<p class="text-slate-500 text-xs mt-0.5">Brug disse kredentialer når du logger ind i Tink Demo Bank. OTP er altid <code class="text-violet-300">1234</code> hvor det er påkrævet.</p>
</div>
<button onclick="document.getElementById('demo-users-modal').classList.add('hidden')"
class="text-slate-500 hover:text-white transition p-1.5 rounded-lg hover:bg-slate-800">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="overflow-auto max-h-96">
<table class="w-full text-sm">
<thead class="bg-slate-800/50 sticky top-0">
<tr class="text-slate-400 text-xs uppercase tracking-wider">
<th class="px-4 py-2.5 text-left">Market</th>
<th class="px-4 py-2.5 text-left">Brugernavn</th>
<th class="px-4 py-2.5 text-left">Password</th>
<th class="px-4 py-2.5 text-left">OTP</th>
<th class="px-4 py-2.5 text-left">Scenarie</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
{% for u in demo_bank_users %}
<tr class="hover:bg-slate-800/40 transition">
<td class="px-4 py-2.5 text-slate-300 font-medium">{{ u.market }}</td>
<td class="px-4 py-2.5">
<div class="flex items-center gap-2">
<code class="font-mono text-emerald-300 text-xs">{{ u.username }}</code>
<button onclick="navigator.clipboard.writeText('{{ u.username }}')" title="Kopier"
class="text-slate-600 hover:text-slate-300 transition">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
</button>
</div>
</td>
<td class="px-4 py-2.5">
<div class="flex items-center gap-2">
<code class="font-mono text-violet-300 text-xs">{{ u.password }}</code>
<button onclick="navigator.clipboard.writeText('{{ u.password }}')" title="Kopier"
class="text-slate-600 hover:text-slate-300 transition">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
</button>
</div>
</td>
<td class="px-4 py-2.5 font-mono text-xs text-slate-400">{{ u.otp or "—" }}</td>
<td class="px-4 py-2.5">
<span class="px-2 py-0.5 rounded-full text-xs font-medium {% if 'fejl' in u.scenario.lower() %}bg-red-900/40 text-red-400{% else %}bg-emerald-900/40 text-emerald-400{% endif %}">
{{ u.scenario }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="px-6 py-3 border-t border-slate-800 text-xs text-slate-500">
Kilde: <a href="https://docs.tink.com/resources/tutorials/test-your-integration-with-demo-bank" target="_blank" class="text-violet-400 hover:text-violet-300 underline">Tink Demo Bank dokumentation</a>
</div>
</div>
</div>
{% endif %}
</div>
<!-- FALLBACK: console.tink.com/callback + manual code paste -->
<details class="bg-slate-900 border border-slate-700/30 rounded-xl overflow-hidden">
<summary class="px-5 py-3 cursor-pointer text-slate-500 hover:text-slate-400 text-xs flex items-center gap-2 select-none">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Alternativ: manuel kode-indsætning via console.tink.com
<span class="ml-auto"></span>
</summary>
<div class="px-5 pb-4 pt-3 border-t border-slate-800 space-y-3">
<a href="{{ dev_tink_link_url }}" target="_blank"
class="inline-flex items-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm rounded-lg transition">
Åbn med console callback
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
</a>
<form method="POST" action="/demo/step/3" class="flex gap-2 items-stretch">
<input type="text" name="code" placeholder="Indsæt code=XXXX fra console.tink.com/callback..."
class="flex-1 bg-slate-800 border border-slate-700 text-slate-200 rounded-lg px-3 py-2 text-sm font-mono placeholder-slate-600 focus:outline-none focus:border-emerald-600" required>
<button type="submit"
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-600 text-white text-sm font-semibold rounded-lg transition whitespace-nowrap">
Brug kode
</button>
</form>
</div>
</details>
{% endif %}{# end not cb_success #}
{% endif %}{# end tink_link_url #}
<!-- Error state -->
{% if error %}
<div class="bg-red-950/50 border border-red-800/50 rounded-xl p-5">
<div class="flex items-start gap-3">
<span class="text-red-400 text-xl flex-shrink-0"></span>
<div>
<h3 class="text-red-300 font-semibold mb-1">Fejl</h3>
<pre class="text-red-400 text-sm font-mono whitespace-pre-wrap">{{ error }}</pre>
</div>
</div>
</div>
{% endif %}
<!-- JSON response -->
{% if result %}
<div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-800">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
<span class="text-sm text-slate-300 font-semibold">Response</span>
<span class="text-xs text-emerald-400 font-mono">200 OK</span>
{% if is_demo %}
<span class="text-xs px-2 py-0.5 rounded-full font-semibold bg-amber-900/60 text-amber-300 border border-amber-700/40">⚠ Sample Data</span>
{% endif %}
</div>
<button onclick="copyToClipboard('json-{{ step }}')"
class="text-xs text-slate-400 hover:text-white transition px-2 py-1 rounded bg-slate-800 hover:bg-slate-700">
Kopier JSON
</button>
</div>
<div class="p-4 max-h-[520px] overflow-y-auto">
<pre id="json-{{ step }}" class="raw-json">{{ result | tojson(indent=2) }}</pre>
</div>
</div>
{% elif not error %}
<!-- Waiting state -->
<div class="bg-slate-900 border border-slate-800 rounded-xl p-10 flex flex-col items-center justify-center gap-3 text-center">
<div class="w-12 h-12 rounded-full bg-slate-800 flex items-center justify-center">
<svg class="w-6 h-6 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
</div>
<p class="text-slate-400 text-sm">Klik på knappen ovenfor for at køre dette API-kald og se svaret her.</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}

164
src/templates/step2.html Normal file
View File

@@ -0,0 +1,164 @@
{% extends "base.html" %}
{% block title %} — Step 2: Opret Bruger{% endblock %}
{% block stepper %}
<div class="bg-slate-900/60 border-b border-slate-800">
<div class="max-w-6xl mx-auto px-4 py-3">
<div class="flex items-center gap-1 overflow-x-auto">
{% set step_names = ["Auth", "Opret Bruger", "Tilslut Bank", "Konti", "Transaktioner", "Webhooks"] %}
{% for i in range(1, 7) %}
<a href="/demo/step/{{ i }}"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm whitespace-nowrap transition
{% if i == 2 %}bg-violet-600 text-white font-semibold
{% elif i < 2 %}text-slate-300 hover:text-white
{% else %}text-slate-600 hover:text-slate-400{% endif %}">
<span class="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center
{% if i < 2 %}bg-slate-700 text-slate-300
{% elif i == 2 %}bg-violet-500 text-white
{% else %}bg-slate-800 text-slate-600{% endif %}">{{ i }}</span>
{{ step_names[i-1] }}
</a>
{% if i < 6 %}
<span class="text-slate-700"></span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="mb-6">
<div class="flex items-center gap-2 mb-1">
<span class="w-8 h-8 rounded-full bg-violet-600 text-white text-sm font-bold flex items-center justify-center">2</span>
<div>
<h2 class="text-xl font-bold text-white">Opret Bruger</h2>
<p class="text-slate-400 text-sm">Create Test Customer</p>
</div>
<span class="ml-2 text-xs px-2 py-0.5 rounded-full font-mono font-semibold bg-slate-800 text-slate-400 border border-slate-700">v1</span>
</div>
<p class="text-slate-400 text-sm mt-2 max-w-2xl">
Opret en Tink-bruger med en <code class="text-violet-300">tink_external_ref</code>
MoneyCapp's interne reference til kunden i Tink. Gemmes i jeres kundedatabase som <code class="text-violet-300">tink_external_ref</code> (ikke jeres interne <code class="text-slate-400">customer_id</code>).
Tink returnerer et <code class="text-violet-300">user_id</code> som bruges i efterfølgende kald.
</p>
</div>
{% if error %}
<div class="bg-red-950/50 border border-red-800/50 rounded-xl p-4 mb-6">
<pre class="text-red-400 text-sm font-mono whitespace-pre-wrap">{{ error }}</pre>
</div>
{% endif %}
{% if existing_user_id %}
<!-- Already created this session -->
<div class="bg-emerald-950/30 border border-emerald-800/40 rounded-xl p-5 mb-6">
<div class="flex items-center gap-3 mb-3">
<span class="w-8 h-8 rounded-full bg-emerald-600/20 border border-emerald-600/40 flex items-center justify-center text-emerald-400 text-lg"></span>
<div>
<p class="text-white font-semibold">Bruger allerede oprettet i denne session</p>
<p class="text-slate-400 text-sm">Du kan fortsætte til Step 3, eller oprette en ny bruger nedenfor.</p>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3">
<div class="bg-slate-900 rounded-lg p-3">
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">external_user_id</p>
<code class="text-emerald-300 font-mono text-sm font-semibold">{{ existing_external_id }}</code>
</div>
<div class="bg-slate-900 rounded-lg p-3">
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">Tink user_id</p>
<code class="text-slate-300 font-mono text-xs">{{ existing_user_id }}</code>
</div>
</div>
<div class="mt-4">
<a href="/demo/step/3"
class="inline-flex items-center gap-2 px-5 py-2.5 bg-violet-600 hover:bg-violet-500 text-white rounded-xl text-sm font-semibold transition">
Fortsæt til Step 3 — Tilslut Bank →
</a>
</div>
</div>
<details class="mb-6">
<summary class="cursor-pointer text-sm text-slate-500 hover:text-slate-300 transition">Opret en ny bruger i stedet</summary>
<div class="mt-4">
{% else %}
<div>
{% endif %}
<!-- Create user form -->
<div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden max-w-xl">
<div class="px-5 py-4 border-b border-slate-800">
<p class="text-sm font-semibold text-white">Hvem opretter vi?</p>
<p class="text-xs text-slate-400 mt-0.5">
<code class="text-violet-300">tink_external_ref</code> = jeres reference til kunden i Tink —
adskilt fra jeres interne <code class="text-slate-400">customer_id</code>
</p>
</div>
<form method="POST" action="/demo/step/2" class="p-5 space-y-4">
<div>
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Kundenavn</label>
<input type="text" name="customer_name"
placeholder="fx. Henrik Jess, Kunde A, Test User 1"
value="Henrik Jess"
class="w-full bg-slate-950 border border-slate-700 rounded-lg px-4 py-2.5 text-white text-sm font-mono
placeholder-slate-600 focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/30 transition">
<p class="text-xs text-slate-600 mt-1.5"><code class="text-violet-300/70">tink_external_ref</code> = <code class="text-violet-300/70">moneycapp-henrik-jess-a3f9c1</code></p>
</div>
<div>
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Marked</label>
<select name="market"
class="w-full bg-slate-950 border border-slate-700 rounded-lg px-4 py-2.5 text-white text-sm
focus:outline-none focus:border-violet-500 transition">
<option value="DK" selected>DK — Danmark</option>
<option value="SE">SE — Sverige</option>
<option value="NO">NO — Norge</option>
<option value="GB">GB — United Kingdom</option>
<option value="DE">DE — Deutschland</option>
</select>
</div>
<button type="submit"
{% if not app_token_ok %}disabled{% endif %}
class="w-full py-3 bg-violet-600 hover:bg-violet-500 disabled:bg-slate-700 disabled:cursor-not-allowed
text-white rounded-xl text-sm font-semibold transition">
{% if app_token_ok %}Opret bruger i Tink →{% else %}Kør Step 1 først{% endif %}
</button>
</form>
</div>
{% if existing_user_id %}
</div>
</details>
{% else %}
</div>
{% endif %}
<!-- API info -->
<div class="mt-6 bg-slate-900/50 border border-slate-800 rounded-xl p-4">
<p class="text-xs text-slate-500 uppercase tracking-wider mb-3">API endpoint</p>
<code class="text-emerald-400 font-mono text-sm">POST https://api.tink.com/api/v1/user/create</code>
<div class="mt-3 bg-slate-950 rounded-lg p-3 overflow-x-auto">
<pre class="text-xs text-amber-300 font-mono whitespace-pre"># MoneyCapp DB:
# customer_id = 42 ← jeres interne ID (Tink ser det aldrig)
# tink_external_ref = "moneycapp-42-a3f9c1" ← Tink-reference
# Request body
{
"external_user_id": "moneycapp-&lt;ref&gt;", ← tink_external_ref
"market": "DK",
"locale": "da_DK"
}
# Response
{
"user_id": "abc123..." ← Tinks interne ID, gem og brug fremadrettet
}</pre>
</div>
</div>
<!-- Navigation -->
<div class="mt-6 flex justify-start">
<a href="/demo/step/1"
class="px-4 py-2.5 border border-slate-700 text-slate-300 hover:text-white hover:border-slate-500 rounded-xl text-sm transition">
← Step 1
</a>
</div>
{% endblock %}

245
src/templates/step6.html Normal file
View File

@@ -0,0 +1,245 @@
{% extends "base.html" %}
{% block title %} — Step 6: Webhooks & Events{% endblock %}
{% block stepper %}
<div class="bg-slate-900/60 border-b border-slate-800">
<div class="max-w-6xl mx-auto px-4 py-3">
<div class="flex items-center gap-1 overflow-x-auto">
{% set step_names = ["Auth", "Opret Bruger", "Tilslut Bank", "Konti", "Transaktioner", "Webhooks"] %}
{% for i in range(1, 7) %}
<a href="/demo/step/{{ i }}"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm whitespace-nowrap transition
{% if i == 6 %}bg-violet-600 text-white font-semibold
{% elif i < 6 %}text-slate-300 hover:text-white
{% else %}text-slate-600 hover:text-slate-400{% endif %}">
<span class="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center
{% if i < 6 %}bg-slate-700 text-slate-300
{% elif i == 6 %}bg-violet-500 text-white
{% else %}bg-slate-800 text-slate-600{% endif %}">{{ i }}</span>
{{ step_names[i-1] }}
</a>
{% if i < 6 %}
<span class="text-slate-700"></span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="mb-6">
<div class="flex items-center gap-2 mb-1">
<span class="w-8 h-8 rounded-full bg-violet-600 text-white text-sm font-bold flex items-center justify-center">6</span>
<div>
<h2 class="text-xl font-bold text-white">Webhooks & Real-time Events</h2>
<p class="text-slate-400 text-sm">Push-notifikationer til din backend</p>
</div>
</div>
<p class="text-slate-400 text-sm mt-2 max-w-2xl">
Registrér dit endpoint hos Tink — så sender de automatisk en HTTP POST til dig
hver gang en transaktion bogføres, opdateres eller en konto ændres. Zero polling.
</p>
</div>
{% if error %}
<div class="bg-red-950/50 border border-red-800/50 rounded-xl p-5 mb-6">
<p class="text-red-300 text-sm font-semibold mb-1">Webhook API fejl</p>
<pre class="text-red-400 text-sm font-mono whitespace-pre-wrap">{{ error }}</pre>
</div>
{% endif %}
<!-- How it works banner -->
<div class="bg-slate-900/50 border border-violet-800/30 rounded-xl p-5 mb-6">
<h3 class="text-white font-semibold mb-3 text-sm">Hvordan webhooks virker</h3>
<div class="flex flex-wrap items-center gap-2 text-sm">
<div class="bg-slate-800 rounded-lg px-3 py-2 text-slate-300">
<span class="text-violet-400 font-mono text-xs block mb-0.5">Din app</span>
Brugeren kobler bank
</div>
<span class="text-slate-600 text-lg"></span>
<div class="bg-slate-800 rounded-lg px-3 py-2 text-slate-300">
<span class="text-emerald-400 font-mono text-xs block mb-0.5">Tink</span>
Henter transaktioner
</div>
<span class="text-slate-600 text-lg"></span>
<div class="bg-violet-900/50 border border-violet-700/50 rounded-lg px-3 py-2 text-slate-200 font-semibold">
<span class="text-violet-300 font-mono text-xs block mb-0.5">POST /webhooks/tink</span>
Dit endpoint modtager event
</div>
<span class="text-slate-600 text-lg"></span>
<div class="bg-slate-800 rounded-lg px-3 py-2 text-slate-300">
<span class="text-amber-400 font-mono text-xs block mb-0.5">Din app</span>
Opdaterer UI / notifikation
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- List webhooks -->
<div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
<div class="px-4 py-3 border-b border-slate-800">
<div class="flex items-center justify-between">
<div>
<span class="text-sm font-semibold text-white">Registrerede webhooks</span>
<code class="block text-xs text-emerald-400 font-mono mt-0.5">GET /api/v1/webhooks</code>
</div>
<span class="text-xs px-2 py-0.5 rounded-full bg-slate-800 text-slate-400 border border-slate-700 font-mono">app token</span>
</div>
</div>
<div class="px-4 py-3 border-b border-slate-800 bg-slate-950/40">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-slate-500 uppercase tracking-wider">cURL</span>
<button onclick="copyToClipboard('curl-list')"
class="text-xs text-slate-400 hover:text-white transition px-2 py-1 rounded bg-slate-800">Kopier</button>
</div>
<pre id="curl-list" class="text-xs text-amber-300 font-mono whitespace-pre-wrap">{{ curl_list }}</pre>
</div>
<div class="p-4 max-h-80 overflow-y-auto">
{% if result_webhooks %}
{% if is_demo %}<p class="text-xs text-amber-400/70 mb-2 font-semibold">⚠ Sample Data</p>{% endif %}
<pre class="raw-json">{{ result_webhooks | tojson(indent=2) }}</pre>
{% else %}
<p class="text-slate-500 text-sm text-center py-6">Ingen webhooks registreret endnu.</p>
{% endif %}
</div>
</div>
<!-- Register webhook -->
<div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
<div class="px-4 py-3 border-b border-slate-800">
<div class="flex items-center justify-between">
<div>
<span class="text-sm font-semibold text-white">Registrér webhook endpoint</span>
<code class="block text-xs text-emerald-400 font-mono mt-0.5">POST /api/v1/webhooks</code>
</div>
<span class="text-xs px-2 py-0.5 rounded-full bg-emerald-900/50 text-emerald-300 border border-emerald-800/50 font-mono">registreret ✓</span>
</div>
</div>
<div class="px-4 py-3 border-b border-slate-800 bg-slate-950/40">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-slate-500 uppercase tracking-wider">cURL</span>
<button onclick="copyToClipboard('curl-register')"
class="text-xs text-slate-400 hover:text-white transition px-2 py-1 rounded bg-slate-800">Kopier</button>
</div>
<pre id="curl-register" class="text-xs text-amber-300 font-mono whitespace-pre-wrap">{{ curl_register }}</pre>
</div>
<div class="p-4 max-h-80 overflow-y-auto">
{% if webhook_registered %}
{% if is_demo %}<p class="text-xs text-amber-400/70 mb-2 font-semibold">⚠ Sample Data</p>{% endif %}
<pre class="raw-json">{{ webhook_registered | tojson(indent=2) }}</pre>
{% else %}
<p class="text-slate-500 text-sm text-center py-6">Webhook ikke registreret.</p>
{% endif %}
</div>
</div>
</div>
<!-- Sample event payload -->
<div class="mt-6 bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
<div class="px-4 py-3 border-b border-slate-800">
<div class="flex items-center justify-between">
<div>
<span class="text-sm font-semibold text-white">Sample Webhook Payload</span>
<p class="text-xs text-slate-400 mt-0.5">Sådan ser en event ud når Tink poster til dit endpoint</p>
</div>
<span class="text-xs px-2 py-0.5 rounded-full bg-amber-900/50 text-amber-300 border border-amber-800/50 font-mono">incoming POST</span>
</div>
</div>
<div class="p-4 overflow-x-auto">
<pre class="text-xs text-emerald-300 font-mono whitespace-pre">{
"event": "account-booked-transaction:created",
"context": {
"userId": "a8b3c2d1-...",
"externalUserId": "moneycapp-user-42"
},
"content": {
"id": "tx_9f3a2b1c...",
"accountId": "acc_7e1d4f2a...",
"amount": {
"currencyCode": "DKK",
"value": { "scale": 2, "unscaledValue": "-24900" }
},
"dates": {
"booked": "2025-05-22",
"value": "2025-05-22"
},
"descriptions": {
"display": "Netto Albertslund",
"original": "NETTO ALBERTSLUND"
},
"merchantInformation": {
"merchantCategoryCode": "5411",
"merchantName": "Netto"
},
"status": "BOOKED",
"types": { "type": "DEFAULT" }
}
}</pre>
</div>
</div>
<!-- Your receiver endpoint -->
<div class="mt-4 bg-slate-900/60 border border-slate-700/50 rounded-xl p-4">
<div class="flex items-center gap-3 mb-3">
<div class="w-8 h-8 rounded-full bg-emerald-600/20 border border-emerald-600/40 flex items-center justify-center">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
<div>
<p class="text-white text-sm font-semibold">Dit webhook modtager endpoint er live</p>
<code class="text-emerald-400 text-xs font-mono">POST {{ app_base_url }}/webhooks/tink</code>
</div>
</div>
<details class="text-sm text-slate-400">
<summary class="cursor-pointer hover:text-white transition text-xs font-semibold uppercase tracking-wider text-slate-500">Se receiver kode</summary>
<div class="mt-3 bg-slate-950 rounded-lg p-4 overflow-x-auto">
<pre class="text-xs text-emerald-300 font-mono whitespace-pre">@router.post("/webhooks/tink")
async def tink_webhook(request: Request):
payload = await request.json()
event_type = payload.get("event", "unknown")
content = payload.get("content", {})
# Her ville MoneyCapp opdatere sin database,
# sende push-notifikation, opdatere UI via SSE etc.
print(f"Tink event: {event_type}")
print(f"Transaction ID: {content.get('id')}")
print(f"Amount: {content.get('amount')}")
return {"status": "received"}</pre>
</div>
</details>
</div>
<!-- Final CTA -->
<div class="mt-8 bg-gradient-to-br from-violet-900/30 to-indigo-900/20 border border-violet-700/30 rounded-2xl p-8 text-center">
<div class="text-4xl mb-3">🎉</div>
<h3 class="text-2xl font-bold text-white mb-2">Det var hele flowet</h3>
<p class="text-slate-400 mb-2 max-w-lg mx-auto">
Fra brugeroprettelse over live bankdata til real-time webhooks — alt via Tink API.
</p>
<p class="text-slate-500 text-sm mb-6 max-w-xl mx-auto">
Vi har vist: auth → bruger → bank-tilslutning → konti (v2) → transaktioner (v2) → webhooks.
Det er præcis hvad MoneyCapp mangler for at gøre deres integration robust.
</p>
<div class="flex gap-3 justify-center flex-wrap">
<a href="/demo/reset" class="px-5 py-2.5 border border-slate-600 text-slate-300 hover:text-white hover:border-slate-400 rounded-xl text-sm transition">↺ Kør demo igen</a>
<a href="https://docs.tink.com/api-introduction" target="_blank"
class="px-5 py-2.5 bg-violet-600 hover:bg-violet-500 text-white rounded-xl text-sm font-semibold transition">
Tink Docs →
</a>
</div>
</div>
<!-- Navigation -->
<div class="mt-4 flex justify-start">
<a href="/demo/step/5"
class="px-4 py-2.5 border border-slate-700 text-slate-300 hover:text-white hover:border-slate-500 rounded-xl text-sm transition">
← Step 5
</a>
</div>
{% endblock %}