File size: 9,169 Bytes
09eafd8
 
666e616
 
 
09eafd8
 
 
 
 
666e616
09eafd8
666e616
09eafd8
 
 
 
666e616
09eafd8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666e616
09eafd8
 
 
666e616
09eafd8
 
 
 
 
 
 
 
 
 
 
 
666e616
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09eafd8
 
 
 
 
 
 
 
 
 
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
// CyberMax Fun Pass: ONE optional supporter pass for every CyberMax game (copied into each game's web/ folder).
// Free games stay free. The pass removes the supporter nudges and unlocks extra packs, the full archive and themes.
//   Monthly $2.99 · Yearly $19 · Lifetime $29.99 once (Stripe Payment Links, pack revenue/packs/stripe/fun-pass).
// Buy: counted redirect https://data.cybermaxtools.com/buy/st-fun-pass-<plan>?s=game-<id> -> Stripe checkout.
// After checkout Stripe sends the buyer to data.cybermaxtools.com/unlock/fun-pass?session_id=..., which checks
// the Checkout Session (and, for a subscription, that it is still active) and returns to /games/ with #cmx-pro=<code>.
// Every game on the same site reads the same browser key, so one pass covers them all. The code on the receipt page
// unlocks any other device or browser. No account, no cookies, nothing personal stored. (c) 2026 CyberMax.
import { count, store, toast } from './cmx.js';

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 = ['No supporter nudges, ever', 'Extra puzzle packs in every game', 'The full archive of past dailies', 'Custom colour themes'];

let state = store(KEY);
const listeners = [];
let cfg = { game: 'games', name: 'CyberMax games', perks: [] };

export const isPro = () => !!(state && state.code);
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) {
  try {
    const r = await fetch(`${DATA}/unlock/fun-pass?json=1&session_id=${encodeURIComponent(code)}`);
    const j = await r.json();
    return j && typeof j.pro === 'boolean' ? j.pro : null;
  } catch { return null; }
}

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();
  } else 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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const buyUrl = (plan) => `${DATA}/buy/st-fun-pass-${plan}?s=game-${cfg.game}`;

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;
}

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()) {
    d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">CyberMax Fun Pass</p><h2>You're a supporter. Thank you!</h2>
      <p>The pass is on in this browser for every CyberMax game: ${perks.map(esc).join(' · ')}.</p>
      <p class="fp-small">Your code (paste it on another device to turn the pass on there):</p><code class="fp-code">${esc(state.code)}</code>
      <p class="fp-small">Manage or cancel a subscription any time from the link in your Stripe receipt email.</p></div>`;
    return;
  }
  d.innerHTML = `<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">${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('')}</div>
    <p class="fp-small">Secure Stripe checkout. Works in every CyberMax game. The free daily games stay free for everyone.</p>
    <details><summary>Already have a pass?</summary><form class="fp-form"><input aria-label="Pass code" autocomplete="off" placeholder="Receipt code (cs_live_…) or Gumroad licence key"><button class="btn sm" type="submit">Turn on</button></form></details></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}`)));
}

/** 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()) { 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];
  el.innerHTML = `<p><b>Want more today?</b> ${esc(cfg.extra || 'The Fun Pass unlocks extra packs, the full archive and themes in every CyberMax game.')}</p>
    <p class="fp-row"><a class="btn sm" href="${buyUrl(best.id)}-after" rel="nofollow" data-after>Get the Fun Pass: ${esc(best.price)}${esc(best.per)}</a> <button class="btn sm ghost" type="button" data-plans>See all plans</button></p>
    <p class="fp-small">Secure Stripe checkout · the pass turns on as soon as you pay · 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 free game stays free.</p>
    <div class="fp-cap"></div>`;
  el.querySelector('[data-after]').addEventListener('click', () => { count(cfg.game, 'cta', `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 Pro perks (listed first). */
export function init(options) {
  cfg = { ...cfg, ...options };
  const m = /[#&]cmx-pro=([^&]+)/.exec(location.hash || '');
  if (m && SID.test(decodeURIComponent(m[1]))) {
    history.replaceState(null, '', location.pathname + location.search);
    unlock(decodeURIComponent(m[1]));
  } else recheck();
  emit();
}