fix: customer-facing cleanup — remove internal branding, print→logging, dynamic dates, sandbox note, next steps
All checks were successful
Build and Deploy / deploy (push) Successful in 49s
All checks were successful
Build and Deploy / deploy (push) Successful in 49s
- main.py: neutral API description (remove 'sales demo') - base.html: 'Open Banking Demo' nav, neutral footer - demo.py: /debug-session gated behind DEMO_MODE, all print() → logging, webhook receiver has C# signature verification example + Tink docs link, removed duplicate import - demo_data.py: all hardcoded 2026 dates replaced with dynamic date helpers - step2.html: external_user_id terminology (remove tink_external_ref) - step.html: sandbox note on Step 3 (anonymous vs production flow) - step6.html: 'Next Steps' section for C#/.NET implementation
This commit is contained in:
@@ -5,6 +5,12 @@ Used when DEMO_MODE=true or when no bank is connected.
|
||||
Data mimics real Tink API v2 responses for a Danish user.
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
_today = date.today()
|
||||
_d = lambda offset=0: (_today + timedelta(days=offset)).isoformat()
|
||||
_dt = lambda offset=0: f"{_d(offset)}T08:00:00Z"
|
||||
|
||||
MOCK_CREDENTIALS = {
|
||||
"credentials": [
|
||||
{
|
||||
@@ -12,8 +18,8 @@ MOCK_CREDENTIALS = {
|
||||
"providerName": "dk-demobank-open-banking-embedded",
|
||||
"type": "PASSWORD",
|
||||
"status": "UPDATED",
|
||||
"statusUpdated": "2026-05-22T08:00:00Z",
|
||||
"updated": "2026-05-22T08:00:00Z",
|
||||
"statusUpdated": _dt(),
|
||||
"updated": _dt(),
|
||||
"fields": {"username": "demo_user_001"},
|
||||
}
|
||||
]
|
||||
@@ -37,7 +43,7 @@ MOCK_ACCOUNTS = {
|
||||
"iban": {"iban": "DK5000400440116243"},
|
||||
"financialInstitution": {"accountNumber": "0440116243"},
|
||||
},
|
||||
"dates": {"lastRefreshed": "2026-05-22T08:00:00Z"},
|
||||
"dates": {"lastRefreshed": _dt()},
|
||||
"financialInstitutionId": "dk-demobank",
|
||||
},
|
||||
{
|
||||
@@ -53,7 +59,7 @@ MOCK_ACCOUNTS = {
|
||||
"iban": {"iban": "DK5000400440116244"},
|
||||
"financialInstitution": {"accountNumber": "0440116244"},
|
||||
},
|
||||
"dates": {"lastRefreshed": "2026-05-22T08:00:00Z"},
|
||||
"dates": {"lastRefreshed": _dt()},
|
||||
"financialInstitutionId": "dk-demobank",
|
||||
},
|
||||
],
|
||||
@@ -66,7 +72,7 @@ MOCK_TRANSACTIONS = {
|
||||
"id": "tx-001-demo",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"amount": {"value": {"unscaledValue": "-89900", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"dates": {"booked": "2026-05-22", "value": "2026-05-22"},
|
||||
"dates": {"booked": _d(), "value": _d()},
|
||||
"description": "Spotify AB",
|
||||
"status": "BOOKED",
|
||||
"categories": {"pfm": {"id": "expenses.entertainment.streaming", "name": "Streaming"}},
|
||||
@@ -77,7 +83,7 @@ MOCK_TRANSACTIONS = {
|
||||
"id": "tx-002-demo",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"amount": {"value": {"unscaledValue": "-45000", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"dates": {"booked": "2026-05-21", "value": "2026-05-21"},
|
||||
"dates": {"booked": _d(-1), "value": _d(-1)},
|
||||
"description": "Netto Supermarked",
|
||||
"status": "BOOKED",
|
||||
"categories": {"pfm": {"id": "expenses.food.groceries", "name": "Dagligvarer"}},
|
||||
@@ -88,8 +94,8 @@ MOCK_TRANSACTIONS = {
|
||||
"id": "tx-003-demo",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"amount": {"value": {"unscaledValue": "3500000", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"dates": {"booked": "2026-05-20", "value": "2026-05-20"},
|
||||
"description": "Løn maj 2026",
|
||||
"dates": {"booked": _d(-2), "value": _d(-2)},
|
||||
"description": f"Løn {_today.strftime('%b %Y')}",
|
||||
"status": "BOOKED",
|
||||
"categories": {"pfm": {"id": "income.salary", "name": "Løn"}},
|
||||
"types": {"type": "DEFAULT"},
|
||||
@@ -98,7 +104,7 @@ MOCK_TRANSACTIONS = {
|
||||
"id": "tx-004-demo",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"amount": {"value": {"unscaledValue": "-120000", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"dates": {"booked": "2026-05-19", "value": "2026-05-19"},
|
||||
"dates": {"booked": _d(-3), "value": _d(-3)},
|
||||
"description": "Matas Strøget",
|
||||
"status": "BOOKED",
|
||||
"categories": {"pfm": {"id": "expenses.personal.health", "name": "Helse & Skønhed"}},
|
||||
@@ -109,7 +115,7 @@ MOCK_TRANSACTIONS = {
|
||||
"id": "tx-005-demo",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"amount": {"value": {"unscaledValue": "-8500000", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"dates": {"booked": "2026-05-01", "value": "2026-05-01"},
|
||||
"dates": {"booked": _d(-21), "value": _d(-21)},
|
||||
"description": "Husleje maj",
|
||||
"status": "BOOKED",
|
||||
"categories": {"pfm": {"id": "expenses.housing.rent", "name": "Husleje"}},
|
||||
@@ -119,7 +125,7 @@ MOCK_TRANSACTIONS = {
|
||||
"id": "tx-006-demo",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"amount": {"value": {"unscaledValue": "-35000", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"dates": {"booked": "2026-05-18", "value": "2026-05-18"},
|
||||
"dates": {"booked": _d(-4), "value": _d(-4)},
|
||||
"description": "DSB Rejse",
|
||||
"status": "BOOKED",
|
||||
"categories": {"pfm": {"id": "expenses.transport.public", "name": "Offentlig transport"}},
|
||||
@@ -130,7 +136,7 @@ MOCK_TRANSACTIONS = {
|
||||
"id": "tx-007-demo",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"amount": {"value": {"unscaledValue": "-19900", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"dates": {"booked": "2026-05-17", "value": "2026-05-17"},
|
||||
"dates": {"booked": _d(-5), "value": _d(-5)},
|
||||
"description": "Netflix International",
|
||||
"status": "BOOKED",
|
||||
"categories": {"pfm": {"id": "expenses.entertainment.streaming", "name": "Streaming"}},
|
||||
@@ -141,7 +147,7 @@ MOCK_TRANSACTIONS = {
|
||||
"id": "tx-008-demo",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"amount": {"value": {"unscaledValue": "-62500", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"dates": {"booked": "2026-05-16", "value": "2026-05-16"},
|
||||
"dates": {"booked": _d(-6), "value": _d(-6)},
|
||||
"description": "Fakta Falkoner",
|
||||
"status": "BOOKED",
|
||||
"categories": {"pfm": {"id": "expenses.food.groceries", "name": "Dagligvarer"}},
|
||||
@@ -152,7 +158,7 @@ MOCK_TRANSACTIONS = {
|
||||
"id": "tx-009-demo",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"amount": {"value": {"unscaledValue": "-15000", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"dates": {"booked": "2026-05-23", "value": "2026-05-23"},
|
||||
"dates": {"booked": _d(1), "value": _d(1)},
|
||||
"description": "7-Eleven Nørreport",
|
||||
"status": "PENDING",
|
||||
"categories": {"pfm": {"id": "expenses.food.restaurants", "name": "Mad & Drikke"}},
|
||||
@@ -169,26 +175,26 @@ MOCK_EVENTS_BOOKED = {
|
||||
"id": "evt-booked-001",
|
||||
"type": "account-booked-transaction",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"created": "2026-05-22T08:12:33Z",
|
||||
"created": _dt(),
|
||||
"transaction": {
|
||||
"id": "tx-001-demo",
|
||||
"description": "Spotify AB",
|
||||
"amount": {"value": {"unscaledValue": "-89900", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"status": "BOOKED",
|
||||
"dates": {"booked": "2026-05-22"},
|
||||
"dates": {"booked": _d()},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "evt-booked-002",
|
||||
"type": "account-booked-transaction",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"created": "2026-05-21T14:22:10Z",
|
||||
"created": _dt(-1),
|
||||
"transaction": {
|
||||
"id": "tx-002-demo",
|
||||
"description": "Netto Supermarked",
|
||||
"amount": {"value": {"unscaledValue": "-45000", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"status": "BOOKED",
|
||||
"dates": {"booked": "2026-05-21"},
|
||||
"dates": {"booked": _d(-1)},
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -201,26 +207,26 @@ MOCK_EVENTS_ALL = {
|
||||
"id": "evt-pending-001",
|
||||
"type": "account-transaction",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"created": "2026-05-23T11:05:44Z",
|
||||
"created": _dt(1),
|
||||
"transaction": {
|
||||
"id": "tx-009-demo",
|
||||
"description": "7-Eleven Nørreport",
|
||||
"amount": {"value": {"unscaledValue": "-15000", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"status": "PENDING",
|
||||
"dates": {"booked": "2026-05-23"},
|
||||
"dates": {"booked": _d(1)},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "evt-booked-001",
|
||||
"type": "account-booked-transaction",
|
||||
"accountId": "acc-8f3a2e1b-demo",
|
||||
"created": "2026-05-22T08:12:33Z",
|
||||
"created": _dt(),
|
||||
"transaction": {
|
||||
"id": "tx-001-demo",
|
||||
"description": "Spotify AB",
|
||||
"amount": {"value": {"unscaledValue": "-89900", "scale": "2"}, "currencyCode": "DKK"},
|
||||
"status": "BOOKED",
|
||||
"dates": {"booked": "2026-05-22"},
|
||||
"dates": {"booked": _d()},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -13,7 +13,7 @@ settings = get_settings()
|
||||
|
||||
app = FastAPI(
|
||||
title="Tink API Demo",
|
||||
description="MoneyCapp × Tink — sales demo showing v2 API endpoints",
|
||||
description="Tink Open Banking API — integration walkthrough covering auth, users, accounts (v2), transactions (v2) and webhooks.",
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ with the live JSON response and a curl example.
|
||||
import json
|
||||
import uuid
|
||||
import secrets
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Request, Form, HTTPException
|
||||
@@ -18,6 +21,7 @@ from src import demo_data
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _client(log_cb=None) -> TinkClient:
|
||||
@@ -40,7 +44,6 @@ def _session(request: Request) -> dict:
|
||||
# Server-side token store — keeps JWTs OUT of the session cookie
|
||||
# (cookie limit is 4KB; two JWTs alone are ~1.3KB before base64 overhead)
|
||||
# ---------------------------------------------------------------------------
|
||||
import asyncio
|
||||
|
||||
_token_store: dict[str, dict] = {} # sid → {"app_token": str, "user_token": str}
|
||||
_callback_locks: dict[str, asyncio.Lock] = {} # sid → Lock (prevents concurrent code exchange)
|
||||
@@ -112,7 +115,10 @@ async def reset_demo(request: Request):
|
||||
|
||||
@router.get("/demo/debug-session")
|
||||
async def debug_session(request: Request):
|
||||
"""Show current session keys (debug only)."""
|
||||
"""Session state inspector — only available when DEMO_MODE=true."""
|
||||
s = get_settings()
|
||||
if not s.demo_mode:
|
||||
raise HTTPException(status_code=404)
|
||||
sess = _session(request)
|
||||
safe = {
|
||||
k: (v[:20] + "…" if isinstance(v, str) and len(v) > 20 else v)
|
||||
@@ -430,9 +436,9 @@ async def tink_callback(request: Request, code: Optional[str] = None,
|
||||
credentials_id: Optional[str] = None):
|
||||
"""Tink Link OAuth callback — exchange code for user token."""
|
||||
sess = _session(request)
|
||||
print(f"[CALLBACK] code={code!r} error={error!r} session_keys={list(sess.keys())}")
|
||||
logger.info("Tink callback: code=%r error=%r", code, error)
|
||||
if error:
|
||||
print(f"[CALLBACK] Tink returned error: {error}")
|
||||
logger.warning("Tink returned error: %s", error)
|
||||
return RedirectResponse(f"/demo/step/3?error={error}")
|
||||
if code:
|
||||
sid = sess.get("sid", "unknown")
|
||||
@@ -440,31 +446,29 @@ async def tink_callback(request: Request, code: Optional[str] = None,
|
||||
_callback_locks[sid] = asyncio.Lock()
|
||||
async with _callback_locks[sid]:
|
||||
if _load_token(sess, "user_token"):
|
||||
print(f"[CALLBACK] Already have user_token — skipping duplicate exchange")
|
||||
logger.debug("Already have user_token — skipping duplicate code exchange")
|
||||
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)
|
||||
try:
|
||||
s = get_settings()
|
||||
print(f"[CALLBACK] Exchanging code, redirect_uri={s.tink_redirect_uri!r}")
|
||||
logger.info("Exchanging code for token, redirect_uri=%r", s.tink_redirect_uri)
|
||||
client = _client(log_cb=_logger(sess))
|
||||
tokens = await client.exchange_code_for_token(
|
||||
code, redirect_uri=s.tink_redirect_uri
|
||||
)
|
||||
print(f"[CALLBACK] Token response keys: {list(tokens.keys())}")
|
||||
user_token = tokens.get("access_token", "")
|
||||
if not user_token:
|
||||
print(f"[CALLBACK] ERROR: access_token missing, got: {tokens}")
|
||||
logger.error("Token exchange succeeded but access_token missing: %s", tokens)
|
||||
return RedirectResponse(
|
||||
f"/demo/step/3?cb_error=Token+exchange+ok+but+access_token+missing+in+response",
|
||||
status_code=303,
|
||||
)
|
||||
_store_token(sess, "user_token", user_token)
|
||||
print(f"[CALLBACK] SUCCESS — user_token saved ({len(user_token)} chars)")
|
||||
logger.info("user_token saved (%d chars)", len(user_token))
|
||||
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[CALLBACK] EXCEPTION: {e}\n{traceback.format_exc()}")
|
||||
logger.exception("Token exchange failed: %s", e)
|
||||
return RedirectResponse(f"/demo/step/3?cb_error={str(e)}", status_code=303)
|
||||
print(f"[CALLBACK] No code — bare redirect to step 3")
|
||||
logger.debug("Callback with no code — redirecting to step 3")
|
||||
return RedirectResponse("/demo/step/3", status_code=303)
|
||||
|
||||
|
||||
@@ -695,8 +699,20 @@ async def clear_log(request: Request):
|
||||
|
||||
@router.post("/webhooks/tink")
|
||||
async def webhook_receiver(request: Request):
|
||||
"""Receive Tink webhook events (configure URL in Tink Console)."""
|
||||
"""
|
||||
Receive Tink webhook events posted by Tink after bank data refresh.
|
||||
|
||||
IMPORTANT — In production, verify Tink's HMAC-SHA256 signature before processing:
|
||||
Docs: https://docs.tink.com/api#webhook/webhook-endpoints
|
||||
|
||||
C# example:
|
||||
var signature = Request.Headers["X-Tink-Signature"];
|
||||
var isValid = VerifyHmacSha256(requestBody, signature, webhookSecret);
|
||||
if (!isValid) return Unauthorized();
|
||||
|
||||
Store the event payload in your database and trigger business logic
|
||||
(e.g., refresh account balances, notify the customer).
|
||||
"""
|
||||
body = await request.json()
|
||||
# In production you'd verify the signature and store events
|
||||
print(f"[WEBHOOK] {json.dumps(body, indent=2)}")
|
||||
logger.info("Webhook received: %s", json.dumps(body)[:200])
|
||||
return {"status": "received"}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<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>
|
||||
<span class="text-slate-400 text-sm ml-2">Open Banking Demo</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-slate-800 text-center text-slate-500 text-xs py-4">
|
||||
Tink API Demo — i80.dk
|
||||
Tink Open Banking API Demo
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -114,9 +114,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Not yet connected — show connection UI -->
|
||||
|
||||
<!-- Sandbox note: anonymous flow vs production -->
|
||||
<div class="bg-amber-950/40 border border-amber-700/40 rounded-xl px-5 py-3.5 flex items-start gap-3 text-sm">
|
||||
<svg class="w-4 h-4 text-amber-400 flex-shrink-0 mt-0.5" 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>
|
||||
<div class="text-amber-200/80 leading-relaxed">
|
||||
<span class="font-semibold text-amber-300">Sandbox-note:</span>
|
||||
Dette demo bruger <em>anonymous Tink Link flow</em> (ingen <code class="font-mono text-xs">authorization_code</code> i URL'en) —
|
||||
en sandbox-begrænsning. I produktion <strong>skal</strong> du kalde
|
||||
<code class="font-mono text-xs">authorization-grant/delegate</code> og sende koden med til Tink Link,
|
||||
så bank-forbindelsen bindes til den korrekte Tink-bruger.
|
||||
<a href="https://docs.tink.com/resources/tink-link/tink-link-web-permanent-users" target="_blank" class="text-amber-400 underline hover:text-amber-300 ml-1">Docs ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
|
||||
@@ -38,9 +38,11 @@
|
||||
<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.
|
||||
Opret en Tink-bruger med et <code class="text-violet-300">external_user_id</code> —
|
||||
jeres interne reference til kunden (f.eks. et kundenummer eller UUID fra jeres database).
|
||||
Gem dette som en kolonne i jeres kundedatabase (<code class="text-violet-300">tink_user_ref</code> el.lign.) —
|
||||
det er ikke det samme som jeres interne <code class="text-slate-400">customer_id</code>.
|
||||
Tink returnerer et <code class="text-violet-300">user_id</code> (UUID) der bruges i efterfølgende kald.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -89,8 +91,8 @@
|
||||
<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>
|
||||
<code class="text-violet-300">external_user_id</code> = jeres interne kundereference —
|
||||
gemmes i jeres database som f.eks. <code class="text-slate-400">tink_user_ref</code>
|
||||
</p>
|
||||
</div>
|
||||
<form method="POST" action="/demo/step/2" class="p-5 space-y-4">
|
||||
@@ -101,7 +103,7 @@
|
||||
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>
|
||||
<p class="text-xs text-slate-600 mt-1.5">→ <code class="text-violet-300/70">external_user_id</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>
|
||||
@@ -136,13 +138,13 @@
|
||||
<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
|
||||
<pre class="text-xs text-amber-300 font-mono whitespace-pre"># Your DB schema (example):
|
||||
# customer_id = 42 ← your internal ID (Tink never sees this)
|
||||
# tink_user_ref = "moneycapp-42-a3f9c1" ← store Tink's external_user_id here
|
||||
|
||||
# Request body
|
||||
{
|
||||
"external_user_id": "moneycapp-<ref>", ← tink_external_ref
|
||||
"external_user_id": "moneycapp-<ref>", ← your reference, stored in DB
|
||||
"market": "DK",
|
||||
"locale": "da_DK"
|
||||
}
|
||||
|
||||
@@ -224,7 +224,6 @@ async def tink_webhook(request: Request):
|
||||
</p>
|
||||
<p class="text-slate-500 text-sm mb-6 max-w-xl mx-auto">
|
||||
Auth → bruger → bank-tilslutning → konti (v2) → transaktioner (v2) → webhooks.
|
||||
Et komplet Tink-flow — klar til at blive bygget ind i jeres platform.
|
||||
</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>
|
||||
@@ -235,6 +234,33 @@ async def tink_webhook(request: Request):
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Next Steps for implementation -->
|
||||
<div class="mt-6 bg-slate-900/60 border border-slate-800 rounded-2xl p-7">
|
||||
<h4 class="text-white font-semibold text-base mb-4 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
|
||||
Næste skridt mod en produktion-klar integration
|
||||
</h4>
|
||||
<div class="grid md:grid-cols-2 gap-3 text-sm text-slate-400">
|
||||
<div class="bg-slate-800/60 rounded-lg p-4 space-y-1.5">
|
||||
<p class="text-violet-300 font-semibold text-xs uppercase tracking-wider mb-2">Backend (C# / .NET)</p>
|
||||
<p>1. Lav en <code class="text-violet-300 font-mono text-xs">TinkApiClient</code> wrapper (brug <code class="text-xs font-mono">tink/client.py</code> som reference)</p>
|
||||
<p>2. Gem <code class="text-violet-300 font-mono text-xs">external_user_id</code> + <code class="text-violet-300 font-mono text-xs">user_id</code> i din kundedatabase</p>
|
||||
<p>3. Implementér <code class="text-violet-300 font-mono text-xs">/callback</code> endpoint med token exchange</p>
|
||||
<p>4. Gem tokens sikkert (encrypted, server-side — ikke i cookie)</p>
|
||||
</div>
|
||||
<div class="bg-slate-800/60 rounded-lg p-4 space-y-1.5">
|
||||
<p class="text-violet-300 font-semibold text-xs uppercase tracking-wider mb-2">Webhooks & Production</p>
|
||||
<p>5. Byg webhook receiver med <a href="https://docs.tink.com/api#webhook/webhook-endpoints" target="_blank" class="text-violet-400 hover:text-violet-300 underline">HMAC-SHA256 signature verification</a></p>
|
||||
<p>6. Skift til production Tink-credentials (Tink Console)</p>
|
||||
<p>7. Registrér din production callback URI i Tink Console</p>
|
||||
<p>8. Brug <code class="text-violet-300 font-mono text-xs">authorization-grant/delegate</code> i prod-flowet (ikke anon)</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-600 mt-4">
|
||||
Kildekoden til dette demo (<code class="font-mono">src/tink/client.py</code> og <code class="font-mono">src/routes/demo.py</code>) er skrevet for at være letlæselig og direkte overførbar til andre platforme.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="mt-4 flex justify-start">
|
||||
<a href="/demo/step/5"
|
||||
|
||||
Reference in New Issue
Block a user