Spaces:
Running
Running
File size: 15,392 Bytes
e2c452c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | // 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 = /^(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: [] };
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()); }
function emit() {
document.documentElement.classList.toggle('cmx-pro', isPro());
listeners.forEach((fn) => { try { fn(isPro()); } catch { /* game hook */ } });
}
async function check(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(`${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 (!isPro() || Date.now() - (state.checked || 0) < RECHECK) return;
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() ? `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 = [...own, ...(own.some((p) => /archive/i.test(p)) ? [] : ['The full archive of past dailies']), 'Every bonus mode in this game'].slice(0, 6);
d.innerHTML = sellsGame()
? `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name)} · full game</p>
<h2>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</i><b>Full ${esc(OFFER.name)}</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:</p>
<div class="fp-plans">${planHtml()}</div>
<p class="fp-small">Secure Stripe checkout · turns on as soon as you pay · your code works on any device · 14-day promise: if it doesn't work as described, we fix it or refund you. The daily challenge and the first ${OFFER.demo || ''} levels stay free for everyone.</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 Stripe checkout. 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. */
export function afterWin(host, from = 'win') {
if (!host) return;
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 ? `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 Stripe checkout · turns on as soon as you pay · ${game ? 'one payment, no subscription' : 'cancel any time from your receipt email'} · 14-day promise: if it doesn't work as described, we fix it or refund you (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). 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);
}
/** 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-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();
}
|