Files
rm-circle-team-router/public/join-now.js
T
martbost 7e390aa48e Join: let chosen positions default to direct placement
The moving link is team-first by design: it routes a new join to whoever needs
directs next, so the position whose link was actually clicked earns no entry
reward. Measured on #21 that came to 80 joins in seven days and zero entry
rewards, while the traffic paying for those joins was the owner's.

For a position being deliberately built out, that default is backwards. Any ID
listed in the new directDefaultIds config now behaves as ?direct=1 on its own
/join/<id> link - including every promo tool, flyer QR and downline-builder
entry that resolves through it - without needing the parameter appended.

?direct=0 still forces rotation for a single share, so the team-first behaviour
stays available rather than being replaced.

Config rather than hard-coded, and editable from the admin settings form, so
which positions get this can change without a deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 10:29:55 -05:00

238 lines
15 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.
fetch('/api/public/config').then(function(r){return r.json()}).then(function(c){if(c&&c.polUsd>0)window.__polUsd=c.polUsd;}).catch(function(){});
/* 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']); }
// Immutable contract constant (getAllCosts premium[0], verified on-chain).
// Fallback when the WALLET's internal RPC hiccups (MetaMask iOS surfaces
// that as "Load failed") — prices can never change, so baking it is safe.
var REG_PREM_WEI=343406593406593400832n;
async function loadCosts(){
try{
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) };
}catch(e){ costs={ regPrem:[REG_PREM_WEI] }; }
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 qp=new URLSearchParams(location.search);
var ref=qp.get('ref');
// Direct link (?direct=1): opt out of rotation — register straight under
// the inviter (spillover in their team), never the next-to-qualify member.
if(ref&&/^\d{1,15}$/.test(ref)&&qp.get('direct')==='1'){
sponsorId=String(ref); $('sponsorLine').innerHTML='You’ll join <strong>directly under #'+ref+'</strong> — a spillover placement in their team.'; return;
}
if(ref&&/^\d{1,15}$/.test(ref)){
try{ var rm=await (await fetch('/api/public/member?id='+ref)).json();
// position configured to default to direct, not explicitly overridden
if(rm&&rm.directDefault&&qp.get('direct')!=='0'){
sponsorId=String(ref); $('sponsorLine').innerHTML='You’ll join <strong>directly under #'+ref+'</strong> — a spillover placement in their team.'; return;
}
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.'; }
}
var lastBal=null;
async function refreshBalance(){ try{ var b=await req('eth_getBalance',[account,'latest']); lastBal=BigInt(b); $('bal').textContent=polStr(lastBal)+' POL'; updateFundBox(); }catch(e){} }
// Card on-ramp: shown only when the connected wallet can't cover the entry.
function updateFundBox(){
var box=$('fundBox'); if(!box||!costs||lastBal===null) return;
var need=regValue();
if(lastBal>=need){ box.style.display='none'; return; }
var shortPol=Math.max(62,Math.ceil(Number(need-lastBal)/1e18)+5);
$('fundMsg').textContent='This wallet holds '+polStr(lastBal)+' POL; the Premium entry is '+polStr(need)+' POL plus a little gas (like a car, every blockchain transaction burns a bit of fuel - keep some in the tank). You can buy the remaining ~'+shortPol+' POL'+(window.__polUsd>0?' (about $'+Math.round(Number(shortPol.replace(/,/g,''))*window.__polUsd)+')':'')+' with a debit/credit card, Apple Pay, or Google Pay — delivered straight to this wallet.';
box.dataset.pol=shortPol;
box.style.display='block';
}
async function openMoonpay(){
try{
var box=$('fundBox');
var r=await (await fetch('/api/public/moonpay-url?address='+encodeURIComponent(account||'')+'&pol='+encodeURIComponent((box&&box.dataset.pol)||''))).json();
if(r&&r.url){ window.open(r.url,'_blank','noopener'); log(r.signed?'MoonPay opened in a new tab with your wallet address pre-filled — complete the purchase there, then come back and refresh your balance.':'MoonPay opened in a new tab. Choose POL on the Polygon network and paste YOUR wallet address ('+account+') as the destination, then come back and refresh your balance.','ok'); }
}catch(e){ log('Could not open MoonPay: '+(e.message||e),'err'); }
}
async function finishConnect(){
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';
}
async function connect(){
try{
eth=await window.RMCWallet.pick();
if(!eth){ showWalletHelp(); log('No wallet detected in this browser. On a phone, use the "Open in your wallet" buttons above — on a computer, install/unlock MetaMask and refresh.','err'); return; }
attachWalletEvents();
log('Requesting wallet connection…');
var accs=await req('eth_requestAccounts'); account=accs[0];
await ensurePolygon();
await finishConnect();
log('Connected. Review the amount, then Join.','ok');
}catch(e){
var m=String((e&&e.message)||e);
if(/load failed|failed to fetch|network|timeout/i.test(m)){
log('Your wallet’s network connection hiccuped (this happens inside wallet apps sometimes) — tap <strong>Connect wallet</strong> once more and it usually goes right through.','err');
}else{
log('Connect failed: '+m,'err');
}
}
}
// Inside a wallet's in-app browser the wallet is often ALREADY connected —
// recognize it silently on load (eth_accounts never prompts) instead of
// greeting a connected user like a stranger. Skipped when multiple wallets
// are injected (desktop) so no picker UI ever pops uninvited.
async function autoDetect(){
try{
var list=window.RMCWallet.list();
var p=list.length===1?list[0].provider:(list.length===0?window.ethereum:null);
if(!p) return;
eth=p; attachWalletEvents();
var accs=await req('eth_accounts',[]);
if(!accs||!accs.length){ log('✅ Wallet detected in this browser — tap "Connect wallet" to continue.','ok'); return; }
account=accs[0];
var cid=await req('eth_chainId');
if(cid===POLYGON_HEX){
await finishConnect();
log('✅ Already connected as '+account.slice(0,6)+'…'+account.slice(-4)+'. Review the amount, then Join.','ok');
}else{
$('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4);
$('connected').style.display='block';
log('Wallet connected ('+account.slice(0,6)+'…'+account.slice(-4)+') but on another network — tap "Connect wallet" to switch to Polygon and continue.','ok');
}
}catch(e){}
}
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){}
try{sessionStorage.setItem('rmc.fresh','1');}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){}
}
// Mobile browsers have no injected wallet — offer deep links that reopen
// THIS page (query string included, so ?ref= survives) inside the wallet
// app's own browser, where window.ethereum exists.
function showWalletHelp(){
var box=$('walletHelp'); if(!box) return;
var here=location.host+location.pathname+location.search;
var mm=$('mmDeep'); if(mm) mm.href='https://metamask.app.link/dapp/'+here;
var tw=$('twDeep'); if(tw) tw.href='https://link.trustwallet.com/open_url?coin_id=966&url='+encodeURIComponent(location.href);
// Fill the "type this in your wallet's browser" hint with the sponsor's
// real join path so SafePal/Coinbase/etc. users land on the right page.
var wb=$('wbId'); if(wb){ var ref=new URLSearchParams(location.search).get('ref'); wb.parentNode.textContent='rmcircle.team'+(ref&&/^\d{1,15}$/.test(ref)?'/join/'+ref:'/join-now'); }
box.style.display='block';
}
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();
// Telegram Mini App webview: no wallet can ever be injected here, so lead
// with the deep links immediately — the join finishes in the wallet app's
// own browser (same page, sponsor + attribution carried in the URL).
if(window.__rmcInTg){
showWalletHelp();
var hd=$('walletHelp')&&$('walletHelp').firstElementChild;
if(hd)hd.textContent='📱 One tap to finish in your wallet app.';
log('You’re in Telegram — joining happens in your wallet app’s secure browser. Tap your wallet above; this page reopens there with your sponsor already set.','ok');
}
$('connectBtn').addEventListener('click',connect);
$('joinBtn').addEventListener('click',doJoin);
var fb=$('fundBtn'); if(fb)fb.addEventListener('click',openMoonpay);
var fr=$('fundRefresh'); if(fr)fr.addEventListener('click',function(){ log('Checking your balance…'); refreshBalance(); });
setTimeout(autoDetect,350); // let EIP-6963 announcements land first
});
})();