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
| Claim | Kontrol |
|---|---|
iss | https://api.tsoftapps.com olmalı |
aud | Uygulamanızın prefix'i olmalı |
exp, nbf | Süre içinde olmalı (birkaç saniye saat kayması payı bırakın) |
store_id | Mağaza kimliği; sakladığınız istemci bilgisiyle eşleyin |
dest | Mağaza alan adı |
sub | Uygulamayı açan panel kullanıcısı |
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
}
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>
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.
pathuygulamanızın kendi adresi altında,/ile başlayan bir yol olmalı. Mutlak ya da başka siteye giden adres vejavascript:reddedilir. Tek bir geçersiz bağlantı bütün listeyiinvalid_payloadile 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.
/gonderilerbağlantısı/gonderiler/42yolunda da etkin sayılır. Uygulama içinde gezindikçenavigate.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.
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ı):
[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.
| Alan | Açıklama |
|---|---|
target | Aksiyonun görüneceği yer (aşağıdaki tablo) |
label | Menü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 |
path | Uygulamanı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ı |
target | Nerede |
|---|---|
admin.order.list.actions | Sipariş listesi, seçili siparişler |
admin.order.detail.actions | Sipariş 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.actions | Müşteri listesi, seçili müşteriler |
admin.customer.detail.actions | Müşteri detayı, o müşteri |
admin.order.detail.action | Sipariş detayı, modalda açılır |
admin.product.detail.action | Ürün detayı, modalda açılır |
admin.customer.detail.action | Müş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ıpathiki kez kullanılamaz. - Aksiyon için
app_urldolu olmalı. Aksiyon tanımlıykenapp_urlya da aksiyonun kullandığı izin kaldırılamaz. - Kurallara uymayan tanım
422ile 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>
| Parametre | Değer |
|---|---|
target | Aksiyonun hangi sayfadan çağrıldığı. Aynı path'i birden çok hedefte kullanıyorsanız bununla ayırın |
ids | Virgü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 |
session | Her 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ün | GET /catalog/products?filter[id]=12,15 (Ürünler) |
| Müşteri | GET /customer/customers?filter[id]=318,319 (Müşteriler) |
// 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)
}
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.
Modal aksiyonlar
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.apppanel adresini değiştirmez;navigate.panelmodalı kapatıp panelde o sayfaya gider.- Modal ekranın en çok %80'i kadar yüksektir;
autoResizebu sınırın içinde çalışır.
[[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.
[[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"
target | Nerede |
|---|---|
admin.order.detail.block | Sipariş detayı, sağ sütun |
admin.product.detail.block | Ürün düzenleme, detay sekmesinin üstü |
admin.customer.detail.block | Müşteri detayı, sağ sütun |
admin.home.block | Panel 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_idile mağazanın Admin API token'ını bulun,id'li kaydı kendiniz okuyun. Ana sayfa kartındaidyoktur. - 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" } }
]
}
| Öğe | Alanlar |
|---|---|
t-stack | direction (column/row), gap (sm/md), children |
t-text | text (en çok 500), tone, weight (regular/bold) |
t-heading | text |
t-badge | text (en çok 40), tone |
t-key-value | items: [{ label, value }], 1–10 satır |
t-banner | text, tone (info/warning/critical/success) |
t-button | label, variant (primary/secondary), action |
t-link | label, 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:
action | Ne 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. |