[object Object]

← back to CelebritySignatures

wear v1 soft-launch: browsable catalog live, sales gated behind WEAR_SALES_LIVE (coming-soon/notify-me buy button; no test-checkout exposed to real visitors)

5ed1ce734a6273227ce27ad9f48662c3aee989db · 2026-08-10 10:59:15 -0700 · Steve Abrams

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

Files touched

Diff

commit 5ed1ce734a6273227ce27ad9f48662c3aee989db
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 10:59:15 2026 -0700

    wear v1 soft-launch: browsable catalog live, sales gated behind WEAR_SALES_LIVE (coming-soon/notify-me buy button; no test-checkout exposed to real visitors)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/wear.html | 27 ++++++++++++++-------------
 server.js        |  5 +++++
 2 files changed, 19 insertions(+), 13 deletions(-)

diff --git a/public/wear.html b/public/wear.html
index 00e1fdb..4adbba1 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -118,13 +118,9 @@ async function boot(){
   const r = await fetch('/api/wear/signatures').then(r=>r.json());
   state.data = r;
   const d = document.getElementById('disclosure');
-  if (r.disclosure.screenComplete) {
-    d.className = 'disclosure';
-    d.innerHTML = `<b>Trademark-screened.</b> ${r.count.toLocaleString()} signatures are legally cleared for sale — screened against the USPTO register${r.disclosure.screenedAt? ' ('+new Date(r.disclosure.screenedAt).toLocaleDateString()+')':''}. Names with a live apparel trademark are excluded.`;
-  } else {
-    d.className = 'disclosure warn';
-    d.innerHTML = `<b>Trademark screening in progress.</b> The current screen is a sample, not the full USPTO register, so <b>no signatures are cleared for sale yet</b>. Only legally-clear signatures will ever be offered here.`;
-  }
+  d.className = 'disclosure' + (r.disclosure.purchasable ? '' : ' warn');
+  d.innerHTML = `<b>${(r.count||0).toLocaleString()} historic signatures</b> — public-domain figures (deceased on or before ${r.disclosure.cutoffDeathYear||1900}) on left-chest apparel.` +
+    (r.disclosure.purchasable ? '' : ' <b>Sales open soon</b> — browse now; add your email to be notified.');
   render();
 }
 
@@ -135,12 +131,10 @@ function render(){
   if (sort==='name') sigs.sort((a,b)=>a.full_name.localeCompare(b.full_name));
   else if (sort==='cat') sigs.sort((a,b)=>(a.category||'').localeCompare(b.category||'')||a.full_name.localeCompare(b.full_name));
   else if (sort==='shuffle') sigs.sort(()=>Math.random()-0.5);
-  document.getElementById('count').textContent = sigs.length ? `${sigs.length.toLocaleString()} cleared` : '';
+  document.getElementById('count').textContent = sigs.length ? `${sigs.length.toLocaleString()} available` : '';
   if (!sigs.length){
     grid.style.display='none'; empty.style.display='block';
-    empty.innerHTML = state.data.disclosure.screenComplete
-      ? 'No signatures matched the eligibility gate.'
-      : 'Nothing is cleared for sale yet — the full trademark screen has not been run. Once it is, cleared signatures appear here automatically.';
+    empty.innerHTML = 'No signatures matched the eligibility gate.';
     return;
   }
   grid.style.display='grid'; empty.style.display='none';
@@ -160,6 +154,11 @@ function openProduct(qid){
   document.getElementById('pCat').textContent = s.category||'';
   document.getElementById('email').value = '';
   document.getElementById('msg').textContent = '';
+  const live = !!(state.data.disclosure && state.data.disclosure.purchasable);
+  const bb = document.getElementById('buy'); bb.disabled=false; bb.textContent = live ? 'Add to cart' : 'Notify me when live';
+  const fine = document.querySelector('.fine'); if(fine) fine.textContent = live
+    ? 'Signature placed at the left chest. Fulfilled by our print partner.'
+    : 'Signature placed at the left chest. Apparel sales open soon — add your email and we’ll notify you the moment it goes live.';
   buildOptions();
   loadSigThenDraw();
   document.getElementById('scrim').classList.add('on');
@@ -226,17 +225,19 @@ document.getElementById('close').onclick = ()=>document.getElementById('scrim').
 document.getElementById('scrim').onclick = e => { if(e.target.id==='scrim') document.getElementById('scrim').classList.remove('on'); };
 
 document.getElementById('buy').onclick = async ()=>{
+  const live = !!(state.data && state.data.disclosure && state.data.disclosure.purchasable);
   const email = document.getElementById('email').value.trim();
   const msg = document.getElementById('msg'); msg.style.color='#c0392b';
   if(!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)){ msg.textContent='Enter a valid email.'; return; }
-  const btn = document.getElementById('buy'); btn.disabled=true; btn.textContent='Starting checkout…';
+  const btn = document.getElementById('buy'); btn.disabled=true; btn.textContent = live ? 'Starting checkout…' : 'Saving…';
   try{
     const r = await fetch('/api/wear-checkout',{method:'POST',headers:{'Content-Type':'application/json'},
       body:JSON.stringify({ qid:state.sig.qid, garment:state.garment.id, color:state.color.id, size:state.size, email })}).then(r=>r.json());
     if(r.ok && r.url){ location.href = r.url; return; }
+    if(r.comingSoon){ msg.style.color='#2e7d32'; msg.textContent = `You're on the list — we'll email ${email} the moment ${state.sig.full_name} apparel goes live.`; btn.textContent='On the list ✓'; return; }
     msg.textContent = r.error || 'Checkout unavailable.';
   }catch(e){ msg.textContent='Network error.'; }
-  btn.disabled=false; btn.textContent='Add to cart';
+  btn.disabled=false; btn.textContent = live ? 'Add to cart' : 'Notify me when live';
 };
 
 // restore density pref
diff --git a/server.js b/server.js
index 4580f5e..cb17d18 100644
--- a/server.js
+++ b/server.js
@@ -774,6 +774,7 @@ ${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
         garments: tpl.garments || [], placement: tpl.placement || 'left_chest',
         disclosure: {
           basis: 'public-domain-historical', cutoffDeathYear: WEAR_PD_DEATH_CUTOFF,
+          purchasable: envVal('WEAR_SALES_LIVE') === '1',
           note: 'Only public-domain historical signatures (figures deceased on or before ' + WEAR_PD_DEATH_CUTOFF + ') are offered — no living or modern figures. Live USPTO trademark screening is deferred.',
         },
         count: sigs.length, signatures: sigs,
@@ -786,6 +787,10 @@ ${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
     // payment, a POD-order draft is appended to data/pod-order-drafts.jsonl; NO POD
     // API call is made here.
     if (path === '/api/wear-checkout' && M === 'POST') {
+      // v1 soft-launch: catalog is public + browsable, but real sales stay OFF until Steve
+      // flips WEAR_SALES_LIVE=1 (paired with live Stripe + a wired POD). Until then, no visitor
+      // is sent to a test-mode checkout — they get a friendly "opening soon".
+      if (envVal('WEAR_SALES_LIVE') !== '1') return sendJSON(res, 200, { ok: false, comingSoon: true, error: 'Apparel sales open soon — thanks for your interest!' });
       if (!STRIPE_DOWNLOAD_KEY) return sendJSON(res, 503, { ok: false, error: 'apparel not purchasable yet (awaiting Stripe test key)' });
       const b = await readBody(req);
       const email = String(b.email || '').trim();

← 96c795b wear v1 (TK-10286, Steve GO): ship public-domain historical  ·  back to CelebritySignatures  ·  wear/POD: wire Printful sender + go-live path (gated OFF) — 0fa8dbf →