diff --git a/accounts.js b/accounts.js
index 6ab9912..bbb538a 100644
--- a/accounts.js
+++ b/accounts.js
@@ -16,6 +16,18 @@ function load() {
try { db = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) {}
if (!db || !db.v) db = { v: 2, byEmail: {}, byAddress: {}, joins: 0 };
if (db.v === 1) { db.v = 2; db.byEmail = db.byEmail || {}; } // early rehearsal file
+ if (!db.byCode) db.byCode = {};
+ // every account carries a share code from day one (backfill older records)
+ for (const a of Object.values(db.byEmail)) {
+ if (!a.code) { a.code = genCode(); db.byCode[a.code] = a.email; }
+ else if (!db.byCode[a.code]) db.byCode[a.code] = a.email;
+ }
+}
+function genCode() {
+ let c;
+ do { c = crypto.randomBytes(5).toString('base64url').replace(/[-_]/g, '').slice(0, 7).toLowerCase(); }
+ while (!c || c.length < 6 || (db.byCode && db.byCode[c]) || /^\d+$/.test(c));
+ return c;
}
function save() {
try {
@@ -45,21 +57,24 @@ const normEmail = e => String(e || '').trim().toLowerCase();
const normAddr = a => String(a || '').trim().toLowerCase();
// ---- email accounts (the normal join path) ----
-function signup(email, password, sponsorId) {
+function signup(email, password, sponsorRef) {
const e = normEmail(email);
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
if (String(password || '').length < 8) return { error: 'Password needs at least 8 characters.' };
if (db.byEmail[e]) return { error: 'That email already has an account. Log in instead.' };
+ const code = genCode();
db.byEmail[e] = {
email: e,
pass: hashPassword(password),
- sponsorId: Number(sponsorId) || 0, // first touch, written on-chain at first purchase
+ sponsorRef: String(sponsorRef || ''), // first touch; resolved to a chain id at buy time
+ code,
address: null,
created: Date.now()
};
+ db.byCode[code] = e;
db.joins += 1;
save();
- return { ok: true, account: publicView(db.byEmail[e]) };
+ return { ok: true, created: true, account: publicView(db.byEmail[e]) };
}
function login(email, password) {
const e = normEmail(email);
@@ -70,15 +85,23 @@ function login(email, password) {
}
// Passwordless path: a verified email code proves ownership, so the account
// may exist with no password at all.
-function ensure(email, sponsorId) {
+function ensure(email, sponsorRef) {
const e = normEmail(email);
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
+ let created = false;
if (!db.byEmail[e]) {
- db.byEmail[e] = { email: e, pass: null, sponsorId: Number(sponsorId) || 0, address: null, created: Date.now() };
+ const code = genCode();
+ db.byEmail[e] = { email: e, pass: null, sponsorRef: String(sponsorRef || ''), code, address: null, created: Date.now() };
+ db.byCode[code] = e;
db.joins += 1;
+ created = true;
save();
}
- return { ok: true, account: publicView(db.byEmail[e]) };
+ return { ok: true, created, account: publicView(db.byEmail[e]) };
+}
+function byCode(code) {
+ const e = db.byCode[String(code || '').toLowerCase()];
+ return e ? publicView(db.byEmail[e]) : null;
}
function byEmail(email) { const a = db.byEmail[normEmail(email)]; return a ? publicView(a) : null; }
function byAddress(address) {
@@ -104,8 +127,9 @@ function linkWallet(email, address) {
}
function publicView(a) {
- return { email: a.email, sponsorId: a.sponsorId || 0, address: a.address || null, created: a.created };
+ return { email: a.email, sponsorRef: a.sponsorRef || String(a.sponsorId || '') || '',
+ code: a.code || null, address: a.address || null, created: a.created };
}
function count() { return Object.keys(db.byEmail).length; }
-module.exports = { init, signup, login, ensure, byEmail, byAddress, linkWallet, count };
+module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, linkWallet, count };
diff --git a/public/assets/home.js b/public/assets/home.js
index 5c18c54..e30021d 100644
--- a/public/assets/home.js
+++ b/public/assets/home.js
@@ -5,10 +5,11 @@
IAP.$('contractLink').href = c.explorer + '/address/' + c.contract;
const sp = await (await fetch('/api/sponsor')).json();
- if (sp.sponsorId) {
+ if (sp.invited) {
const el = IAP.$('sponsorLine');
el.hidden = false;
- el.textContent = 'You were invited by member #' + sp.sponsorId + '. Your purchases pay their team, and your own link will do the same for you.';
+ el.textContent = (sp.sponsorId ? 'You were invited by member #' + sp.sponsorId + '.' : 'You arrived through a member’s invite.')
+ + ' Your purchases pay their team, and your own link will do the same for you.';
}
async function loadLadder() {
@@ -41,7 +42,10 @@
await IAPWallet.signIn();
}
IAP.status('Confirm the purchase in your wallet…');
- const r = await IAPWallet.buy(Number(btn.dataset.id), sp.sponsorId || 0, btn.dataset.cost);
+ // resolve the sponsor at buy time: a code referrer who activated since
+ // page load still gets locked in
+ const spNow = await (await fetch('/api/sponsor')).json();
+ const r = await IAPWallet.buy(Number(btn.dataset.id), spNow.sponsorId || 0, btn.dataset.cost);
if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.');
IAP.status('Purchase settled on-chain. Credits are yours, payouts delivered. Watch it on the ledger.', 'ok');
IAP.refreshNavWallet();
diff --git a/public/assets/my.js b/public/assets/my.js
index b0d8325..c28f88e 100644
--- a/public/assets/my.js
+++ b/public/assets/my.js
@@ -34,21 +34,25 @@
$('campaignCard').hidden = !me.memberId;
if (me.memberId) loadCampaigns();
+ // the share link works from day one; codes resolve to your chain id later
+ if (me.refCode || me.memberId) {
+ $('inviteLine').textContent = location.origin + '/join/' + (me.refCode || me.memberId);
+ $('copyInvite').hidden = false;
+ } else {
+ $('inviteLine').textContent = 'Sign in with your email to get your link.';
+ $('copyInvite').hidden = true;
+ }
if (me.memberId) {
const bc = me.buyerCount || 0;
$('qualLine').innerHTML = '' + bc + ' qualifying buyer(s) referred
'
+ (bc >= 5 ? 'Level 3 unlocked: full three-level earnings'
: bc >= 2 ? 'Level 2 unlocked · ' + (5 - bc) + ' more for level 3'
: (2 - bc) + ' more buyer(s) of $20+ unlock level 2');
- $('inviteLine').textContent = location.origin + '/join/' + me.memberId;
- $('copyInvite').hidden = false;
loadActivity();
} else {
- $('qualLine').textContent = 'Level 1 pays the moment you are on-chain. Referrals who buy packages of $20 or more unlock levels 2 and 3.';
- $('inviteLine').textContent = me.address
- ? 'Activate payouts above and your invite link appears here.'
- : 'Link a wallet and switch on payouts to get your invite link.';
- $('copyInvite').hidden = true;
+ $('qualLine').innerHTML = 'Share your link now. Then switch on payouts (free, above) '
+ + 'before your people start buying: the contract locks each buyer to their sponsor at '
+ + 'their first purchase, and payments only route to wallets that are switched on.';
}
}
diff --git a/public/contract.html b/public/contract.html
index 0ee2007..6df0384 100644
--- a/public/contract.html
+++ b/public/contract.html
@@ -5,7 +5,7 @@