UNPKG

@smoud/playable-sdk

Version:

It's unified Playable SDK that supports MRAID, Google, Facebook, Vungle, and many more Ad Networks, simplifying playable ad development with a standardized interface for seamless cross-network compatibility.

651 lines (646 loc) 19 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { default: () => sdk, sdk: () => sdk }); module.exports = __toCommonJS(index_exports); // src/events.ts var registeredEvents = {}; function onEvent(event, fn, context, once) { let listeners = registeredEvents[event]; if (!listeners) { listeners = registeredEvents[event] = { list: [] }; } listeners.list.push(fn, context || null, once || false); } function offEvent(event, fn, context) { const listeners = registeredEvents[event]; if (!listeners) return; const fnArray = listeners.list; if (!fn) { fnArray.length = 0; } else { let i = fnArray.length; while (i > 0) { i -= 3; if (fnArray[i] == fn && (context !== void 0 || fnArray[i + 1] == context)) { fnArray.splice(i, 3); } } } if (fnArray.length == 0) { delete registeredEvents[event]; } } function emitEvent(event, a1, a2, a3) { const listeners = registeredEvents[event]; if (!listeners) return; const fnArray = listeners.list; const len = arguments.length; let fn, ctx; for (let i = 0; i < fnArray.length; i += 3) { fn = fnArray[i]; ctx = fnArray[i + 1]; if (fnArray[i + 2]) { fnArray.splice(i, 3); i -= 3; } if (len <= 1) fn.call(ctx); else if (len == 2) fn.call(ctx, a1); else if (len == 3) fn.call(ctx, a1, a2); else fn.call(ctx, a1, a2, a3); } if (fnArray.length == 0) { delete registeredEvents[event]; } } // src/protocols.ts var uid = 0; var NONE = uid++; var MRAID = uid++; var DAPI = uid++; var NUCLEO = uid++; var FACEBOOK = uid++; var GOOGLE = uid++; var MINTEGRAL = uid++; var TAPJOY = uid++; var TIKTOK = uid++; var actualProtocol = NONE; function isMraid() { return actualProtocol === MRAID; } function isDapi() { return actualProtocol === DAPI; } function isNucleo() { return actualProtocol === NUCLEO; } function isFacebook() { return actualProtocol === FACEBOOK; } function isGoogle() { return actualProtocol === GOOGLE; } function isMintegral() { return actualProtocol === MINTEGRAL; } function isTapjoy() { return actualProtocol === TAPJOY; } function isTikTok() { return actualProtocol === TIKTOK; } function ensureProtocol() { if ("mraid" === AD_PROTOCOL) { try { mraid.getState(); actualProtocol = MRAID; } catch (error) { } } else if ("dapi" === AD_PROTOCOL) { try { dapi.isReady(); actualProtocol = DAPI; } catch (error) { } } else if ("facebook" === AD_NETWORK) { try { if (FbPlayableAd) actualProtocol = FACEBOOK; } catch (error) { } } else if ("google" === AD_NETWORK) { try { if (ExitApi) actualProtocol = GOOGLE; } catch (error) { } } else if ("mintegral" === AD_NETWORK) { if (window.gameReady) actualProtocol = MINTEGRAL; } else if ("tapjoy" === AD_NETWORK) { if (window.TJ_API) actualProtocol = TAPJOY; } else if ("tiktok" === AD_NETWORK) { if (window.openAppStore) actualProtocol = TIKTOK; } } // src/core.ts var destinationUrl = ""; var isSDKInitialized = false; var isProtocolInitialized = false; var isForcePaused = false; var isInstallClicked = false; var actualVolume = 1; var initCallback = () => { }; function bootAd() { if (sdk.isReady) return; emitEvent("boot"); document.body.oncontextmenu = function() { return false; }; initCallback(sdk.maxWidth, sdk.maxHeight); emitEvent("ready"); sdk.isReady = true; } function fireVolumeChange(value) { emitEvent("volume", value); } function changeVolume(value) { sdk.volume = value; fireVolumeChange(value); } function handleVolumeChange(value) { actualVolume = value; if (!sdk.isPaused) changeVolume(actualVolume); } function handlePause() { changeVolume(0); sdk.isPaused = true; emitEvent("pause"); } function handleResume() { if (isForcePaused) return; changeVolume(actualVolume); sdk.isPaused = false; emitEvent("resume"); } function fireResizeEvent(width, height) { handleResize(width, height); } function startMraidProtocol() { if ("mraid" !== AD_PROTOCOL) return; if (isProtocolInitialized) return; mraid.removeEventListener("ready", startMraidProtocol); function mraidStateChanged(viewable) { if (viewable) { handleResume(); if (!sdk.isReady) { const maxSize = mraid.getMaxSize(); sdk.maxWidth = Math.floor(maxSize.width); sdk.maxHeight = Math.floor(maxSize.height); bootAd(); } } else { handlePause(); } } function isViewable() { return mraid.isViewable() && "hidden" !== mraid.getState(); } function stateChanged() { mraidStateChanged(isViewable()); } mraid.addEventListener("viewableChange", stateChanged); mraid.addEventListener("stateChange", stateChanged); if (isViewable()) { mraidStateChanged(true); } if (mraid.getAudioVolume) { const isAudioEnabled = mraid.getAudioVolume(); if (isAudioEnabled) { handleVolumeChange(1); } else { handleVolumeChange(0); } } mraid.addEventListener("audioVolumeChange", function(newVolume) { if (newVolume !== null) { if (newVolume > 0) { handleVolumeChange(1); } else { handleVolumeChange(0); } } }); mraid.addEventListener("error", function(t, e) { console.log("mraid error: " + t + " action: " + e); }); mraid.addEventListener("sizeChange", function() { const maxSize = mraid.getMaxSize(); fireResizeEvent(maxSize.width, maxSize.height); }); isProtocolInitialized = true; } function startDapiProtocol() { if ("dapi" !== AD_PROTOCOL) return; if (isProtocolInitialized) return; function dapiIsViewable(event) { if (event.isViewable) { handleResume(); if (!sdk.isReady) { const screenSize = dapi.getScreenSize(); screenSize.width = Math.floor(screenSize.width); screenSize.height = Math.floor(screenSize.height); bootAd(); } } else { handlePause(); } } dapi.removeEventListener("ready", startDapiProtocol); const isAudioEnabled = dapi.getAudioVolume(); if (isAudioEnabled) { handleVolumeChange(1); } else { handleVolumeChange(0); } dapi.addEventListener("audioVolumeChange", function(volume) { const isAudioEnabled2 = !!volume; if (isAudioEnabled2) { handleVolumeChange(1); } else { handleVolumeChange(0); } }); dapi.addEventListener("adResized", function(event) { const maxSize = dapi.getScreenSize(); fireResizeEvent(event.width || maxSize.width, event.height || maxSize.height); }); if (dapi.isViewable()) { dapiIsViewable({ isViewable: true }); } dapi.addEventListener("viewableChange", dapiIsViewable); isProtocolInitialized = true; } function startDefaultProtocol() { if (isProtocolInitialized) return; if ("mintegral" === AD_NETWORK) { window.mintGameStart = function() { handleResume(); handleResize(sdk.maxWidth, sdk.maxHeight); }; window.mintGameClose = function() { handlePause(); }; } document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { handleResume(); if (!sdk.isReady && document.readyState === "complete") { bootAd(); } } else { handlePause(); } }); function documentIsReady() { if (document.visibilityState === "visible") bootAd(); } if (document.readyState === "complete") { documentIsReady(); } else { window.addEventListener("load", documentIsReady); } window.addEventListener("resize", function() { handleResize(); }); if ("tapjoy" === AD_NETWORK && isTapjoy()) { const tapjoyApi = { skipAd: function() { try { sdk.finish(); } catch (e) { console.warn("Could not skip ad! | " + e); } } }; window.TJ_API.setPlayableAPI && window.TJ_API.setPlayableAPI(tapjoyApi); } isProtocolInitialized = true; } function finishTapjoy() { window.TJ_API.objectiveComplete && window.TJ_API.objectiveComplete(); window.TJ_API.playableFinished && window.TJ_API.playableFinished(); window.TJ_API.gameplayFinished && window.TJ_API.gameplayFinished(); } function handleResize(width, height) { sdk.maxWidth = Math.floor(width || window.innerWidth); sdk.maxHeight = Math.floor(height || window.innerHeight); sdk.isLandscape = sdk.maxWidth > sdk.maxHeight; emitEvent("resize", sdk.maxWidth, sdk.maxHeight); } var isListeningToTouchEvents = false; var isTouchEventRegistered = false; function onUserInteraction(event) { if (typeof TouchEvent !== "undefined" && event instanceof TouchEvent) { isListeningToTouchEvents = true; } if (isListeningToTouchEvents && event instanceof MouseEvent) return; sdk.interactions += 1; emitEvent("interaction", sdk.interactions); } function registerTouchHandlers() { if (!isTouchEventRegistered) { document.addEventListener("mousedown", onUserInteraction); document.addEventListener("touchstart", onUserInteraction); isTouchEventRegistered = true; } } function initSDK() { destinationUrl = /android/i.test(navigator.userAgent) ? GOOGLE_PLAY_URL : APP_STORE_URL; ensureProtocol(); if ("mraid" === AD_PROTOCOL && isMraid()) { mraid.getState() === "loading" ? mraid.addEventListener("ready", startMraidProtocol) : startMraidProtocol(); } else if ("dapi" === AD_PROTOCOL && isDapi()) { dapi.isReady() ? startDapiProtocol() : dapi.addEventListener("ready", startDapiProtocol); } else { startDefaultProtocol(); } emitEvent("init"); } var _sdk = class _sdk { /** * Initializes the SDK and sets up protocol-specific handlers. * This must be called as earlier as possible. * * @param callback Optional function called when ad container is ready * @example * // Basic initialization * sdk.init(); * * // Initialization with callback * sdk.init((width, height) => { * new App(width, height) * }); * * @fires init When initialization starts * @fires boot When the ad begins booting * @fires ready When Ad Network is ready and playable ad can be initialized */ static init(callback) { if (isSDKInitialized) return; if (callback) initCallback = callback; document.readyState === "loading" ? window.addEventListener("DOMContentLoaded", initSDK) : initSDK(); isSDKInitialized = true; } /** * Starts the playable ad experience. * Should be called after all resources are loaded and first frame is rendered. * * @example * // Call just after all resources are preloaded and first frame is rendered * sdk.start(); * * @fires start When the playable ad starts * @fires resize When the ad container is initially sized */ static start() { if (_sdk.isStarted) return; _sdk.isStarted = true; emitEvent("start"); registerTouchHandlers(); if ("mintegral" === AD_NETWORK && isMintegral()) { _sdk.resize(); handlePause(); window.gameReady && window.gameReady(); } else { if ("tapjoy" === AD_NETWORK && isTapjoy()) { window.TJ_API.setPlayableBuild({ orientation: this.isLandscape ? "landscape" : "portrait", buildID: BUILD_HASH }); } fireVolumeChange(_sdk.volume); _sdk.resize(); } } /** * Marks the playable ad as finished. * This triggers network-specific completion handlers. * * @example * // Call when game/experience is complete * sdk.finish(); * * @fires finish When the playable ad is marked as finished */ static finish() { _sdk.isFinished = true; if ("tapjoy" === AD_NETWORK && isTapjoy()) { finishTapjoy(); } else if ("mintegral" === AD_NETWORK && isMintegral()) { window.gameEnd && window.gameEnd(); } else if ("vungle" === AD_NETWORK) { parent.postMessage("complete", "*"); } emitEvent("finish"); } /** * Triggers a retry/restart of the playable ad. * Behavior varies by ad network. * * @example * // Allow user to try again * retryButton.onclick = () => sdk.retry(); * * @fires retry When a retry is triggered */ static retry() { if ("mintegral" === AD_NETWORK && isMintegral()) { } else if ("nucleo" === AD_PROTOCOL && isNucleo()) { NUC.trigger.tryAgain(); } emitEvent("retry"); } /** * Triggers the install/download action for the advertised app. * Handles different store opening methods across ad networks. * * @example * // Call when user wants to install * installButton.onclick = () => sdk.install(); * * @fires finish If the ad hasn't been marked as finished * @fires install When the install action is triggered */ static install() { if (!_sdk.isFinished) { _sdk.isFinished = true; let timeout = 0; if ("tapjoy" === AD_NETWORK && isTapjoy()) { finishTapjoy(); timeout = 300; } emitEvent("finish"); setTimeout(function() { _sdk.install(); }, timeout); return; } if (isInstallClicked) return; isInstallClicked = true; setTimeout(function() { isInstallClicked = false; }, 500); emitEvent("install"); if ("mraid" === AD_PROTOCOL && isMraid()) { mraid.open(destinationUrl); } else if ("dapi" === AD_PROTOCOL && isDapi()) { dapi.openStoreUrl(); } else if ("nucleo" === AD_PROTOCOL && isNucleo()) { NUC.trigger.convert(destinationUrl); } else if ("facebook" == AD_NETWORK && isFacebook()) { FbPlayableAd.onCTAClick(); } else if ("vungle" == AD_NETWORK) { parent.postMessage("download", "*"); } else if ("google" == AD_NETWORK && isGoogle()) { ExitApi.exit(); } else if ("mintegral" === AD_NETWORK && isMintegral()) { window.install && window.install(); } else if ("tapjoy" === AD_NETWORK && isTapjoy()) { window.TJ_API.click(); } else if ("tiktok" === AD_NETWORK && isTikTok()) { window.openAppStore(); } else { window.open(destinationUrl); } } /** * Trigger force resize event * Useful when container size changes need to be manually propagated. * * @example * sdk.resize(); * * @fires resize With current maxWidth and maxHeight */ static resize() { handleResize(_sdk.maxWidth, _sdk.maxHeight); } /** * Forces the playable ad into a paused state. * * @example * // Pause the experience * pauseButton.onclick = () => sdk.pause(); * * @fires pause When the ad enters paused state */ static pause() { if (!isForcePaused) { isForcePaused = true; handlePause(); } } /** * Resumes the playable ad from a forced pause state. * Only works if the ad was paused via sdk.pause(). * * @example * // Resume from pause * resumeButton.onclick = () => sdk.resume(); * * @fires resume When the ad resumes from pause */ static resume() { if (isForcePaused) { isForcePaused = false; handleResume(); } } /** * Registers an event listener. * * @param event Name of the event to listen for * @param fn Callback function to execute when event occurs * @param context Optional 'this' context for the callback * * @example * // Listen for user interactions * sdk.on('interaction', (count) => { * console.log(`User interaction #${count}`); * }); * * // Listen for resize with context * sdk.on('resize', function(width, height) { * this.updateLayout(width, height); * }, gameInstance); */ static on(event, fn, context) { onEvent(event, fn, context); } /** * Registers a one-time event listener that removes itself after execution. * * @param event Name of the event to listen for * @param fn Callback function to execute when event occurs * @param context Optional 'this' context for the callback * * @example * // Listen for first interaction only * sdk.once('interaction', () => { * console.log('First user interaction occurred!'); * }); */ static once(event, fn, context) { onEvent(event, fn, context, true); } /** * Removes an event listener. * * @param event Name of the event to stop listening for * @param fn Optional callback function to remove (if not provided, removes all listeners for the event) * @param context Optional 'this' context to match when removing * * @example * // Remove specific listener * const handler = () => console.log('Interaction'); * sdk.off('interaction', handler); * * // Remove all listeners for an event * sdk.off('interaction'); */ static off(event, fn, context) { offEvent(event, fn, context); } }; /** Current version of the SDK */ _sdk.version = "1.0.18"; /** Current maximum width of the playable ad container in pixels */ _sdk.maxWidth = Math.floor(window.innerWidth); /** Current maximum height of the playable ad container in pixels */ _sdk.maxHeight = Math.floor(window.innerHeight); /** Indicates if the current orientation is landscape (width > height) */ _sdk.isLandscape = window.innerWidth > window.innerHeight; /** Indicates if the Ad Network is ready and playable ad can be initialized */ _sdk.isReady = false; /** Indicates if all playable ad resources are loaded and gameplay has started */ _sdk.isStarted = false; /** Indicates if the playable ad is currently paused */ _sdk.isPaused = false; /** Indicates if the playable ad has finished */ _sdk.isFinished = false; /** Current volume level (0-1) */ _sdk.volume = actualVolume; /** Number of user interactions with the playable ad */ _sdk.interactions = 0; var sdk = _sdk; window["console"].log( `%c @smoud/playable-sdk %c v${sdk.version} `, "background: #007acc; color: #fff; font-size: 14px; padding: 4px 8px; border-top-left-radius: 4px; border-bottom-left-radius: 4px;", "background: #e1e4e8; color: #333; font-size: 14px; padding: 4px 8px; border-top-right-radius: 4px; border-bottom-right-radius: 4px;" ); window.PlayableSDK = sdk; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { sdk });