App Bridge, panel içinde iframe olarak açılan uygulama arayüzünüz ile Tecof paneli arasındaki mesajlaşma katmanıdır. Oturum token'ı almak, bildirim göstermek, onay penceresi açmak, panelde başka bir sayfaya gitmek ve aksiyon penceresini kapatmak bu kanal üzerinden yapılır. Altta window.postMessage kullanılır; @tecof/app-bridge paketi protokolü sizin yerinize yönetir.
Kurulum#
npm i @tecof/app-bridgePaket tarayıcıda çalışır ve bağımlılık içermez. React için @tecof/app-bridge/react alt yolu vardır.
Protokol#
Her mesaj aynı zarfı taşır:
{ "tecof": "app-bridge", "v": 1, "id": "req_1", "type": "toast", "payload": { "message": "Kaydedildi" } }| Alan | Açıklama |
|---|---|
tecof | Her zaman "app-bridge" |
v | Protokol sürümü: 1 |
id | İsteğe bağlı; istek–yanıt eşleştirmesi için |
type | Mesaj tipi |
payload | Tipe özgü yük |
Panelin yanıtı:
{ "tecof": "app-bridge", "v": 1, "id": "req_1", "type": "response", "ok": true, "payload": {} }Hata durumunda ok: false ve error: { code, message } döner.
Güvenlik kuralları#
| Taraf | Kural |
|---|---|
| Panel | Yalnız event.origin uygulamanızın origin'ine ve event.source iframe'in contentWindow'una eşit olan mesajları işler. Yanıtı postMessage(msg, appOrigin) ile yalnız uygulama origin'ine gönderir |
| Uygulama (SDK) | Yalnız açılış adresindeki host parametresinde (base64url) bildirilen panel origin'inden gelen mesajları kabul eder |
SDK'sız çalışıyorsanız bu iki kuralı kendiniz uygulayın; "*" hedef origin'iyle mesaj göndermeyin.
Mesaj Tipleri#
type (uygulama → panel) | payload | Yanıt payload |
|---|---|---|
handshake | { sdkVersion } | { context } |
sessionToken | — | { token, expiresAt } |
toast | { message, kind?, duration? } — kind: success, error, info, warning | {} |
navigate | { to } — yalnız /app/ ile başlayan panel yolu | {} |
open | { url } — https adresi, yeni sekmede açılır | {} |
confirm | { title, message, confirmLabel?, cancelLabel?, destructive? } | { confirmed } |
setTitle | { title } | {} |
setLoading | { loading } | {} |
resize | { height } — modal/çekmece yüksekliği | {} |
close | { refresh?, message?, kind? } — aksiyon penceresini kapatır | {} |
openBilling | — | {} — plan seçim çekmecesini açar |
Panelden uygulamaya kendiliğinden gelen olay:
{ "tecof": "app-bridge", "v": 1, "type": "context", "payload": { "context": { } } }Bu olay tema ya da dil değiştiğinde güncel bağlamla gönderilir.
Bağlam (context) Nesnesi#
{
"merchant": { "id": "66f0c0ffee0000000000abcd", "name": "Örnek Mağaza", "currency": "TRY", "languages": ["tr", "en"] },
"user": { "id": "66f0aa00bb11cc22dd33ee66", "name": "Örnek Kullanıcı", "locale": "tr" },
"installation": {
"id": "6720aa00bb11cc22dd33ee55",
"scopes": ["orders:read", "customers:read", "products:read"],
"plan": { "key": "standart", "status": "active" }
},
"app": { "clientId": "tcfa_xxxxxxxxxxxxxxxx", "slug": "isbasi", "name": "Logo İşbaşı" },
"theme": "light",
"locale": "tr",
"action": { "runId": "6720cc00dd11ee22ff33aa55", "key": "create-invoice", "placement": "order.detail", "ids": ["6720bb00cc11dd22ee33ff44"] }
}| Alan | Açıklama |
|---|---|
merchant | Mağaza kimliği, adı, para birimi, dilleri |
user | Paneli kullanan kişi |
installation.scopes | Verilmiş kapsamlar |
installation.plan | { key, status } ya da plan yoksa null. Bkz. Ücretlendirme |
app | Uygulamanın istemci kimliği, slug'ı, adı |
theme | light ya da dark — arayüzünüzü panelle uyumlu boyayın |
locale | Panel dili |
action | Yalnız iframe aksiyonu olarak açıldığında: { runId, key, placement, ids } |
Bağlam arayüz içindir. Sunucu tarafında yetki kararı için oturum token'ını doğrulayın; bkz. Kimlik Doğrulama.
SDK Kullanımı#
import { createAppBridge, isEmbedded } from "@tecof/app-bridge";
if (!isEmbedded()) {
// Sayfa panel dışında açıldı (window.top === window.self ya da embedded=1 yok)
}
const bridge = createAppBridge({ debug: false }); // host origin'i URL'deki `host` parametresinden okunur
await bridge.ready(); // handshake → bridge.context dolar
console.log(bridge.context.merchant.name);
// Oturum token'ı: önbellekli, bitişe 10 sn kala yenilenir
const token = await bridge.getSessionToken();
// Kendi sunucunuza Authorization: Bearer <oturum token'ı> ile istek
const res = await bridge.fetch("/api/orders");
bridge.toast("Ayarlar kaydedildi", { kind: "success" });
bridge.navigate("/app/ecommerce/orders/6720bb00cc11dd22ee33ff44");
bridge.open("https://ornek.com/yardim");
const confirmed = await bridge.confirm({
title: "Faturayı iptal et",
message: "Bu işlem geri alınamaz.",
confirmLabel: "İptal et",
cancelLabel: "Vazgeç",
destructive: true,
});
bridge.setTitle("Faturalar");
bridge.setLoading(true);
bridge.resize(640);
bridge.close({ refresh: true, message: "Tamamlandı", kind: "success" });
bridge.openBilling();
const unsubscribe = bridge.on("context", (context) => {
document.documentElement.dataset.theme = context.theme;
});| Metot | Açıklama |
|---|---|
createAppBridge({ debug? }) | Köprüyü oluşturur |
ready() | Handshake'i tamamlar; ardından bridge.context kullanılabilir |
context | Güncel bağlam |
getSessionToken() | Önbellekli oturum token'ı |
fetch(input, init) | Authorization: Bearer <oturum token'ı> ekleyerek fetch |
toast(message, { kind?, duration? }) | Panel bildirimi |
navigate(to) | Panel içi yönlendirme (/app/…) |
open(url) | Yeni sekmede https adresi |
confirm({...}) | Panel onay penceresi → boolean |
setTitle, setLoading, resize | Başlık, yükleniyor durumu, yükseklik |
close({ refresh?, message?, kind? }) | Aksiyon penceresini kapatır |
openBilling() | Plan seçim çekmecesi |
on("context", cb) | Bağlam değişimini dinler; aboneliği kaldıran fonksiyon döner |
isEmbedded() | Panel içinde mi (window.top !== window.self ve embedded=1) |
React#
// app/layout.tsx
import { AppShell } from "./AppShell";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="tr">
<body>
<AppShell>{children}</AppShell>
</body>
</html>
);
}// app/AppShell.tsx
"use client";
import { AppBridgeProvider } from "@tecof/app-bridge/react";
export function AppShell({ children }: { children: React.ReactNode }) {
return (
<AppBridgeProvider fallback={<p>Yükleniyor…</p>}>
{children}
</AppBridgeProvider>
);
}// app/invoices/page.tsx
"use client";
import { useEffect, useState } from "react";
import { useAppBridge, useAppContext, useSessionFetch } from "@tecof/app-bridge/react";
type InvoiceRow = { orderNumber: string; invoiceNumber: string };
export default function InvoicesPage() {
const bridge = useAppBridge();
const context = useAppContext();
const sessionFetch = useSessionFetch();
const [rows, setRows] = useState<InvoiceRow[]>([]);
useEffect(() => {
bridge.setTitle("Faturalar");
sessionFetch("/api/invoices")
.then((res) => res.json())
.then((data) => setRows(data.items))
.catch(() => bridge.toast("Faturalar yüklenemedi", { kind: "error" }));
}, [bridge, sessionFetch]);
return (
<main data-theme={context.theme}>
<h1>{context.merchant.name} — Faturalar</h1>
<ul>
{rows.map((row) => (
<li key={row.invoiceNumber}>{row.orderNumber}: {row.invoiceNumber}</li>
))}
</ul>
</main>
);
}| API | Açıklama |
|---|---|
<AppBridgeProvider fallback?> | Köprüyü kurar, handshake bitene kadar fallback'i gösterir |
useAppBridge() | Köprü nesnesi |
useAppContext() | Güncel bağlam (context olaylarıyla güncellenir) |
useSessionFetch() | Oturum token'ını ekleyen fetch |
useSessionFetch ile çağrılan sunucu ucunuz token'ı tecof.sessionFromRequest(req) ile doğrular. Bkz. Kimlik Doğrulama.
SDK'sız Kullanım#
Farklı bir çatı kullanıyorsanız protokolü doğrudan uygulayabilirsiniz:
function decodeBase64Url(value) {
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
return atob(normalized + "=".repeat((4 - (normalized.length % 4)) % 4));
}
const params = new URLSearchParams(window.location.search);
const panelOrigin = decodeBase64Url(params.get("host"));
const pending = new Map();
let seq = 0;
window.addEventListener("message", (event) => {
if (event.origin !== panelOrigin) return; // yalnız panel origin'i
const msg = event.data;
if (!msg || msg.tecof !== "app-bridge" || msg.v !== 1) return;
if (msg.type === "response" && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id);
pending.delete(msg.id);
msg.ok ? resolve(msg.payload) : reject(msg.error);
}
if (msg.type === "context") {
// msg.payload.context ile arayüzü güncelle
}
});
function send(type, payload) {
const id = `req_${++seq}`;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
window.parent.postMessage({ tecof: "app-bridge", v: 1, id, type, payload }, panelOrigin);
});
}
const { context } = await send("handshake", { sdkVersion: "custom-1" });
const { token } = await send("sessionToken");
await send("toast", { message: `Merhaba ${context.user.name}`, kind: "info" });