T-SOFT/Uygulama Geliştirme/Panele Gömülü Uygulamalar

Panele Gömülü Uygulamalar

Uygulama kaydınızda app_url tanımlıysa panel uygulamanızı kendi içinde, /apps/open/<prefix> sayfasındaki bir iframe'de açar. Mağaza sahibi ayrıca giriş yapmaz: panel, kimin hangi mağazadan geldiğini söyleyen kısa ömürlü bir oturum token'ı gönderir.

Açılış

Panel iframe adresine oturum token'ını ekler:

https://uygulamaniz.com/app?session=<jwt>

Kurulumdan hemen sonraki ilk açılışta adreste ayrıca tek kullanımlık code bulunur. Bu kodu OAuth Akışı'ndaki gibi takas edin; dönen store_id token'daki store_id ile aynı olmalıdır.

Oturum token'ını doğrulama

Token ES256 ile imzalıdır ve 60 saniye geçerlidir. Açık anahtarları JWKS adresinden alın, kid ile eşleştirin; birkaç dakika önbelleğe alabilirsiniz:

GET https://api.tsoftapps.com/api/integration/v3/apps/jwks.json
ClaimKontrol
isshttps://api.tsoftapps.com olmalı
audUygulamanızın prefix'i olmalı
exp, nbfSüre içinde olmalı (birkaç saniye saat kayması payı bırakın)
store_idMağaza kimliği; sakladığınız istemci bilgisiyle eşleyin
destMağaza alan adı
subUygulamayı açan panel kullanıcısı
session.ts
import { createRemoteJWKSet, jwtVerify } from "jose"

const jwks = createRemoteJWKSet(new URL("https://api.tsoftapps.com/api/integration/v3/apps/jwks.json"))

export async function verifySession(token: string) {
  const { payload } = await jwtVerify(token, jwks, {
    algorithms: ["ES256"],
    issuer: "https://api.tsoftapps.com",
    audience: process.env.TSOFT_APP_PREFIX,
    clockTolerance: 10,
  })
  if (payload.event) throw new Error("not a session token")
  return payload
}
Oturum token'ı Admin API çağrısı için değildir; uygulamanızın kendi sunucusuyla konuşmak içindir. Admin API'yi, sakladığınız mağaza istemci bilgisiyle aldığınız token ile çağırın.

Yalnız panelde gömülebilme

Yanıtlarınızda şu başlığı gönderin; böylece uygulamanız yalnız T-SOFT panelinde iframe'e alınabilir:

Content-Security-Policy: frame-ancestors https://tsoftpanel.com https://*.tsoftpanel.com https://paneltsoft.com https://*.paneltsoft.com https://<dest>

Üçüncü taraf çerezleri iframe içinde engellenebilir; oturumu çereze değil token'a dayandırın.

Panel köprüsü

Panel köprüsü, iframe içindeki uygulamanızın panelin kendi bildirim, onay penceresi, ürün seçicisi ve gezinmesini kullanmasını sağlar. @tekrom/tsoft-app-gate paketiyle gelir; bağımlılığı yoktur ve TypeScript tipleri içerir.

npm install @tekrom/tsoft-app-gate

Paketleyici kullanmıyorsanız tarayıcı derlemesini yükleyin; aynı API TSoftAppGate global'i altında gelir (TSoftAppGate.createApp()):

<script src="https://cdn.jsdelivr.net/npm/@tekrom/tsoft-app-gate@0.4/dist/tsoft-app-gate.min.js"></script>
app.ts
import { createApp, isEmbedded, AppBridgeError } from "@tekrom/tsoft-app-gate"

if (isEmbedded()) {
  const app = createApp()

  app.claims?.store_id                              // oturum token'ının içeriği (doğrulamayı sunucuda yapın)
  const token = await app.getSessionToken()         // süresi dolmak üzereyse panelden yenisini alır
  await app.toast("Kaydedildi")                     // { variant: "error" } ile hata bildirimi
  const ok = await app.confirm({ title: "Silinsin mi?", message: "Bu işlem geri alınamaz.", confirmText: "Sil" })
  const { ids, cancelled } = await app.pickResource({ type: "product", multiple: true })   // read_products izni gerekir
  await app.navigate.app("/ayarlar")                // uygulama içi yol panel adresine yansır, yenilemede korunur
  await app.navigate.panel("order", 123)            // order, orders, product, products, apps
  await app.nav.set([                               // sol menüde uygulamanın alt menüsü (en çok 8)
    { label: "Gönderiler", path: "/gonderiler" },
    { label: "Ayarlar", path: "/ayarlar" },
  ])
  app.autoResize()                                  // iframe yüksekliğini içeriğe göre ayarlar
}

Her çağrı promise döner; panel bir isteği reddederse AppBridgeError alırsınız ve code alanı nedenini söyler (missing_scope, invalid_route, timeout…). createApp() yalnız T-SOFT panel alan adlarından (*.tsoftpanel.com, *.tsoftpanel.net, *.paneltsoft.com) birinde açıldığında çalışır.

Uygulama menüsü

app.nav.set() ile verdiğiniz bağlantılar panelin sol menüsünde (ve mobil menüde) uygulamanızın adının altında görünür. Uygulama panelde açıkken ya da menüye sabitlenmişken gösterilir.

  • En fazla 8 bağlantı verilebilir. Etiket en fazla 40 karakter olabilir.
  • path uygulamanızın kendi adresi altında, / ile başlayan bir yol olmalı. Mutlak ya da başka siteye giden adres ve javascript: reddedilir. Tek bir geçersiz bağlantı bütün listeyi invalid_payload ile reddeder.
  • Bağlantıya tıklanınca panel iframe'i o yolla, yeni oturum token'ıyla yeniden yükler.
  • Etkin bağlantı iframe'in o anki yoluna göre vurgulanır. /gonderiler bağlantısı /gonderiler/42 yolunda da etkin sayılır. Uygulama içinde gezindikçe navigate.app çağırırsanız vurgu güncel kalır.
  • Panel son listeyi tarayıcıda saklar. Menü bir sonraki açılışta uygulama yüklenmeden görünür. Listeyi her açılışta göndermeniz yeterlidir; boş liste ([]) menüyü kaldırır.

Tüm API, hata kodları ve köprüyü kendi kodunuzla konuşturmak isterseniz mesaj sözleşmesi paketin README'sinde anlatılır.

Daha önce developer.tsoft.com.tr/app-bridge/v1/tsoft-app-bridge.js adresinden sunulan TSoftBridge script'i kaldırıldı. Geçiş tablosu README'nin Geçiş bölümündedir.

Panel aksiyonları

Panel aksiyonu, uygulamanızı mağaza panelinin sipariş, ürün ve müşteri sayfalarındaki Uygulamalar menüsüne ekler. Mağaza sahibi listede kayıt seçip aksiyona tıklar ya da detay sayfasında tıklar. Panel uygulamanızı kendi içinde, seçilen kayıtlarla açar. Örneğin bir e-fatura uygulaması seçili siparişlere fatura kesebilir, bir etiket uygulaması seçili ürünlerin etiketini basabilir.

Tanımlama

Aksiyonu tsoft.app.toml dosyasında tanımlayıp tsoft app deploy ile gönderin (tsoft-cli 3.19 ve sonrası):

tsoft.app.toml
[app]
url = "https://uygulamaniz.com/app"

[access]
scopes = ["read_orders"]

[[extensions]]
target = "admin.order.list.actions"
label = "Fatura kes"
label_translations = { en = "Create invoice" }
path = "/invoice"
scope = "read_orders"

[[extensions]]
target = "admin.order.detail.actions"
label = "Fatura kes"
path = "/invoice"

Aynı listeyi partner panelinde uygulamanın Ayarlar sekmesindeki Panel aksiyonları kartından da düzenleyebilirsiniz. Liste, uygulama ayarlarının diğer alanları gibi sürümlenir. Panelde yapılan değişikliği bir sonraki tsoft app deploy onaya sorar; tsoft app config pull listeyi dosyaya geri yazar. Dosyanın başına extensions = [] yazmak listeyi temizler.

AlanAçıklama
targetAksiyonun görüneceği yer (aşağıdaki tablo)
labelMenüde görünen etiket. Düz metin, en çok 40 karakter
label_translationsİsteğe bağlı. Panel dillerine göre etiket: tr, en, ar, de, az. Panelin dilinde çeviri yoksa label görünür. tsoft-cli 3.19.0 bu alanı dosyada tanımaz; bir sonraki sürüme kadar çevirileri partner panelinden girin
pathUygulamanızın app_url dizini altında açılacak yol. / ile başlar; //, .., ? ve # içeremez
scopeİsteğe bağlı. Mağaza bu izni vermediyse aksiyon o mağazada görünmez. Uygulamanın access.scopes listesinde olmalı
targetNerede
admin.order.list.actionsSipariş listesi, seçili siparişler
admin.order.detail.actionsSipariş detayı, o sipariş
admin.product.list.actionsÜrün listesi, seçili ürünler
admin.product.detail.actionsÜrün detayı, o ürün
admin.customer.list.actionsMüşteri listesi, seçili müşteriler
admin.customer.detail.actionsMüşteri detayı, o müşteri
admin.order.detail.actionSipariş detayı, modalda açılır
admin.product.detail.actionÜrün detayı, modalda açılır
admin.customer.detail.actionMüşteri detayı, modalda açılır

Kurallar:

  • Bir sayfaya en çok 3 aksiyon eklenebilir; detay sayfasının link (.actions) ve modal (.action) aksiyonları birlikte sayılır. Aynı sayfada aynı path iki kez kullanılamaz.
  • Aksiyon için app_url dolu olmalı. Aksiyon tanımlıyken app_url ya da aksiyonun kullandığı izin kaldırılamaz.
  • Kurallara uymayan tanım 422 ile reddedilir; deploy hiçbir şey yazmaz.
  • Aksiyon yalnız uygulamanızı kurmuş mağazalarda görünür.

Uygulamanıza gelen adres

Tıklamada panel iframe'i app_url dizini ile path'i birleştirerek açar ve sorguya üç değer ekler:

https://uygulamaniz.com/app/invoice?target=admin.order.list.actions&ids=1042,1043,1044&session=<jwt>
ParametreDeğer
targetAksiyonun hangi sayfadan çağrıldığı. Aynı path'i birden çok hedefte kullanıyorsanız bununla ayırın
idsVirgülle ayrılmış kayıt id'leri. Sipariş numarası ya da ürün kodu değil, Admin API'deki id değeridir. Bir tıklamada en çok 250 kayıt gelir; detay sayfasından tek id gelir
sessionHer gömülü açılıştaki oturum token'ı. Önce bunu doğrulayın

ids yalnız kayıtları bulmanız için verilir; sır değildir ve tek başına veriye erişim sağlamaz. Kayıtları, oturum token'ındaki store_id için sakladığınız Admin API token'ıyla çekin:

Kayıt türüİstek
SiparişGET /order/orders?filter[id]=1042,1043 (Siparişler)
ÜrünGET /catalog/products?filter[id]=12,15 (Ürünler)
MüşteriGET /customer/customers?filter[id]=318,319 (Müşteriler)
invoice.ts
// GET /app/invoice — panel aksiyonunun açtığı sayfa
export async function invoicePage(request: Request) {
  const url = new URL(request.url)
  const session = await verifySession(url.searchParams.get("session") ?? "")

  if (url.searchParams.get("target") !== "admin.order.list.actions" && url.searchParams.get("target") !== "admin.order.detail.actions") {
    return new Response("Beklenmeyen hedef", { status: 400 })
  }

  const ids = (url.searchParams.get("ids") ?? "").split(",").filter((id) => /^\d+$/.test(id))
  const token = await adminTokenFor(session.store_id)   // kurulumda sakladığınız mağaza istemci bilgisiyle

  const response = await fetch(
    `https://${session.dest}/api/v3/admin/2026-01/order/orders?filter[id]=${ids.join(",")}&page[size]=250`,
    { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.api+json" } },
  )
  const { data: orders } = await response.json()

  return renderInvoiceForm(orders)
}
Aksiyonun yolu uygulamanızda gerçekten var olan bir sayfa olmalı. Yönlendiriciniz tanımadığı yolu ana sayfaya atarsa target ve ids kaybolur. Tek sayfalı bir uygulamada navigate.app ile yolu değiştirirken bu sayfada kalın ya da ids'i sorguda koruyun; aksi halde panel adresi yeni yola güncellenir ve sayfa yenilenince seçim gelmez.

Seçimi her zaman sunucuda, mağazanın token'ıyla yeniden okuyun. Adresteki id'lerin o mağazaya ait olduğunu varsaymayın: başka mağazanın id'si o mağazanın token'ıyla sorgulandığında boş döner.

Hedef .detail.action ile bitiyorsa aksiyon sayfadan ayrılmadan bir modal içinde açılır. Adres, target/ids/session sorgusu ve panel köprüsü tam sayfadakiyle aynıdır; farklar:

  • İşi bitince app.modal.close() modalı kapatır. app.modal.close({ refresh: true }) panelin sipariş, ürün ya da müşteri verisini yeniden yüklemesini ister (örneğin siparişe fatura yazdıysanız). Paket 0.5.0 ve sonrası.
  • navigate.app panel adresini değiştirmez; navigate.panel modalı kapatıp panelde o sayfaya gider.
  • Modal ekranın en çok %80'i kadar yüksektir; autoResize bu sınırın içinde çalışır.
tsoft.app.toml
[[extensions]]
target = "admin.order.detail.action"
label = "Kargo etiketi bas"
path = "/modal/label"
scope = "read_orders"

Sayfada kartlar

Uygulamanız sipariş, ürün ve müşteri detayında ve panel ana sayfasında kendi kartını gösterebilir. Kartın içeriğini uygulamanızın bir ucu JSON olarak döner; kartı panel kendi bileşenleriyle çizer. Panelde sizin kodunuz çalışmaz, kart panelin görünümünü bozamaz.

tsoft.app.toml
[[extensions]]
target = "admin.order.detail.block"
label = "Kargo durumu"                 # kartın başlığı
label_translations = { en = "Shipping" }
path = "/blocks/shipment"              # kartın JSON'unu dönen uç, app_url altında
scope = "read_orders"
targetNerede
admin.order.detail.blockSipariş detayı, sağ sütun
admin.product.detail.blockÜrün düzenleme, detay sekmesinin üstü
admin.customer.detail.blockMüşteri detayı, sağ sütun
admin.home.blockPanel ana sayfası, sağ sütun

Her hedefe uygulama başına 1 kart. Kartlar aksiyon sınırına sayılmaz.

Kartın ucu

Sayfa açılınca T-SOFT sunucusu (tarayıcı değil) ucunuzu çağırır:

GET https://uygulamaniz.com/app/blocks/shipment?target=admin.order.detail.block&id=1042
Authorization: Bearer <oturum token'ı>
  • Token oturum token'ıdır; doğrulayın, store_id ile mağazanın Admin API token'ını bulun, id'li kaydı kendiniz okuyun. Ana sayfa kartında id yoktur.
  • 2 saniye içinde, en çok 32 KB yanıt verin; yönlendirme izlenmez. Aşılırsa kart "yüklenemedi" olarak çizilir, sayfa beklemez.
  • Kart sayfa her açıldığında istenir; pahalı işi önbelleğe alın.

Kart JSON'u

{
  "version": 1,
  "body": [
    { "type": "t-key-value", "items": [
      { "label": "Kargo", "value": "Yurtiçi" },
      { "label": "Takip no", "value": "TR123456" }
    ]},
    { "type": "t-badge", "text": "Dağıtımda", "tone": "success" },
    { "type": "t-button", "label": "Etiket bas",
      "action": { "type": "modal", "path": "/modal/label" } }
  ]
}
ÖğeAlanlar
t-stackdirection (column/row), gap (sm/md), children
t-texttext (en çok 500), tone, weight (regular/bold)
t-headingtext
t-badgetext (en çok 40), tone
t-key-valueitems: [{ label, value }], 1–10 satır
t-bannertext, tone (info/warning/critical/success)
t-buttonlabel, variant (primary/secondary), action
t-linklabel, action
t-divider—

tone: default, subdued, success, critical, warning, info. Bir kart en çok 50 öğe ve 4 seviye iç içe taşır. Metinler düz metindir, HTML yorumlanmaz. Tanınmayan alan atılır; tanınmayan öğe ya da aşılan sınır kartın tamamını "yüklenemedi" yapar.

action üç biçimden biridir:

actionNe olur
{ "type": "modal", "path": "/modal/label" }Uygulamanızın o sayfası modalda açılır (?target=&ids= ile, modal aksiyonlar gibi). app.modal.close({ refresh: true }) kartı yeniler.
{ "type": "panel", "resource": "order", "id": "1042" }Panelde o sayfaya gider (orders, order, products, product, apps).
{ "type": "post", "path": "/blocks/shipment/refresh", "confirm": "Yenilensin mi?" }confirm varsa önce onay sorulur; T-SOFT sunucusu path'e oturum token'ıyla POST {target, id} atar. Yanıt yeni bir kart JSON'uysa kart onunla değişir, değilse yeniden istenir.

Sıradaki adım