feat(storefront): script tag toggle injection
Made-with: Cursor
This commit is contained in:
parent
8af711a6c9
commit
ec600012d1
@ -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
|
||||
|
||||
84
src/app/storefront-chat/widget-injector/route.ts
Normal file
84
src/app/storefront-chat/widget-injector/route.ts
Normal 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"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user