feat(founder): payment→register flow + founder-only registration gate

- Next API routes: /api/register + /api/checkout (Stripe session) + /api/stripe-webhook
  (signature-verified; fulfils through be-crm), ported from the landing's serverless
  functions. Shared founder lead validation in src/lib/founder/lead.ts.
- /api/founder/lookup + /api/founder/session proxy be-crm's public endpoints.
- /thanks polls session_id → founder token → redirects to /portal/register?ft=<token>;
  falls back to the emailed link if the webhook is briefly delayed.
- register-flow: reads ?ft=, validates + prefills (email locked, name, phone) from the
  paid founder record, passes founderToken to crm.account.register (claims token +
  provisions owner). Open registration is now blocked — only a paid founder (ft) or a
  team invitee may register; everyone else sees a 'Founder access required' gate.
This commit is contained in:
tanweer919
2026-07-18 00:23:55 +05:30
parent a459fefb7f
commit d82d71d0b2
8 changed files with 605 additions and 5 deletions
+39 -2
View File
@@ -81,7 +81,8 @@
<div class="step"><span class="n">3</span><span class="t"><b>Watch for launch updates.</b> As a Founding Member you'll hear first about new features and your rollout timeline.</span></div>
</div>
<a class="cta" href="/">Close</a>
<a class="cta" id="setupCta" href="/portal/register">Set up your account</a>
<p class="fine" id="setupStatus" style="margin-top:10px;">Preparing your account…</p>
<p class="fine">
Questions? Email <a href="mailto:support@lynkeduppro.com">support@lynkeduppro.com</a> and reference your order below.
@@ -92,9 +93,10 @@
<script>
(function () {
var q = new URLSearchParams(location.search);
var demo = q.get("demo") === "1";
// Demo mode ($0 no-card flow from page/19.html): reframe the page so nobody
// thinks a real charge happened.
if (q.get("demo") === "1") {
if (demo) {
document.getElementById("badge").textContent = "🎬 Demo complete";
document.getElementById("headline").innerHTML = "That's the full flow — start to finish";
document.getElementById("lead").textContent =
@@ -104,6 +106,41 @@
// Show the Stripe session id (from success_url) so support can look up the order.
var id = q.get("session_id");
if (id) document.getElementById("ref").textContent = "Order reference: " + id;
var status = document.getElementById("setupStatus");
var cta = document.getElementById("setupCta");
if (demo || !id) {
// No real purchase to attribute — just offer the register page.
if (status) status.textContent = "";
return;
}
// Poll be-crm (via our proxy) for the founder token minted by the Stripe
// webhook, then redirect to the prefilled register page. The welcome email
// carries the same link, so this is a convenience — nothing is lost if the
// webhook is briefly delayed.
var tries = 0, MAX = 15; // ~30s at 2s intervals
function poll() {
tries++;
fetch("/api/founder/session?session_id=" + encodeURIComponent(id), { cache: "no-store" })
.then(function (r) { return r.json(); })
.then(function (d) {
if (d && d.status === "ready" && d.token) {
var url = "/portal/register?ft=" + encodeURIComponent(d.token);
if (cta) cta.setAttribute("href", url);
if (status) status.textContent = "Redirecting you to finish setup…";
window.location.replace(url);
return;
}
if (tries < MAX) { setTimeout(poll, 2000); return; }
if (status) status.innerHTML = "We emailed you a secure link to finish setting up your account — check your inbox (and spam).";
})
.catch(function () {
if (tries < MAX) { setTimeout(poll, 2000); return; }
if (status) status.innerHTML = "We emailed you a secure link to finish setting up your account — check your inbox (and spam).";
});
}
poll();
})();
</script>
</body>