From d4777ebed828d5b56807841b432e86bb4b0c0b2c Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 7 Oct 2026 13:20:25 -0700 Subject: [PATCH] Invite homepage visitors to support AC and attribute gifts --- system/netlify/functions/give.js | 6 +++- .../aesthetic.computer/disks/prompt.mjs | 6 ++-- .../public/give.aesthetic.computer/give.mjs | 3 ++ system/tests/give-checkout-browser.test.mjs | 5 +-- system/tests/give-checkout.test.mjs | 34 +++++++++++++++++++ 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/system/netlify/functions/give.js b/system/netlify/functions/give.js index ecdab2a478..a5e0620f30 100644 --- a/system/netlify/functions/give.js +++ b/system/netlify/functions/give.js @@ -42,6 +42,7 @@ export async function handler(event, context) { const amountCents = parseInt(body.amount) || 2500; const recurring = body.recurring === true; const email = body.email; // Optional: prefill email for logged-in users + const attribution = body.source === "homepage" ? { source: "homepage" } : {}; // Validate amount for currency if (amountCents < currencyConfig.min || amountCents > currencyConfig.max) { @@ -55,12 +56,13 @@ export async function handler(event, context) { const giveBaseUrl = dev ? `https://${event.headers.host}` : "https://give.aesthetic.computer"; const sessionConfig = { success_url: `${giveBaseUrl}/thanks.html?amount=${amountDisplay}¤cy=${currency}${recurring ? '&recurring=true' : ''}`, - cancel_url: `${giveBaseUrl}/`, + cancel_url: `${giveBaseUrl}/${attribution.source ? "?source=homepage" : ""}`, billing_address_collection: "auto", // Prefill email if user is logged in (makes checkout easier & ties to account) ...(email && { customer_email: email }), metadata: { type: recurring ? "subscription" : "gift", + ...attribution, amount: amountDisplay, currency: currency, }, @@ -78,6 +80,7 @@ export async function handler(event, context) { if (recurring) { // Monthly subscription sessionConfig.mode = "subscription"; + if (attribution.source) sessionConfig.subscription_data = { metadata: attribution }; sessionConfig.line_items = [ { price_data: { @@ -98,6 +101,7 @@ export async function handler(event, context) { } else { // One-time payment sessionConfig.mode = "payment"; + if (attribution.source) sessionConfig.payment_intent_data = { metadata: attribution }; // Note: Stripe submit_type only allows: auto, pay, book, donate - no "give" option // Using "pay" instead of "donate" for a more neutral label sessionConfig.line_items = [ diff --git a/system/public/aesthetic.computer/disks/prompt.mjs b/system/public/aesthetic.computer/disks/prompt.mjs index bb9058e1da..8b1170b9e8 100644 --- a/system/public/aesthetic.computer/disks/prompt.mjs +++ b/system/public/aesthetic.computer/disks/prompt.mjs @@ -6281,7 +6281,7 @@ function paint($) { // A steady link to Give, independent of the rotating promotional slot. if (showLoginCurtain && !$.system.prompt.input.canType && !$.system.prompt.input.text && activeSubscribers !== null && screen.height >= 280) { - const label = `${activeSubscribers.toLocaleString()} monthly subscriber${activeSubscribers === 1 ? "" : "s"}`; + const label = `${activeSubscribers.toLocaleString()} monthly supporter${activeSubscribers === 1 ? "" : "s"} ยท Join`; const position = { screen, center: "x", y: Math.floor(screen.height / 2 + 65) }; if (!subscribersBtn) subscribersBtn = new $.ui.TextButtonSmall(label, position); else subscribersBtn.reposition(position, label); @@ -8833,13 +8833,13 @@ function act({ } - // Subscriber count opens Give. + // Homepage support invitation opens Give. if (subscribersBtn && !subscribersBtn.btn.disabled) { subscribersBtn.btn.act(e, { down: () => downSound(), push: () => { pushSound(); - const url = "https://give.aesthetic.computer"; + const url = "https://give.aesthetic.computer/?source=homepage"; if (net.iframe) send({ type: "post-to-parent", content: { type: "openExternal", url } }); else jump(url); }, diff --git a/system/public/give.aesthetic.computer/give.mjs b/system/public/give.aesthetic.computer/give.mjs index 2d9e3c2a7b..6045019c0a 100644 --- a/system/public/give.aesthetic.computer/give.mjs +++ b/system/public/give.aesthetic.computer/give.mjs @@ -2,6 +2,8 @@ const $ = (id) => document.getElementById(id); const api = 'https://aesthetic.computer/api'; const params = new URLSearchParams(location.search); const prefillEmail = params.get('email'); +// Keep attribution to a reviewed placement, never arbitrary query text. +const source = params.get('source') === 'homepage' ? 'homepage' : undefined; if (prefillEmail) { const cleanUrl = new URL(location.href); cleanUrl.searchParams.delete('email'); @@ -79,6 +81,7 @@ $('form').addEventListener('submit', async (event) => { try { const payload = { amount: Math.round(Number($('amount').value) * 100), currency: $('currency').value, recurring: monthly() }; if (prefillEmail) payload.email = prefillEmail; + if (source) payload.source = source; const res = await fetch(`${api}/give`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: AbortSignal.timeout(20000), diff --git a/system/tests/give-checkout-browser.test.mjs b/system/tests/give-checkout-browser.test.mjs index 9d53abf665..49f5bb0f3c 100644 --- a/system/tests/give-checkout-browser.test.mjs +++ b/system/tests/give-checkout-browser.test.mjs @@ -23,13 +23,14 @@ test("Give opens standard and custom Stripe checkout and rejects other destinati return route.fulfill({ contentType: "text/html", body: "Checkout destination" }); }); const page = await context.newPage(); - for (const host of ["checkout.stripe.com", "pay.aesthetic.computer"]) { + for (const [host, source] of [["checkout.stripe.com", ""], ["pay.aesthetic.computer", "homepage"], ["pay.aesthetic.computer", "unreviewed-private-text"]]) { destination = `https://${host}/test-checkout`; - await page.goto("https://give.aesthetic.computer/"); + await page.goto(`https://give.aesthetic.computer/${source ? `?source=${source}` : ""}`); await page.locator("#give").click(); await page.waitForURL(destination); assert.equal(submitted.at(-1).amount, 800); assert.equal(submitted.at(-1).recurring, true); + assert.equal(submitted.at(-1).source, source === "homepage" ? "homepage" : undefined); } for (const rejected of ["https://pay.aesthetic.computer.evil.test/", "http://pay.aesthetic.computer/"]) { destination = rejected; diff --git a/system/tests/give-checkout.test.mjs b/system/tests/give-checkout.test.mjs index 5a39b5e449..c7a17e00b8 100644 --- a/system/tests/give-checkout.test.mjs +++ b/system/tests/give-checkout.test.mjs @@ -1,5 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; +import Stripe from "stripe"; import { handler } from "../netlify/functions/give.js"; test("Give constructs the installed Stripe client before validating amounts", async () => { @@ -19,3 +20,36 @@ test("Give constructs the installed Stripe client before validating amounts", as else process.env[key] = previous; } }); + + +test("Give retains only reviewed placement attribution on checkout and payment records", async (t) => { + const key = process.env.CONTEXT === "dev" ? "STRIPE_API_TEST_PRIV_KEY" : "STRIPE_API_PRIV_KEY"; + const previous = process.env[key]; + process.env[key] = "sk_test_validation_only"; + const sessions = Object.getPrototypeOf(new Stripe(process.env[key]).checkout.sessions); + let submitted; + t.mock.method(sessions, "create", async (config) => { + submitted = config; + return { id: "cs_test_attribution", url: "https://pay.aesthetic.computer/test" }; + }); + try { + for (const recurring of [true, false]) { + for (const source of ["homepage", undefined, "unreviewed-private-text", { homepage: true }]) { + const result = await handler({ httpMethod: "POST", headers: {}, + body: JSON.stringify({ amount: 800, currency: "usd", recurring, source }) }); + assert.equal(result.statusCode, 200); + const expected = source === "homepage" ? "homepage" : undefined; + assert.equal(submitted.metadata.source, expected); + const record = recurring ? submitted.subscription_data : submitted.payment_intent_data; + assert.deepEqual(record, expected ? { metadata: { source: expected } } : undefined); + assert.equal(new URL(submitted.cancel_url).searchParams.get("source"), expected ?? null); + assert.equal(submitted.mode, recurring ? "subscription" : "payment"); + assert.equal(submitted.line_items[0].price_data.unit_amount, 800); + assert.ok(!JSON.stringify(submitted).includes("unreviewed-private-text")); + } + } + } finally { + if (previous === undefined) delete process.env[key]; + else process.env[key] = previous; + } +}); -- 2.51.2