Files
rm-circle-team-router/public/suite.js
T
martbost 8964dfcf97 Detect in-app browsers — the actual reason #34 could not connect
Janie's screenshot showed the X / chevron / share / kebab chrome of TELEGRAM'S
in-app browser, not Trust's DApp browser. She tapped her sponsor's link in a
Telegram chat, Telegram opened it in its own webview, and that webview can
never hold a wallet. The page said "no wallet in this browser", she said she
was using Trust — both true, completely at cross purposes.

Our advice made it worse: "open your wallet's browser and type the address" is
useless to someone whose whole reason for being on the page is that they
tapped a link.

Now we name the browser they are actually in — Telegram, Facebook, Instagram,
LINE, Messenger, X, or a generic Android WebView — say plainly that it cannot
hold a wallet and that this is not their fault, and give them the one useful
thing: a button that copies the address to the clipboard so they can paste it
into Trust. There is an execCommand fallback because older webviews have no
clipboard API, and leaving a button that silently does nothing would be worse
than not having one.

Detection deliberately requires the absence of window.ethereum before calling
something an in-app browser: wallet browsers ARE WebViews, and those are
exactly the ones that work. Verified against Telegram, Android WebView, Trust
(correctly ignored) and mobile Safari (correctly ignored).

This will hit far more members than Janie — every link tapped inside a chat
app lands in the same trap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 06:18:35 -05:00

134 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The Circle Suite — P0 tool wall. Wallet sign-in (same challenge/verify flow
// as the training gate) -> live level from the contract -> tiles light up.
// Every tile is always VISIBLE: unlocked at-or-below the member's level,
// dimmed above it. Future tools carry an honest "in development" tag.
(function () {
'use strict';
var LEVELS = ['Scintilla', 'Ascensus', 'Fabrica', 'Culmen', 'Apex', 'Fastigium', 'Vertex', 'Corona'];
var TOOLS = [
{ lv: 1, ico: '🎬', name: 'Promo Center', desc: 'Videos, posts, swipes, emails and the banner kit — every piece pre-personalized with your link.', href: '/tools', live: true },
{ lv: 1, ico: '🖨️', name: 'Printable Handouts', desc: 'Five full-color designs that print two per page — with YOUR QR code on the paper.', href: '/flyers', live: true },
{ lv: 1, ico: '✅', name: 'Fast Start + Weekly Rhythm', desc: 'The 48-hour launch checklist and the 20-minute weekly routine, personalized and printable.', href: '/fast-start', live: true },
{ lv: 1, ico: '📊', name: 'Generation Pay Chart', desc: 'Your personal when-does-each-generation-pay-me chart, read live from the contract.', href: '/generation-pay', live: true },
{ lv: 1, ico: '🎓', name: 'The Circle Method', desc: 'The 10-lesson recruiting and coaching course. Lessons unlock with this same wallet.', href: '/training', live: true },
{ lv: 1, ico: '📈', name: 'Live Dashboard', desc: 'Your pipeline, organization bar, coach panel and wallet-verified team messages.', href: '/my', live: true },
{ lv: 1, ico: '🤖', name: 'AI Coach', desc: '24/7 answers about the system, in 21+ languages — the chat bubble on every page of this site.', href: '#', live: true, coach: true },
{ lv: 2, ico: '✍️', name: 'Copy Engine', desc: 'An AI copywriter tuned to this business: posts, DMs, follow-ups and objection replies with your link filled in.', href: '/suite/copy', live: true },
{ lv: 2, ico: '🧱', name: 'Page Builder', desc: 'Answer four questions, get your own hosted invitation page — your story, your angle video, your QR.', href: '/suite/page', live: true },
{ lv: 3, ico: '📧', name: 'Email Engine', desc: 'Welcome series, follow-up sequences and broadcasts in the team voice — exportable to any autoresponder.', href: '/suite/email', live: true },
{ lv: 3, ico: '🎥', name: 'Video Maker', desc: 'The team’s master promo videos rendered with your personal end-card — your name, your QR, your link.', href: '/suite/video', live: true },
{ lv: 4, ico: '🗣️', name: 'Voice Profile', desc: 'Teach the Copy Engine and Email Engine how you actually talk, so everything they write for you stops sounding like a company.', href: '/suite/voice', live: true },
{ lv: 5, ico: '🔬', name: 'Split Tester', desc: 'Run two or three versions of an ad head to head and find out which wording people actually click. Apex is where your impressions jump to 50,000 — enough for the answer to mean something.', href: '/suite/split', live: true },
{ lv: 1, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising — run banners or AI-written text ads on the team’s own ad network. Monthly impressions scale with your level: 2,500 at Scintilla up to 150,000 at Corona.', href: '/suite/traffic', live: true },
{ lv: 6, ico: '🎨', name: 'Co-Branded Pages', desc: 'Your own name, monogram and colour on every page you build — so it stops looking like a page on the team site and starts looking like yours.', href: '/suite/brand', live: true },
{ lv: 6, ico: '🏭', name: 'Funnel Factory', desc: 'A separate hosted landing page for every audience you talk to — each on its own address, each one something you can point a different ad at.', href: '/suite/funnel', live: true },
{ lv: 7, ico: '🎁', name: 'Team Grants', desc: 'Hand Suite capacity down into your own organisation — impressions, generations, page builds. Your level stops being something you bought for yourself and becomes something you give your team.', href: '/suite/grants', live: true },
{ lv: 8, ico: '🛰️', name: 'Network Intelligence', desc: 'What is actually working across every ad the whole team runs — best angles, headlines and creative, pooled and anonymised. Nobody can buy this; it is the team\'s own accumulated result, and it compounds every month.', href: '/suite/intel', live: true },
{ lv: 7, ico: '🧭', name: 'Leader Ops', desc: 'Everyone below you, read live from the contract and sorted by who needs you most — plus a written coaching plan you can teach forward to your own two.', href: '/suite/leader', live: true },
{ lv: 8, ico: '👑', name: 'Founder Desk', desc: 'A whole week of promotion built in one pass — posts, outreach messages, an email and text ads — plus an API key to pull your position, your organisation and the writer into anything you already run.', href: '/suite/founder', live: true }
];
var $ = function (id) { return document.getElementById(id); };
var me = null; // {id, level, levelName, tierName, directCount, inOrg}
function pips() {
var host = $('suPips');
host.querySelectorAll('.su-pip').forEach(function (p) { p.remove(); });
for (var k = 1; k <= 8; k++) {
var p = document.createElement('span');
p.className = 'su-pip' + (me && me.inOrg && me.level >= k ? (k === 8 ? ' gold' : ' on') : '');
p.title = 'Level ' + k + ' · ' + LEVELS[k - 1];
host.appendChild(p);
}
}
function render() {
var grid = $('suGrid');
grid.innerHTML = '';
var lastLv = 0;
// Render in LEVEL order, not array order. The array is maintained by hand
// and a tile whose level changes (the Traffic Desk moved from L5 to L1)
// would otherwise emit a stray section header in the middle of the wall.
// Sorting here means the array can be edited in any order, forever.
TOOLS.slice().sort(function (a, b) { return a.lv - b.lv; }).forEach(function (t) {
if (t.lv !== lastLv) {
lastLv = t.lv;
var h = document.createElement('div');
h.className = 'su-lvhead';
h.innerHTML = '<span class="n">L' + t.lv + '</span><span class="nm">' + LEVELS[t.lv - 1] + '</span><span class="rule"></span>';
grid.appendChild(h);
}
var open = !!(me && me.inOrg && me.level >= t.lv);
var d = document.createElement('div');
d.className = 'su-tile ' + (me ? (open ? 'open' : 'locked') : 'locked');
var badge = me
? (open ? '✓ YOURS' : 'UNLOCKS AT ' + LEVELS[t.lv - 1].toUpperCase())
: 'LEVEL ' + t.lv;
d.innerHTML = '<span class="badge">' + badge + '</span><span class="ico">' + t.ico + '</span><h3>' + t.name + '</h3><p>' + t.desc + '</p>'
+ (t.live
? (open || !me ? '<a class="go" href="' + t.href + '"' + (t.coach ? ' data-coach="1"' : '') + '>Open →</a>' : '')
: '<span class="dev">IN DEVELOPMENT — ships with the Suite build-out</span>');
grid.appendChild(d);
});
var coach = grid.querySelector('[data-coach]');
if (coach) coach.addEventListener('click', function (ev) { ev.preventDefault(); var f = document.querySelector('.chat-fab'); if (f) f.click(); });
}
async function refreshMe() {
try {
var r = await fetch('/api/public/suite-me');
if (!r.ok) return false;
me = await r.json();
if (me.beta && !me.allowed) {
$('suMe').style.display = 'block';
$('suMe').innerHTML = '<b>#' + me.id + '</b> — you’re early! The Circle Suite is in <b>team beta</b> right now. Your license is already reserved by your position, and every tool below activates for you the moment we open the doors. Nothing to do — watch the team channel.';
$('suConnect').style.display = 'none';
me = null; pips(); render();
return true;
}
$('suMe').style.display = 'block';
$('suMe').innerHTML = me.inOrg
? '<b>#' + me.id + '</b> · ' + me.tierName + ' · Level ' + me.level + ' (' + me.levelName + ')' +
(me.directCount >= 2 ? ' · <span style="color:var(--teal)">✓ qualified</span>' : ' · ' + me.directCount + '/2 directs') +
'<br>Your license covers <b>Level ' + me.level + '</b> and everything below it' +
(me.level < 8 ? ' — your next upgrade extends it to ' + LEVELS[me.level] + '.' : ' — full Suite.')
: '<b>#' + me.id + '</b> — this position isn’t inside our team’s organization. The Suite is a benefit of building with our circle; ask the person who shared this page about joining through the team.';
$('suConnect').style.display = 'none';
pips(); render();
return true;
} catch (e) { return false; }
}
async function connect() {
var btn = $('suConnect'), err = $('suErr');
err.style.display = 'none';
btn.disabled = true; btn.textContent = 'Connecting…';
try {
var eth = await window.RMCWallet.pick();
if (!eth) { {var _ia=(window.RMCInApp&&window.RMCInApp.notice('/suite'))||'';if(_ia){err.innerHTML=_ia;if(err.style)err.style.display='block';window.RMCInApp.bind('/suite');}else{err.textContent = 'This browser has no wallet in it. Open this page INSIDE your wallet app instead: open Trust, MetaMask, SafePal or Coinbase, find its Browser (or DApps) tab, and type rmcircle.team/suite into that address bar. On iPhone, Trust no longer has a browser — use MetaMask or SafePal, or ask your sponsor to link you instead.';}} err.style.display = 'block'; btn.disabled = false; btn.textContent = '🔑 Connect wallet — light up my tools'; return; }
var accs = await eth.request({ method: 'eth_requestAccounts' });
var account = accs[0];
try { await window.RMCWallet.ensureChain(eth); } catch (ce) {}
var ch = await (await fetch('/api/public/msg-challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: account }) })).json();
if (!ch.message) throw new Error(ch.error || 'Could not start sign-in.');
var sig = await eth.request({ method: 'personal_sign', params: [ch.message, account] });
var vr = await (await fetch('/api/public/msg-verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: account, signature: sig }) })).json();
if (!vr.ok) throw new Error(vr.error || 'Signature check failed.');
await refreshMe();
} catch (e) {
var m = String((e && e.message) || e);
err.textContent = /not registered|no position/i.test(m)
? 'This wallet doesn’t hold a position yet. The Suite comes with one — see the Getting Started page.'
: 'Sign-in didn’t complete: ' + m + ' — tap the button to try again.';
err.style.display = 'block';
}
btn.disabled = false; btn.textContent = '🔑 Connect wallet — light up my tools';
}
document.addEventListener('DOMContentLoaded', function () {
pips(); render();
$('suConnect').addEventListener('click', connect);
refreshMe(); // silent: already signed in (training gate / Mini App bridge)
});
})();