Lead capture pages + follow-up email sequence
- /join/<token>[?v=angle] now serves a capture page (email first, wallet later) with angle-matched hook copy, sponsor line, worked-example ledger, how-it-works, live package ladder, and per-angle og tags (og:url keeps ?v=). The sponsor cookie is set exactly as before; ?v= is remembered and stored on the account as joined_via (shown in admin Members). - drip.js: 4-step getting-started sequence (24h/48h/96h/168h) queued when a free account is created with the pre-checked opt-in; ticker every 10 min; signed /unsubscribe link in every email; MySQL + JSON storage. - Admin > Settings: edit the sequence as JSON, reset to defaults, send any step to the admin inbox; Overview shows follow-ups in flight. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+9
-9
@@ -33,7 +33,7 @@ function newCode(taken) {
|
||||
return c;
|
||||
}
|
||||
const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null,
|
||||
username: a.username || null, memberId: a.memberId || 0,
|
||||
username: a.username || null, memberId: a.memberId || 0, joinedVia: a.joinedVia || null,
|
||||
lineBannerUrl: a.lineBannerUrl || null, lineTargetUrl: a.lineTargetUrl || null,
|
||||
avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials: a.socials || null,
|
||||
chatAvailable: a.chatAvailable === false ? false : true, lastSeen: a.lastSeen || 0,
|
||||
@@ -74,11 +74,11 @@ const J = {
|
||||
if (!a || !a.pass || !checkPassword(password, a.pass)) return { error: 'Wrong email or password.' };
|
||||
return { ok: true, account: pub(a) };
|
||||
},
|
||||
async ensure(e, ref) {
|
||||
async ensure(e, ref, via) {
|
||||
let created = false;
|
||||
if (!this.db.byEmail[e]) {
|
||||
const code = newCode(c => this.db.byCode[c]);
|
||||
this.db.byEmail[e] = { email: e, pass: null, sponsorRef: ref, code, address: null, created: Date.now() };
|
||||
this.db.byEmail[e] = { email: e, pass: null, sponsorRef: ref, code, address: null, created: Date.now(), joinedVia: via || null };
|
||||
this.db.byCode[code] = e;
|
||||
created = true;
|
||||
this.save();
|
||||
@@ -171,7 +171,7 @@ const J = {
|
||||
|
||||
// ---- MySQL mode ----
|
||||
const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code,
|
||||
username: r.username, memberId: r.member_id || 0,
|
||||
username: r.username, memberId: r.member_id || 0, joinedVia: r.joined_via || null,
|
||||
lineBannerUrl: r.line_banner_url, lineTargetUrl: r.line_target_url,
|
||||
avatarUrl: r.avatar_url, bio: r.bio, socials: r.socials,
|
||||
chatAvailable: r.chat_available === 0 ? false : true, lastSeen: Number(r.last_seen || 0),
|
||||
@@ -195,12 +195,12 @@ const D = {
|
||||
if (!rows.length || !rows[0].pass || !checkPassword(password, rows[0].pass)) return { error: 'Wrong email or password.' };
|
||||
return { ok: true, account: rowPub(rows[0]) };
|
||||
},
|
||||
async ensure(e, ref) {
|
||||
async ensure(e, ref, via) {
|
||||
const code = newCode();
|
||||
let created = false;
|
||||
try {
|
||||
await db.q('INSERT INTO accounts (email,pass,sponsor_ref,code,address,created) VALUES (?,NULL,?,?,NULL,?)',
|
||||
[e, ref, code, Date.now()]);
|
||||
await db.q('INSERT INTO accounts (email,pass,sponsor_ref,code,address,created,joined_via) VALUES (?,NULL,?,?,NULL,?,?)',
|
||||
[e, ref, code, Date.now(), via || null]);
|
||||
created = true;
|
||||
} catch (err) {
|
||||
if (err.code !== 'ER_DUP_ENTRY') throw err;
|
||||
@@ -294,10 +294,10 @@ async function signup(email, password, sponsorRef) {
|
||||
return impl().signup(e, String(password), String(sponsorRef || ''));
|
||||
}
|
||||
async function login(email, password) { return impl().login(normEmail(email), String(password || '')); }
|
||||
async function ensure(email, sponsorRef) {
|
||||
async function ensure(email, sponsorRef, via) {
|
||||
const e = normEmail(email);
|
||||
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
|
||||
return impl().ensure(e, String(sponsorRef || ''));
|
||||
return impl().ensure(e, String(sponsorRef || ''), String(via || '').toLowerCase().slice(0, 20) || null);
|
||||
}
|
||||
async function byEmail(email) { return impl().byEmail(normEmail(email)); }
|
||||
async function byAddress(address) { return impl().byAddress(normAddr(address)); }
|
||||
|
||||
Reference in New Issue
Block a user