Instantly share code, notes, and snippets.
Last active
May 8, 2026 10:27
-
Star
0
(0)
You must be signed in to star a gist -
Fork
1
(1)
You must be signed in to fork a gist
-
-
Save dvygolov/9bfad72e5046d44cb65c00f10b4d1d7d to your computer and use it in GitHub Desktop.
This script helps to share meta pixel to an ad account in a business manager when you have problems with sharing using standart methods
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function bmPixelShareManagerApp() { | |
| const APP_ID = "bm-pixel-share-ui"; | |
| const STYLE_ID = `${APP_ID}-style`; | |
| const GRAPH_VERSION = "v22.0"; | |
| const INTERNAL_GRAPHQL_ENDPOINT = "/api/graphql/?_callFlowletID=0&_triggerFlowletID=1&qpl_active_e2e_trace_ids="; | |
| const INTERNAL_GRAPHQL_CALLER = "RelayModern"; | |
| const INTERNAL_PIXEL_LIST_QUERY = { | |
| friendlyName: "BusinessCometBizSuiteSettingsEventsDatasetRootQuery", | |
| docId: "27075127438747082", | |
| }; | |
| const INTERNAL_CONNECTED_ASSETS_QUERY = { | |
| friendlyName: "BizKitSettingsAssetToAssetConnectionListContainerQuery", | |
| docId: "25989701040671987", | |
| }; | |
| const INTERNAL_REMOVE_CONNECTED_ASSET_MUTATION = { | |
| friendlyName: "BizKitSettingsRemoveAssetToAssetConnectionMutation", | |
| docId: "23922031614068524", | |
| }; | |
| const DEFAULT_AD_ACCOUNT_STATUSES = [ | |
| "ACTIVE", | |
| "DISABLED", | |
| "IN_GRACE_PERIOD", | |
| "PENDING_CLOSURE", | |
| "PENDING_RISK_REVIEW", | |
| "PENDING_SETTLEMENT", | |
| "UNSETTLED", | |
| ]; | |
| const BUSINESS_URL = (businessId) => | |
| `https://business.facebook.com/latest/settings/ad_accounts/?business_id=${encodeURIComponent( | |
| businessId | |
| )}`; | |
| const previousRoot = document.getElementById(APP_ID); | |
| if (previousRoot) { | |
| previousRoot.remove(); | |
| } | |
| const previousStyle = document.getElementById(STYLE_ID); | |
| if (previousStyle) { | |
| previousStyle.remove(); | |
| } | |
| const state = { | |
| accessToken: "", | |
| businessId: "", | |
| businesses: [], | |
| pixels: [], | |
| accounts: [], | |
| selectedPixelId: "", | |
| selectedAccountId: "", | |
| connectedAssets: [], | |
| loadingBusinesses: false, | |
| loadingContext: false, | |
| loadingConnectedAssets: false, | |
| sharing: false, | |
| removingAssetIds: [], | |
| status: { type: "info", text: "Initializing..." }, | |
| lastShare: null, | |
| pixelMappingError: "", | |
| connectedAssetsError: "", | |
| }; | |
| let asyncGraphqlTemplatePromise = null; | |
| let connectedAssetsLoadToken = 0; | |
| const style = document.createElement("style"); | |
| style.id = STYLE_ID; | |
| style.textContent = ` | |
| #${APP_ID} { | |
| position: fixed; | |
| inset: 0; | |
| z-index: 2147483647; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 24px; | |
| background: | |
| radial-gradient(circle at top left, rgba(20, 157, 221, 0.18), transparent 34%), | |
| radial-gradient(circle at bottom right, rgba(30, 92, 184, 0.18), transparent 42%), | |
| rgba(10, 18, 35, 0.48); | |
| backdrop-filter: blur(12px); | |
| color: #10203a; | |
| font-family: "Segoe UI", "Helvetica Neue", sans-serif; | |
| } | |
| #${APP_ID} * { | |
| box-sizing: border-box; | |
| } | |
| #${APP_ID} .bmps-card { | |
| width: min(560px, 100%); | |
| max-height: min(88vh, 860px); | |
| overflow: auto; | |
| border-radius: 28px; | |
| padding: 28px; | |
| background: | |
| linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(245, 248, 255, 0.97)); | |
| box-shadow: | |
| 0 30px 80px rgba(10, 22, 48, 0.28), | |
| inset 0 1px 0 rgba(255, 255, 255, 0.85); | |
| border: 1px solid rgba(116, 146, 199, 0.24); | |
| } | |
| #${APP_ID} .bmps-header { | |
| display: flex; | |
| gap: 16px; | |
| align-items: flex-start; | |
| justify-content: space-between; | |
| margin-bottom: 20px; | |
| } | |
| #${APP_ID} .bmps-title { | |
| margin: 0; | |
| font-size: 24px; | |
| line-height: 1.15; | |
| font-weight: 700; | |
| letter-spacing: -0.03em; | |
| color: #11264a; | |
| } | |
| #${APP_ID} .bmps-subtitle { | |
| margin: 8px 0 0; | |
| font-size: 13px; | |
| line-height: 1.5; | |
| color: #5d708f; | |
| } | |
| #${APP_ID} .bmps-close { | |
| width: 38px; | |
| height: 38px; | |
| border: 0; | |
| border-radius: 999px; | |
| background: rgba(17, 38, 74, 0.07); | |
| color: #11264a; | |
| font-size: 20px; | |
| cursor: pointer; | |
| transition: transform 120ms ease, background 120ms ease; | |
| } | |
| #${APP_ID} .bmps-close:hover { | |
| transform: scale(1.04); | |
| background: rgba(17, 38, 74, 0.12); | |
| } | |
| #${APP_ID} .bmps-meta { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin-bottom: 18px; | |
| } | |
| #${APP_ID} .bmps-chip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 9px 12px; | |
| border-radius: 999px; | |
| background: rgba(21, 72, 151, 0.07); | |
| color: #21497d; | |
| font-size: 12px; | |
| font-weight: 600; | |
| } | |
| #${APP_ID} .bmps-panel { | |
| padding: 18px; | |
| border-radius: 22px; | |
| background: linear-gradient(180deg, rgba(245, 249, 255, 0.92), rgba(238, 244, 252, 0.92)); | |
| border: 1px solid rgba(142, 167, 214, 0.24); | |
| margin-bottom: 16px; | |
| } | |
| #${APP_ID} .bmps-panel-title { | |
| margin: 0 0 6px; | |
| font-size: 15px; | |
| font-weight: 700; | |
| color: #17345e; | |
| } | |
| #${APP_ID} .bmps-panel-text { | |
| margin: 0; | |
| font-size: 13px; | |
| line-height: 1.55; | |
| color: #5d708f; | |
| } | |
| #${APP_ID} .bmps-grid { | |
| display: grid; | |
| gap: 14px; | |
| } | |
| #${APP_ID} .bmps-field { | |
| display: grid; | |
| gap: 8px; | |
| } | |
| #${APP_ID} .bmps-select-shell { | |
| position: relative; | |
| } | |
| #${APP_ID} .bmps-select-shell::after { | |
| content: ""; | |
| position: absolute; | |
| top: 50%; | |
| right: 18px; | |
| width: 10px; | |
| height: 10px; | |
| border-right: 2px solid #6d84ad; | |
| border-bottom: 2px solid #6d84ad; | |
| transform: translateY(-65%) rotate(45deg); | |
| pointer-events: none; | |
| transition: transform 120ms ease, border-color 120ms ease; | |
| } | |
| #${APP_ID} .bmps-select-shell:focus-within::after { | |
| border-color: #1d67d6; | |
| transform: translateY(-35%) rotate(225deg); | |
| } | |
| #${APP_ID} .bmps-label { | |
| font-size: 12px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: #5470a1; | |
| } | |
| #${APP_ID} .bmps-select, | |
| #${APP_ID} .bmps-button { | |
| width: 100%; | |
| min-height: 50px; | |
| border-radius: 16px; | |
| border: 1px solid rgba(100, 130, 188, 0.28); | |
| font-size: 14px; | |
| } | |
| #${APP_ID} .bmps-select { | |
| appearance: none; | |
| padding: 0 52px 0 18px; | |
| background: | |
| linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 248, 255, 0.98)); | |
| color: #213756; | |
| outline: none; | |
| font-weight: 500; | |
| cursor: pointer; | |
| box-shadow: | |
| inset 0 1px 0 rgba(255, 255, 255, 0.92), | |
| 0 8px 20px rgba(41, 88, 166, 0.08); | |
| transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease; | |
| } | |
| #${APP_ID} .bmps-select:focus { | |
| border-color: rgba(29, 103, 214, 0.6); | |
| box-shadow: | |
| 0 0 0 4px rgba(29, 103, 214, 0.12), | |
| 0 10px 22px rgba(41, 88, 166, 0.12); | |
| } | |
| #${APP_ID} .bmps-select:hover:not(:disabled) { | |
| background: | |
| linear-gradient(180deg, rgba(255, 255, 255, 1), rgba(240, 246, 255, 1)); | |
| } | |
| #${APP_ID} .bmps-select:disabled { | |
| cursor: wait; | |
| } | |
| #${APP_ID} .bmps-actions { | |
| display: flex; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| margin-top: 4px; | |
| } | |
| #${APP_ID} .bmps-button { | |
| border: 0; | |
| cursor: pointer; | |
| padding: 0 18px; | |
| font-weight: 700; | |
| transition: transform 120ms ease, opacity 120ms ease, filter 120ms ease; | |
| } | |
| #${APP_ID} .bmps-button:hover:not(:disabled) { | |
| transform: translateY(-1px); | |
| filter: saturate(1.05); | |
| } | |
| #${APP_ID} .bmps-button:disabled { | |
| opacity: 0.55; | |
| cursor: wait; | |
| } | |
| #${APP_ID} .bmps-button-primary { | |
| background: linear-gradient(135deg, #1677ff, #0f56c9); | |
| color: #fff; | |
| box-shadow: 0 12px 28px rgba(15, 86, 201, 0.24); | |
| } | |
| #${APP_ID} .bmps-button-secondary { | |
| background: rgba(15, 86, 201, 0.08); | |
| color: #17427a; | |
| } | |
| #${APP_ID} .bmps-status { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-start; | |
| padding: 14px 16px; | |
| border-radius: 18px; | |
| font-size: 13px; | |
| line-height: 1.5; | |
| margin-top: 16px; | |
| border: 1px solid transparent; | |
| } | |
| #${APP_ID} .bmps-status-info { | |
| background: rgba(17, 119, 255, 0.08); | |
| color: #134589; | |
| border-color: rgba(17, 119, 255, 0.12); | |
| } | |
| #${APP_ID} .bmps-status-success { | |
| background: rgba(28, 156, 98, 0.1); | |
| color: #0d6a3f; | |
| border-color: rgba(28, 156, 98, 0.16); | |
| } | |
| #${APP_ID} .bmps-status-error { | |
| background: rgba(211, 60, 78, 0.1); | |
| color: #9c2438; | |
| border-color: rgba(211, 60, 78, 0.16); | |
| } | |
| #${APP_ID} .bmps-list { | |
| display: grid; | |
| gap: 10px; | |
| margin-top: 14px; | |
| } | |
| #${APP_ID} .bmps-business { | |
| display: grid; | |
| grid-template-columns: 1fr auto; | |
| gap: 12px; | |
| align-items: center; | |
| padding: 14px 16px; | |
| border-radius: 18px; | |
| background: rgba(255, 255, 255, 0.78); | |
| border: 1px solid rgba(140, 165, 208, 0.22); | |
| } | |
| #${APP_ID} .bmps-business-name { | |
| margin: 0 0 3px; | |
| font-size: 14px; | |
| font-weight: 700; | |
| color: #123258; | |
| } | |
| #${APP_ID} .bmps-business-id { | |
| margin: 0; | |
| font-size: 12px; | |
| color: #6480ab; | |
| } | |
| #${APP_ID} .bmps-connected-panel { | |
| padding: 16px 18px; | |
| border-radius: 20px; | |
| background: rgba(246, 250, 255, 0.9); | |
| border: 1px solid rgba(142, 167, 214, 0.22); | |
| } | |
| #${APP_ID} .bmps-connected-head { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 12px; | |
| align-items: baseline; | |
| margin-bottom: 12px; | |
| } | |
| #${APP_ID} .bmps-connected-count { | |
| font-size: 12px; | |
| font-weight: 700; | |
| color: #5875a7; | |
| } | |
| #${APP_ID} .bmps-connected-list { | |
| display: grid; | |
| gap: 8px; | |
| } | |
| #${APP_ID} .bmps-connected-item { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| padding: 11px 13px; | |
| border-radius: 14px; | |
| background: rgba(255, 255, 255, 0.86); | |
| border: 1px solid rgba(124, 151, 202, 0.18); | |
| color: #1f385b; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| } | |
| #${APP_ID} .bmps-connected-main { | |
| min-width: 0; | |
| flex: 1; | |
| } | |
| #${APP_ID} .bmps-connected-actions { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| flex-shrink: 0; | |
| } | |
| #${APP_ID} .bmps-connected-item--selected { | |
| border-color: rgba(24, 112, 231, 0.36); | |
| box-shadow: 0 0 0 3px rgba(24, 112, 231, 0.08); | |
| } | |
| #${APP_ID} .bmps-connected-badge { | |
| padding: 4px 8px; | |
| border-radius: 999px; | |
| background: rgba(24, 112, 231, 0.1); | |
| color: #1655ab; | |
| font-size: 11px; | |
| font-weight: 700; | |
| white-space: nowrap; | |
| } | |
| #${APP_ID} .bmps-connected-remove { | |
| min-width: 84px; | |
| min-height: 34px; | |
| padding: 0 12px; | |
| border: 1px solid rgba(203, 74, 95, 0.22); | |
| border-radius: 999px; | |
| background: rgba(203, 74, 95, 0.08); | |
| color: #a12c3f; | |
| font-size: 12px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: background 120ms ease, border-color 120ms ease, color 120ms ease, opacity 120ms ease; | |
| } | |
| #${APP_ID} .bmps-connected-remove:hover:not(:disabled) { | |
| background: rgba(203, 74, 95, 0.12); | |
| border-color: rgba(203, 74, 95, 0.34); | |
| } | |
| #${APP_ID} .bmps-connected-remove:disabled { | |
| opacity: 0.58; | |
| cursor: wait; | |
| } | |
| #${APP_ID} .bmps-inline-note { | |
| margin: 4px 0 0; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| color: #5f7393; | |
| } | |
| #${APP_ID} .bmps-inline-note-error { | |
| color: #9c2438; | |
| } | |
| #${APP_ID} .bmps-inline-note-success { | |
| color: #0d6a3f; | |
| } | |
| #${APP_ID} .bmps-footer-note { | |
| margin-top: 14px; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| color: #687ca1; | |
| } | |
| #${APP_ID} .bmps-bookmark-link { | |
| display: block; | |
| margin-top: 10px; | |
| font-size: 12px; | |
| line-height: 1.4; | |
| color: #215ec7; | |
| text-decoration: underline; | |
| text-align: center; | |
| cursor: pointer; | |
| } | |
| @media (max-width: 640px) { | |
| #${APP_ID} { | |
| padding: 14px; | |
| } | |
| #${APP_ID} .bmps-card { | |
| padding: 18px; | |
| border-radius: 22px; | |
| } | |
| #${APP_ID} .bmps-business { | |
| grid-template-columns: 1fr; | |
| } | |
| #${APP_ID} .bmps-connected-head { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| #${APP_ID} .bmps-connected-item { | |
| align-items: flex-start; | |
| flex-direction: column; | |
| } | |
| #${APP_ID} .bmps-connected-actions { | |
| width: 100%; | |
| justify-content: space-between; | |
| } | |
| } | |
| `; | |
| document.documentElement.appendChild(style); | |
| const root = document.createElement("div"); | |
| root.id = APP_ID; | |
| document.body.appendChild(root); | |
| const escapeHtml = (value) => | |
| String(value ?? "") | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """) | |
| .replaceAll("'", "'"); | |
| const setStatus = (type, text) => { | |
| state.status = { type, text }; | |
| render(); | |
| }; | |
| const closeOverlay = () => { | |
| root.remove(); | |
| style.remove(); | |
| }; | |
| const copyScriptAsBase64Bookmarklet = () => { | |
| try { | |
| const scriptContent = `${bmPixelShareManagerApp.toString()} | |
| window.bmPixelShareManagerApp = bmPixelShareManagerApp; | |
| bmPixelShareManagerApp();`; | |
| const base64Content = btoa(unescape(encodeURIComponent(scriptContent))); | |
| const bookmarkletCode = `javascript:(async()=>{const code=decodeURIComponent(escape(atob("${base64Content}")));const url=URL.createObjectURL(new Blob([code],{type:"text/javascript"}));try{await import(url);}finally{setTimeout(()=>URL.revokeObjectURL(url),15000)}})();`; | |
| const fallbackCopy = () => { | |
| const textArea = document.createElement("textarea"); | |
| textArea.value = bookmarkletCode; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand("copy"); | |
| document.body.removeChild(textArea); | |
| }; | |
| if (navigator.clipboard?.writeText) { | |
| navigator.clipboard | |
| .writeText(bookmarkletCode) | |
| .then(() => { | |
| setStatus("success", "Bookmarklet copied to clipboard."); | |
| }) | |
| .catch(() => { | |
| fallbackCopy(); | |
| setStatus("success", "Bookmarklet copied to clipboard."); | |
| }); | |
| return; | |
| } | |
| fallbackCopy(); | |
| setStatus("success", "Bookmarklet copied to clipboard."); | |
| } catch (error) { | |
| setStatus("error", `Could not create bookmarklet: ${error.message}`); | |
| } | |
| }; | |
| const getCurrentBusinessId = () => { | |
| try { | |
| const url = new URL(window.location.href); | |
| const directMatch = url.searchParams.get("business_id"); | |
| if (directMatch) { | |
| return directMatch; | |
| } | |
| } catch {} | |
| const candidates = [window.location.search, window.location.hash.replace(/^#/, "?"), window.location.href]; | |
| for (const candidate of candidates) { | |
| const match = String(candidate).match(/[?&#]business_id=(\d+)/); | |
| if (match?.[1]) { | |
| return match[1]; | |
| } | |
| } | |
| return ""; | |
| }; | |
| const getModule = (name) => { | |
| try { | |
| return typeof globalThis.require === "function" ? globalThis.require(name) : null; | |
| } catch { | |
| return null; | |
| } | |
| }; | |
| const getCurrentUserId = () => { | |
| const moduleUser = getModule("CurrentUserInitialData"); | |
| const runtimeCandidates = [ | |
| moduleUser?.USER_ID, | |
| moduleUser?.ACCOUNT_ID, | |
| globalThis.CurrentUserInitialData?.USER_ID, | |
| globalThis.CurrentUserInitialData?.ACCOUNT_ID, | |
| globalThis.Env?.user, | |
| globalThis.__user, | |
| ]; | |
| for (const candidate of runtimeCandidates) { | |
| if (typeof candidate === "string" && candidate.trim()) { | |
| return candidate.trim(); | |
| } | |
| } | |
| const html = document.documentElement.innerHTML; | |
| return ( | |
| html.match(/"USER_ID":"(\d+)"/)?.[1] || | |
| html.match(/"ACCOUNT_ID":"(\d+)"/)?.[1] || | |
| html.match(/"actorID":"(\d+)"/)?.[1] || | |
| "" | |
| ); | |
| }; | |
| const getLsdToken = () => { | |
| const moduleLsd = getModule("LSD"); | |
| const runtimeCandidates = [ | |
| moduleLsd?.token, | |
| globalThis.LSD?.token, | |
| document.querySelector('input[name="lsd"]')?.value, | |
| ]; | |
| for (const candidate of runtimeCandidates) { | |
| if (typeof candidate === "string" && candidate.trim()) { | |
| return candidate.trim(); | |
| } | |
| } | |
| const html = document.documentElement.innerHTML; | |
| return ( | |
| html.match(/\["LSD",\[],\{"token":"([^"]+)"/)?.[1] || | |
| html.match(/name="lsd" value="([^"]+)"/)?.[1] || | |
| "" | |
| ); | |
| }; | |
| const extractAccessTokenFromPage = () => { | |
| const runtimeCandidates = [ | |
| globalThis.__accessToken, | |
| globalThis.accessToken, | |
| globalThis.apiAccessToken, | |
| globalThis.__ACCOUNTS_CENTER_ACCESS_TOKEN__, | |
| ]; | |
| for (const candidate of runtimeCandidates) { | |
| if (typeof candidate === "string" && candidate.trim()) { | |
| return candidate.trim(); | |
| } | |
| } | |
| const html = document.documentElement.innerHTML; | |
| const regexes = [ | |
| /(?:apiAccessToken|accessToken)":"(EAAG[^"]+)/, | |
| /(?:apiAccessToken|accessToken)\\":\\"(EAAG[^\\"]+)/, | |
| /"accessToken":"(EAAG[^"]+)/, | |
| /"accessToken"\\s*:\\s*"(EAAG[^"]+)/, | |
| ]; | |
| for (const regex of regexes) { | |
| const match = html.match(regex); | |
| if (match?.[1]) { | |
| return match[1].replaceAll("\\/", "/"); | |
| } | |
| } | |
| throw new Error("Could not find an access token on the current page."); | |
| }; | |
| const extractAccessToken = (businessId) => { | |
| const runtimeToken = globalThis.__accessToken; | |
| if (typeof runtimeToken === "string" && runtimeToken.trim()) { | |
| return runtimeToken.trim(); | |
| } | |
| if (!businessId) { | |
| throw new Error("Outside Business Manager, __accessToken was not found."); | |
| } | |
| return extractAccessTokenFromPage(); | |
| }; | |
| const uniqueBy = (items, keyGetter) => { | |
| const map = new Map(); | |
| for (const item of items) { | |
| const key = keyGetter(item); | |
| if (!key || map.has(key)) { | |
| continue; | |
| } | |
| map.set(key, item); | |
| } | |
| return [...map.values()]; | |
| }; | |
| const ensureAccessTokenInUrl = (value) => { | |
| const url = new URL(value, `https://graph.facebook.com/${GRAPH_VERSION}/`); | |
| if (!url.searchParams.get("access_token")) { | |
| url.searchParams.set("access_token", state.accessToken); | |
| } | |
| return url.toString(); | |
| }; | |
| const graphFetch = async (pathOrUrl, options = {}) => { | |
| const method = options.method || "GET"; | |
| const isFullUrl = /^https?:\/\//i.test(pathOrUrl); | |
| const url = isFullUrl | |
| ? new URL(ensureAccessTokenInUrl(pathOrUrl)) | |
| : new URL( | |
| `https://graph.facebook.com/${GRAPH_VERSION}/${String(pathOrUrl).replace(/^\/+/, "")}` | |
| ); | |
| if (!isFullUrl) { | |
| const params = options.params || {}; | |
| Object.entries(params).forEach(([key, value]) => { | |
| if (value !== undefined && value !== null && value !== "") { | |
| url.searchParams.set(key, String(value)); | |
| } | |
| }); | |
| url.searchParams.set("access_token", state.accessToken); | |
| } | |
| const fetchOptions = { | |
| method, | |
| credentials: "include", | |
| headers: { | |
| "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", | |
| }, | |
| }; | |
| if (options.body) { | |
| const body = new URLSearchParams(); | |
| Object.entries(options.body).forEach(([key, value]) => { | |
| if (value !== undefined && value !== null) { | |
| body.append(key, String(value)); | |
| } | |
| }); | |
| fetchOptions.body = body; | |
| } | |
| const response = await fetch(url.toString(), fetchOptions); | |
| const json = await response.json(); | |
| if (!response.ok || json?.error) { | |
| throw new Error(json?.error?.message || `Graph API error (${response.status})`); | |
| } | |
| return json; | |
| }; | |
| const fetchAllPages = async (path, params = {}) => { | |
| const rows = []; | |
| let next = path; | |
| let nextParams = params; | |
| while (next) { | |
| const payload = await graphFetch(next, { params: nextParams }); | |
| rows.push(...(payload?.data || [])); | |
| next = payload?.paging?.next || ""; | |
| nextParams = {}; | |
| } | |
| return rows; | |
| }; | |
| const captureAsyncGraphqlTemplate = async (businessId) => { | |
| if (asyncGraphqlTemplatePromise) { | |
| return asyncGraphqlTemplatePromise; | |
| } | |
| asyncGraphqlTemplatePromise = new Promise((resolve, reject) => { | |
| if (typeof globalThis.AsyncRequest !== "function") { | |
| reject(new Error("AsyncRequest is not available on this page.")); | |
| return; | |
| } | |
| const originalOpen = XMLHttpRequest.prototype.open; | |
| const originalSend = XMLHttpRequest.prototype.send; | |
| const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; | |
| const capturedHeaders = {}; | |
| let finished = false; | |
| const cleanup = () => { | |
| XMLHttpRequest.prototype.open = originalOpen; | |
| XMLHttpRequest.prototype.send = originalSend; | |
| XMLHttpRequest.prototype.setRequestHeader = originalSetRequestHeader; | |
| }; | |
| const finish = (handler, value) => { | |
| if (finished) { | |
| return; | |
| } | |
| finished = true; | |
| cleanup(); | |
| handler(value); | |
| }; | |
| XMLHttpRequest.prototype.open = function patchedOpen(method, url) { | |
| this.__bmpsCaptureUrl = url; | |
| return originalOpen.apply(this, arguments); | |
| }; | |
| XMLHttpRequest.prototype.setRequestHeader = function patchedSetRequestHeader(name, value) { | |
| capturedHeaders[name] = value; | |
| return originalSetRequestHeader.apply(this, arguments); | |
| }; | |
| XMLHttpRequest.prototype.send = function patchedSend(body) { | |
| if (!finished && String(this.__bmpsCaptureUrl || "").includes("/api/graphql/")) { | |
| finish(resolve, { | |
| url: String(this.__bmpsCaptureUrl || ""), | |
| body: String(body || ""), | |
| headers: { ...capturedHeaders }, | |
| }); | |
| return; | |
| } | |
| return originalSend.apply(this, arguments); | |
| }; | |
| try { | |
| const request = new AsyncRequest(); | |
| request.setMethod("POST"); | |
| request.setURI("/api/graphql/"); | |
| request.setData({ | |
| doc_id: "0", | |
| variables: "{}", | |
| __aaid: 0, | |
| __bid: businessId || "0", | |
| }); | |
| request.send(); | |
| } catch (error) { | |
| finish(reject, error); | |
| return; | |
| } | |
| window.setTimeout(() => { | |
| finish(reject, new Error("Could not capture the internal GraphQL request template.")); | |
| }, 3000); | |
| }).catch((error) => { | |
| asyncGraphqlTemplatePromise = null; | |
| throw error; | |
| }); | |
| return asyncGraphqlTemplatePromise; | |
| }; | |
| const internalGraphqlFetch = async ({ friendlyName, docId, variables, businessId }) => { | |
| const template = await captureAsyncGraphqlTemplate(businessId); | |
| const params = new URLSearchParams(template.body); | |
| const currentUserId = getCurrentUserId(); | |
| const lsdToken = getLsdToken() || template.headers["X-FB-LSD"] || ""; | |
| const asbdId = template.headers["X-ASBD-ID"] || "359341"; | |
| if (!currentUserId) { | |
| throw new Error("Could not determine the current user for the internal GraphQL request."); | |
| } | |
| params.set("av", currentUserId); | |
| params.set("__aaid", "0"); | |
| params.set("__bid", businessId); | |
| params.set("fb_api_caller_class", INTERNAL_GRAPHQL_CALLER); | |
| params.set("fb_api_req_friendly_name", friendlyName); | |
| params.set("server_timestamps", "true"); | |
| params.set("variables", JSON.stringify(variables)); | |
| params.set("doc_id", docId); | |
| const headers = { | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| "X-FB-Friendly-Name": friendlyName, | |
| }; | |
| if (lsdToken) { | |
| headers["X-FB-LSD"] = lsdToken; | |
| } | |
| if (asbdId) { | |
| headers["X-ASBD-ID"] = asbdId; | |
| } | |
| const response = await fetch(INTERNAL_GRAPHQL_ENDPOINT, { | |
| method: "POST", | |
| credentials: "include", | |
| headers, | |
| body: params.toString(), | |
| }); | |
| const text = await response.text(); | |
| const payloadText = text.replace(/^for\s*\(\s*;\s*;\s*\);\s*/, ""); | |
| let json; | |
| try { | |
| json = JSON.parse(payloadText); | |
| } catch { | |
| throw new Error("Could not parse the internal GraphQL response."); | |
| } | |
| if (!response.ok || json?.error || json?.errors?.length) { | |
| throw new Error( | |
| json?.errorDescription || | |
| json?.errorSummary || | |
| json?.errors?.[0]?.message || | |
| `Internal GraphQL error (${response.status})` | |
| ); | |
| } | |
| return json; | |
| }; | |
| const normalizeName = (value) => String(value || "").trim().toLowerCase(); | |
| const fetchBusinesses = async () => { | |
| const attempts = [ | |
| async () => fetchAllPages("/me/businesses", { fields: "id,name", limit: 200 }), | |
| async () => { | |
| const payload = await graphFetch("/me", { params: { fields: "businesses{id,name}" } }); | |
| return payload?.businesses?.data || []; | |
| }, | |
| ]; | |
| const errors = []; | |
| const collected = []; | |
| for (const attempt of attempts) { | |
| try { | |
| collected.push(...(await attempt())); | |
| } catch (error) { | |
| errors.push(error.message); | |
| } | |
| } | |
| const normalized = uniqueBy( | |
| collected | |
| .map((item) => ({ | |
| id: String(item?.id || ""), | |
| name: item?.name || "Untitled", | |
| })) | |
| .filter((item) => item.id), | |
| (item) => item.id | |
| ); | |
| if (!normalized.length && errors.length) { | |
| throw new Error(errors[0]); | |
| } | |
| return normalized.sort((a, b) => a.name.localeCompare(b.name, "ru")); | |
| }; | |
| const fetchBusinessPixels = async (businessId) => { | |
| const edges = ["owned_pixels", "client_pixels", "adspixels"]; | |
| const results = []; | |
| const errors = []; | |
| for (const edge of edges) { | |
| try { | |
| const items = await fetchAllPages(`/${businessId}/${edge}`, { | |
| fields: "id,name", | |
| limit: 200, | |
| }); | |
| results.push(...items); | |
| } catch (error) { | |
| errors.push(`${edge}: ${error.message}`); | |
| } | |
| } | |
| const normalized = uniqueBy( | |
| results | |
| .map((item) => ({ | |
| id: String(item?.id || ""), | |
| name: item?.name || "Untitled", | |
| label: `${item?.name || "Untitled"} (${item?.id || "no ID"})`, | |
| })) | |
| .filter((item) => item.id), | |
| (item) => item.id | |
| ); | |
| if (!normalized.length && errors.length === edges.length) { | |
| throw new Error("Could not load pixels for this BM."); | |
| } | |
| return normalized.sort((a, b) => a.name.localeCompare(b.name, "ru")); | |
| }; | |
| const fetchBusinessAccounts = async (businessId) => { | |
| const edges = ["owned_ad_accounts", "client_ad_accounts"]; | |
| const results = []; | |
| const errors = []; | |
| for (const edge of edges) { | |
| try { | |
| const items = await fetchAllPages(`/${businessId}/${edge}`, { | |
| fields: "id,account_id,name", | |
| limit: 200, | |
| }); | |
| results.push(...items); | |
| } catch (error) { | |
| errors.push(`${edge}: ${error.message}`); | |
| } | |
| } | |
| const normalized = uniqueBy( | |
| results | |
| .map((item) => { | |
| const rawId = String(item?.account_id || item?.id || "").replace(/^act_/, ""); | |
| const name = item?.name || "Untitled"; | |
| return { | |
| id: rawId, | |
| apiId: String(item?.id || ""), | |
| name, | |
| label: `${name} (${rawId || "no ID"})`, | |
| }; | |
| }) | |
| .filter((item) => item.id), | |
| (item) => item.id | |
| ); | |
| if (!normalized.length && errors.length === edges.length) { | |
| throw new Error("Could not load ad accounts for this BM."); | |
| } | |
| return normalized.sort((a, b) => a.name.localeCompare(b.name, "ru")); | |
| }; | |
| const fetchInternalPixelAssets = async (businessId) => { | |
| const payload = await internalGraphqlFetch({ | |
| friendlyName: INTERNAL_PIXEL_LIST_QUERY.friendlyName, | |
| docId: INTERNAL_PIXEL_LIST_QUERY.docId, | |
| businessId, | |
| variables: { | |
| assetFilters: { | |
| ad_account_statuses: DEFAULT_AD_ACCOUNT_STATUSES, | |
| }, | |
| assetTypes: ["EVENTS_DATASET_NEW", "PIXEL"], | |
| businessID: businessId, | |
| count: 200, | |
| shouldCountAdmin: false, | |
| }, | |
| }); | |
| const edges = payload?.data?.business?.connected_objects?.edges || []; | |
| return uniqueBy( | |
| edges | |
| .map((edge) => { | |
| const node = edge?.node || {}; | |
| const name = | |
| edge?.nameColumn?.bizkit_settings_render_strategy_no_business_id?.business_object | |
| ?.business_object_name || | |
| edge?.phoneColumn?.business_object_name || | |
| ""; | |
| const assetId = String(node?.assetID || node?.id || ""); | |
| return { | |
| assetId, | |
| assetType: String(node?.assetType || ""), | |
| name: name || "Untitled", | |
| }; | |
| }) | |
| .filter((item) => item.assetId && item.name), | |
| (item) => item.assetId | |
| ).sort((a, b) => a.name.localeCompare(b.name, "ru")); | |
| }; | |
| const mergePixelsWithInternalAssets = (pixels, internalAssets) => { | |
| const assetsByName = internalAssets.reduce((map, item) => { | |
| const key = normalizeName(item.name); | |
| if (!key) { | |
| return map; | |
| } | |
| const current = map.get(key) || []; | |
| current.push(item); | |
| map.set(key, current); | |
| return map; | |
| }, new Map()); | |
| return pixels.map((pixel) => { | |
| const matches = assetsByName.get(normalizeName(pixel.name)) || []; | |
| const internalMatch = matches.length === 1 ? matches[0] : null; | |
| return { | |
| ...pixel, | |
| internalAssetId: internalMatch?.assetId || "", | |
| internalAssetType: internalMatch?.assetType || "", | |
| internalMatchCount: matches.length, | |
| }; | |
| }); | |
| }; | |
| const fetchConnectedAssets = async (businessId, internalAssetId) => { | |
| const payload = await internalGraphqlFetch({ | |
| friendlyName: INTERNAL_CONNECTED_ASSETS_QUERY.friendlyName, | |
| docId: INTERNAL_CONNECTED_ASSETS_QUERY.docId, | |
| businessId, | |
| variables: { | |
| businessID: businessId, | |
| fromAssetID: internalAssetId, | |
| toAssetType: "AD_ACCOUNT", | |
| searchTerm: null, | |
| }, | |
| }); | |
| const edges = payload?.data?.fromAsset?.connectedAssets?.edges || []; | |
| return uniqueBy( | |
| edges | |
| .map((edge) => { | |
| const node = edge?.node || {}; | |
| const legacyAccountId = String(node?.legacy_account_id || ""); | |
| const fallbackId = String(node?.assetID || ""); | |
| const id = legacyAccountId || fallbackId; | |
| const name = node?.assetName || "Untitled"; | |
| return { | |
| id, | |
| assetId: fallbackId, | |
| name, | |
| label: `${name} (${id || "no ID"})`, | |
| }; | |
| }) | |
| .filter((item) => item.id), | |
| (item) => item.id | |
| ).sort((a, b) => a.name.localeCompare(b.name, "ru")); | |
| }; | |
| const getSelectedPixel = () => state.pixels.find((item) => item.id === state.selectedPixelId) || null; | |
| const getSelectedAccount = () => | |
| state.accounts.find((item) => item.id === state.selectedAccountId) || null; | |
| const isSelectedAccountConnected = () => | |
| !!state.selectedAccountId && | |
| state.connectedAssets.some((item) => item.id === state.selectedAccountId); | |
| const isRemovingConnectedAsset = (assetId) => state.removingAssetIds.includes(String(assetId || "")); | |
| const loadConnectedAssetsForSelectedPixel = async () => { | |
| const pixel = getSelectedPixel(); | |
| const loadId = ++connectedAssetsLoadToken; | |
| state.connectedAssets = []; | |
| state.connectedAssetsError = ""; | |
| if (!pixel?.id) { | |
| render(); | |
| return; | |
| } | |
| if (!pixel.internalAssetId) { | |
| state.loadingConnectedAssets = false; | |
| state.connectedAssetsError = | |
| state.pixelMappingError || | |
| "Connected assets are not available for this pixel on the current page."; | |
| render(); | |
| return; | |
| } | |
| state.loadingConnectedAssets = true; | |
| render(); | |
| try { | |
| const connectedAssets = await fetchConnectedAssets(state.businessId, pixel.internalAssetId); | |
| if (loadId !== connectedAssetsLoadToken) { | |
| return; | |
| } | |
| state.connectedAssets = connectedAssets; | |
| state.connectedAssetsError = ""; | |
| } catch (error) { | |
| if (loadId !== connectedAssetsLoadToken) { | |
| return; | |
| } | |
| state.connectedAssets = []; | |
| state.connectedAssetsError = error.message; | |
| } finally { | |
| if (loadId !== connectedAssetsLoadToken) { | |
| return; | |
| } | |
| state.loadingConnectedAssets = false; | |
| render(); | |
| } | |
| }; | |
| const loadBusinessesOnly = async () => { | |
| state.loadingBusinesses = true; | |
| setStatus("info", "Loading business managers..."); | |
| try { | |
| state.businesses = await fetchBusinesses(); | |
| setStatus( | |
| "success", | |
| state.businesses.length | |
| ? "Choose a business manager and open it." | |
| : "No business managers were found for this account." | |
| ); | |
| } catch (error) { | |
| setStatus("error", error.message); | |
| } finally { | |
| state.loadingBusinesses = false; | |
| render(); | |
| } | |
| }; | |
| const loadBusinessContext = async (businessId) => { | |
| state.loadingContext = true; | |
| state.loadingConnectedAssets = false; | |
| state.connectedAssets = []; | |
| state.connectedAssetsError = ""; | |
| state.pixelMappingError = ""; | |
| state.businessId = businessId; | |
| state.lastShare = null; | |
| setStatus("info", `Loading BM data for ${businessId}...`); | |
| try { | |
| const [businesses, pixels, accounts, internalAssets] = await Promise.all([ | |
| fetchBusinesses().catch(() => []), | |
| fetchBusinessPixels(businessId), | |
| fetchBusinessAccounts(businessId), | |
| fetchInternalPixelAssets(businessId).catch((error) => { | |
| state.pixelMappingError = error.message; | |
| return []; | |
| }), | |
| ]); | |
| state.businesses = businesses; | |
| state.accounts = accounts; | |
| state.pixels = mergePixelsWithInternalAssets(pixels, internalAssets); | |
| state.selectedPixelId = state.pixels[0]?.id || ""; | |
| state.selectedAccountId = accounts[0]?.id || ""; | |
| if (!state.pixels.length) { | |
| setStatus("error", "No pixels were found for this BM."); | |
| } else if (!accounts.length) { | |
| setStatus("error", "No ad accounts were found for this BM."); | |
| } else { | |
| setStatus("success", "Data loaded. You can now share a pixel."); | |
| } | |
| await loadConnectedAssetsForSelectedPixel(); | |
| } catch (error) { | |
| state.pixels = []; | |
| state.accounts = []; | |
| state.connectedAssets = []; | |
| state.connectedAssetsError = ""; | |
| setStatus("error", error.message); | |
| } finally { | |
| state.loadingContext = false; | |
| render(); | |
| } | |
| }; | |
| const sharePixel = async () => { | |
| if (!state.selectedPixelId || !state.selectedAccountId || !state.businessId) { | |
| setStatus("error", "Select a pixel and an ad account first."); | |
| return; | |
| } | |
| if (isSelectedAccountConnected()) { | |
| setStatus("info", "The selected ad account is already connected to this pixel."); | |
| return; | |
| } | |
| state.sharing = true; | |
| state.lastShare = null; | |
| setStatus("info", "Sending share request..."); | |
| try { | |
| const payload = await graphFetch(`/${state.selectedPixelId}/shared_accounts`, { | |
| method: "POST", | |
| body: { | |
| method: "POST", | |
| business: state.businessId, | |
| account_id: state.selectedAccountId, | |
| }, | |
| }); | |
| const pixel = getSelectedPixel(); | |
| const account = getSelectedAccount(); | |
| state.lastShare = { | |
| ok: true, | |
| pixel: pixel?.label || state.selectedPixelId, | |
| account: account?.label || state.selectedAccountId, | |
| payload, | |
| }; | |
| setStatus( | |
| "success", | |
| `Pixel ${pixel?.label || state.selectedPixelId} was shared to ${ | |
| account?.label || state.selectedAccountId | |
| } successfully.` | |
| ); | |
| await loadConnectedAssetsForSelectedPixel(); | |
| } catch (error) { | |
| state.lastShare = { ok: false, error: error.message }; | |
| setStatus("error", error.message); | |
| } finally { | |
| state.sharing = false; | |
| render(); | |
| } | |
| }; | |
| const removeConnectedAsset = async (assetId) => { | |
| const pixel = getSelectedPixel(); | |
| const asset = state.connectedAssets.find((item) => item.id === assetId); | |
| if (!state.businessId || !pixel?.internalAssetId || !asset?.assetId) { | |
| setStatus("error", "Could not determine the connected asset to remove."); | |
| return; | |
| } | |
| if (isRemovingConnectedAsset(asset.id)) { | |
| return; | |
| } | |
| state.removingAssetIds = [...state.removingAssetIds, asset.id]; | |
| setStatus("info", `Removing ${asset.label} from ${pixel.label}...`); | |
| render(); | |
| try { | |
| await internalGraphqlFetch({ | |
| friendlyName: INTERNAL_REMOVE_CONNECTED_ASSET_MUTATION.friendlyName, | |
| docId: INTERNAL_REMOVE_CONNECTED_ASSET_MUTATION.docId, | |
| businessId: state.businessId, | |
| variables: { | |
| businessID: state.businessId, | |
| fromAssetID: pixel.internalAssetId, | |
| toAssetID: asset.assetId, | |
| connectedAssetTypes: ["AD_ACCOUNT", "BUSINESS_RESOURCE_GROUP"], | |
| }, | |
| }); | |
| setStatus("success", `${asset.label} was removed from ${pixel.label}.`); | |
| await loadConnectedAssetsForSelectedPixel(); | |
| } catch (error) { | |
| setStatus("error", error.message); | |
| } finally { | |
| state.removingAssetIds = state.removingAssetIds.filter((item) => item !== asset.id); | |
| render(); | |
| } | |
| }; | |
| const renderBusinesses = () => { | |
| if (state.loadingBusinesses) { | |
| return ` | |
| <div class="bmps-panel"> | |
| <h3 class="bmps-panel-title">Business Managers</h3> | |
| <p class="bmps-panel-text">Finding available BMs for this account...</p> | |
| </div> | |
| `; | |
| } | |
| return ` | |
| <div class="bmps-panel"> | |
| <h3 class="bmps-panel-title">You are not inside a specific BM</h3> | |
| <p class="bmps-panel-text"> | |
| Pick a business manager and open it in Business Settings. | |
| </p> | |
| <div class="bmps-list"> | |
| ${ | |
| state.businesses.length | |
| ? state.businesses | |
| .map( | |
| (business) => ` | |
| <div class="bmps-business"> | |
| <div> | |
| <p class="bmps-business-name">${escapeHtml(business.name)}</p> | |
| <p class="bmps-business-id">ID: ${escapeHtml(business.id)}</p> | |
| </div> | |
| <button | |
| class="bmps-button bmps-button-primary" | |
| data-action="open-business" | |
| data-business-id="${escapeHtml(business.id)}" | |
| > | |
| Open | |
| </button> | |
| </div> | |
| ` | |
| ) | |
| .join("") | |
| : `<div class="bmps-business"><div><p class="bmps-business-name">No results</p><p class="bmps-business-id">No accessible business managers were found.</p></div></div>` | |
| } | |
| </div> | |
| <div class="bmps-actions"> | |
| <button class="bmps-button bmps-button-secondary" data-action="reload-businesses"> | |
| Refresh list | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| }; | |
| const renderConnectedAssets = () => { | |
| const selectedPixel = getSelectedPixel(); | |
| const selectedAccountConnected = isSelectedAccountConnected(); | |
| const countLabel = | |
| state.connectedAssets.length === 1 ? "1 account" : `${state.connectedAssets.length} accounts`; | |
| let content = ""; | |
| if (!selectedPixel?.id) { | |
| content = `<p class="bmps-inline-note">Select a pixel to load connected assets.</p>`; | |
| } else if (state.loadingConnectedAssets) { | |
| content = `<p class="bmps-inline-note">Loading connected assets...</p>`; | |
| } else if (state.connectedAssetsError) { | |
| content = `<p class="bmps-inline-note bmps-inline-note-error">${escapeHtml( | |
| state.connectedAssetsError | |
| )}</p>`; | |
| } else if (!state.connectedAssets.length) { | |
| content = `<p class="bmps-inline-note">No connected ad accounts were found for this pixel.</p>`; | |
| } else { | |
| content = ` | |
| <div class="bmps-connected-list"> | |
| ${state.connectedAssets | |
| .map( | |
| (asset) => ` | |
| <div class="bmps-connected-item ${ | |
| asset.id === state.selectedAccountId ? "bmps-connected-item--selected" : "" | |
| }"> | |
| <div class="bmps-connected-main">${escapeHtml(asset.label)}</div> | |
| <div class="bmps-connected-actions"> | |
| ${ | |
| asset.id === state.selectedAccountId | |
| ? `<div class="bmps-connected-badge">Selected</div>` | |
| : "" | |
| } | |
| <button | |
| class="bmps-connected-remove" | |
| data-action="remove-connected" | |
| data-connected-asset-id="${escapeHtml(asset.id)}" | |
| ${isRemovingConnectedAsset(asset.id) ? "disabled" : ""} | |
| > | |
| ${isRemovingConnectedAsset(asset.id) ? "Removing..." : "Remove"} | |
| </button> | |
| </div> | |
| </div> | |
| ` | |
| ) | |
| .join("")} | |
| </div> | |
| `; | |
| } | |
| return ` | |
| <div class="bmps-connected-panel"> | |
| <div class="bmps-connected-head"> | |
| <div class="bmps-panel-title">Connected Assets</div> | |
| <div class="bmps-connected-count">${ | |
| state.loadingConnectedAssets ? "Loading..." : countLabel | |
| }</div> | |
| </div> | |
| ${content} | |
| ${ | |
| selectedAccountConnected | |
| ? `<p class="bmps-inline-note bmps-inline-note-success">The selected ad account is already connected to this pixel.</p>` | |
| : "" | |
| } | |
| </div> | |
| `; | |
| }; | |
| const renderContext = () => { | |
| const pixelOptions = state.pixels.length | |
| ? state.pixels | |
| .map( | |
| (pixel) => ` | |
| <option value="${escapeHtml(pixel.id)}" ${ | |
| pixel.id === state.selectedPixelId ? "selected" : "" | |
| }> | |
| ${escapeHtml(pixel.label)} | |
| </option> | |
| ` | |
| ) | |
| .join("") | |
| : `<option value="">No pixels found</option>`; | |
| const accountOptions = state.accounts.length | |
| ? state.accounts | |
| .map( | |
| (account) => ` | |
| <option value="${escapeHtml(account.id)}" ${ | |
| account.id === state.selectedAccountId ? "selected" : "" | |
| }> | |
| ${escapeHtml(account.label)} | |
| </option> | |
| ` | |
| ) | |
| .join("") | |
| : `<option value="">No accounts found</option>`; | |
| const shareDisabled = | |
| state.sharing || | |
| state.loadingContext || | |
| !state.selectedPixelId || | |
| !state.selectedAccountId || | |
| isSelectedAccountConnected(); | |
| return ` | |
| <div class="bmps-meta"> | |
| <div class="bmps-chip">ID: ${escapeHtml(state.businessId)}</div> | |
| <div class="bmps-chip">Pixels: ${state.pixels.length}</div> | |
| <div class="bmps-chip">Accounts: ${state.accounts.length}</div> | |
| </div> | |
| <div class="bmps-grid"> | |
| <label class="bmps-field"> | |
| <span class="bmps-label">Pixel</span> | |
| <div class="bmps-select-shell"> | |
| <select class="bmps-select" data-role="pixel-select" ${ | |
| state.loadingContext ? "disabled" : "" | |
| }> | |
| ${pixelOptions} | |
| </select> | |
| </div> | |
| </label> | |
| ${renderConnectedAssets()} | |
| <label class="bmps-field"> | |
| <span class="bmps-label">Ad Account</span> | |
| <div class="bmps-select-shell"> | |
| <select class="bmps-select" data-role="account-select" ${ | |
| state.loadingContext ? "disabled" : "" | |
| }> | |
| ${accountOptions} | |
| </select> | |
| </div> | |
| </label> | |
| </div> | |
| <div class="bmps-actions"> | |
| <button | |
| class="bmps-button bmps-button-primary" | |
| data-action="share" | |
| ${shareDisabled ? "disabled" : ""} | |
| > | |
| ${ | |
| state.sharing | |
| ? "Sharing..." | |
| : isSelectedAccountConnected() | |
| ? "Already connected" | |
| : "Share" | |
| } | |
| </button> | |
| <button | |
| class="bmps-button bmps-button-secondary" | |
| data-action="reload-context" | |
| ${state.loadingContext ? "disabled" : ""} | |
| > | |
| Refresh data | |
| </button> | |
| </div> | |
| ${ | |
| state.lastShare?.ok | |
| ? ` | |
| <div class="bmps-footer-note"> | |
| The last action completed successfully. | |
| </div> | |
| ` | |
| : "" | |
| } | |
| `; | |
| }; | |
| const render = () => { | |
| root.innerHTML = ` | |
| <div class="bmps-card"> | |
| <div class="bmps-header"> | |
| <div> | |
| <h2 class="bmps-title">Pixel Share Manager</h2> | |
| <p class="bmps-subtitle"> | |
| ${ | |
| state.businessId | |
| ? "Current mode: inside a specific Business Manager." | |
| : "Current mode: choose a Business Manager first." | |
| } | |
| </p> | |
| </div> | |
| <button class="bmps-close" data-action="close" aria-label="Close">×</button> | |
| </div> | |
| ${state.businessId ? renderContext() : renderBusinesses()} | |
| <div class="bmps-status bmps-status-${escapeHtml(state.status.type)}"> | |
| <div>${state.status.type === "error" ? "!" : state.status.type === "success" ? "✓" : "i"}</div> | |
| <div>${escapeHtml(state.status.text)}</div> | |
| </div> | |
| <a href="#" class="bmps-bookmark-link" data-action="copy-bookmark">Copy as bookmark</a> | |
| </div> | |
| `; | |
| }; | |
| root.addEventListener("click", async (event) => { | |
| if (event.target === root) { | |
| closeOverlay(); | |
| return; | |
| } | |
| const target = event.target.closest("[data-action]"); | |
| if (!target) { | |
| return; | |
| } | |
| const action = target.getAttribute("data-action"); | |
| if (action === "close") { | |
| closeOverlay(); | |
| return; | |
| } | |
| if (action === "reload-businesses") { | |
| await loadBusinessesOnly(); | |
| return; | |
| } | |
| if (action === "copy-bookmark") { | |
| event.preventDefault(); | |
| copyScriptAsBase64Bookmarklet(); | |
| return; | |
| } | |
| if (action === "reload-context") { | |
| await loadBusinessContext(state.businessId); | |
| return; | |
| } | |
| if (action === "share") { | |
| await sharePixel(); | |
| return; | |
| } | |
| if (action === "remove-connected") { | |
| const assetId = target.getAttribute("data-connected-asset-id"); | |
| if (assetId) { | |
| await removeConnectedAsset(assetId); | |
| } | |
| return; | |
| } | |
| if (action === "open-business") { | |
| const businessId = target.getAttribute("data-business-id"); | |
| if (businessId) { | |
| window.location.href = BUSINESS_URL(businessId); | |
| } | |
| } | |
| }); | |
| root.addEventListener("change", async (event) => { | |
| const target = event.target; | |
| if (target.matches('[data-role="pixel-select"]')) { | |
| state.selectedPixelId = target.value; | |
| await loadConnectedAssetsForSelectedPixel(); | |
| return; | |
| } | |
| if (target.matches('[data-role="account-select"]')) { | |
| state.selectedAccountId = target.value; | |
| render(); | |
| } | |
| }); | |
| const init = async () => { | |
| try { | |
| state.businessId = getCurrentBusinessId(); | |
| state.accessToken = extractAccessToken(state.businessId); | |
| render(); | |
| if (state.businessId) { | |
| await loadBusinessContext(state.businessId); | |
| } else { | |
| await loadBusinessesOnly(); | |
| } | |
| } catch (error) { | |
| setStatus("error", error.message); | |
| } | |
| }; | |
| render(); | |
| void init(); | |
| } | |
| window.bmPixelShareManagerApp = bmPixelShareManagerApp; | |
| bmPixelShareManagerApp(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment