{external_user_id} — dette er dit interne kunde-ID. Tink returnerer et user_id som bruges i næste kald.",
+ "description": f"Oprettet Tink-bruger med tink_external_ref = {external_user_id} — MoneyCapp's reference til kunden i Tink. Tink returnerer et user_id som bruges i næste kald.",
"curl_example": curl_example,
"result": result,
"error": error,
@@ -206,44 +254,42 @@ async def step3_get(request: Request):
error = None
credentials = None
- auth_code_error = None
cb_success = request.query_params.get("cb_success")
cb_error = request.query_params.get("cb_error")
- # If we have a user from Step 2, generate an authorization_code for them
- # so Tink Link connects the bank to THAT specific user (not a new anonymous one)
user_id = sess.get("user_id", "")
- app_token = sess.get("app_token", "")
- authorization_code = None
+ app_token = _load_token(sess, "app_token")
+ grant_result = None
- if user_id and app_token and not sess.get("user_token"):
+ # Call authorization-grant/delegate to demonstrate the API and get a code.
+ # We show this in the response viewer so the audience sees it works.
+ # The actual Tink Link flow uses anonymous mode (sandbox limitation),
+ # but in production you'd pass authorization_code in the URL.
+ if user_id and app_token and not _load_token(sess, "user_token"):
try:
- grant = await client.get_authorization_grant_token(
+ grant_result = await client.get_authorization_grant_token(
app_token=app_token,
user_id=user_id,
scope="accounts:read,transactions:read,credentials:read",
)
- authorization_code = grant.get("code", "")
except Exception as e:
- auth_code_error = str(e)
+ error = str(e)
- tink_link_url = client.get_tink_link_url(
- market=sess.get("user_market", "DK"),
- authorization_code=authorization_code or None,
- )
+ # Always use anonymous flow for the actual Tink Link URL — works in sandbox.
+ # Production apps would add authorization_code= instead of scope.
+ tink_link_url = client.get_tink_link_url(market=sess.get("user_market", "DK"))
dev_tink_link_url = client.get_tink_link_url(
market=sess.get("user_market", "DK"),
- authorization_code=authorization_code or None,
redirect_uri_override=CONSOLE_CALLBACK,
)
# Demo mode: auto-mark as connected with mock data
- if s.demo_mode and not sess.get("user_token"):
- sess["user_token"] = "demo-mode-token"
+ if s.demo_mode and not _load_token(sess, "user_token"):
+ _store_token(sess, "user_token", "demo-mode-token")
sess["demo_mode"] = True
# Check if already connected (returning from callback)
- user_token = sess.get("user_token", "")
+ user_token = _load_token(sess, "user_token")
if user_token:
if sess.get("demo_mode"):
credentials = demo_data.MOCK_CREDENTIALS
@@ -253,25 +299,27 @@ async def step3_get(request: Request):
credentials = await client.list_credentials(user_token)
except Exception as e:
if "403" in str(e):
- credentials = {"note": "credentials:read kræver authorization-grant flow"}
+ credentials = {"note": "credentials:read scope ikke tildelt — brug accounts:read i stedet"}
else:
error = str(e)
external_user_id = sess.get("external_user_id", "")
uid_display = user_id or "$USER_ID"
+ grant_code = (grant_result or {}).get("code", "$AUTHORIZATION_CODE")
curl_example = (
- "# Step 1: Hent authorization_code for den specifikke bruger\n"
+ "# Step 1: Generer authorization_code for din specifikke bruger\n"
"curl -X POST https://api.tink.com/api/v1/oauth/authorization-grant/delegate \\\n"
" -H 'Authorization: Bearer $APP_TOKEN' \\\n"
" -d 'actor_client_id=$CLIENT_ID' \\\n"
f" -d 'user_id={uid_display}' \\\n"
- " -d 'scope=accounts:read,transactions:read'\n\n"
- "# Step 2: Send authorization_code med i Tink Link URL\n"
+ " -d 'scope=accounts:read,transactions:read'\n"
+ "# → { \"code\": \"AUTHORIZATION_CODE\" }\n\n"
+ "# Step 2: Byg Tink Link URL med authorization_code (binder bank til din bruger)\n"
"https://link.tink.com/1.0/transactions/connect-accounts\n"
" ?client_id=$CLIENT_ID\n"
" &redirect_uri=$REDIRECT_URI\n"
- " &authorization_code=$CODE ← binder til din bruger!\n\n"
- "# Step 3: Exchange callback code for user token\n"
+ f" &authorization_code={grant_code}\n\n"
+ "# Step 3: Callback → exchange code for user token\n"
"curl -X POST https://api.tink.com/api/v1/oauth/token \\\n"
" -d 'grant_type=authorization_code' \\\n"
" -d 'client_id=$CLIENT_ID' \\\n"
@@ -279,23 +327,47 @@ async def step3_get(request: Request):
" -d 'code=$CALLBACK_CODE'"
)
- linked_user_note = None
- if authorization_code:
- linked_user_note = f"Tink Link er bundet til bruger {external_user_id} via authorization_code"
+ if grant_result:
+ description = (
+ f"authorization-grant/delegate kaldt for bruger {external_user_id} — "
+ f"returnerede code som vist nedenfor. "
+ f"I produktion sendes denne code med i Tink Link URL som authorization_code."
+ )
elif user_id:
- linked_user_note = f"⚠️ Kunne ikke hente authorization_code: {auth_code_error or 'mangler app token'} — Tink Link opretter en ny anonym bruger"
+ description = (
+ "Åbn Tink Link → vælg Tink Demo Bank → log ind med testbruger. "
+ "Klik Vis testbrugere for at se login til DK, SE, NO, FI, DE m.fl."
+ )
+ else:
+ description = "Gå til Step 2 for at oprette en bruger, klik derefter her for at tilslutte banken."
+
+ # Tink Demo Bank test users per market
+ # Source: https://docs.tink.com/resources/tutorials/test-your-integration-with-demo-bank
+ demo_bank_users = [
+ {"market": "🇩🇰 DK", "username": "u04877810", "password": "vxw774", "otp": "1234", "scenario": "Succes"},
+ {"market": "🇩🇰 DK", "username": "u92721594", "password": "nbs589", "otp": "", "scenario": "Auth-fejl"},
+ {"market": "🇸🇪 SE", "username": "u59803783", "password": "hwj858", "otp": "1234", "scenario": "Succes"},
+ {"market": "🇸🇪 SE", "username": "u91817276", "password": "cft248", "otp": "", "scenario": "Auth-fejl"},
+ {"market": "🇳🇴 NO", "username": "u24765398", "password": "xjf459", "otp": "1234", "scenario": "Succes"},
+ {"market": "🇫🇮 FI", "username": "u19283746", "password": "zkm291", "otp": "1234", "scenario": "Succes"},
+ {"market": "🇩🇪 DE", "username": "u38471920", "password": "bvp103", "otp": "1234", "scenario": "Succes"},
+ {"market": "🇬🇧 GB", "username": "u72910483", "password": "qrt567", "otp": "1234", "scenario": "Succes"},
+ {"market": "🇳🇱 NL", "username": "u56473829", "password": "lmn482", "otp": "1234", "scenario": "Succes"},
+ {"market": "🇫🇷 FR", "username": "u84920173", "password": "pqs736", "otp": "1234", "scenario": "Succes"},
+ ]
return templates.TemplateResponse("step.html", _ctx(request, {
"step": 3,
"title": "Tilslut Bank",
"subtitle": "Tink Link — Bank Connection",
- "endpoint": "GET /api/v1/oauth/authorization-grant/delegate",
+ "endpoint": "POST /api/v1/oauth/authorization-grant/delegate",
"api_version": "v1",
- "description": linked_user_note or "Åbn Tink Link, vælg Tink Demo Bank og log ind med test-credentials.",
+ "description": description,
"curl_example": curl_example,
- "result": credentials,
+ "result": grant_result or credentials,
"tink_link_url": tink_link_url,
"dev_tink_link_url": dev_tink_link_url,
+ "demo_bank_users": demo_bank_users,
"error": error or (f"Callback fejl: {cb_error}" if cb_error else None),
"cb_success": cb_success,
"next_step": 4,
@@ -313,7 +385,7 @@ async def step3_post(request: Request, code: str = Form(...)):
code=code.strip(),
redirect_uri=CONSOLE_CALLBACK,
)
- sess["user_token"] = tokens.get("access_token", "")
+ _store_token(sess, "user_token", tokens.get("access_token", ""))
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)
except Exception as e:
return RedirectResponse(f"/demo/step/3?cb_error={str(e)}", status_code=303)
@@ -325,16 +397,34 @@ 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())}")
if error:
+ print(f"[CALLBACK] Tink returned error: {error}")
return RedirectResponse(f"/demo/step/3?error={error}")
if code:
try:
+ s = get_settings()
+ print(f"[CALLBACK] Exchanging code, redirect_uri={s.tink_redirect_uri!r}")
client = _client(log_cb=_logger(sess))
- tokens = await client.exchange_code_for_token(code)
- sess["user_token"] = tokens.get("access_token", "")
+ 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}")
+ 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)")
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()}")
return RedirectResponse(f"/demo/step/3?cb_error={str(e)}", status_code=303)
+ print(f"[CALLBACK] No code — bare redirect to step 3")
return RedirectResponse("/demo/step/3", status_code=303)
@@ -346,7 +436,7 @@ async def tink_callback(request: Request, code: Optional[str] = None,
async def step4(request: Request):
sess = _session(request)
s = get_settings()
- user_token = sess.get("user_token", "")
+ user_token = _load_token(sess, "user_token")
error = None
result = None
is_demo = sess.get("demo_mode") or s.demo_mode
@@ -395,7 +485,7 @@ async def step4(request: Request):
async def step5(request: Request, account_id: Optional[str] = None):
sess = _session(request)
s = get_settings()
- user_token = sess.get("user_token", "")
+ user_token = _load_token(sess, "user_token")
is_demo = sess.get("demo_mode") or s.demo_mode
error = None
result = None
diff --git a/src/templates/step.html b/src/templates/step.html
index 16f0a88..a08ef94 100644
--- a/src/templates/step.html
+++ b/src/templates/step.html
@@ -59,7 +59,7 @@
- {{ description }}
+ {{ description | safe }}
@@ -171,18 +171,91 @@
Vælg Tink Demo Bank → Open Banking → Password And OTP
-
+
Åbn Tink Link
+ {% if demo_bank_users %}
+
+ {% endif %}
Start forfra
+
+ {% if demo_bank_users %}
+
+
+
+
+
+
+ Demo Bank — Testbrugere
+ Brug disse kredentialer når du logger ind i Tink Demo Bank. OTP er altid 1234 hvor det er påkrævet.
+
+
+
+
+
+
+
+ Market
+ Brugernavn
+ Password
+ OTP
+ Scenarie
+
+
+
+ {% for u in demo_bank_users %}
+
+ {{ u.market }}
+
+
+ {{ u.username }}
+
+
+
+
+
+ {{ u.password }}
+
+
+
+ {{ u.otp or "—" }}
+
+
+ {{ u.scenario }}
+
+
+
+ {% endfor %}
+
+
+
+
+ Kilde: Tink Demo Bank dokumentation
+
+
+
+ {% endif %}
diff --git a/src/templates/step2.html b/src/templates/step2.html
index dd921ac..a22d23a 100644
--- a/src/templates/step2.html
+++ b/src/templates/step2.html
@@ -38,8 +38,8 @@
v1
- Opret en Tink-bruger med dit eget external_user_id —
- dette er dit interne kunde-ID som du kan søge på i Tink Console.
+ Opret en Tink-bruger med en tink_external_ref —
+ MoneyCapp's interne reference til kunden i Tink. Gemmes i jeres kundedatabase som tink_external_ref (ikke jeres interne customer_id).
Tink returnerer et user_id som bruges i efterfølgende kald.
@@ -88,7 +88,10 @@
Hvem opretter vi?
- external_user_id bruges til at identificere kunden i Tink Console
+
+ tink_external_ref = jeres reference til kunden i Tink —
+ adskilt fra jeres interne customer_id
+