Files
rm-circle-team-router/public/join-now.js
T
martbost 9dbe7eddcb Track the join funnel end-to-end through the self-enroll page
- Self-enroll pageviews now tracked as the "joinnow" event (track.js loaded
  on the page; server whitelist + admin tiles/columns extended)
- A confirmed on-chain registration in the wallet-connect flow now auto-posts
  to the submit-id API, so every self-enroll join records a submission with
  source + clickid attribution and fires the purchase event and BeMob postback
  with no manual ID entry
- Optional name/handle field on the join page feeds the submission and the
  team Telegram alert
- Admin Traffic panel: Join-now views tile, Join-now -> Confirmed conversion,
  and a per-source Confirmed column for campaign-level ROI reading

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 16:37:09 -05:00

138 lines
8.7 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.
/* RM Circle — public direct-join engine (dormant fallback; served only when
config.dappFallbackPublic is on). Talks only to window.ethereum (CSP-safe).
Auto-assigns the current rotation sponsor, registers the member's own wallet,
then sends them to their live position page /my/<newId>. Zero custody. */
(function(){
'use strict';
var CONTRACT='0x33bdaeefd6d17d80ae53816c916dfb26c4fb2daf';
var POLYGON_HEX='0x89';
var SEL_REGISTER='0x30de37e4'; // register(uint48 sponsorId, uint8 tier) payable
var SEL_GETMEMBER='0x2ada2596'; // getMember(address)
var SEL_GETCOSTS='0x735f87b9'; // getAllCosts()
var T_REGISTERED='0xe4a74887d749eb048f14bfef37b204477f3a5ff67055908b7c8cc62c202aef17';
var $=function(id){return document.getElementById(id);};
var eth=null; // resolved via the wallet picker on connect
var account=null, costs=null, sponsorId=null;
function log(msg,kind){var b=$('log');if(!b)return;var d=document.createElement('div');d.className='dj-log '+(kind||'');d.innerHTML=msg;b.appendChild(d);b.scrollTop=b.scrollHeight;}
function word(n){return BigInt(n).toString(16).padStart(64,'0');}
function addrWord(a){return a.toLowerCase().replace(/^0x/,'').padStart(64,'0');}
function polStr(wei){return (Number(wei)/1e18).toFixed(2);}
function hexWei(wei){return '0x'+BigInt(wei).toString(16);}
async function req(method,params){ if(!eth) throw new Error('No wallet found'); return await eth.request({method:method,params:params||[]}); }
async function ethCall(data){ return await req('eth_call',[{to:CONTRACT,data:data},'latest']); }
async function loadCosts(){
var r=await ethCall(SEL_GETCOSTS); var d=r.slice(2), w=[];
for(var i=0;i<d.length;i+=64) w.push(BigInt('0x'+d.slice(i,i+64)));
costs={ regPrem:w.slice(8,16) }; return costs;
}
function regValue(){ return costs.regPrem[0]*105n/100n; } // premium base * 1.05 (verified)
async function readPosition(addr){
try{ var r=await ethCall(SEL_GETMEMBER+addrWord(addr)); if(!r||r==='0x') return null;
var d=r.slice(2), level=parseInt(d.slice(5*64,6*64),16); return level?{level:level}:null; }catch(e){ return null; }
}
async function ensurePolygon(){
var cid=await req('eth_chainId'); if(cid===POLYGON_HEX) return true;
try{ await req('wallet_switchEthereumChain',[{chainId:POLYGON_HEX}]); return true; }
catch(e){ if(e&&e.code===4902){ await req('wallet_addEthereumChain',[{chainId:POLYGON_HEX,chainName:'Polygon Mainnet',nativeCurrency:{name:'POL',symbol:'POL',decimals:18},rpcUrls:['https://polygon-rpc.com'],blockExplorerUrls:['https://polygonscan.com']}]); return true; } throw e; }
}
async function waitReceipt(tx){ for(var i=0;i<60;i++){ try{ var r=await req('eth_getTransactionReceipt',[tx]); if(r) return r; }catch(e){} await new Promise(function(s){setTimeout(s,3000);}); } return null; }
function receiptEventId(rc){ var id=null; (rc.logs||[]).forEach(function(l){ if(l.address&&l.address.toLowerCase()===CONTRACT&&l.topics&&l.topics[0]===T_REGISTERED) id=parseInt(l.topics[1],16); }); return id; }
async function loadSponsor(){
try{
// personal invite: ?ref=<id> -> place in that member's leg via the site's moving-link joinTarget
var ref=new URLSearchParams(location.search).get('ref');
if(ref&&/^\d{1,15}$/.test(ref)){
try{ var rm=await (await fetch('/api/public/member?id='+ref)).json(); var t=rm&&rm.joinTarget;
if(t&&t.id){ sponsorId=String(t.id); $('sponsorLine').innerHTML='You’ll join under <strong>#'+t.id+'</strong>'+(String(t.id)!==String(ref)?' — the next open spot in #'+ref+'’s team':'')+'.'; return; }
}catch(e){}
}
var j=await (await fetch('/api/public/current-sponsor')).json(); var sp=j&&j.sponsor;
if(sp&&sp.id){ sponsorId=String(sp.id); $('sponsorLine').innerHTML='You’ll join the team under <strong>#'+sp.id+(sp.name?' ('+sp.name+')':'')+'</strong> — the current team placement.'; }
else $('sponsorLine').textContent='Loading the current team placement…';
}catch(e){ $('sponsorLine').textContent='Could not load the current sponsor — please refresh.'; }
}
async function refreshBalance(){ try{ var b=await req('eth_getBalance',[account,'latest']); $('bal').textContent=polStr(BigInt(b))+' POL'; }catch(e){} }
async function connect(){
try{
eth=await window.RMCWallet.pick();
if(!eth){ log('No wallet selected or detected. Install/unlock MetaMask, Trust, or SafePal and try again.','err'); return; }
attachWalletEvents();
log('Requesting wallet connection…');
var accs=await req('eth_requestAccounts'); account=accs[0];
await ensurePolygon(); await loadCosts();
$('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4);
$('connected').style.display='block'; $('connectBtn').style.display='none';
await refreshBalance();
$('joinCost').textContent=polStr(regValue())+' POL';
var pos=await readPosition(account);
if(pos){ $('already').style.display='block'; $('already').innerHTML='This wallet already holds position — <a href="/my">open your dashboard</a>. To create a <em>new</em> position, connect a different wallet.'; $('joinBtn').disabled=true; }
$('joinBox').style.display='block';
log('Connected. Review the amount, then Join.','ok');
}catch(e){ log('Connect failed: '+(e.message||e),'err'); }
}
async function doJoin(){
try{
if(!sponsorId){ log('Still loading the current sponsor — one moment.','err'); await loadSponsor(); if(!sponsorId) return; }
await ensurePolygon();
var val=regValue();
var data=SEL_REGISTER+word(sponsorId)+word(2);
log('Joining under #'+sponsorId+', Premium — paying '+polStr(val)+' POL. Confirm in your wallet…');
$('joinBtn').disabled=true;
var tx=await req('eth_sendTransaction',[{from:account,to:CONTRACT,value:hexWei(val),data:data}]);
log('Submitted — waiting for confirmation… <a href="https://polygonscan.com/tx/'+tx+'" target="_blank" rel="noopener">view tx</a>','ok');
var rc=await waitReceipt(tx);
if(!rc){ log('Still pending. Once it mines, open your dashboard at /my.',''); $('joinBtn').disabled=false; return; }
if(rc.status==='0x0'){ log('⚠️ The transaction reverted — no position created, and your POL was returned (minus gas). Please try again.','err'); $('joinBtn').disabled=false; return; }
var newId=receiptEventId(rc);
if(newId){
reportJoin(newId);
$('result').style.display='block';
$('resultId').textContent='#'+newId;
$('resultSub').innerHTML='Welcome to the team! Taking you to your position page…';
$('resultLink').href='/my/'+newId; $('resultLink').style.display='inline-flex';
log('🎉 Confirmed — your position is <strong>#'+newId+'</strong>. Redirecting to your dashboard…','ok');
try{ el_scroll('result'); }catch(e){}
setTimeout(function(){ location.href='/my/'+newId; }, 4500);
} else {
log('Confirmed on-chain. Open your dashboard at <a href="/my">/my</a> and enter your new ID (also shown on Polygonscan).','ok');
$('joinBtn').disabled=false;
}
}catch(e){ log('Join failed / rejected: '+(e.message||e),'err'); $('joinBtn').disabled=false; }
}
function el_scroll(id){ var e=$(id); if(e&&e.scrollIntoView) e.scrollIntoView({behavior:'smooth',block:'center'}); }
// Auto-confirm the join server-side: records the submission (source + clickid
// attribution), fires the purchase event + BeMob postback, enqueues rotation
// joins, and alerts the team — same pipeline as the manual submit-ID form.
function reportJoin(newId){
try{
var nameEl=$('memberName');
var name=(nameEl&&nameEl.value?nameEl.value.trim().slice(0,60):'')||('Self-enrolled #'+newId);
var src=(window.ctbGetSource?window.ctbGetSource():'(direct)');
var cid=(window.ctbGetClickId?window.ctbGetClickId():'');
fetch('/api/public/submit-id',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({newId:String(newId),memberName:name,sponsorId:String(sponsorId||'?'),source:src,clickid:cid})
}).catch(function(){});
}catch(e){}
}
function attachWalletEvents(){
if(!eth||!eth.on||eth.__rmcBound) return; eth.__rmcBound=true;
eth.on('accountsChanged',function(accs){ account=(accs&&accs[0])||null; if(!account){ $('connected').style.display='none'; $('connectBtn').style.display=''; } else { $('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4); refreshBalance(); } });
eth.on('chainChanged',function(){ if(account) refreshBalance(); });
}
document.addEventListener('DOMContentLoaded',function(){
loadSponsor();
$('connectBtn').addEventListener('click',connect);
$('joinBtn').addEventListener('click',doJoin);
});
})();