[object Object]

← back to Dw Vendor Microsites

sample flow: wire Shopify Storefront checkout (buybutton.js + config route + submit branch); falls back to no-payment request until token+sample-variant set

3da6af6f35bcd8cd81da04ff3497bc0e789216af · 2026-06-30 14:20:52 -0700 · Steve

Files touched

Diff

commit 3da6af6f35bcd8cd81da04ff3497bc0e789216af
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 30 14:20:52 2026 -0700

    sample flow: wire Shopify Storefront checkout (buybutton.js + config route + submit branch); falls back to no-payment request until token+sample-variant set
---
 template/public/buybutton.js | 70 ++++++++++++++++++++++++++++++++++++++++++++
 template/public/index.html   |  9 ++++++
 template/server.js           |  7 ++++-
 3 files changed, 85 insertions(+), 1 deletion(-)

diff --git a/template/public/buybutton.js b/template/public/buybutton.js
new file mode 100644
index 0000000..f479e9a
--- /dev/null
+++ b/template/public/buybutton.js
@@ -0,0 +1,70 @@
+/* Shopify sample checkout for the DW vendor microsites.
+ *
+ * These vendor lines are OFF Shopify (no per-product Shopify variant), so "Request
+ * Sample" cannot add a per-product variant. Instead it uses ONE generic "Memo Sample"
+ * Shopify product/variant and adds it to a real Shopify cart once per requested swatch,
+ * carrying each swatch's pattern/color/SKU/vendor as line-item custom properties, then
+ * opens Shopify's secure hosted checkout. The order line-items tell fulfillment which
+ * swatches to pull.
+ *
+ * Config is injected by the server at /config.js as window.DWV_CFG = {
+ *   domain: '<store>.myshopify.com', storefrontToken: '<token>', sampleVariantId: '<gid or numeric>'
+ * }. Until storefrontToken + sampleVariantId are set, this falls back to the no-payment
+ * JSONL sample-request endpoint (POST /api/sample-request) so the button always works.
+ */
+(function () {
+  var CFG = window.DWV_CFG || {};
+  var ready = CFG.storefrontToken && CFG.sampleVariantId && CFG.domain;
+  var client = null, checkoutId = null;
+
+  function loadSDK(cb) {
+    if (window.ShopifyBuy && window.ShopifyBuy.buildClient) return cb();
+    var s = document.createElement('script');
+    s.src = 'https://sdks.shopifycdn.com/js-buy-sdk/latest/index.umd.min.js';
+    s.onload = cb; s.onerror = function () { ready = false; cb(); };
+    document.head.appendChild(s);
+  }
+
+  function ensureClient(cb) {
+    if (client) return cb();
+    loadSDK(function () {
+      if (!window.ShopifyBuy || !ready) { ready = false; return cb(); }
+      client = window.ShopifyBuy.buildClient({ domain: CFG.domain, storefrontAccessToken: CFG.storefrontToken });
+      client.checkout.create().then(function (co) { checkoutId = co.id; cb(); }).catch(function () { ready = false; cb(); });
+    });
+  }
+
+  // items: [{sku, mfr_sku, pattern_name, color_name, line}]  -> Shopify checkout
+  function shopifyCheckout(items, contact) {
+    return new Promise(function (resolve, reject) {
+      ensureClient(function () {
+        if (!ready || !client || !checkoutId) return reject(new Error('shopify-unavailable'));
+        var lines = items.slice(0, 250).map(function (it) {
+          return {
+            variantId: CFG.sampleVariantId,
+            quantity: 1,
+            customAttributes: [
+              { key: 'Pattern', value: String(it.pattern_name || it.sku || '') },
+              { key: 'Color', value: String(it.color_name || '') },
+              { key: 'SKU', value: String(it.mfr_sku || it.sku || '') },
+              { key: 'Line', value: String(it.line || '') }
+            ]
+          };
+        });
+        var attrs = [];
+        if (contact && contact.name) attrs.push({ key: 'Requested by', value: contact.name });
+        var p = client.checkout.addLineItems(checkoutId, lines);
+        p.then(function (co) {
+          if (attrs.length) return client.checkout.updateAttributes(checkoutId, { customAttributes: attrs }).then(function () { return co; });
+          return co;
+        }).then(function (co) { window.location.href = co.webUrl; resolve(co); }).catch(reject);
+      });
+    });
+  }
+
+  // Expose for the viewer's sample modal. Returns true if Shopify path is wired.
+  window.DWV_SAMPLE = {
+    available: function () { return !!ready; },
+    checkout: shopifyCheckout
+  };
+})();
diff --git a/template/public/index.html b/template/public/index.html
index 00101ee..ce0d82b 100644
--- a/template/public/index.html
+++ b/template/public/index.html
@@ -365,6 +365,11 @@ $('#reqform').addEventListener('submit',async e=>{
   const payload={items:cart};
   for(const [k,v] of fd.entries()) payload[k]=v;
   const btn=$('#submitbtn'); btn.disabled=true; btn.textContent='Submitting…';
+  // Shopify checkout path (real sample order) when wired; else no-payment request below.
+  if(window.DWV_SAMPLE && window.DWV_SAMPLE.available()){
+    try{ btn.textContent='Opening secure checkout…'; await window.DWV_SAMPLE.checkout(cart,{name:payload.name,email:payload.email}); return; }
+    catch(err){ /* fall through to the no-payment request flow */ }
+  }
   try{
     const r=await fetch('/api/sample-request',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
     const d=await r.json();
@@ -387,6 +392,10 @@ $('#reqform').addEventListener('submit',async e=>{
       $('#brand').innerHTML = esc(cfg.title)+'<small>Designer Wallcoverings</small>';
       NS = 'dwvm.'+cfg.title.replace(/[^a-z0-9]+/gi,'_').toLowerCase();
     }
+    window.DWV_CFG = cfg.shopify || {};
+    if(window.DWV_CFG.storefrontToken && window.DWV_CFG.sampleVariantId){
+      await new Promise(res=>{const s=document.createElement('script');s.src='/buybutton.js';s.onload=res;s.onerror=res;document.head.appendChild(s);});
+    }
   }catch{}
   sort = LS.get('sort','newest');
   density = LS.get('density',5);
diff --git a/template/server.js b/template/server.js
index 51f28e7..9904dbf 100644
--- a/template/server.js
+++ b/template/server.js
@@ -319,7 +319,12 @@ const server = http.createServer((req, res) => {
   const u = new URL(req.url, 'http://localhost');
 
   if (u.pathname === '/api/config') {
-    return json(res, 200, { title: TITLE, samples_only: SAMPLES_ONLY, total: ROWS.length });
+    return json(res, 200, { title: TITLE, samples_only: SAMPLES_ONLY, total: ROWS.length,
+      shopify: {
+        domain: process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com',
+        storefrontToken: process.env.SHOPIFY_STOREFRONT_TOKEN || '',
+        sampleVariantId: process.env.SHOPIFY_SAMPLE_VARIANT_ID || ''
+      } });
   }
 
   if (u.pathname === '/api/facets') {

← 4946bd5 exclude armani_casa — Steve: DO NOT SHOW; took down live sit  ·  back to Dw Vendor Microsites  ·  exclude spoonflower — marketplace scrape, not Steve's own de bade609 →