From e14c541e5e78e7a12f74a86ec78ece18a07687b5 Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 18 Aug 2026 07:09:57 -0500 Subject: [PATCH] Add dormant public direct-join fallback (/join-now) + post-join redirect /join-now: public self-enroll page gated behind config.dappFallbackPublic (default off -> redirects to /start, invisible until needed). Auto-assigns the current rotation sponsor via /api/public/current-sponsor, registers the user's own wallet, then redirects to /my/ so new members land on their live position instead of guessing their ID. HTML lives in private/, served only via the flagged route. Reuses the proven contract engine. Admin /direct-join now also shows a 'View position ->' link to /my/ after a successful join. Co-Authored-By: Claude Opus 4.8 --- private/direct-join.html | 1 + private/join-now.html | 52 ++++++++++++++++++ public/direct-join.js | 4 +- public/join-now.js | 112 +++++++++++++++++++++++++++++++++++++++ server.js | 4 +- 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 private/join-now.html create mode 100644 public/join-now.js diff --git a/private/direct-join.html b/private/direct-join.html index 09b59db..1c170ee 100644 --- a/private/direct-join.html +++ b/private/direct-join.html @@ -68,6 +68,7 @@
✅ Success — save this
—
+
diff --git a/private/join-now.html b/private/join-now.html new file mode 100644 index 0000000..4675190 --- /dev/null +++ b/private/join-now.html @@ -0,0 +1,52 @@ +Join the Team | RM Circle + + + + +
+
Direct enrollment

Join the RM Circle team.

Connect your wallet and enter your Premium position on the Polygon smart contract — paid straight from your wallet, verifiable on-chain. You’ll be placed under the current team position automatically. New here? Watch the training first.

+ +

Loading the current team placement…

+ +
+

1 · Connect your wallet

+

Use MetaMask or Trust Wallet on Polygon, funded with enough POL for your Premium entry plus a little gas.

+ + + +
+ + + + + +

Activity

+ +

No income is guaranteed. Cryptocurrency and smart-contract participation carry risk. Only use funds you can afford to lose.

+
+ + diff --git a/public/direct-join.js b/public/direct-join.js index ab7080d..41d5e7a 100644 --- a/public/direct-join.js +++ b/public/direct-join.js @@ -78,7 +78,7 @@ function receiptEventId(rc,topic0){ var id=null; (rc.logs||[]).forEach(function(l){ if(l.address&&l.address.toLowerCase()===CONTRACT&&l.topics&&l.topics[0]===topic0) id=parseInt(l.topics[1],16); }); return id; } - function showResult(big,sub){ var el=$('result'); if(!el) return; $('resultId').textContent=big; $('resultSub').textContent=sub; el.style.display='block'; try{el.scrollIntoView({behavior:'smooth',block:'center'});}catch(e){} } + function showResult(big,sub,linkId){ var el=$('result'); if(!el) return; $('resultId').textContent=big; $('resultSub').textContent=sub; var lk=$('resultLink'); if(lk){ if(linkId){ lk.href='/my/'+linkId; lk.style.display='inline-flex'; } else lk.style.display='none'; } el.style.display='block'; try{el.scrollIntoView({behavior:'smooth',block:'center'});}catch(e){} } async function connect(){ if(!eth){ log('No wallet detected. Open this page in a browser with MetaMask (extension or the MetaMask in-app browser).','err'); return; } @@ -123,7 +123,7 @@ if(rc.status==='0x0'){ log('⚠️ Reverted on-chain — no position created; your POL was returned (minus gas). Re-check the sponsor ID and amount.','err'); return; } refreshBalance(); var newId=receiptEventId(rc,T_REGISTERED); - if(newId){ showResult('#'+newId,'Your new position under sponsor #'+sponsor+' is live. Save this ID.'); log('🎉 Confirmed — your new position is #'+newId+'.','ok'); } + if(newId){ showResult('#'+newId,'Your new position under sponsor #'+sponsor+' is live. Save this ID.',newId); log('🎉 Confirmed — your new position is #'+newId+'.','ok'); } else log('Confirmed on-chain, but could not read the ID from the receipt — check the tx on Polygonscan.','ok'); }catch(e){ log('Register failed / rejected: '+(e.message||e),'err'); } } diff --git a/public/join-now.js b/public/join-now.js new file mode 100644 index 0000000..0799d86 --- /dev/null +++ b/public/join-now.js @@ -0,0 +1,112 @@ +/* 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/. 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=window.ethereum; + 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 — 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(){ + if(!eth){ log('No wallet detected. Open this page in MetaMask or Trust Wallet (extension, or the wallet app’s in-app browser).','err'); return; } + try{ + 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 — open your dashboard. To create a new 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… view tx','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){ + $('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 #'+newId+'. 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 /my 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'}); } + + document.addEventListener('DOMContentLoaded',function(){ + loadSponsor(); + $('connectBtn').addEventListener('click',connect); + $('joinBtn').addEventListener('click',doJoin); + if(eth&ð.on){ + 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(); }); + } + if(!eth) log('No wallet detected. Use MetaMask or Trust Wallet (extension, or the wallet app’s in-app browser).','err'); + }); +})(); diff --git a/server.js b/server.js index ff5b380..376925e 100644 --- a/server.js +++ b/server.js @@ -557,7 +557,7 @@ async function handleApi(req,res,pathname){ const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors}); } if(req.method==='PATCH'&&pathname==='/api/admin/config'){ - const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next}); + const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next}); } const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/); if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'}); @@ -619,7 +619,7 @@ const server=http.createServer(async(req,res)=>{ if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]); } let file; - if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/start'});return res.end();}else{ + if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/start'});return res.end();}else{ const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file=''; } if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);