İçeriğe geç
Docs

App Bridge

Panel ile gömülü uygulama arasındaki postMessage protokolü, bağlam nesnesi, @tecof/app-bridge ve React sağlayıcısı.

Son güncelleme:

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#

Terminal
npm i @tecof/app-bridge

Paket 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:

JSON
{ "tecof": "app-bridge", "v": 1, "id": "req_1", "type": "toast", "payload": { "message": "Kaydedildi" } }
AlanAçıklama
tecofHer zaman "app-bridge"
vProtokol sürümü: 1
idİsteğe bağlı; istek–yanıt eşleştirmesi için
typeMesaj tipi
payloadTipe özgü yük

Panelin yanıtı:

JSON
{ "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ı#

TarafKural
PanelYalnı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)payloadYanı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:

JSON
{ "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#

JSON
{
  "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"] }
}
AlanAçıklama
merchantMağaza kimliği, adı, para birimi, dilleri
userPaneli kullanan kişi
installation.scopesVerilmiş kapsamlar
installation.plan{ key, status } ya da plan yoksa null. Bkz. Ücretlendirme
appUygulamanın istemci kimliği, slug'ı, adı
themelight ya da dark — arayüzünüzü panelle uyumlu boyayın
localePanel dili
actionYalnı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ı#

TypeScript
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;
});
MetotAçıklama
createAppBridge({ debug? })Köprüyü oluşturur
ready()Handshake'i tamamlar; ardından bridge.context kullanılabilir
contextGü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, resizeBaş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#

TSX
// 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>
  );
}
TSX
// 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>
  );
}
TSX
// 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>
  );
}
APIAçı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:

JavaScript
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" });