@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
JavaScript
;
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
});