Spaces:
Running
Running
Download hushfox/funpass.js from CyberMax-tools/cybermax: direct link, hf CLI and curl.
- Browser
- Download file 28.8 kB
-
https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/hushfox/funpass.js
- Command line
-
hf download hf://spaces/CyberMax-tools/cybermax/hushfox/funpass.js
-
curl -L -o funpass.js https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/hushfox/funpass.js
28.8 kB
| // CyberMax games checkout: the per-game "full game" unlock + the Fun Pass (every game), copied into each game's web/ folder. | |
| // Model (games lane GM, 9 Oct 2026; evidence docs/ceo/games-monetization-2026-10-09.md): | |
| // - Free demo: every game's daily challenge stays free for everyone, plus the first levels (OFFER.demo once the per-game | |
| // unlock is live; levels 1-20 until then). A level the player has opened is never locked again. | |
| // - Full game: ONE payment per game (OFFER.price, Stripe pack revenue/packs/stripe/<game>-full) unlocks every level, | |
| // mode, the daily archive and themes in that game, for good. Shown only when OFFER.live is true (owner-approved). | |
| // - Fun Pass = the all-games bundle: Monthly $2.99 · Yearly $19 · Lifetime $29.99 (pack revenue/packs/stripe/fun-pass). | |
| // - Portal builds (scripts/games/portals.py) never show a checkout; where the portal SDK offers rewarded ads, the player | |
| // may watch one to open the full game for an hour (shared/portal-adapter/cmx-portal.js CMXPortal.rewarded()). | |
| // Buy: counted redirect https://data.cybermaxtools.com/buy/st-<pack>-<plan>?s=game-<id> -> Stripe checkout. | |
| // After checkout the data Worker (/unlock/fun-pass or /unlock/<game>-full) checks the Checkout Session and returns with | |
| // #cmx-pro=<code> (the full-game return adds ?unlock=full). The code on the receipt page unlocks any other device. | |
| // No account, no cookies, nothing personal stored. (c) 2026 CyberMax. All rights reserved. | |
| import { count, store, toast } from './cmx.js'; | |
| import { OFFER } from './offer.js'; // written by scripts/games/sync_kit.py from products/fun-pass/games.json | |
| export { OFFER }; | |
| const DATA = 'https://data.cybermaxtools.com'; | |
| const KEY = 'cmx-pro:fun-pass'; | |
| const SID = /^(cmxk1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|cs_(live|test)_[A-Za-z0-9]{10,200}|[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8})$/i; // Stripe order code or Gumroad licence key | |
| const RECHECK = 3 * 864e5; // subscriptions are re-checked every 3 days; a cancelled pass turns off by itself | |
| export const PLANS = [ | |
| { id: 'monthly', label: 'Monthly', price: '$2.99', per: '/month', note: 'Cancel any time' }, | |
| { id: 'yearly', label: 'Yearly', price: '$19', per: '/year', note: 'Save 47%', best: true }, | |
| { id: 'unlock', label: 'Lifetime', price: '$29.99', per: ' once', note: 'Pay once, keep forever' }, | |
| ]; | |
| export const COMMON_PERKS = ['Every level and mode in every CyberMax game', 'The full archive of past dailies', 'Custom colour themes', 'New games included as they launch']; | |
| const FULL_KEY = `cmx-full:${OFFER.game}`; | |
| const REWARD_KEY = `cmx-reward:${OFFER.game}`; | |
| const REWARD_MS = 60 * 60 * 1000; // one rewarded ad on a portal build opens the full game for an hour | |
| let state = store(KEY); | |
| let full = store(FULL_KEY); | |
| const listeners = []; | |
| let cfg = { game: 'games', name: 'CyberMax games', perks: [] }; | |
| // Per-game offer naming (17 Oct 2026 conversion fix): games whose one-time unlock is not a "full game" (no levels, every | |
| // mode free) pass init({ title: 'Stoopshake Crew Pass', exact: true, free: '...' }): the sheet then names the offer exactly | |
| // as the Stripe product, lists only its own perks and states what stays free, instead of "every level / bonus mode". | |
| const fullTitle = () => cfg.title || `Full ${OFFER.name}`; | |
| const passOn = () => !!(state && state.code); | |
| export const ownsGame = () => !!(full && full.code); | |
| const rewardOn = () => { const r = store(REWARD_KEY); return !!(r && Date.now() - r.at < REWARD_MS); }; | |
| /** True when this player may use everything in this game: Fun Pass, this game's full unlock, or a portal reward hour. */ | |
| export const isPro = () => passOn() || ownsGame() || rewardOn(); | |
| /** Free levels in this game's demo: OFFER.demo once the per-game unlock is live, else the game's current number. */ | |
| export const freeLevels = (current = 20) => (OFFER.live && OFFER.demo ? Math.min(current, OFFER.demo) : current); | |
| const portal = () => (typeof window !== 'undefined' && window.CMXPortal && window.CMXPortal.portal) || ''; | |
| export function onChange(fn) { listeners.push(fn); fn(isPro()); } | |
| // The header pass button names the real price (convert lane, 15 Oct 2026): "Full game $2.99" where this game sells its | |
| // own unlock, else the Fun Pass monthly price. Pass holders see the button's own label. | |
| function priceButton() { | |
| const btn = typeof document !== 'undefined' && document.getElementById('pass-btn'); | |
| if (!btn) return; | |
| if (!btn.dataset.label) btn.dataset.label = btn.textContent; | |
| btn.textContent = isPro() || portal() ? btn.dataset.label : sellsGame() ? `✨ ${cfg.title ? cfg.short || cfg.title : 'Full game'} ${OFFER.price}` : `✨ Fun Pass ${PLANS[0].price}/mo`; | |
| } | |
| function emit() { | |
| document.documentElement.classList.toggle('cmx-pro', isPro()); | |
| priceButton(); | |
| paintShop(); | |
| listeners.forEach((fn) => { try { fn(isPro()); } catch { /* game hook */ } }); | |
| } | |
| // Offline Pro keys (17 Oct 2026): "cmxk1.<payload>.<Ed25519 signature>" from data.cybermaxtools.com/unlock is verified here | |
| // with WebCrypto and this public key, so an unlock needs no Worker call. Expired, foreign-signed or unverifiable keys fall | |
| // back to the online check of the order code inside them, as before. | |
| const CMX_PUB = 'u0FDxbu0d9B9Sz5-yLT6AuL5IagyTlFcbBgEDqZxWtk'; | |
| const b64d = (s) => { s = s.replace(/-/g, '+').replace(/_/g, '/'); while (s.length % 4) s += '='; return Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); }; | |
| async function cmxKeyOk(k, app, online) { | |
| const [tag, body, sig] = k.split('.'); | |
| let p; | |
| try { p = JSON.parse(new TextDecoder().decode(b64d(body))); } catch { return false; } | |
| if (!p || p.a !== app) return false; | |
| try { | |
| const key = await crypto.subtle.importKey('raw', b64d(CMX_PUB), { name: 'Ed25519' }, false, ['verify']); | |
| if (await crypto.subtle.verify({ name: 'Ed25519' }, key, b64d(sig), new TextEncoder().encode(tag + '.' + body)) && (!p.x || p.x * 1000 > Date.now())) return true; | |
| } catch { /* no Ed25519 here: online check below */ } | |
| return p.c ? online(p.c) : false; | |
| } | |
| async function check(code, what = 'fun-pass') { | |
| if (/^cmxk1\./.test(code)) return cmxKeyOk(code, what, (c) => checkOnline(c, what)); | |
| return checkOnline(code, what); | |
| } | |
| async function checkOnline(code, what = 'fun-pass') { | |
| try { | |
| const r = await fetch(`${DATA}/unlock/${what}?json=1&session_id=${encodeURIComponent(code)}`); | |
| const j = await r.json(); | |
| return j && typeof j.pro === 'boolean' ? j.pro : null; | |
| } catch { return null; } | |
| } | |
| /** Turn on a full-game code for this game (receipt code of the <game>-full Payment Link). */ | |
| export async function unlockGame(code) { | |
| const ok = await check(code, `${OFFER.game}-full`); | |
| if (ok) { | |
| full = store(FULL_KEY, { code, at: new Date().toISOString() }); | |
| count(cfg.game, 'pro', 'full-unlocked'); | |
| toast(cfg.title ? `${cfg.title} is on. Thank you for supporting CyberMax games!` : `${OFFER.name}: the full game is on. Thank you for supporting CyberMax games!`, 3500); | |
| emit(); render(); | |
| } else if (ok === false) toast('That code is not a full-game order for this game.', 3500); | |
| else toast('Could not reach the unlock check. Try again in a minute.', 3500); | |
| return ok; | |
| } | |
| /** Turn on a pasted or returned code: a Fun Pass code, or (where this game sells its own unlock) a full-game code. */ | |
| export async function unlock(code) { | |
| const ok = await check(code); | |
| if (ok) { | |
| state = store(KEY, { code, at: new Date().toISOString(), checked: Date.now() }); | |
| count(cfg.game, 'pro', 'unlocked'); | |
| toast('Fun Pass is on. Thank you for supporting CyberMax games!', 3500); | |
| emit(); | |
| render(); | |
| return ok; | |
| } | |
| if (sellsGame() && (await check(code, `${OFFER.game}-full`))) return unlockGame(code); | |
| if (ok === false) toast('That code is not an active Fun Pass.', 3500); | |
| else toast('Could not reach the pass check. Try again in a minute.', 3500); | |
| return ok; | |
| } | |
| async function recheck() { | |
| if (!passOn() || Date.now() - (state.checked || 0) < RECHECK) return; // only a Fun Pass is re-checked (a full-game or Founder owner has no pass state) | |
| const ok = await check(state.code); | |
| if (ok === false) { state = null; try { localStorage.removeItem(KEY); } catch { /* */ } emit(); render(); toast('Your Fun Pass has ended. Everything free is still here.', 4000); } | |
| else if (ok) state = store(KEY, { ...state, checked: Date.now() }); | |
| } | |
| const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); | |
| const buyUrl = (plan) => `${DATA}/buy/st-fun-pass-${plan}?s=game-${cfg.game}`; | |
| const gameUrl = (from = '') => `${DATA}/buy/st-${OFFER.game}-full-unlock?s=game-${cfg.game}${from ? `-${from}` : ''}`; | |
| const sellsGame = () => !!(OFFER.live && OFFER.price && OFFER.game !== 'games'); | |
| function dialog() { | |
| let d = document.getElementById('funpass'); | |
| if (!d) { | |
| d = document.createElement('dialog'); | |
| d.id = 'funpass'; | |
| d.className = 'funpass'; | |
| document.body.appendChild(d); | |
| d.addEventListener('click', (e) => { if (e.target === d || e.target.closest('[data-close]')) d.close(); }); | |
| } | |
| return d; | |
| } | |
| const planHtml = () => PLANS.map((p) => `<a class="fp-plan${p.best ? ' best' : ''}" href="${buyUrl(p.id)}" rel="nofollow" data-plan="${p.id}">${p.best ? '<i>Best value</i>' : ''}<b>${p.label}</b><span class="fp-price">${p.price}<small>${p.per}</small></span><span class="fp-note">${p.note}</span></a>`).join(''); | |
| const codeForm = `<details><summary>Already bought?</summary><form class="fp-form"><input aria-label="Order code" autocomplete="off" placeholder="Receipt code (cs_live_…) or Gumroad licence key"><button class="btn sm" type="submit">Turn on</button></form></details>`; | |
| function render() { | |
| const d = dialog(); | |
| const perks = [...(cfg.perks || []), ...COMMON_PERKS.filter((p) => !(cfg.perks || []).some((q) => q.split(' ')[0] === p.split(' ')[0]))].slice(0, 6); | |
| if (isPro()) { | |
| const what = passOn() ? 'The Fun Pass is on in this browser for every CyberMax game' : ownsGame() ? (cfg.title ? `You own the ${esc(cfg.title)}` : `You own the full ${esc(OFFER.name)}`) : 'The full game is open for the next hour (rewarded ad)'; | |
| const code = passOn() ? state.code : ownsGame() ? full.code : ''; | |
| d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${passOn() ? 'CyberMax Fun Pass' : esc(OFFER.name)}</p><h2>Everything is open. Thank you!</h2> | |
| <p>${what}: ${perks.map(esc).join(' · ')}.</p> | |
| ${code ? `<p class="fp-small">Your code (paste it on another device to turn it on there):</p><code class="fp-code">${esc(code)}</code>` : ''} | |
| ${passOn() ? '<p class="fp-small">Manage or cancel a subscription any time from the link in your Stripe receipt email.</p>' : ''}</div>`; | |
| return; | |
| } | |
| if (portal()) { // portal build: no outside checkout; a rewarded ad (when the portal offers one) opens the game for an hour | |
| d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name || 'Full game')}</p> | |
| <h2>Open the full game for an hour</h2><ul class="fp-perks">${perks.slice(0, 4).map((p) => `<li>${esc(p)}</li>`).join('')}</ul> | |
| <p><button class="btn" type="button" data-reward>▶ Watch a short ad</button></p> | |
| <p class="fp-small">Optional. The daily challenge and the free levels stay free without ads.</p></div>`; | |
| d.querySelector('[data-reward]').addEventListener('click', async () => { | |
| count(cfg.game, 'cta', 'reward-ad'); | |
| const ok = await window.CMXPortal.rewarded().catch(() => false); | |
| if (ok) { store(REWARD_KEY, { at: Date.now() }); count(cfg.game, 'pro', 'reward-hour'); toast('Everything is open for the next hour. Have fun!', 3000); emit(); d.close(); } | |
| else toast('No ad is available right now. Try again later.', 3000); | |
| }); | |
| return; | |
| } | |
| const allGames = `${OFFER.games || 'every'} CyberMax games`; | |
| const own = cfg.perks || []; | |
| const gamePerks = cfg.exact ? own.slice(0, 6) : [...own, ...(own.some((p) => /archive/i.test(p)) ? [] : ['The full archive of past dailies']), 'Every bonus mode in this game'].slice(0, 6); | |
| const freeLine = cfg.free || `The daily challenge and ${OFFER.levels === false ? 'the free modes stay' : `the first ${OFFER.demo || ''} levels stay`} free for everyone.`; | |
| d.innerHTML = sellsGame() | |
| ? `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name)} · ${cfg.title ? 'one payment' : 'full game'}</p> | |
| <h2>${cfg.title ? `Get the ${esc(cfg.title)}` : `Unlock the full ${esc(OFFER.name)}`}</h2> | |
| <ul class="fp-perks">${gamePerks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul> | |
| <a class="fp-plan fp-game" href="${gameUrl()}" rel="nofollow" data-game><i>This game only</i><b>${esc(fullTitle())}</b><span class="fp-price">${esc(OFFER.price)}<small> once</small></span><span class="fp-note">Pay once, keep it forever. No subscription.</span></a> | |
| <p class="fp-or"><b>Or get all ${esc(allGames)}</b> with the Fun Pass (a separate pass for every game):</p> | |
| <div class="fp-plans">${planHtml()}</div> | |
| <p class="fp-small">Secure checkout by Stripe (shown as Link) · turns on as soon as you pay · your code works on any device · 14-day fix-or-refund if it doesn't work as described (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). ${esc(freeLine)} Prices in USD; tax for your country is added at checkout.</p> | |
| ${codeForm}</div>` | |
| : `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">CyberMax Fun Pass</p> | |
| <h2>One pass. Every game. More to play.</h2> | |
| <ul class="fp-perks">${perks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul> | |
| <div class="fp-plans">${planHtml()}</div> | |
| <p class="fp-small">Secure checkout by Stripe (shown as Link; card, Apple Pay, Google Pay) · turns on as soon as you pay · cancel any time from your receipt email · 14-day fix-or-refund if it doesn't work as described (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). Prices in USD, + tax where applicable. Works in every CyberMax game. The daily challenges and the free levels stay free for everyone.</p> | |
| ${codeForm}</div>`; | |
| d.querySelector('.fp-form').addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const c = e.target.querySelector('input').value.trim(); | |
| if (SID.test(c)) unlock(c); else toast('Paste the code from your receipt page (cs_live_…) or your Gumroad licence key.', 3500); | |
| }); | |
| d.querySelectorAll('[data-plan]').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', `pass-${a.dataset.plan}`))); | |
| d.querySelector('[data-game]')?.addEventListener('click', () => count(cfg.game, 'cta', 'full-game')); | |
| } | |
| /** Open the pass sheet; `from` names the button that opened it (counted as cta). */ | |
| export function open(from = 'button') { | |
| render(); | |
| const d = dialog(); | |
| if (d.showModal) d.showModal(); else d.setAttribute('open', ''); | |
| count(cfg.game, 'cta', `pass-open-${from}`); | |
| } | |
| /** After a finished round (the value moment): one small inline card under the result, never a pop-up. Shows the | |
| * pass price, the honest trust line and an optional "email me" weekly reminder (double opt-in). | |
| * `host` is the result panel; the card is added once and removed for pass holders. */ | |
| // One quiet line under the result: CyberMax's free Roblox games (24 Oct 2026). Not on game portals (their rules ban | |
| // outside links), never a pop-up; links carry the game id so clicks can be told apart. | |
| const ROBLOX = [['Skyline Flight Circuit', 'https://www.roblox.com/games/89786604994377'], ['Hatchlume', 'https://www.roblox.com/games/80122923062327'], | |
| ['Sled Volt', 'https://www.roblox.com/games/134881648422275']]; | |
| function robloxRow(host) { | |
| if (portal() || host.querySelector('.fp-rbx')) return; | |
| const p = document.createElement('p'); p.className = 'fp-rbx'; | |
| const tag = `?utm_source=cybermax&utm_medium=game&utm_campaign=${encodeURIComponent(cfg.game || 'games')}-end`; | |
| p.innerHTML = 'Play our Roblox games: ' + ROBLOX.map(([n, u]) => `<a href="${u}${tag}" target="_blank" rel="noopener">${esc(n)}</a>`).join(' · '); | |
| p.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', 'roblox-end'))); | |
| host.appendChild(p); | |
| } | |
| export function afterWin(host, from = 'win') { | |
| if (!host) return; | |
| robloxRow(host); | |
| if (isPro() || portal()) { host.querySelector('.fp-after')?.remove(); return; } | |
| let el = host.querySelector('.fp-after'); | |
| if (!el) { el = document.createElement('div'); el.className = 'fp-after'; host.appendChild(el); } | |
| const best = PLANS.find((p) => p.best) || PLANS[0]; | |
| const game = sellsGame(); | |
| const href = game ? gameUrl('after') : `${buyUrl(best.id)}-after`; | |
| const label = game ? (cfg.title ? `Get the ${cfg.title}: ${OFFER.price} once` : `Get the full ${OFFER.name}: ${OFFER.price} once`) : `Get the Fun Pass: ${best.price}${best.per}`; | |
| const line = game ? (cfg.extra || `The full game opens every level, mode, the daily archive and themes in ${OFFER.name}.`) | |
| : (cfg.extra || 'The Fun Pass opens every level, mode, the full archive and themes in every CyberMax game.'); | |
| el.innerHTML = `<p><b>Want more today?</b> ${esc(line)}</p> | |
| <p class="fp-row"><a class="btn sm" href="${href}" rel="nofollow" data-after>${esc(label)}</a> <button class="btn sm ghost" type="button" data-plans>${game ? 'All games pass' : 'See all plans'}</button></p> | |
| <p class="fp-small">Secure checkout by Stripe (shown as Link) · turns on as soon as you pay · ${game ? 'one payment, no subscription' : 'cancel any time from your receipt email'} · 14-day fix-or-refund if it doesn't work as described (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). Prices in USD, + tax where applicable. Today's daily challenge stays free.</p> | |
| <div class="fp-cap"></div>`; | |
| el.querySelector('[data-after]').addEventListener('click', () => { count(cfg.game, 'cta', game ? 'full-game-after' : `pass-${best.id}-after`); kit((K) => K.event('upgrade-click', cfg.game)); }); | |
| el.querySelector('[data-plans]').addEventListener('click', () => open(`after-${from}`)); | |
| kit((K) => { | |
| K.event('result', cfg.game); K.event('upgrade-view', cfg.game); | |
| K.capture(el.querySelector('.fp-cap'), { list: `weekly-${cfg.game}`, src: `game-${cfg.game}`, label: cfg.remind || 'Email me a weekly reminder with new puzzles (free)', button: 'Remind me' }); | |
| }); | |
| } | |
| // Convert kit (funnel beacons + email capture) from the CyberMax site; optional, the game works without it. | |
| let kitQ = null; | |
| function kit(fn) { | |
| if (window.CMXConvert) return fn(window.CMXConvert); | |
| if (kitQ) return kitQ.push(fn); | |
| kitQ = [fn]; | |
| const sc = document.createElement('script'); | |
| sc.src = 'https://cybermaxtools.com/store/convert.js'; sc.async = true; | |
| sc.onload = () => kitQ.splice(0).forEach((f) => { try { f(window.CMXConvert); } catch { /* optional */ } }); | |
| document.head.appendChild(sc); | |
| } | |
| // ------------------------------------------------------------------ the Shop (owner, 29 Oct 2026: "there's nothing to buy") | |
| // One clear Shop button on the title/menu screen and one quiet button on the results screen open ONE sheet that lists | |
| // everything this game sells, cheapest first (OFFER.shop, written by scripts/games/sync_kit.py from products/fun-pass/shop.json; | |
| // prices come from the Stripe packs): coin packs, Skin/Look/Founder Packs, the full game, the Fun Pass and the Arcade | |
| // Lifetime. Buttons are plain markup with data-cmx-shop="<where>" (menu, title, results); this file labels them, opens | |
| // the sheet and hides them on portal and Telegram builds (no outside checkout there). Never shown during play: the | |
| // buttons live on menu/results screens or above the game, so the play HUD is unchanged. Buy links carry | |
| // s=game-<id>-shop-<where>; cmx.js adds the first-touch tag on click. Nothing pops up by itself. | |
| const SHOP = Array.isArray(OFFER.shop) ? OFFER.shop : []; | |
| const TG = typeof location !== 'undefined' && (/[?&]tg(=|&|$)/.test(location.search) || /tgWebAppData=/.test(location.hash)); | |
| /** True where no web checkout may show: portal builds and the Telegram Mini App (Stars only). */ | |
| export const noShop = () => !!portal() || TG || !SHOP.length; | |
| const slug = (s) => String(s || 'shop').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 24) || 'shop'; | |
| const shopHref = (offer, from) => `${DATA}/buy/${offer}?s=game-${OFFER.game}-shop-${slug(from)}`; | |
| const ownedKind = (it) => { | |
| if (it.kind === 'pass') return passOn(); | |
| if (it.kind === 'full') return ownsGame(); | |
| if (it.kind === 'skins') return !!store(`cmx-skins:${OFFER.game}`) || !!store(`cmx-founder:${OFFER.game}`); | |
| if (it.kind === 'founder') return !!store(`cmx-founder:${OFFER.game}`); | |
| if (it.kind === 'collector') return !!store(`cmx-collector:${OFFER.game}`); // looks Collector Packs (skins.js v2) | |
| return false; // coin packs, looks packs and the Arcade Lifetime can always be bought | |
| }; | |
| /** The Shop items, cheapest first, with an owned flag. */ | |
| export const shopItems = () => SHOP.slice().sort((a, b) => a.cents - b.cents).map((it) => ({ ...it, owned: ownedKind(it) })); | |
| /** Label for a Shop button: the menu/title one names the lowest price, the results one the most relevant offer. */ | |
| export function shopLabel(where = 'menu') { | |
| if (where === 'results') return esc(OFFER.shopResults || '🛍️ See the shop →'); | |
| return `🛍️ Shop${OFFER.shopFrom ? ` <small>from ${esc(OFFER.shopFrom)}</small>` : ''}`; | |
| } | |
| /** Markup for a Shop button in screens a game draws itself ('' where no checkout may show). */ | |
| export function shopButtonHtml(where = 'menu', cls = '') { | |
| if (noShop()) return ''; | |
| const c = where === 'results' ? 'btn ghost sm cmx-shopres' : 'btn cmx-shopbtn'; | |
| return `<button class="${c}${cls ? ` ${cls}` : ''}" type="button" data-cmx-shop="${esc(where)}">${shopLabel(where)}</button>`; | |
| } | |
| function paintShop() { | |
| if (typeof document === 'undefined') return; | |
| const off = noShop(); | |
| document.documentElement.classList.toggle('cmx-noshop', off); | |
| for (const b of document.querySelectorAll('[data-cmx-shop]')) { | |
| if (off) { if (!b.hasAttribute('data-cmx-keep')) b.hidden = true; continue; } // data-cmx-keep: the game's own button, its own handler | |
| const where = b.dataset.cmxShop || 'menu'; | |
| const html = shopLabel(where === 'results' ? 'results' : 'menu'); | |
| if (b.dataset.cmxLabel !== html) { b.innerHTML = html; b.dataset.cmxLabel = html; } | |
| if (b.hasAttribute('data-cmx-hidden')) continue; // the game shows/hides this one itself | |
| b.hidden = false; | |
| } | |
| } | |
| const swatch = (it) => `background:${it.sw && it.sw.length > 1 ? `linear-gradient(135deg,${it.sw.map(esc).join(',')})` : esc((it.sw || ['#FF6A3D'])[0])}`; | |
| function shopDialog() { | |
| let d = document.getElementById('cmxshop'); | |
| if (!d) { | |
| d = document.createElement('dialog'); | |
| d.id = 'cmxshop'; | |
| d.className = 'funpass cmxshop'; | |
| d.setAttribute('aria-label', `${OFFER.name} shop`); | |
| document.body.appendChild(d); | |
| d.addEventListener('click', (e) => { if (e.target === d || e.target.closest('[data-close]')) d.close(); }); | |
| } | |
| return d; | |
| } | |
| function renderShop(from) { | |
| const d = shopDialog(); | |
| const rows = shopItems().map((it) => { | |
| const img = it.img ? `<img src="${esc(it.img)}" alt="" loading="lazy" onerror="this.remove()">` : ''; | |
| const buy = it.owned ? '<span class="sh-own">Owned ✓</span>' | |
| : `<a class="btn sm pass sh-buy" href="${shopHref(it.offer, from)}" rel="nofollow" data-offer="${esc(it.offer)}">${esc(it.price)}<small>${esc(it.per)}</small></a>`; | |
| const alt = !it.owned && it.alt ? `<span class="sh-alt">or ${it.alt.map((a) => `<a href="${shopHref(a.offer, from)}" rel="nofollow" data-offer="${esc(a.offer)}">${esc(a.label)}</a>`).join(' · ')}</span>` : ''; | |
| return `<li class="sh-i sh-${esc(it.kind)}"><span class="sh-pv" style="${swatch(it)}" aria-hidden="true"><i>${esc(it.icon || '🛍️')}</i>${img}</span>` | |
| + `<span class="sh-t"><b>${esc(it.name)}</b><span class="sh-w"><em>What you get:</em> ${esc(it.what)}</span>${alt}</span>${buy}</li>`; | |
| }).join(''); | |
| const store = OFFER.shopStore ? ` · <a href="${esc(OFFER.shopStore)}" target="_blank" rel="noopener">details in the store</a>` : ''; | |
| d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name)} shop</p> | |
| <h2>Looks, packs and more</h2><ul class="sh-list">${rows}</ul> | |
| <p class="fp-small">One-time prices unless marked /month or /year. Secure checkout by Stripe (shown as Link) · turns on as soon as you pay · your receipt code works on any device · 14-day fix-or-refund if it doesn't work as described (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). Prices in USD, + tax where applicable. Looks never change scores or the daily board; the daily challenge stays free for everyone${store}.</p> | |
| <p class="fp-small"><button class="sh-code" type="button" data-code>Already bought? Turn on your code</button></p></div>`; | |
| d.querySelectorAll('a[data-offer]').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', `shop-buy-${a.dataset.offer.replace(/^st-/, '')}`.slice(0, 40)))); | |
| d.querySelector('[data-code]').addEventListener('click', () => { d.close(); open('shop-code'); }); | |
| } | |
| /** Open the Shop sheet; `from` names the button that opened it (counted, and in the buy tag). */ | |
| export function openShop(from = 'button') { | |
| if (noShop()) { if (portal()) open(from); return; } // a portal build keeps its rewarded-ad sheet | |
| renderShop(from); | |
| const d = shopDialog(); | |
| if (d.showModal) { if (!d.open) d.showModal(); } else d.setAttribute('open', ''); | |
| count(cfg.game, 'cta', `shop-open-${slug(from)}`); | |
| } | |
| if (typeof document !== 'undefined' && !window.__cmxShop) { | |
| window.__cmxShop = 1; | |
| // capture phase on the document: runs before the game's own handlers on that button, and stops them | |
| document.addEventListener('click', (e) => { | |
| const b = e.target && e.target.closest && e.target.closest('[data-cmx-shop]'); | |
| if (!b || noShop()) return; // portal / Telegram: the game's own handler (if any) runs as before | |
| e.preventDefault(); e.stopPropagation(); | |
| openShop(b.dataset.cmxShop || 'menu'); | |
| }, true); | |
| for (const ev of ['pointerdown', 'mousedown', 'touchstart']) { // tap-to-retry overlays never see a Shop tap | |
| document.addEventListener(ev, (e) => { if (!noShop() && e.target && e.target.closest && e.target.closest('[data-cmx-shop]')) e.stopPropagation(); }, true); | |
| } | |
| paintShop(); | |
| } | |
| /** Call once per page: game id, display name and the game's own perks (listed first). Reads #cmx-pro=<code> after | |
| * checkout (?unlock=full marks this game's full-game return). Skin-pack codes are taken first by skins.js capture(). */ | |
| export function init(options) { | |
| cfg = { ...cfg, ...options }; | |
| const m = /[#&]cmx-key=([^&]+)/.exec(location.hash || '') || /[#&]cmx-pro=([^&]+)/.exec(location.hash || ''); | |
| if (m && SID.test(decodeURIComponent(m[1]))) { | |
| const q = new URLSearchParams(location.search); | |
| const fullReturn = q.get('unlock') === 'full'; | |
| q.delete('unlock'); | |
| history.replaceState(null, '', location.pathname + (q.toString() ? `?${q}` : '')); | |
| if (fullReturn) unlockGame(decodeURIComponent(m[1])); else unlock(decodeURIComponent(m[1])); | |
| } else recheck(); | |
| emit(); | |
| } | |
| // Direct checkout (17 Oct 2026, Workers cap fix): a click on a data.cybermaxtools.com/buy/<offer> link goes straight to the | |
| // Stripe/Polar checkout (static map https://cybermaxtools.com/store/buy-direct.json); the click is counted by a beacon. | |
| (() => { | |
| if (typeof document === 'undefined' || window.__cmxBd) return; | |
| window.__cmxBd = 1; | |
| let MAP = null; | |
| try { MAP = JSON.parse(sessionStorage.getItem('cmx-bd') || 'null'); } catch { /* none */ } | |
| if (!MAP) fetch('https://cybermaxtools.com/store/buy-direct.json').then((r) => r.json()).then((j) => { MAP = j; try { sessionStorage.setItem('cmx-bd', JSON.stringify(j)); } catch { /* full */ } }).catch(() => {}); | |
| document.addEventListener('click', (e) => { | |
| const a = e.target && e.target.closest && e.target.closest('a[href*="data.cybermaxtools.com/buy/"]'); | |
| if (!a || !MAP || !MAP.offers) return; | |
| try { | |
| const u = new URL(a.href); | |
| const k = /^\/buy\/([a-z0-9-]+)\/?$/.exec(u.pathname); | |
| const t = k && MAP.offers[k[1]]; | |
| if (!t) return; | |
| const s = String(u.searchParams.get('s') || 'unknown').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 80) || 'unknown'; | |
| a.href = t.split(MAP.ph).join(s); | |
| if (!navigator.webdriver) navigator.sendBeacon(u.href + (u.search ? '&' : '?') + 'b=1'); | |
| } catch { /* keep the counted link */ } | |
| }, true); | |
| })(); | |