resolve/close ZORUNLU ✓ CanlıBir hook'a iframe UI tanımladıysan (ör. table.close, packet.close), panel o iframe'i açıp BEKLER. Akışı devam ettirmek senin sayfanın sorumluluğundadır — sayfan App Bridge resolve/close çağırmazsa gate sonuçlanmaz, işlem iptal olur, kullanıcı tekrar denediğinde iframe yeniden açılır (sonsuz döngü gibi görünür). Bu bir bug değil — eksik resolve.
← Hook'lar · ilgili: Custom UI Sayfaları.
resolve(formData) (devam) veya close() (iptal) çağırana kadar sonuçlanmaz. "iframe sürekli açılıyor / işlem ilerlemiyor" = sayfan bu çağrıları yapmıyor demektir.resolve/close zorunluluğu yalnız bir hook'un UI'sı CUSTOM IFRAME ise vardır. Diğer tüm yüzeyler etkilenmez:
| UI yüzeyi | resolve/close? | Neden |
|---|---|---|
Hook gate iframe (table.close, packet.close — UI'sı iframe) | ✅ EVET | Panel açar + bekler; resolve(formData)/close ile sonuçlanmalı, yoksa tıkanır |
| Hook gate declarative form | ❌ Hayır | Form'u panel render eder, submit = resolve (sen kod yazmazsın) |
packet.status.update | ❌ Hayır | Sunucu-içi inline gate (panel iframe açmaz; webhook karar verir) |
Aksiyon butonu (ui:button) | ❌ Hayır | Senkron POST → actionUrl; iframe handshake yok |
Nav/ayar sayfası (ui:page) | ❌ Hayır | Normal iframe; menüden açılır, gate değil — resolve beklenmez |
Webhook event (*.created vb.) | ❌ Hayır | Async bildirim; UI yok |
resolve/close = bir hook'un UI'sı CUSTOM IFRAME ise. Yani yalnız before-action gate iframe'leri (hook + iframe UI). Event'ler, action butonları, normal nav/ayar sayfaları ve declarative-form hook'lar etkilenmez.1) Kullanıcı işlemi tetikler (ör. paketi Kapat) → panel GATE iframe'ini açar ve BEKLER (promise).
2) Sayfan App Bridge'den getContext ile bağlamı alır (hangi paket/masa: refId).
3) Kullanıcı sayfanda seçim yapar →
resolve(formData) → iframe kapanır; panel topladığın formData ile plugins/hook'u POST eder →
webhook'un (actionUrl) { decision:'allow'|'deny', message } döner →
allow ise işlem DEVAM eder, deny ise mesajın panelde gösterilir.
close() → gate { allow:false } → işlem İPTAL edilir (webhook çağrılmaz).resolve "kullanıcı etkileşimi bitti, veriler bunlar" demektir; nihai allow/deny kararını yine webhook'un verir (resolve'daki formData webhook'a aynen ulaşır). submit, resolve'un alias'ıdır.
resolve(formData) ile veri taşı) basit akışlar için yeterlidir. Kritik akışlarda Desen B (ÖNERİLEN) kullanın: veriyi iframe'inizden kendi backend'inize yazıp kararı webhook'ta kendi state'inizden verin (aşağıda).function bridgeCall(action, params) {
return new Promise((res) => {
const requestId = Math.random().toString(36).slice(2);
function onMsg(e) {
const d = e.data;
if (!d || d.type !== "restomenum-bridge-response" || d.requestId !== requestId) return;
window.removeEventListener("message", onMsg);
res(d.result);
}
window.addEventListener("message", onMsg);
window.parent.postMessage({ type: "restomenum-bridge", requestId, action, params }, "*");
});
}
// Sayfa açılınca bağlamı al (hangi kayıt?):
const ctx = await bridgeCall("getContext"); // → { serverId, pluginId, locale, refId } (refId = paket/masa id)
// Kullanıcı onayladı → işleme DEVAM ET (params = formData; webhook'una aynen gider):
await bridgeCall("resolve", { courierCalled: true }); // veya { decision: "allow" } gibi KENDİ alanların
// Kullanıcı vazgeçti → işlemi İPTAL ET:
await bridgeCall("close");| Action | İş |
|---|---|
| getContext | { serverId, pluginId, locale, refId } — refId = işlemin nesnesi (paket/masa id) |
| resolve (≡ submit) | Etkileşim bitti → params = formData, panel webhook'unu çağırır (allow/deny webhook'ta) |
| close | İşlemi iptal et (gate { allow:false }) |
| resize | { height: 480 } — iframe yüksekliği |
| toast | { message: "…" } — kullanıcıya kısa bilgi |
| getSessionToken | { token, tokenType:"Bearer", expiresIn:120 } — imzalı JWT (TTL 120 sn, fetch'ten hemen önce al; backend doğrulaması için — Desen B + Session Token) |
bridgeCall cevapları { success, data } zarfındadır — örneklerde ctx/tok = result.data varsayıldı; köprü helper'ınız zarfı açıyorsa birebir kullanın.
formData client'ta toplanır; imza onu panel→size yolda korur ama içeriğini garanti etmez. packet.close gate'i sunucu-içinde de çağrılır — o çağrıda formData olmayabilir. Desen B'de kararınız formData'ya değil kendi DB'nize dayandığı için hook nereden çağrılırsa çağrılsın tutarlı cevap verirsiniz. (Shopify App Bridge + session token deseniyle birebir aynı.)
customUiOrigin'inizde çalışır)// bridgeCall: yukarıdaki App Bridge köprüsü. Cevaplar { success, data } zarfındadır → ctx/tok = result.data.
const ctx = await bridgeCall("getContext");
// → { serverId, pluginId, locale, refId } (refId = gate hedefi: paket/masa doc id)
// Kullanıcı sayfanızda gerekli işlemi yapar (mutabakat onayı, kod girişi…) → fetch'ten HEMEN ÖNCE:
const tok = await bridgeCall("getSessionToken");
// → { token, tokenType: "Bearer", expiresIn: 120 } — TTL 120 sn, önceden alıp bekletmeyin
await fetch("https://api.SIZIN-DOMAIN.com/gate-approvals", {
method: "POST",
headers: { "Authorization": "Bearer " + tok.token, "Content-Type": "application/json" },
body: JSON.stringify({ refId: ctx.refId, approved: true /* + kendi alanlarınız */ }),
});
resolve(); // ← veri TAŞIMIYOR; yalnız "etkileşim bitti" sinyali. close() = iptal.Session token, OAuth exchange'de aldığınız webhookSecret ile imzalı bir JWT'dir (HS256) — Restomenum'a sormadan lokal doğrularsınız.
const crypto = require("crypto");
function verifySessionToken(token, webhookSecret, myPluginId) {
const parts = String(token || "").split(".");
if (parts.length !== 3) return null;
const [h, p, s] = parts;
const expected = crypto.createHmac("sha256", webhookSecret).update(h + "." + p).digest("base64url");
const a = Buffer.from(s), b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;
const payload = JSON.parse(Buffer.from(p, "base64url").toString("utf8"));
if (payload.iss !== "restomenum") return null;
if (payload.aud !== myPluginId) return null; // başka eklentinin token'ı işe yaramaz
if (Math.floor(Date.now() / 1000) > payload.exp) return null;
return payload; // { tenantId, sub: <userId>, role: "manager"|"staff", pluginId, iat, exp }
}
// POST /gate-approvals
app.post("/gate-approvals", (req, res) => {
const claims = verifySessionToken(
req.headers.authorization?.replace(/^Bearer /, ""), WEBHOOK_SECRET, MY_PLUGIN_ID);
if (!claims) return res.status(401).end();
// State: tenant + refId anahtarıyla, KISA TTL ile (ör. 10 dk) sakla
db.set(`gate:${claims.tenantId}:${req.body.refId}`,
{ approved: true, by: claims.sub, role: claims.role, at: Date.now() },
{ ttlSec: 600 });
res.json({ ok: true });
});refId'yi client gövdesinden alıyorsunuz ama anahtarı claims.tenantId ile birleştiriyorsunuz — başka tenant'ın kaydını yazamaz. İsterseniz paketi packets/get?packetId= ile doğrulayın. TTL şart: "onay verildi" kaydı sonsuza yaşarsa saatler sonra alakasız bir kapanış da allow alır.// actionUrl/webhookUrl handler'ınızda (imza doğrulaması sonrası):
if (body.type === "hook" && (body.event === "packet.close" || body.event === "table.close")) {
const key = `gate:${body.tenantId}:${body.target.id}`;
const state = await db.get(key);
if (state && state.approved) return res.json({ decision: "allow" });
return res.json({ decision: "deny", message: "Önce eklenti panelindeki doğrulamayı tamamlayın." });
}refId için hook birden çok kez gelebilir (panel + sunucu-içi gate, retry). State okuma idempotenttir; kaydı allow sonrası silmeyin (kısa TTL yetsin).timeoutMs (1–10 sn) — DB okuması hızlı olmalı; aşımda failMode uygulanır.Personel "Kapat"a basar
→ panel gate iframe'inizi açar
→ iframe: getContext (refId) → kullanıcı işlemi → getSessionToken
→ iframe → SİZİN backend: POST /gate-approvals (Bearer sessionToken, refId) ← veri SİZİN kanaldan
→ iframe: resolve() ← sadece sinyal
→ panel/sunucu hook'unuzu çağırır: { event, target:{id: refId}, tenantId, ... }
→ backend'iniz: gate:{tenantId}:{refId} var mı? → { decision: allow/deny }
→ allow ise işlem yapılır; deny ise mesajınız personele gösterilirgetContext cevabında refId döner, getSessionToken her aktif kurulumda kayıtlıdır (rate-limit'li). Kozmetik nüans: getContext tenant kimliğini serverId adıyla döndürür (diğer yüzeylerde tenantId) — işlevsel etkisi yok, ikisini de kabul edecek şekilde okuyun.resolve/close bekler. Menüden (ui:nav / ui:page) açtığın normal ayar/panel sayfaların gate DEĞİLDİR — orada resolve gerekmez.resolve/close çağırmıyor demektir.customUiOrigin'ine postalanır; gelen mesajda origin + kaynak pencere doğrulanır. Sayfanın kayıtlı domain'inde kalması yeterli (iframe Güvenliği).resolve formData'sına decision gibi alanlar koyup webhook'unda ona göre allow/deny dönebilirsin; ya da webhook kendi kuralını uygular. Panel formData'yı yalnız taşır.| Belirti | Sebep | Çözüm |
|---|---|---|
| Seçim sonrası akış devam etmiyor, iframe tekrar açılıyor | Sayfa resolve/close çağırmıyor | Onayda resolve(formData), iptalde close() çağır |
| Webhook hiç çağrılmıyor | close çağrılmış (veya kullanıcı ✕ ile kapatmış) → iptal | Devam için resolve kullan (close yalnız iptal) |
deny döndün ama mesaj görünmüyor | webhook message alanı boş | { decision:'deny', message:'…' } döndür |