Replace @zestyxyz/babylonjs-sdk with local ES-module ZestyBanner
All checks were successful
Build / build (push) Successful in 1m46s

The SDK referenced a global BABYLON namespace which broke ES module imports.
Rewrote as three local modules (zestyBanner, zestyNetworking, zestyFormats)
using proper named imports from @babylonjs/core, with safe camera access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2026-03-04 21:19:07 -06:00
parent 6ce2b86569
commit a6715c62bc
4 changed files with 187 additions and 0 deletions

77
src/ads/zestyBanner.ts Normal file
View File

@ -0,0 +1,77 @@
import {
MeshBuilder, ActionManager, ExecuteCodeAction,
StandardMaterial, Texture, WebXRState,
} from '@babylonjs/core';
import type { Scene, Mesh, WebXRDefaultExperience } from '@babylonjs/core';
import { formats } from './zestyFormats';
import {
fetchCampaignAd, sendOnLoadMetric, sendOnClickMetric,
AD_REFRESH_INTERVAL,
} from './zestyNetworking';
import type { AdData } from './zestyNetworking';
function getCamera(scene: Scene, xr?: WebXRDefaultExperience) {
if (xr?.baseExperience?.state === WebXRState.IN_XR) {
return xr.baseExperience.camera;
}
return scene.cameras.length > 0 ? scene.cameras[0] : null;
}
async function loadAd(
banner: Mesh, scene: Scene, adUnitId: string,
format: string, adRef: { current: AdData }
): Promise<void> {
const ad = await fetchCampaignAd(adUnitId, format);
adRef.current = ad;
const mat = banner.material as StandardMaterial;
mat.diffuseTexture?.dispose();
const tex = new Texture(ad.assetUrl, scene);
tex.hasAlpha = true;
mat.diffuseTexture = tex;
if (ad.campaignId) sendOnLoadMetric(adUnitId, ad.campaignId);
}
function openUrl(url: string): void {
window.open(url, '_blank');
}
export function createZestyBanner(
adUnitId: string, format: string, height: number,
scene: Scene, xr?: WebXRDefaultExperience
): Mesh {
const fmt = formats[format] ?? formats['billboard'];
const planeOpts = { width: fmt.width * height, height };
const banner = MeshBuilder.CreatePlane('zestybanner', planeOpts, scene);
const mat = new StandardMaterial('zestyMat', scene);
mat.diffuseTexture = new Texture(fmt.defaultImage, scene);
(mat.diffuseTexture as Texture).hasAlpha = true;
banner.material = mat;
const adRef = { current: { assetUrl: '', ctaUrl: '', campaignId: '' } };
loadAd(banner, scene, adUnitId, format, adRef);
banner.actionManager = new ActionManager(scene);
banner.actionManager.registerAction(
new ExecuteCodeAction(ActionManager.OnPickTrigger, () => {
if (!adRef.current.ctaUrl) return;
if (xr?.baseExperience) {
xr.baseExperience.sessionManager.exitXRAsync()
.then(() => openUrl(adRef.current.ctaUrl));
} else {
openUrl(adRef.current.ctaUrl);
}
if (adRef.current.campaignId) {
sendOnClickMetric(adUnitId, adRef.current.campaignId);
}
})
);
setInterval(() => {
const cam = getCamera(scene, xr);
if (!cam || !scene.isActiveMesh(banner)) return;
loadAd(banner, scene, adUnitId, format, adRef);
}, AD_REFRESH_INTERVAL);
return banner;
}

20
src/ads/zestyFormats.ts Normal file
View File

@ -0,0 +1,20 @@
const CDN_BASE = 'https://cdn.zesty.xyz/sdk/assets';
export interface FormatInfo {
width: number;
height: number;
defaultImage: string;
}
export const formats: Record<string, FormatInfo> = {
billboard: {
width: 3.88,
height: 1,
defaultImage: `${CDN_BASE}/zesty-default-billboard.png`,
},
'medium-rectangle': {
width: 1.2,
height: 1,
defaultImage: `${CDN_BASE}/zesty-default-medium-rectangle.png`,
},
};

View File

@ -0,0 +1,78 @@
import { formats } from './zestyFormats';
const API_BASE = 'https://api.zesty.market/api';
const GRAPHQL_URL = 'https://market.zesty.xyz/graphql';
export const AD_REFRESH_INTERVAL = 30000;
export interface AdData {
assetUrl: string;
ctaUrl: string;
campaignId: string;
}
function detectPlatform(): { name: string; confidence: string } {
const ua = navigator.userAgent;
if (/OculusBrowser/i.test(ua)) return { name: 'Oculus', confidence: 'high' };
if (/Wolvic/i.test(ua)) return { name: 'Wolvic', confidence: 'high' };
if (/Pico/i.test(ua)) return { name: 'Pico', confidence: 'medium' };
return { name: 'Desktop', confidence: 'low' };
}
export async function fetchCampaignAd(
adUnitId: string, format: string
): Promise<AdData> {
try {
const url = encodeURI(window.location.href).replace(/\/$/, '');
const res = await fetch(
`${API_BASE}/ad?ad_unit_id=${adUnitId}&url=${url}`
);
if (res.status === 200) {
const json = await res.json();
if (json.Ads?.length) {
return {
assetUrl: json.Ads[0].asset_url,
ctaUrl: json.Ads[0].cta_url,
campaignId: json.CampaignId ?? '',
};
}
}
} catch {
console.warn('Zesty: failed to fetch ad, using default');
}
const fmt = formats[format] ?? formats['billboard'];
return { assetUrl: fmt.defaultImage, ctaUrl: '', campaignId: '' };
}
async function sendMetric(
eventType: string, adUnitId: string, campaignId: string
): Promise<void> {
const { name, confidence } = detectPlatform();
const query = `mutation { increment(
eventType: ${eventType},
spaceId: "${adUnitId}",
campaignId: "${campaignId}",
platform: { name: ${name}, confidence: ${confidence} }
) { message } }`;
try {
await fetch(GRAPHQL_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
} catch {
console.warn(`Zesty: failed to send ${eventType} metric`);
}
}
export async function sendOnLoadMetric(
adUnitId: string, campaignId: string
): Promise<void> {
await sendMetric('visits', adUnitId, campaignId);
}
export async function sendOnClickMetric(
adUnitId: string, campaignId: string
): Promise<void> {
await sendMetric('clicks', adUnitId, campaignId);
}

View File

@ -5,6 +5,7 @@ import {
import { DefaultScene } from "./defaultScene";
import { InputControlManager } from "../ship/input/inputControlManager";
import log from './logger';
import { createZestyBanner } from '../ads/zestyBanner';
const XR_RENDERING_GROUP = 3;
const FADE_DELAY_MS = 500;
@ -54,7 +55,18 @@ async function createXRExperience(): Promise<void> {
disablePointerSelection: true // Disable to re-enable with custom options
});
log.debug(WebXRFeaturesManager.GetAvailableFeatures());
try {
const banner = createZestyBanner("a2170882-f232-4da0-9315-747ee049e642",
"billboard", 10, DefaultScene.MainScene, DefaultScene.XR);
banner.position.z = 50;
banner.position.y = 5;
banner.rotation.y = Math.PI;
banner.renderOverlay = true;
banner.overlayColor = Color3.Red();
} catch (e) {
log.debug("Zesty banner init failed:", e);
}
// Enable pointer selection with renderingGroupId so laser is never occluded
DefaultScene.XR.baseExperience.featuresManager.enableFeature(
WebXRFeatureName.POINTER_SELECTION,