Files
rm-circle-team-router/public/start.js
T
martbost 8987451cf6 Carry the placement choice with the referral too
Auditing the rest of the prospect paths after Terry's leak. Every entry
point funnels through /join/<id> — the Telegram Mini App redirects there,
member page-builder pages link there, the public /my/<id> share panel
links there — so the cookie covers all of them.

One gap left: ?direct=1 is the inviter's explicit decision to take the
join under themselves rather than route it down their leg, and only the
id was being remembered. A prospect who opened a direct link and then
wandered through the nav came back through the moving link instead — the
opposite of what the shared link asked for.

The flag now rides along in its own cookie, and both /start and /join-now
prefer an explicit choice over the position's default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:25:34 -05:00

110 lines
8.5 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.
let currentSponsor=null;
// Who invited this visitor, if anyone. A prospect who arrived on /join/<id> and then tapped
// a nav link used to land here and be offered the COMPANY rotation position instead — the
// inviter lost the referral just because the prospect clicked "Training" first. The invite
// is remembered in the rmc_ref cookie (set when the invite page was served) and can also be
// passed explicitly as ?ref=<id>.
function invitedBy(){
try{
const q=new URLSearchParams(location.search).get('ref');
if(q&&/^\d{1,15}$/.test(q))return q;
const m=document.cookie.match(/(?:^|;\s*)rmc_ref=(\d{1,15})/);
if(m)return m[1];
}catch(e){}
return null;
}
async function load(){
try{
const ref=invitedBy();
const [cr,sr]=await Promise.all([
fetch('/api/public/config'),
fetch(ref?('/api/public/member?id='+encodeURIComponent(ref)):'/api/public/current-sponsor')
]);
const c=await cr.json(); const s=await sr.json();
if(!sr.ok)throw new Error(s.error||'No sponsor assigned');
if(ref&&s.registered){
// Route exactly the way that member's own invite page would: their moving link sends
// the join to the next position in their leg, unless their position is set to take
// direct placements. Same rules, so the two pages can never disagree.
// an explicit ?direct=1/0 from the invite link wins over the position's default
const dq=(function(){try{const q=new URLSearchParams(location.search).get('direct');
if(q==='1'||q==='0')return q;
const m=document.cookie.match(/(?:^|;\s*)rmc_direct=([01])/);return m?m[1]:null;}catch(e){return null}})();
const goDirect=dq==='1'||(dq!=='0'&&(s.directDefault||!s.joinTarget));
const t=goDirect?{id:s.id,referralUrl:s.referralUrl,directCount:s.directCount}:s.joinTarget;
currentSponsor={id:t.id,name:null,directs:(t.directCount!==undefined?t.directCount:0),goal:2,referralUrl:t.referralUrl};
// say whose team this is, so it never reads as an anonymous "current placement"
const badge=document.querySelector('.live-badge');
if(badge){
const dot=badge.querySelector('.dot');
badge.textContent=' '+(String(t.id)===String(ref)?('Invited by Member #'+ref):('Member #'+ref+"'s team"));
if(dot)badge.insertBefore(dot,badge.firstChild);
}
const jb=document.getElementById('joinButton');
if(jb)jb.textContent=(String(t.id)===String(ref)?('Join Member #'+ref+"'s Team →"):('Join '+'#'+ref+"'s Team →"));
const note=document.getElementById('queueText');
if(note&&String(t.id)!==String(ref)){
note.textContent='Member #'+ref+' is already qualified, so your entry fills the next open spot in their team (#'+t.id+').';
}
}else{
currentSponsor=s.sponsor;
}
if(c.siteName){document.title=`Get Started | ${c.siteName}`;const bn=document.getElementById('brandName');if(bn)bn.textContent=c.siteName}
document.getElementById('sponsorId').textContent=`ID ${currentSponsor.id}`;
document.getElementById('sponsorIdInline').textContent=currentSponsor.id;
document.getElementById('sponsorName').textContent=currentSponsor.name||'Current RM Circle placement';
document.getElementById('progressText').textContent=`${currentSponsor.directs} / 2`;
document.getElementById('progressBar').style.width=`${Math.min(100,(currentSponsor.directs/2)*100)}%`;
document.getElementById('entryPol').textContent=c.premiumEntryPol;
document.getElementById('entryPol2').textContent=c.premiumEntryPol;
if(c.polUsd>0){
var usd=Math.round(c.premiumEntryPol*c.polUsd);
['entryUsd','entryUsd2'].forEach(function(id){var el=document.getElementById(id);if(el)el.textContent=' (~$'+usd+' today — POL’s price moves)';});
}
// carry the inviter through to enrolment — /join-now already knows how to place a
// ?ref properly (direct placement or next open spot in their leg). Without this the
// button dropped the referral on the floor and enrolled under the company rotation.
document.getElementById('joinButton').href='/join-now'+(ref?('?ref='+encodeURIComponent(ref)+(dq?('&direct='+dq):'')):'');
{const dl=document.getElementById('dappJoinLink');if(dl&&currentSponsor.referralUrl){dl.href=currentSponsor.referralUrl;dl.classList.remove('hidden');}}
// the company-queue line only means anything on the company-rotation path
// only on the company-rotation path — the invited path already wrote its own line here
if(!ref)document.getElementById('queueText').textContent=(c.showQueueProgress&&s.waitingCount!==undefined)
?`${s.waitingCount} team placement${s.waitingCount===1?'':'s'} waiting behind the current sponsor.`:'';
document.getElementById('supportBox').textContent=c.supportLabel||'Contact your team sponsor if you need help before joining.';
if(c.telegramUrl){const w=document.getElementById('supportLinkWrap'),a=document.getElementById('supportLink');a.href=c.telegramUrl;w.classList.remove('hidden')}
}catch(e){
document.getElementById('sponsorName').textContent=e.message;
document.getElementById('joinButton').classList.add('hidden');
document.getElementById('copyButton').classList.add('hidden');
}
}
document.getElementById('joinButton').addEventListener('click',()=>{try{if(currentSponsor)sessionStorage.setItem('ctb.joinedSponsor',currentSponsor.id)}catch(e){};fetch('/api/public/join-click',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source:window.ctbGetSource?window.ctbGetSource():'(direct)',clickid:window.ctbGetClickId?window.ctbGetClickId():''})}).catch(()=>{})});
document.getElementById('idSubmitForm').addEventListener('submit',async e=>{
e.preventDefault();
const form=e.currentTarget,input=form.elements.newId,msg=document.getElementById('idSubmitMsg');
const newId=input.value.trim();
const memberName=form.elements.memberName.value.trim();
if(!/^[0-9]{1,10}$/.test(newId)){msg.style.color='var(--danger)';msg.textContent='Numbers only — the ID shown by the RM Circle dApp.';return}
if(!memberName){msg.style.color='var(--danger)';msg.textContent='Add your name or Telegram handle so the team can reach you.';return}
let joinedSponsor='';try{joinedSponsor=sessionStorage.getItem('ctb.joinedSponsor')||''}catch(e){}
const sponsorId=joinedSponsor||(currentSponsor?currentSponsor.id:'?');
msg.style.color='var(--muted)';msg.textContent='Submitting…';
try{
const r=await fetch('/api/public/submit-id',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({newId,memberName,sponsorId,source:window.ctbGetSource?window.ctbGetSource():'(direct)',clickid:window.ctbGetClickId?window.ctbGetClickId():''})});
const d=await r.json();
if(!r.ok)throw new Error(d.error||'Submission failed');
form.classList.add('hidden');
msg.style.color='var(--ok)';
const dash=`<br><a href="/my/${newId}" style="color:var(--gold)">Open your Member Dashboard →</a>`;
if(d.duplicate)msg.innerHTML=`✓ ID <strong>${newId}</strong> was already submitted — you're on the list.${dash}`;
else if(d.path==='rotation')msg.innerHTML=`✓ Verified on the blockchain! ID <strong>${newId}</strong> joined through the team rotation under sponsor <strong>#${d.onchain.referrerId}</strong> (${d.onchain.tier}). The team has been notified and you're in line for the rotation that gets <strong>your</strong> 2.${dash}`;
else if(d.path==='leg')msg.innerHTML=`✓ Verified on the blockchain! ID <strong>${newId}</strong> is registered under sponsor <strong>#${d.onchain.referrerId}</strong>'s team build (${d.onchain.tier}) — your placement is recorded and the team has been notified. Heads up: the sponsor shown on this page is the site's team rotation, a separate group effort — your own team hub is your dashboard.${dash}`;
else if(d.path==='notfound'){msg.style.color='var(--danger)';msg.innerHTML=`⚠ ID <strong>${newId}</strong> isn't on the smart contract yet — double-check the number from the RM Circle dApp. Your submission is saved and the team will verify it manually.`;}
else msg.innerHTML=`✓ Got it! ID <strong>${newId}</strong> is submitted. The team has been notified and will verify your placement.${dash}`;
}catch(x){msg.style.color='var(--danger)';msg.textContent=x.message}
});
document.getElementById('copyButton').addEventListener('click',async()=>{
if(!currentSponsor)return; await navigator.clipboard.writeText(currentSponsor.id); const b=document.getElementById('copyButton'); const old=b.textContent;b.textContent='Copied ✓';setTimeout(()=>b.textContent=old,1400)
});
load();