Required member profile: username + verified email per position, gated at proof of ownership
Marty, 2026-09-16: leaders can write but 94% of positions cannot receive (47 of 771 have ever signed in to messaging, 482 messages sit 85% unread). profiles.js stores username + verified email per POSITION (one wallet holds one position, so a Triple Play holder has three; the person is the email and one email may hold several positions). Seeds the 40 emails already on file from member-alerts.json, pre-filled but unverified so confirming costs one tap. Writes are only ever accepted from a session that PROVED ownership: wallet personal_sign (messages.verifyChallenge) or the Telegram Mini App bridge. The public /my/<id> page is untouched and cannot write a profile, verified by test: all four endpoints 401 unauthenticated while /my/21 stays 200. Endpoints GET /api/public/profile, POST .../username, .../email-start, .../email-verify, plus GET /api/admin/profiles for coverage. Email codes: 6 digits, 15 min, 60s cooldown, 5/day, 6 tries. profile-gate.js is a two-step modal that cannot be dismissed, fired on dashboard boot (covers the Mini App landing) and right after a wallet sign-in. Chatbot canned answer + AI prompt updated. 28 unit tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,8 @@
|
||||
a:()=>`Yes — every position includes a real members area: <strong>The Circle Method</strong> (10-lesson recruiting & coaching course + a real cash-out walkthrough) on the <a href="/training">training page</a>, the <a href="/replays">weekly team webinar replays</a>, your <a href="/my">live coaching dashboard</a>, <a href="/tools">promo tools</a> personalized with your invite link, printable playbooks, wallet-verified team messaging, and the Telegram companion bot + Mini App. No passwords — access is proven by the wallet that owns your position (one free signature that can't move funds), and inside the Telegram Mini App it unlocks automatically. Lesson 1 and all the how-it-works videos are public so you can inspect everything before joining.`},
|
||||
{k:['no money','cant afford','can not afford',"can't afford",'broke','they dont have','waiting on payday','no funds to join'],
|
||||
a:()=>`"No money" is usually one of three things — sort it first. (1) Most often it means "this sounds expensive/complicated": entry is ${pol()} POL${usd()||" — a small one-time amount"} — payable with a regular debit card right on the <a href="/join-now">join page</a>. (2) Sometimes it's a priority thing: send the "Pocket Change" video from <a href="/tools">Promo Tools</a> and let it do the talking. (3) If they're genuinely broke: tell them honestly "don't join yet" — never rent money, never money they can't afford to lose. Ask "when's payday?" and follow up then — their invite link doesn't expire and the team keeps building publicly while they wait. One hard rule: never pay someone's entry for them.`},
|
||||
{k:['username','set up my profile','profile','my email','email address','why do you need my email','asking for my email','confirm my email','verification code','code did not arrive','required profile','member profile'],
|
||||
a:()=>`When you sign in to your position for the first time (one free wallet signature, or automatically inside the Telegram Mini App), you're asked for two things once: a <strong>username</strong> and a <strong>confirmed email</strong>. It takes about a minute and it's required to open the member area. Why: your team leader has no way to reach you otherwise, and your email is how you get a note the moment a payout lands in your wallet. Your email is never shown to other members and never sold, and you can change it any time. The code arrives in seconds — check spam the first time. If you already gave an email for payout alerts, it's pre-filled and you just confirm it. Viewing a position's public page at <a href="/my">rmcircle.team/my</a> never asks for anything.`},
|
||||
{k:['own the level','need to own','have to buy the level','catch without','still at scintilla','upline not upgraded','sponsor not upgraded','only pays 2','only pays two','only 2 payments','seems small','2 then 4','level is your reach'],
|
||||
a:()=>`Two facts that surprise people, both straight from the contract code: (1) To catch your two matrix children's FIRST upgrade (Ascensus), you only need to be <strong>qualified with your two directs</strong> — you do NOT need to own Ascensus yourself, because the contract checks the catcher against the level the buyer is jumping FROM (Scintilla, which everyone has). Owning levels extends your REACH: catching a Fabrica payment from 2 generations down requires Ascensus, and so on — your level is your reach, which is why the team stays one level ahead of its deepest active layer. There's a 97-second video on exactly this — "Your level is your reach" — on the <a href="/training">training page</a> (Video 7). And to watch the whole discipline run on a REAL position (every upgrade funded by the catch before it), see <a href="/training#textbook-play">"The Textbook Play"</a>. (2) Yes, each depth pays a fixed count — 2, then 4, then 8, one payment per person — but each deeper generation pays <strong>twice as much per person at twice the width</strong> (full-generation value quadruples per level), generations keep filling over time via recruiting and spillover, and pass-ups from under-leveled members add catches on top. Exact numbers: <a href="/how-pay-works#exact-costs">how-pay-works#exact-costs</a> — mechanical maximums, never promised income. Want it as a printable chart personalized to YOUR position (shows which generations you're catching now vs. which need your next level)? <a href="/generation-pay">rmcircle.team/generation-pay</a> — enter your ID and print.`},
|
||||
{k:['5 million','5,090','1.7 million','1,696','running total','total through','fabrica is 8','ascensus is 4','rows shifted','one level off','which level pays','level you hold','they pay you when'],
|
||||
|
||||
+1
-1
@@ -31,5 +31,5 @@
|
||||
</section>
|
||||
</main>
|
||||
<footer class="wrap disclaimer">All figures are read live from the RM Circle smart contract on Polygon and are historical facts, not a promise of future results. Participation involves cryptocurrency and smart-contract risk. Never use funds you cannot afford to lose.<div class="footer-links"><a href="/">Home</a><a href="/contract">Contract Security</a><a href="/disclaimer">Disclaimers</a><a href="/tools">Promo Tools</a><a href="/privacy">Privacy</a><a href="/refunds">Refunds</a></div></footer>
|
||||
<script src="/track.js"></script><script src="/qrlib.js"></script><script src="/rmc-wallet.js"></script><script src="/inapp-browser.js"></script><script src="/my.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script><script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/wallet-notice.js" defer></script> <script src="/announce.js?v=20260916a"></script>
|
||||
<script src="/track.js"></script><script src="/qrlib.js"></script><script src="/rmc-wallet.js"></script><script src="/inapp-browser.js"></script><script src="/profile-gate.js?v=20260916a"></script><script src="/my.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script><script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/wallet-notice.js" defer></script> <script src="/announce.js?v=20260916a"></script>
|
||||
</body></html>
|
||||
|
||||
@@ -419,6 +419,7 @@
|
||||
const sig=await eth.request({method:'personal_sign',params:[hex,account]});
|
||||
const v=await(await fetch('/api/public/msg-verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({address:account,signature:sig})})).json();
|
||||
if(!v.ok)throw new Error(v.error||'Verification failed.');
|
||||
try{ if(window.RMCProfile)await window.RMCProfile.require(); }catch(ge){}
|
||||
loadMsgUI(d);
|
||||
}catch(e){if(err)err.textContent=e.message||String(e);}
|
||||
}
|
||||
@@ -756,4 +757,7 @@
|
||||
const id=pathId();
|
||||
if(id&&!pathHasId)openDashTab=true; // bare /my resolved from storage = returning to your own page
|
||||
if(id)load(id);
|
||||
// Required member profile (username + verified email). No-ops for a visitor who
|
||||
// has not proved they own a position: the API 401s and the gate never shows.
|
||||
try{ if(window.RMCProfile)window.RMCProfile.require(); }catch(e){}
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// RM Circle: required member profile gate (Marty, 2026-09-16).
|
||||
//
|
||||
// The member area only opens once a position has a username and a VERIFIED email.
|
||||
// It fires the moment ownership is proved (wallet personal_sign, or the Telegram
|
||||
// Mini App bridge) and cannot be dismissed, because the whole point is that a
|
||||
// leader can reach every member. The public /my/<id> page is untouched: anyone
|
||||
// can still read the chain data there, and nobody can write a profile from it.
|
||||
//
|
||||
// Usage: await window.RMCProfile.require(); // resolves once the profile is complete
|
||||
(function () {
|
||||
'use strict';
|
||||
var back = null, resolveDone = null, state = null;
|
||||
var GOLD = '#d4af37', TEAL = '#4ed6cb';
|
||||
|
||||
function el(tag, css, html) {
|
||||
var e = document.createElement(tag);
|
||||
if (css) e.style.cssText = css;
|
||||
if (html != null) e.innerHTML = html;
|
||||
return e;
|
||||
}
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
|
||||
async function api(path, body) {
|
||||
var r = await fetch(path, body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } : {});
|
||||
var j = null; try { j = await r.json(); } catch (e) { j = {}; }
|
||||
if (!r.ok || j.error) throw new Error(j.error || 'Something went wrong. Try again.');
|
||||
return j;
|
||||
}
|
||||
|
||||
function shell() {
|
||||
back = el('div', 'position:fixed;inset:0;z-index:2147483100;display:flex;align-items:center;justify-content:center;' +
|
||||
'padding:20px;background:rgba(3,7,14,.88);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);overflow:auto;');
|
||||
back.setAttribute('role', 'dialog');
|
||||
back.setAttribute('aria-label', 'Finish setting up your member profile');
|
||||
var card = el('div', 'position:relative;width:100%;max-width:440px;max-height:94vh;overflow:auto;border-radius:20px;' +
|
||||
'background:#0a1119;border:1px solid rgba(212,175,55,.55);box-shadow:0 24px 70px rgba(0,0,0,.7);' +
|
||||
'font-family:system-ui,Segoe UI,Arial,sans-serif;color:#e8eef6;padding:22px 22px 20px;');
|
||||
card.id = 'pgCard';
|
||||
back.appendChild(card);
|
||||
document.body.appendChild(back);
|
||||
return card;
|
||||
}
|
||||
|
||||
function head(card, step) {
|
||||
card.innerHTML = '';
|
||||
card.appendChild(el('div', 'text-align:center;letter-spacing:2px;text-transform:uppercase;font-size:11px;font-weight:700;color:' + GOLD + ';margin-bottom:8px;',
|
||||
'Position #' + esc(state.id) + ' · step ' + step + ' of 2'));
|
||||
return card;
|
||||
}
|
||||
function note(text, color) {
|
||||
return el('p', 'margin:0 0 14px;font-size:13.5px;line-height:1.6;color:' + (color || '#9fb3c8') + ';', text);
|
||||
}
|
||||
function input(id, placeholder, value, type) {
|
||||
var i = el('input');
|
||||
i.id = id; i.type = type || 'text'; i.placeholder = placeholder; i.value = value || '';
|
||||
i.autocomplete = type === 'email' ? 'email' : 'off';
|
||||
i.style.cssText = 'width:100%;box-sizing:border-box;padding:12px 14px;border-radius:11px;border:1px solid rgba(255,255,255,.16);' +
|
||||
'background:rgba(255,255,255,.04);color:#fff;font-size:16px;margin-bottom:10px;';
|
||||
return i;
|
||||
}
|
||||
function button(label) {
|
||||
var b = el('button', 'width:100%;padding:13px 16px;border-radius:11px;border:none;cursor:pointer;font-size:15px;font-weight:700;' +
|
||||
'background:linear-gradient(180deg,' + GOLD + ',#b8932f);color:#1a1205;', label);
|
||||
return b;
|
||||
}
|
||||
function errLine() { return el('p', 'margin:0 0 10px;font-size:13px;color:#ff8b8b;display:none;'); }
|
||||
|
||||
// ---- step 1: username ----
|
||||
function stepUsername() {
|
||||
var card = head(document.getElementById('pgCard'), 1);
|
||||
card.appendChild(el('h2', 'margin:0 0 6px;font-size:21px;line-height:1.25;color:#fff;', 'Pick your username'));
|
||||
card.appendChild(note('This is how your team leader and the people in your line see you, instead of a bare member number. Letters, numbers or underscores, 3 to 20 characters.'));
|
||||
var err = errLine(); card.appendChild(err);
|
||||
var i = input('pgUser', 'e.g. ' + (state.suggest || 'member' + state.id), (state.profile && state.profile.username) || '');
|
||||
card.appendChild(i);
|
||||
var b = button('Save and continue');
|
||||
card.appendChild(b);
|
||||
var go = async function () {
|
||||
err.style.display = 'none'; b.disabled = true; b.textContent = 'Saving…';
|
||||
try {
|
||||
var r = await api('/api/public/profile/username', { username: i.value });
|
||||
state.profile = r.profile;
|
||||
next();
|
||||
} catch (e) {
|
||||
err.textContent = e.message; err.style.display = 'block'; b.disabled = false; b.textContent = 'Save and continue';
|
||||
}
|
||||
};
|
||||
b.addEventListener('click', go);
|
||||
i.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } });
|
||||
setTimeout(function () { i.focus(); }, 60);
|
||||
}
|
||||
|
||||
// ---- step 2: email + code ----
|
||||
function stepEmail() {
|
||||
var card = head(document.getElementById('pgCard'), 2);
|
||||
var prefill = (state.profile && state.profile.email) || '';
|
||||
card.appendChild(el('h2', 'margin:0 0 6px;font-size:21px;line-height:1.25;color:#fff;', 'Confirm your email'));
|
||||
card.appendChild(note('Two reasons this is required. Your leader can actually reach you, and you get a note the moment a payout lands in your wallet. ' +
|
||||
'It is never shown to other members, never sold, and you can change it any time.' +
|
||||
(prefill ? ' We already have this one on file for your payout alerts, so just confirm it.' : '')));
|
||||
var err = errLine(); card.appendChild(err);
|
||||
var i = input('pgEmail', 'you@example.com', prefill, 'email');
|
||||
card.appendChild(i);
|
||||
var b = button(prefill ? 'Send me the code' : 'Send me a code');
|
||||
card.appendChild(b);
|
||||
var codeWrap = el('div', 'display:none;margin-top:14px;padding-top:14px;border-top:1px solid rgba(255,255,255,.1);');
|
||||
var sentNote = note('', TEAL); codeWrap.appendChild(sentNote);
|
||||
var ci = input('pgCode', '6-digit code', '');
|
||||
ci.inputMode = 'numeric'; ci.maxLength = 6;
|
||||
codeWrap.appendChild(ci);
|
||||
var cb = button('Confirm and finish');
|
||||
codeWrap.appendChild(cb);
|
||||
var again = el('p', 'margin:10px 0 0;font-size:12.5px;color:#7f93a8;text-align:center;cursor:pointer;', 'Wrong address? Change it and send a new code.');
|
||||
again.addEventListener('click', function () { codeWrap.style.display = 'none'; b.disabled = false; b.textContent = 'Send me a code'; i.focus(); });
|
||||
codeWrap.appendChild(again);
|
||||
card.appendChild(codeWrap);
|
||||
|
||||
b.addEventListener('click', async function () {
|
||||
err.style.display = 'none'; b.disabled = true; b.textContent = 'Sending…';
|
||||
try {
|
||||
var r = await api('/api/public/profile/email-start', { email: i.value });
|
||||
sentNote.textContent = 'Code sent to ' + (r.to || i.value) + '. It lasts 15 minutes. Check spam the first time.';
|
||||
codeWrap.style.display = 'block'; b.textContent = 'Code sent';
|
||||
setTimeout(function () { ci.focus(); }, 60);
|
||||
} catch (e) {
|
||||
err.textContent = e.message; err.style.display = 'block'; b.disabled = false; b.textContent = 'Send me a code';
|
||||
}
|
||||
});
|
||||
var confirm = async function () {
|
||||
err.style.display = 'none'; cb.disabled = true; cb.textContent = 'Checking…';
|
||||
try {
|
||||
var r = await api('/api/public/profile/email-verify', { code: ci.value });
|
||||
state.profile = r.profile;
|
||||
done();
|
||||
} catch (e) {
|
||||
err.textContent = e.message; err.style.display = 'block'; cb.disabled = false; cb.textContent = 'Confirm and finish';
|
||||
}
|
||||
};
|
||||
cb.addEventListener('click', confirm);
|
||||
ci.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); confirm(); } });
|
||||
setTimeout(function () { i.focus(); }, 60);
|
||||
}
|
||||
|
||||
function done() {
|
||||
var card = head(document.getElementById('pgCard'), 2);
|
||||
card.innerHTML = '<div style="text-align:center;padding:14px 0 6px">' +
|
||||
'<div style="font-size:42px;line-height:1">✅</div>' +
|
||||
'<h2 style="margin:10px 0 6px;font-size:21px;color:#fff">You are all set, @' + esc(state.profile.username) + '</h2>' +
|
||||
'<p style="margin:0 0 16px;font-size:13.5px;line-height:1.6;color:#9fb3c8">Your leader can reach you now, and every payout to your wallet sends you a note.</p></div>';
|
||||
var b = button('Open my dashboard');
|
||||
b.addEventListener('click', close);
|
||||
card.appendChild(b);
|
||||
setTimeout(close, 2600);
|
||||
}
|
||||
function close() {
|
||||
if (back && back.parentNode) back.parentNode.removeChild(back);
|
||||
back = null;
|
||||
if (resolveDone) { var r = resolveDone; resolveDone = null; r(state && state.profile); }
|
||||
}
|
||||
function next() {
|
||||
if (!state.profile || !state.profile.username) return stepUsername();
|
||||
if (!state.profile.emailVerified) return stepEmail();
|
||||
return done();
|
||||
}
|
||||
|
||||
// Resolves once the profile is complete. Safe to call repeatedly: it returns
|
||||
// immediately when there is nothing to collect, and never shows for a visitor
|
||||
// who has not proved they own the position (the API 401s them).
|
||||
async function require_() {
|
||||
try { state = await api('/api/public/profile'); }
|
||||
catch (e) { return null; } // not signed in: nothing to gate
|
||||
if (state.profile && state.profile.complete) return state.profile;
|
||||
if (back) return null; // already open
|
||||
shell();
|
||||
return new Promise(function (res) { resolveDone = res; next(); });
|
||||
}
|
||||
async function status() { try { return await api('/api/public/profile'); } catch (e) { return null; } }
|
||||
|
||||
window.RMCProfile = { require: require_, status: status };
|
||||
})();
|
||||
Reference in New Issue
Block a user