[object Object]

← back to Lawyer Directory Builder

Iter 25: /upgrade/thanks — post-Stripe-success editorial confirmation

bf9516dfbc72fba1ee8a3411b9bd8f3cb5551fd5 · 2026-05-04 00:52:41 -0700 · SteveStudio2

Replaced bare 4-line paid-success page with full editorial confirmation:
- Volume strip masthead 'Edition 2026 · Order Confirmed'
- Italic-metal display 'Order received, counselor.'
- Order № receipt strip (3 cols: Order № EZ-YYYY-MMDD-NNNN / Logged
  timestamp / Charged $amount italic-metal tabular-nums)
- 'What happens next' 3-cell hairline-ruled grid:
  * i. Intake brief — within 24 hours
  * ii. First proof — five business days (7-day go-live clock starts here)
  * iii. Revisions & go-live — 14-day window
- Footer: refund + buyer-delay carve-out + 'Open dashboard →' quiet CTA
- 'For the record' trust block: §6155 + neutral proximity ranking +
  Stripe PCI flow + Rule 7.x buyer-responsibility line

Localhost-only ?_demo=paid flag for visual testing (gated on hostname).

Stripe checkout product description updated: 'live in 7 days' (unconditional)
→ 'typical go-live: 7 business days from content intake' — was emitting on
customer Stripe receipts, fixed at source.

Debate-team-fast critique applied:
1. PROMISE CALIBRATION — defined 'two rounds of substantive revisions'
   as 'copy edits, image swaps, layout adjustments within the original
   four-page scope. New pages or scope changes are quoted separately.'
   Closes the FTC §5 ambiguity around 'substantive.'
2. BUYER-DELAY CARVE-OUT — 'Timeline begins on receipt of your content
   brief; delays in returning the brief shift the proof and go-live
   dates by the same number of days.' Closes the unconditional-SLA gap.
3. RULE 7.X CLARIFICATION — added to trust block: 'Site copy you supply
   or approve remains your professional responsibility under California
   Rule of Professional Conduct 7.x — we can flag obvious advertising-
   rule risks but do not provide legal review.'
4. YEAR-2 HOSTING — 'invoiced separately at our standard rate' →
   'billed separately at our published rate' (rate must be public, not
   discretionary).

Pending for next iter: pull payment-method last-4 + Stripe lockup from
PaymentIntent metadata into the receipt strip ('Visa •••• 4242 ·
processed by Stripe') — currently absent, the only element that reads
'demo' rather than 'production' on this surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit bf9516dfbc72fba1ee8a3411b9bd8f3cb5551fd5
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 4 00:52:41 2026 -0700

    Iter 25: /upgrade/thanks — post-Stripe-success editorial confirmation
    
    Replaced bare 4-line paid-success page with full editorial confirmation:
    - Volume strip masthead 'Edition 2026 · Order Confirmed'
    - Italic-metal display 'Order received, counselor.'
    - Order № receipt strip (3 cols: Order № EZ-YYYY-MMDD-NNNN / Logged
      timestamp / Charged $amount italic-metal tabular-nums)
    - 'What happens next' 3-cell hairline-ruled grid:
      * i. Intake brief — within 24 hours
      * ii. First proof — five business days (7-day go-live clock starts here)
      * iii. Revisions & go-live — 14-day window
    - Footer: refund + buyer-delay carve-out + 'Open dashboard →' quiet CTA
    - 'For the record' trust block: §6155 + neutral proximity ranking +
      Stripe PCI flow + Rule 7.x buyer-responsibility line
    
    Localhost-only ?_demo=paid flag for visual testing (gated on hostname).
    
    Stripe checkout product description updated: 'live in 7 days' (unconditional)
    → 'typical go-live: 7 business days from content intake' — was emitting on
    customer Stripe receipts, fixed at source.
    
    Debate-team-fast critique applied:
    1. PROMISE CALIBRATION — defined 'two rounds of substantive revisions'
       as 'copy edits, image swaps, layout adjustments within the original
       four-page scope. New pages or scope changes are quoted separately.'
       Closes the FTC §5 ambiguity around 'substantive.'
    2. BUYER-DELAY CARVE-OUT — 'Timeline begins on receipt of your content
       brief; delays in returning the brief shift the proof and go-live
       dates by the same number of days.' Closes the unconditional-SLA gap.
    3. RULE 7.X CLARIFICATION — added to trust block: 'Site copy you supply
       or approve remains your professional responsibility under California
       Rule of Professional Conduct 7.x — we can flag obvious advertising-
       rule risks but do not provide legal review.'
    4. YEAR-2 HOSTING — 'invoiced separately at our standard rate' →
       'billed separately at our published rate' (rate must be public, not
       discretionary).
    
    Pending for next iter: pull payment-method last-4 + Stripe lockup from
    PaymentIntent metadata into the receipt strip ('Visa •••• 4242 ·
    processed by Stripe') — currently absent, the only element that reads
    'demo' rather than 'production' on this surface.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 src/server/upgrade.ts | 121 ++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 108 insertions(+), 13 deletions(-)

diff --git a/src/server/upgrade.ts b/src/server/upgrade.ts
index 921c456..9ae7d16 100644
--- a/src/server/upgrade.ts
+++ b/src/server/upgrade.ts
@@ -98,10 +98,33 @@ th{color:var(--ink-mute);font-weight:500;font-size:10px;text-transform:uppercase
 .up-total .val sub{font-size:11px;color:var(--ink-mute);font-family:var(--sans);font-weight:500;margin-left:6px;letter-spacing:.18em;text-transform:uppercase;vertical-align:baseline}
 .up-mini{font-size:11px;color:var(--ink-mute);letter-spacing:.06em;margin:24px 0 0;line-height:1.6}
 .up-mini strong{color:var(--ink-soft);font-weight:500}
+/* /upgrade/thanks editorial confirmation */
+.up-receipt{margin:32px 0 12px;padding:24px 32px;border:1px solid var(--rule);background:linear-gradient(180deg,rgba(184,153,104,.025),transparent);display:grid;grid-template-columns:repeat(3,1fr);gap:32px}
+@media (max-width:720px){.up-receipt{grid-template-columns:1fr;gap:14px;padding:20px 24px}}
+.up-r-row{display:flex;flex-direction:column;gap:4px}
+.up-r-label{font-size:9px;letter-spacing:.3em;text-transform:uppercase;color:var(--ink-mute);font-weight:500}
+.up-r-val{font-family:var(--serif);font-style:italic;color:var(--metal);font-size:18px;font-variant-numeric:tabular-nums;letter-spacing:.02em}
+.up-next{margin:64px 0 0;padding:48px 0 0;border-top:1px solid var(--rule)}
+.up-next-h{font-family:var(--serif);font-weight:400;font-size:24px;color:var(--ink);margin:0 0 28px;letter-spacing:-.005em}
+.up-next-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}
+@media (max-width:780px){.up-next-grid{grid-template-columns:1fr}}
+.up-next-cell{padding:32px 28px;border-right:1px solid var(--rule);display:flex;flex-direction:column;gap:12px}
+.up-next-cell:last-child{border-right:none}
+@media (max-width:780px){.up-next-cell{border-right:none;border-bottom:1px solid var(--rule);padding:28px 20px}.up-next-cell:last-child{border-bottom:none}}
+.up-next-num{font-family:var(--serif);font-style:italic;font-weight:300;color:transparent;-webkit-text-stroke:1px var(--metal);font-size:28px;line-height:1;letter-spacing:-.02em}
+.up-next-cell h4{font-family:var(--serif);font-weight:400;font-size:18px;line-height:1.25;color:var(--ink);margin:0;letter-spacing:-.005em}
+.up-next-cell p{font-size:13px;line-height:1.6;color:var(--ink-mute);margin:0;flex:1}
+.up-footer-row{margin:48px 0 0;padding-top:32px;border-top:1px solid var(--rule);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:16px}
+.up-cta-quiet{padding:16px 28px;background:transparent;color:var(--metal-glow);border:1px solid var(--metal);font-size:11px;letter-spacing:.22em;text-transform:uppercase;font-weight:500;text-decoration:none;display:inline-flex;align-items:center;transition:background 280ms ease,color 280ms ease,letter-spacing 280ms ease}
+.up-cta-quiet:hover{background:var(--metal);color:var(--noir);letter-spacing:.26em}
+.up-trust{padding:24px 28px;border:1px solid var(--rule);background:linear-gradient(180deg,rgba(184,153,104,.018),transparent)}
+.up-trust-eyebrow{font-size:9px;letter-spacing:.3em;text-transform:uppercase;color:var(--metal);font-weight:500;margin:0 0 12px}
+.up-trust p{font-size:13px;line-height:1.7;color:var(--ink-mute);margin:0}
+
 @media (prefers-reduced-motion: no-preference){
   .scroll-reveal{opacity:0;transform:translateY(22px);transition:opacity 900ms cubic-bezier(.16,.84,.3,1),transform 900ms cubic-bezier(.16,.84,.3,1)}
   .scroll-reveal.in-view{opacity:1;transform:none}
-  .scroll-reveal.s1{transition-delay:80ms}.scroll-reveal.s2{transition-delay:160ms}
+  .scroll-reveal.s1{transition-delay:80ms}.scroll-reveal.s2{transition-delay:160ms}.scroll-reveal.s3{transition-delay:240ms}
 }
 *:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(184,153,104,.45);border-radius:1px}
 *:focus:not(:focus-visible){outline:none}
@@ -250,7 +273,7 @@ router.post('/upgrade', async (req, res) => {
           currency: 'usd',
           product_data: {
             name: 'Counsel & Bar — EZ Upgrade',
-            description: 'Personalized website rebuild · live in 7 days · 14-day revisions · 1 year hosting included',
+            description: 'Personalized website rebuild · typical go-live: 7 business days from content intake · 14-day revision window from first proof · first-year hosting included',
           },
           unit_amount: 49900,
         },
@@ -278,30 +301,102 @@ router.post('/upgrade', async (req, res) => {
 
 router.get('/upgrade/thanks', async (req, res) => {
   const sid = (req.query.session_id as string) || '';
-  let paid = false;
+  // Local-dev preview: ?_demo=paid forces the paid branch when accessed from
+  // a loopback host. Never matches the public domain.
+  const isLocal = req.hostname === 'localhost' || req.hostname === '127.0.0.1';
+  const demoPaid = req.query._demo === 'paid' && isLocal;
+  let paid = demoPaid;
+  let orderId: number | null = demoPaid ? 1 : null;
+  let amountPaid = 49900;
   if (stripe && sid) {
     try {
       const s = await stripe.checkout.sessions.retrieve(sid);
       if (s.payment_status === 'paid' && s.metadata?.order_id) {
         paid = true;
+        orderId = parseInt(s.metadata.order_id, 10);
+        if (typeof s.amount_total === 'number') amountPaid = s.amount_total;
         await query(
           `UPDATE upgrade_orders SET status = 'paid', paid_at = NOW() WHERE id = $1 AND status = 'pending_payment'`,
-          [parseInt(s.metadata.order_id, 10)]);
+          [orderId]);
       }
     } catch (e) {
       console.error('[upgrade] thanks lookup err:', e);
     }
   }
+
+  const now = new Date();
+  const pad = (n: number) => String(n).padStart(2, '0');
+  const orderNo = orderId
+    ? `EZ-${now.getFullYear()}-${pad(now.getMonth() + 1)}${pad(now.getDate())}-${String(orderId).padStart(4, '0')}`
+    : `EZ-${now.getFullYear()}-${pad(now.getMonth() + 1)}${pad(now.getDate())}-PEND`;
+  const tstamp = `${pad(now.getHours())}:${pad(now.getMinutes())} PT · ${now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
+  const dollars = `$${(amountPaid / 100).toFixed(0)}`;
+
   const body = paid
-    ? `<p class="eyebrow">Order received · payment confirmed</p>
-       <h1 class="display">Welcome aboard, <em>counselor.</em></h1>
-       <p class="lede">Stripe confirmed your payment. We'll be in touch within twenty-four hours with discovery questions and a project tracker. Live site in seven days.</p>
-       <p><a href="/">← Back to home</a></p>`
-    : `<p class="eyebrow">Almost there</p>
-       <h1 class="display">Reservation noted.</h1>
-       <p class="lede">If your payment is still processing, give it a moment and refresh. If you cancelled, no charge was made — feel free to come back when ready.</p>
-       <p><a href="/upgrade">← Try again</a></p>`;
-  res.send(shellHead('Thanks') + body + FOOT);
+    ? `
+    <div class="up-vol">
+      <span class="v-rule"></span>
+      <span class="v-text">Volume I</span>
+      <span class="v-meta">Edition 2026 · Order Confirmed</span>
+    </div>
+    <h1 class="display">Order <em>received,</em> counselor.</h1>
+    <p class="lede">Payment confirmed by Stripe. Your build slot is reserved and your project tracker is being prepared. Discovery questions arrive in your inbox within <strong>twenty-four business hours</strong>. The seven-business-day clock starts the moment you return your content brief.</p>
+
+    <div class="up-receipt scroll-reveal">
+      <div class="up-r-row">
+        <span class="up-r-label">Order №</span>
+        <span class="up-r-val">${orderNo}</span>
+      </div>
+      <div class="up-r-row">
+        <span class="up-r-label">Logged</span>
+        <span class="up-r-val">${tstamp}</span>
+      </div>
+      <div class="up-r-row">
+        <span class="up-r-label">Charged</span>
+        <span class="up-r-val">${dollars}<sub style="font-size:9px;color:var(--ink-mute);font-family:var(--sans);font-style:normal;letter-spacing:.18em;text-transform:uppercase;margin-left:6px">USD · one-time</sub></span>
+      </div>
+    </div>
+
+    <section class="up-next">
+      <h3 class="up-next-h">What happens next</h3>
+      <div class="up-next-grid">
+        <div class="up-next-cell scroll-reveal s1">
+          <span class="up-next-num">i.</span>
+          <h4>Intake brief — within 24 hours</h4>
+          <p>You'll receive a short content brief by email: firm story, practice areas, photos you'd like used, copy you've already written, links to existing materials. No call required unless you prefer one.</p>
+        </div>
+        <div class="up-next-cell scroll-reveal s2">
+          <span class="up-next-num">ii.</span>
+          <h4>First proof — five business days</h4>
+          <p>Working from your brief, we draft the home, about, services, and contact pages. You receive a private preview link and the seven-day go-live clock starts here.</p>
+        </div>
+        <div class="up-next-cell scroll-reveal s3">
+          <span class="up-next-num">iii.</span>
+          <h4>Revisions &amp; go-live — 14-day window</h4>
+          <p>Mark up the proof. Two rounds of revisions included — copy edits, image swaps, layout adjustments within the original four-page scope. New pages or scope changes are quoted separately. We point your domain on go-live day. First-year hosting included; year two billed separately at our published rate.</p>
+        </div>
+      </div>
+    </section>
+
+    <div class="up-footer-row">
+      <p class="up-mini" style="margin:0;max-width:54ch">Receipt and refund details have been emailed to the address on file. <strong>Full refund</strong> available any time before the first proof is delivered. Timeline begins on receipt of your content brief; delays in returning the brief shift the proof and go-live dates by the same number of days. Site-build deliverable only; the Counsel &amp; Bar listing remains free.</p>
+      <a href="/dashboard" class="up-cta-quiet">Open dashboard <span style="font-style:italic;font-family:var(--serif);margin-left:6px">→</span></a>
+    </div>
+
+    <aside class="up-trust scroll-reveal" style="margin-top:48px">
+      <p class="up-trust-eyebrow">For the record</p>
+      <p>Counsel &amp; Bar is a directory of California-licensed attorneys, not a lawyer referral service under §6155. The EZ Upgrade is an independent site-build service; purchasing it does not grant priority placement, fee-share arrangements, or any change to the directory's neutral proximity-ranked surface. Stripe acts as our payment processor; full card data remains with Stripe per their PCI-compliant flow. Site copy you supply or approve remains your professional responsibility under California Rule of Professional Conduct 7.x — we can flag obvious advertising-rule risks but do not provide legal review.</p>
+    </aside>`
+    : `
+    <div class="up-vol">
+      <span class="v-rule"></span>
+      <span class="v-text">Volume I</span>
+      <span class="v-meta">Edition 2026 · Reservation Pending</span>
+    </div>
+    <h1 class="display">Reservation <em>noted.</em></h1>
+    <p class="lede">If your payment is still processing, give Stripe a few seconds and refresh this page. If you cancelled, no charge was made and your reservation is released — feel free to come back whenever you're ready.</p>
+    <p style="margin-top:32px"><a href="/upgrade" style="font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--metal);border-bottom:1px solid var(--rule);padding-bottom:3px">← Try again</a></p>`;
+  res.send(shellHead(paid ? 'Order Confirmed' : 'Reservation Pending') + body + FOOT);
 });
 
 // ─── POST /webhooks/stripe (signed verification) ───────────────────────────

← b93b18b Iter 24: /upgrade EZ Upgrade page — editorial pass + critiqu  ·  back to Lawyer Directory Builder  ·  upgrade.ts: harden ?_demo=paid gate against Host-header spoo 85d7704 →