feat(storefront): script tag toggle injection

Made-with: Cursor
This commit is contained in:
talatops 2026-04-02 15:15:17 +05:00
parent 8af711a6c9
commit ec600012d1
2 changed files with 166 additions and 3 deletions

View File

@ -137,11 +137,90 @@ export async function PATCH(
await prisma.store.update({
where: { id: storeId },
data: {
storefrontChatEnabled
}
data: { storefrontChatEnabled } as any
})
// Best-effort: keep a Shopify ScriptTag in sync with the checkbox.
// Even if ScriptTag operations fail (missing scopes), the widget still
// won’t render because /api/storefront-chat/config returns the flag.
try {
const appBaseUrl = (process.env.NEXT_PUBLIC_APP_URL || "").trim()
const apiVersion = "2024-01"
const injectorBase = appBaseUrl ? appBaseUrl.replace(/\/$/, "") : ""
const cleanDomain = (store.shopifyDomain || "")
.trim()
.replace(/^https?:\/\//, "")
.replace(/\/$/, "")
.trim()
const accessToken = store.shopifyAccessToken
if (injectorBase && cleanDomain && accessToken) {
const injectorSrc = `${injectorBase}/storefront-chat/widget-injector`
const listUrl = `https://${cleanDomain}/admin/api/${apiVersion}/script_tags.json?src=${encodeURIComponent(injectorSrc)}`
const commonHeaders = {
"X-Shopify-Access-Token": accessToken,
"Content-Type": "application/json"
}
const listRes = await fetch(listUrl, { headers: commonHeaders })
const listData = await listRes.json().catch(() => ({}))
const existing: Array<{ id: number }> = Array.isArray(listData?.script_tags)
? listData.script_tags
: []
if (storefrontChatEnabled) {
if (existing.length === 0) {
const createUrl = `https://${cleanDomain}/admin/api/${apiVersion}/script_tags.json`
const createBody = {
script_tag: {
event: "onload",
src: injectorSrc,
display_scope: "online_store",
cache: false
}
}
const createRes = await fetch(createUrl, {
method: "POST",
headers: commonHeaders,
body: JSON.stringify(createBody)
})
if (!createRes.ok) {
const errText = await createRes.text().catch(() => "")
console.error("[STORE_FRONT_CHAT_SCRIPTTAG_CREATE]", { storeId, status: createRes.status, errText })
}
}
} else {
for (const tag of existing) {
if (!tag?.id) continue
const deleteUrl = `https://${cleanDomain}/admin/api/${apiVersion}/script_tags/${tag.id}.json`
const delRes = await fetch(deleteUrl, {
method: "DELETE",
headers: { "X-Shopify-Access-Token": accessToken }
})
if (!delRes.ok) {
const errText = await delRes.text().catch(() => "")
console.error("[STORE_FRONT_CHAT_SCRIPTTAG_DELETE]", { storeId, tagId: tag.id, status: delRes.status, errText })
}
}
}
} else {
console.warn("[STORE_FRONT_CHAT_SCRIPTTAG_SKIP]", {
storeId,
hasInjectorBase: Boolean(appBaseUrl),
cleanDomain,
hasAccessToken: Boolean(accessToken)
})
}
} catch (scriptError) {
console.error("[STORE_FRONT_CHAT_SCRIPTTAG_ERROR]", scriptError)
}
return NextResponse.json({
success: true,
storefrontChatEnabled

View File

@ -0,0 +1,84 @@
/**
* Shopify ScriptTag loader (v1).
*
* Shopify injects this JS onto the ONLINE_STORE.
* The script then checks our backend config for the current store and, if enabled,
* mounts an iframe pointing to `/storefront-chat/widget?shop=...`.
*/
export async function GET() {
const js = `
(function () {
try {
var script = document.currentScript;
var baseUrl = script && script.src ? new URL(script.src).origin : window.location.origin;
var shop = window.location.hostname;
var containerId = "juno-storefront-chat-container";
var iframeId = "juno-storefront-chat-iframe";
function upsertIframe() {
var container = document.getElementById(containerId);
if (!container) {
container = document.createElement("div");
container.id = containerId;
container.style.position = "fixed";
container.style.bottom = "16px";
container.style.right = "16px";
container.style.zIndex = "2147483647";
container.style.width = "420px";
container.style.maxWidth = "calc(100vw - 32px)";
container.style.height = "640px";
container.style.maxHeight = "calc(100vh - 32px)";
container.style.borderRadius = "16px";
document.body.appendChild(container);
}
var existing = document.getElementById(iframeId);
if (existing) return;
var iframe = document.createElement("iframe");
iframe.id = iframeId;
iframe.title = "JUNO Storefront Chat";
iframe.style.border = "0";
iframe.style.width = "100%";
iframe.style.height = "100%";
iframe.style.borderRadius = "16px";
iframe.style.overflow = "hidden";
iframe.allow = "clipboard-write; fullscreen";
iframe.referrerPolicy = "no-referrer";
iframe.src = baseUrl + "/storefront-chat/widget?shop=" + encodeURIComponent(shop);
container.appendChild(iframe);
}
function removeWidget() {
var container = document.getElementById(containerId);
if (container && container.parentNode) container.parentNode.removeChild(container);
}
// Avoid multiple config calls if Shopify re-runs script.
if (window.__junoStorefrontChatConfigPromise) return;
window.__junoStorefrontChatConfigPromise = fetch(baseUrl + "/api/storefront-chat/config?shop=" + encodeURIComponent(shop))
.then(function (r) { return r.json().catch(function(){ return {}; }); })
.then(function (data) {
var enabled = !!(data && data.enabled);
if (enabled) upsertIframe();
else removeWidget();
})
.catch(function () {
// Fail closed: don't render widget if config check fails.
removeWidget();
});
} catch (e) {
// Fail closed: don't render widget.
}
})();
`.trimStart()
return new Response(js, {
headers: {
"Content-Type": "application/javascript; charset=utf-8",
"Cache-Control": "public, max-age=300"
}
})
}