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:
119
src/templates/base.html
Normal file
119
src/templates/base.html
Normal 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 — MoneyCapp sales prototype — 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
57
src/templates/index.html
Normal 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
99
src/templates/log.html
Normal 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
335
src/templates/step.html
Normal 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 4–6 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
164
src/templates/step2.html
Normal 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-<ref>", ← 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
245
src/templates/step6.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user