fix: free tier checkout - full Stripe form with SetupIntent (card saved, no charge)
Deploy marketing to Cloud Run / deploy (push) Has been cancelled
Deploy marketing to Cloud Run / deploy (push) Has been cancelled
This commit is contained in:
+14
-47
@@ -93,47 +93,15 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
<!-- Right: either free thank-you or sign-in + payment form -->
|
||||
<div class=\"checkout-form-wrap\">
|
||||
|
||||
" + (if is_free { "
|
||||
<p class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\">Create your account.</p>
|
||||
<p class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\">Create an account to reserve your free tier spot. You'll receive your download link and setup instructions at launch.</p>
|
||||
|
||||
<div id=\"auth-section\">
|
||||
<div class=\"checkout-social-btns\">
|
||||
<button type=\"button\" class=\"checkout-social-btn\" id=\"btn-google\" onclick=\"signInWith('google')\">
|
||||
<svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" aria-hidden=\"true\">
|
||||
<path d=\"M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z\" fill=\"#4285F4\"/>
|
||||
<path d=\"M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z\" fill=\"#34A853\"/>
|
||||
<path d=\"M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z\" fill=\"#FBBC05\"/>
|
||||
<path d=\"M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z\" fill=\"#EA4335\"/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</button>
|
||||
<button type=\"button\" class=\"checkout-social-btn\" id=\"btn-github\" onclick=\"signInWith('github')\">
|
||||
<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">
|
||||
<path d=\"M12 0C5.37 0 0 5.37 0 12c0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.385-1.335-1.755-1.335-1.755-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.605-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 21.795 24 17.298 24 12c0-6.63-5.37-12-12-12z\"/>
|
||||
</svg>
|
||||
Continue with GitHub
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id=\"auth-message\" class=\"checkout-message\" style=\"display:none;\"></div>
|
||||
|
||||
<div class=\"checkout-auth-divider\">
|
||||
<span id=\"auth-divider-label\">or create an account with email</span>
|
||||
</div>
|
||||
|
||||
<div id=\"email-auth-form\">
|
||||
<input type=\"email\" id=\"auth-email\" class=\"checkout-input\" placeholder=\"Email address\" autocomplete=\"email\" style=\"width:100%;display:block;margin-bottom:.75rem\">
|
||||
<input type=\"password\" id=\"auth-password\" class=\"checkout-input\" placeholder=\"Password - min 8 characters\" autocomplete=\"new-password\" style=\"width:100%;display:block;margin-bottom:.75rem\">
|
||||
<button type=\"button\" class=\"checkout-email-btn\" onclick=\"signUpWithEmail()\">Reserve free tier →</button>
|
||||
<p class=\"checkout-auth-hint\" style=\"margin-top:.75rem;text-align:center\">Already have an account? <a href=\"#\" onclick=\"showSignIn();return false;\" style=\"color:var(--navy)\">Sign in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
" } else { "
|
||||
<!-- Optional sign-in (collapsed by default - users can purchase without an account) -->
|
||||
<div id=\"auth-section\" style=\"display:none;\">
|
||||
<!-- Auth section: visible immediately for free, collapsed (optional) for paid plans -->
|
||||
<div id=\"auth-section\" " + (if is_free { "" } else { "style=\"display:none;\"" }) + ">
|
||||
" + (if is_free { "
|
||||
<p class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\">Create your account.</p>
|
||||
<p class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\">No charge today. Add your card to reserve your spot - you won't be billed until you upgrade.</p>
|
||||
" } else { "
|
||||
<p class=\"label\" style=\"margin-bottom: 1.25rem;\">Sign in (optional)</p>
|
||||
<p class=\"checkout-auth-hint\">Sign in to link this purchase to an existing account. Or skip and create one later - we'll match it to your email.</p>
|
||||
" }) + "
|
||||
|
||||
<div class=\"checkout-social-btns\">
|
||||
<button type=\"button\" class=\"checkout-social-btn\" id=\"btn-google\" onclick=\"signInWith('google')\">
|
||||
@@ -145,26 +113,24 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
</svg>
|
||||
Continue with Google
|
||||
</button>
|
||||
|
||||
<button type=\"button\" class=\"checkout-social-btn\" id=\"btn-github\" onclick=\"signInWith('github')\">
|
||||
<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">
|
||||
<path d=\"M12 0C5.37 0 0 5.37 0 12c0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.385-1.335-1.755-1.335-1.755-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.605-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 21.795 24 17.298 24 12c0-6.63-5.37-12-12-12z\"/>
|
||||
</svg>
|
||||
Continue with GitHub
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div id=\"auth-message\" class=\"checkout-message\" style=\"display:none;\"></div>
|
||||
|
||||
<div class=\"checkout-auth-divider\">
|
||||
<span id=\"auth-divider-label\">or create an account</span>
|
||||
<span id=\"auth-divider-label\">or " + (if is_free { "create an account with email" } else { "create an account" }) + "</span>
|
||||
</div>
|
||||
|
||||
<div id=\"email-auth-form\">
|
||||
<input type=\"email\" id=\"auth-email\" class=\"checkout-input\" placeholder=\"Email address\" autocomplete=\"email\" style=\"width:100%;display:block;margin-bottom:.75rem\">
|
||||
<input type=\"password\" id=\"auth-password\" class=\"checkout-input\" placeholder=\"Password - min 8 characters\" autocomplete=\"new-password\" style=\"width:100%;display:block;margin-bottom:.75rem\">
|
||||
<button type=\"button\" class=\"checkout-email-btn\" onclick=\"signUpWithEmail()\">Create account →</button>
|
||||
<button type=\"button\" class=\"checkout-email-btn\" onclick=\"signUpWithEmail()\">" + (if is_free { "Create account →" } else { "Create account →" }) + "</button>
|
||||
<p class=\"checkout-auth-hint\" style=\"margin-top:.75rem;text-align:center\">Already have an account? <a href=\"#\" onclick=\"showSignIn();return false;\" style=\"color:var(--navy)\">Sign in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,7 +138,9 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
<!-- Payment form (visible immediately - no auth wall) -->
|
||||
<div id=\"payment-section\">
|
||||
<div id=\"auth-badge\" style=\"display:none; margin-bottom: 1.5rem;\"></div>
|
||||
" + (if is_free { "" } else { "
|
||||
<p class=\"checkout-auth-hint\" id=\"signin-prompt\" style=\"margin-bottom:1.25rem;font-size:.8125rem\">Already have an account? <a href=\"#\" onclick=\"document.getElementById('auth-section').style.display='';this.parentNode.style.display='none';return false;\" style=\"color:var(--navy);text-decoration:underline\">Sign in</a> to link your purchase.</p>
|
||||
" }) + "
|
||||
|
||||
<!-- Founding Member attestation (only shown for founding plan) -->
|
||||
" + (if is_founding { "
|
||||
@@ -189,8 +157,8 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
</div>
|
||||
" } else { "" }) + "
|
||||
|
||||
<!-- Payment timing (Professional only, Founding always charges now) -->
|
||||
" + (if !is_founding { "
|
||||
<!-- Payment timing: Professional only (not Founding, not Free) -->
|
||||
" + (if !is_founding && !is_free { "
|
||||
<div style=\"margin-bottom:1.75rem\">
|
||||
<p class=\"label\" style=\"margin-bottom:1rem\">When would you like to be charged?</p>
|
||||
<div style=\"display:flex;flex-direction:column;gap:.625rem\">
|
||||
@@ -239,7 +207,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
<div id=\"payment-message\" class=\"checkout-message\" style=\"display:none;\"></div>
|
||||
|
||||
<button id=\"submit-btn\" class=\"checkout-submit\" type=\"submit\" disabled>
|
||||
<span id=\"submit-label\">Complete purchase →</span>
|
||||
<span id=\"submit-label\">" + (if is_free { "Reserve free tier →" } else { "Complete purchase →" }) + "</span>
|
||||
<span id=\"submit-spinner\" style=\"display:none;\">Processing…</span>
|
||||
</button>
|
||||
|
||||
@@ -253,7 +221,6 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
|
||||
</form>
|
||||
</div><!-- /payment-section -->
|
||||
" }) + "
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
+21
@@ -579,8 +579,29 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_contains(body, "\"professional\"") {
|
||||
plan = "professional"
|
||||
}
|
||||
if str_contains(body, "\"free\"") {
|
||||
plan = "free"
|
||||
}
|
||||
let timing: String = json_get_string(body, "timing")
|
||||
if str_eq(timing, "") { let timing = "now" }
|
||||
// Free tier: SetupIntent — save card details without charging.
|
||||
// Card is stored on a Stripe Customer; billing begins only if the
|
||||
// user later upgrades to a paid plan.
|
||||
if str_eq(plan, "free") {
|
||||
let si_body: String = "automatic_payment_methods[enabled]=true"
|
||||
+ "&usage=off_session"
|
||||
+ "&metadata[plan]=free"
|
||||
let auth_header: String = "Bearer " + stripe_key
|
||||
let si_resp: String = http_post_form_auth(
|
||||
"https://api.stripe.com/v1/setup_intents",
|
||||
si_body,
|
||||
auth_header)
|
||||
if str_starts_with(si_resp, "{") {
|
||||
let inner: String = str_slice(si_resp, 1, str_len(si_resp))
|
||||
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
|
||||
}
|
||||
return si_resp
|
||||
}
|
||||
// Hard cap: block founding checkouts when 1,000 spots are filled
|
||||
if str_eq(plan, "founding") {
|
||||
let current_sold: Int = get_sold()
|
||||
|
||||
Reference in New Issue
Block a user