@occluze/tether 0.6.8 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -28
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
- package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
- package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
- package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
- package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
- package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
- package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
- package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
- package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
- package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
- package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
- package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
- package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
- package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
- package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
- package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
- package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
- package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -2
- package/dist/presets/barba.esm.js +19 -23
- package/dist/tether.account-pages.js +332 -367
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +253 -492
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +82 -89
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +52 -34
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +42 -37
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1361 -935
- package/dist/tether.markets.js +89 -91
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +0 -0
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +60 -69
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/cart-id.d.ts +2 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +85 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +185 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +134 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +190 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +90 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +24 -24
- package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
- package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
- package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
- package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
- package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
- package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
- package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
- package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
- package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
- package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
- package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
- package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
- package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
- package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
- package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
- package/dist/esm/chunks/search-OU75RNQL.js +0 -1
- package/src/account/addresses-view.js +0 -92
- package/src/account/browser-session.js +0 -359
- package/src/account/markup.js +0 -48
- package/src/account/operations.js +0 -180
- package/src/account/order-lines.js +0 -96
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/page-events.js +0 -288
- package/src/account/server-client.js +0 -172
- package/src/account/shared.js +0 -111
- package/src/account/shopify-auth.js +0 -150
- package/src/account/subscriptions-view.js +0 -158
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account-pages.js +0 -89
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics-shopify.js +0 -241
- package/src/analytics.js +0 -352
- package/src/api-version.js +0 -85
- package/src/astro.js +0 -266
- package/src/cart/checkout.js +0 -97
- package/src/cart/drawer.js +0 -138
- package/src/cart/mutations.js +0 -234
- package/src/cart/render.js +0 -294
- package/src/cart/store.js +0 -326
- package/src/cart.js +0 -391
- package/src/client.js +0 -267
- package/src/collection.js +0 -183
- package/src/config.js +0 -349
- package/src/controls/cart-controls.js +0 -155
- package/src/controls/discount.js +0 -96
- package/src/controls/extras.js +0 -63
- package/src/controls/navigation.js +0 -83
- package/src/diagnose-markup.js +0 -333
- package/src/diagnose.js +0 -295
- package/src/dom.js +0 -287
- package/src/extras.js +0 -145
- package/src/fragments.js +0 -237
- package/src/images.js +0 -104
- package/src/index.js +0 -353
- package/src/initial.js +0 -87
- package/src/lazy-iife.js +0 -116
- package/src/lazy.js +0 -39
- package/src/loader.js +0 -337
- package/src/market-codes.js +0 -35
- package/src/markets-ui.js +0 -204
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/mounting.js +0 -277
- package/src/page-market.js +0 -116
- package/src/parts/account-pages.js +0 -6
- package/src/parts/account.js +0 -6
- package/src/parts/analytics.js +0 -6
- package/src/parts/diagnose.js +0 -6
- package/src/parts/extras.js +0 -6
- package/src/parts/markets.js +0 -6
- package/src/parts/search.js +0 -6
- package/src/presets/barba.js +0 -190
- package/src/product-events.js +0 -130
- package/src/product-render.js +0 -257
- package/src/product-state.js +0 -318
- package/src/product.js +0 -344
- package/src/repeaters.js +0 -173
- package/src/search.js +0 -323
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/shopify-values.js +0 -89
- package/src/status.js +0 -126
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/src/storefront/json-ld.js +0 -167
- package/src/variant-render.js +0 -165
- package/types/astro.d.ts +0 -28
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -577
- package/types/server.d.ts +0 -182
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import{a as m,d as P}from"./chunk-FCSWQDVV.js";import{a as v}from"./chunk-VT4CDUJJ.js";import{c as R}from"./chunk-NEHZ37TU.js";import{e as S}from"./chunk-JOOEXYF3.js";import{B as k,b as i,f as $,m as E,p as w,r as p,s as l,t as f,v as d}from"./chunk-YJLWX2HK.js";import"./chunk-FOMSMIAM.js";var V=/^(\d{4})-(01|04|07|10)$/;function T(e){let n=V.exec(typeof e=="string"?e:"");return n?{year:Number(n[1]),month:Number(n[2])}:null}var q=(e,n)=>`${e}-${String(n).padStart(2,"0")}`;function A(e,n=1){let t=T(e);if(!t)throw new Error(`not a quarterly Shopify API version: ${e}`);let o=t.year*4+(t.month-1)/3+n;return q(Math.floor(o/4),o%4*3+1)}function _(e,n=new Date){if(e==="unstable")return{version:e,ok:!1,note:'"unstable" changes without notice: pin a dated version such as 2026-07'};let t=T(e);if(!t)return{version:e,ok:!1,note:"not a quarterly version (YYYY-01, -04, -07 or -10)"};let o=new Date(Date.UTC(t.year,t.month-1,1)),r=new Date(Date.UTC(t.year,t.month-1+12,1)),s=(r.getTime()-n.getTime())/2630016e3,a=o<=n,c=`supported until about ${r.toISOString().slice(0,7)}`;return a?s<=0?c="no longer supported: Shopify falls back to the oldest supported version. Upgrade (see README)":s<3&&(c=`${c}: plan the upgrade to ${A(e)} or later (see README)`):c="not released yet (release candidate at best)",{version:e,ok:a&&s>0,monthsLeft:Math.round(s),next:A(e),note:c}}var M=new Set(["in-stock","out-of-stock","backorder","low-stock","plenty-in-stock","on-sale","not-on-sale","has-variants","single-variant","subscription-available","subscription-unavailable","subscription-only","one-time-available","subscription-selected","one-time-selected","has-savings","loading","ready","error","not-found","cart-empty","cart-filled","cart-has-subscription","cart-no-subscription","cart-has-discount","cart-error","cart-warning","list-empty","list-filled","search-empty","search-results","logged-in","logged-out","orders-empty","orders-loading","addresses-loading","addresses-empty","addresses-error","subscriptions-loading","subscriptions-empty","subscriptions-error"]),N=new Set(["title","description","description-text","vendor","type","handle","published-at","price","total-price","compare-at-price","unit-price","min-price","max-price","discount-percentage","discount-amount","variant-title","variant-id","sku","barcode","stock","quantity","featured-image","variant-image","images","tags","link","url","one-time-price","subscription-price","subscription-savings","subscription-savings-amount","checkout-charge","plan-name","plan-description","plan-group","option-value","swatch","image"]),D=new Set(`account account-bind account-link account-login account-logout add-label add-to-cart adding-text
|
|
2
|
-
always-enabled bind cart-checkout-charge cart-clear cart-close cart-count cart-discounts cart-drawer cart-label cart-lines
|
|
3
|
-
cart-note cart-open cart-subtotal cart-total cart-warning checkout code collection collection-bind countries country
|
|
4
|
-
country-bind country-label country-select default-plan default-purchase discount discount-code discount-form discount-input
|
|
5
|
-
discount-remove display error group group-add hide-single language limit line line-attribute line-attributes line-id
|
|
6
|
-
line-minus line-plan line-plus line-remove line-show list load-more market-pending one-time-label option option-list
|
|
7
|
-
option-selected option-value order order-bind order-line order-lines order-line-show order-show order-state order-fulfillments
|
|
8
|
-
fulfillment-bind order-return return-quantity return-reason return-note orders profile-form address-form addresses address-bind
|
|
9
|
-
address-show address-edit address-new address-delete address-default subscriptions subscription-bind subscription-lines
|
|
10
|
-
subscription-line subscription-show subscription-action subscription-status subscription-state confirm form-state form-error page-country page-language page-locale page-title plan plan-bind plan-list
|
|
11
|
-
plan-value product products purchase purchase-mode qty qty-minus qty-plus quantity recommendations reverse search
|
|
12
|
-
search-bind search-collections search-input search-limit search-min search-products search-queries search-results
|
|
13
|
-
search-state show soldout-text sort state subscribe-text variant variant-select variant-url max-width max-height crop
|
|
14
|
-
sig label hydrated busy`.split(/\s+/).filter(Boolean)),C=["bind","option-list","option","variant-select","add-to-cart","purchase","plan-list","plan","qty","line-attribute"],H=["option-list","plan-list","cart-lines","list"],b="metafield:",Y=/^image:\d+$/,G=2;function B(e,n){let t=Array.from({length:n.length+1},(o,r)=>r);for(let o=1;o<=e.length;o++){let r=[o];for(let s=1;s<=n.length;s++){let a=(t[s-1]??0)+(e[o-1]===n[s-1]?0:1);r.push(Math.min((t[s]??0)+1,(r[s-1]??0)+1,a))}t=r}return t[n.length]??0}function g(e,n,t=""){let o=null,r=1/0;for(let a of n){let c=B(e,a);c<r&&(r=c,o=a)}let s=Math.max(2,Math.floor(e.length/4));return o!==null&&r<=s?` Did you mean "${t}${o}"?`:""}function L(e){let n=e.id?`#${e.id}`:"",t=e.className&&typeof e.className=="string"?e.className.trim().split(/\s+/):[],o=t.length?`.${t.slice(0,G).join(".")}`:"";return`<${e.tagName.toLowerCase()}${n}${o}>`}var x=()=>`${l("collection")},${l("products")},${l("recommendations")}`;function Q(e,n){d(e,l("product")).forEach(t=>{if(t.closest(x())&&t.closest(l("list")))return;let{key:o,fromUrl:r,raw:s}=m(t);s?r&&!o&&n("warn",`${p("product")}="${s}" but this URL has no value for it (e.g. ?handle=my-product).`,t):n("error",`${p("product")} has no value. Bind it to the Shopify handle (CMS field), or use "url:handle".`,t)})}function W(e,n){for(let t of C)d(e,l(t)).forEach(o=>{o.closest(l("product"))||t==="line-attribute"&&o.closest(l("group"))||n("warn",`${p(t)} outside any ${p("product")}: it does nothing here.`,o)});d(e,l("group-add")).forEach(t=>{t.closest(l("group"))||n("warn",`${p("group-add")} outside a ${p("group")}.`,t)});for(let t of H)d(e,l(t)).forEach(o=>{!o.firstElementChild&&!k(o)&&n("warn",`${p(t)} without a template child: put one example element inside it.`,o)});d(e,x()).forEach(t=>{d(t,l("list")).length||n("error",`List without a ${p("list")} child: products have nowhere to render.`,t)})}function j(e,n){d(e,l("show")).forEach(t=>{let o=f(t,"show")??"";M.has(o)||n("warn",`Unknown ${p("show")}="${o}".${g(o,M)}`,t)}),d(e,l("bind")).forEach(t=>{let o=f(t,"bind")||"";!o||N.has(o)||Y.test(o)||o.startsWith(b)||n("warn",`Unknown ${p("bind")}="${o}": it stays empty.${g(o,N)}`,t)})}function K(e,n){d(e,l("bind")).forEach(t=>{let o=f(t,"bind")||"";if(!o.startsWith(b))return;let r=o.slice(b.length);i.metafields.some(a=>(typeof a=="string"?a:`${a.namespace}.${a.key}`)===r)||n("error",`${p("bind")}="${o}" but "${r}" is not in config.metafields, so it stays empty.`,t)})}function F(e,n){let t=`${i.prefix}-`,o=new Set;d(e,"*").forEach(r=>{for(let{name:s}of r.attributes){if(!s.startsWith(t))continue;let a=s.slice(t.length);D.has(a)||o.has(a)||(o.add(a),n("warn",`Unknown attribute ${s}: Tether ignores it.${g(a,D,t)}`,r))}})}function z(e,n){let t=d(e,l("cart-drawer")).length;d(e,l("cart-open")).length&&!t&&n("warn",`${p("cart-open")} exists but there is no ${p("cart-drawer")} on this page.`),t>1&&n("warn",`More than one ${p("cart-drawer")}.`),d(e,`[${p("state")}="error"]`).forEach(o=>{o.matches(P())&&n("error",`Scope failed to load (${f(o,"error")||"error"}).`,o)})}function U(e=document){let n=[],t=(o,r,s)=>{n.push(s?{level:o,message:r,element:L(s),el:s}:{level:o,message:r})};return Q(e,t),W(e,t),j(e,t),F(e,t),K(e,t),z(e,t),n}var J=20,X=3,O=4;function Z(e){return e?`${e.slice(0,O)}\u2026${e.slice(-O)} (${e.length} chars)`:"(missing)"}function I(e,n){if(e instanceof Error&&e.message)return e.message;let t=typeof e=="object"&&e!==null?Reflect.get(e,"code"):void 0;return typeof t=="string"&&t?t:n}function ee(){return{domain:i.domain||"(missing)",token:Z(i.token),apiVersion:i.apiVersion,endpoint:i.domain?E():null,country:i.country,language:i.language,locale:i.locale,markets:i.markets,prefix:i.prefix,productPath:i.productPath,metafields:i.metafields,autoMount:i.autoMount,observe:i.observe,onError:typeof i.onError=="function"}}async function te(){return{country:i.country||null,language:i.language||null,locale:i.locale||null,detected:i.markets?.detect?(await v("markets")).detectMarket(i.markets):null}}function ne({ok:e,monthsLeft:n,note:t}){return e&&(n===void 0||n>=X)?null:{level:e?"warn":"error",message:`apiVersion ${i.apiVersion}: ${t}`}}async function oe(){if(!$())return{kind:"not-configured"};let e=Date.now();try{let n=await w("query TetherDiagnose { shop { name primaryDomain { url } paymentSettings { currencyCode } } }",{},{context:!1});return{kind:"reachable",shop:{name:n.shop.name,url:n.shop.primaryDomain.url,currency:n.shop.paymentSettings.currencyCode},latencyMs:Date.now()-e}}catch(n){return{kind:"unreachable",error:n,latencyMs:Date.now()-e}}}function se(e,n,t){let o=[],r=t.options.map(a=>a.name),s=d(e,l("product")).filter(a=>m(a).key===n&&a.querySelector(l("add-to-cart")));for(let a of s){let c=new Set(d(a,`${l("option-list")},select${l("option")}`).map(u=>f(u,"option-list")||f(u,"option")));if(!(!c.size&&d(a,`select${l("variant-select")}`).length)){if(!i.language)for(let u of c)!u||r.includes(u)||o.push({level:"warn",message:`Product "${t.handle}" has no option "${u}" (its options: ${r.join(", ")||"none"}).${g(u,r)}`});for(let u of t.options)u.optionValues.length<=1||c.has(u.name)||o.push({level:"warn",message:`Product "${t.handle}": option "${u.name}" (${u.optionValues.length} values) has no tether-option-list, so visitors can't choose it.`})}}return o}async function re(e,n){let t=d(e,l("product")).map(s=>m(s).key),o=[...new Set(t.filter(s=>s!==null&&s!==""))].slice(0,J);(await Promise.allSettled(o.map(s=>R(s)))).forEach((s,a)=>{let c=o[a]??"";if(s.status==="rejected"){let h=I(s.reason,"not loaded");n.products.push({key:c,error:s.reason instanceof Error?s.reason.message:void 0}),n.problems.push({level:"error",message:`Product "${c}": ${h}. Is it published to the Headless channel?`});return}let u=s.value,y=S(u);n.products.push({key:c,handle:u.handle,title:u.title,plans:y.length,requiresSellingPlan:u.requiresSellingPlan});for(let h of y)n.sellingPlans.push({product:u.handle,group:h.group.name,plan:h.name,id:h.id});n.problems.push(...se(e,c,u))})}async function we({root:e=document,silent:n=!1,version:t,now:o=new Date}={}){let r=await oe(),s={version:t,config:ee(),apiVersion:_(i.apiVersion,o),market:await te(),reachable:r.kind==="reachable",shop:r.kind==="reachable"?r.shop:null,latencyMs:r.kind==="not-configured"?null:r.latencyMs,products:[],sellingPlans:[],problems:[]};switch(r.kind){case"not-configured":s.problems.push({level:"error",message:"TETHER_CONFIG needs domain and token."});break;case"unreachable":s.problems.push({level:"error",message:`Storefront API not reachable: ${I(r.error,"unknown error")}`});break;case"reachable":await re(e,s);break}let a=ne(s.apiVersion);return a&&s.problems.push(a),typeof document<"u"&&s.problems.push(...U(e)),n||ae(s),s}function ae(e){let n=e.problems.filter(c=>c.level==="error").length,t=e.problems.length-n;console.group(`[${i.prefix}] diagnose: ${n?`${n} error(s)`:"no errors"}, ${t} warning(s)`),console.log("version",e.version,"| API",e.apiVersion.version,`(${e.apiVersion.note})`),console.log("config",e.config);let{country:o,language:r,detected:s}=e.market,a=[o,r].filter(Boolean).join(" / ")||"the store default";console.log("market",`in use: ${a}`,s?`(detection picks ${JSON.stringify(s)})`:""),console.log(e.shop?`Storefront API reachable: ${e.shop.name} (${e.latencyMs??0} ms)`:"Storefront API NOT reachable"),e.products.length&&console.table(e.products),e.sellingPlans.length?console.table(e.sellingPlans):e.reachable&&console.log("No selling plans on the products of this page.");for(let c of e.problems)c.level==="error"?console.error(c.message,c.el??""):console.warn(c.message,c.el??"");console.groupEnd()}export{_ as apiVersionStatus,U as checkMarkup,we as diagnose,g as didYouMean};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as b}from"./chunk-2FSXS55U.js";import"./chunk-HEQILZVR.js";import"./chunk-FCSWQDVV.js";import{b as p}from"./chunk-MPTQYLSM.js";import"./chunk-NEHZ37TU.js";import"./chunk-JOOEXYF3.js";import{Q as h,R as w,b as s,h as f,i,s as c,t as g,u as d,v as y,x as m}from"./chunk-YJLWX2HK.js";import"./chunk-FOMSMIAM.js";function x(t,r){let e=w(h(t,"line-attributes"));return r?[{key:"_group",value:r},...e]:e}async function V(t){let r=t.closest(c("group"));if(!r)return null;let e=g(r,"group"),a=x(r,e),n=y(r,c("product")).flatMap(u=>{let o=b.get(u);if(!o?.variant?.availableForSale||o.quantity<=0)return[];let l=o.getLine();return l?[{...l,attributes:[...l.attributes??[],...a]}]:[]});if(!n.length)return f("group-add: nothing selected"),null;try{let u=await p.add(n);return m("group-added",{group:e,lines:n},r),u}catch(u){return i(u,"group add"),null}}function $(t){let r;try{r=JSON.parse(t)}catch{return null}if(typeof r!="object"||r===null)return null;let{country:e,language:a,locale:n}=r;return typeof e!="string"||!e?null:{country:e,language:typeof a=="string"?a:null,locale:typeof n=="string"?n:void 0}}function _(t){if(t.key===s.storageKey||t.key===`${s.storageKey}:seen`){p.init().catch(n=>{i(n,"cart from another tab")});return}if(t.key!==`${s.prefix}:market:${s.domain}`||!t.newValue)return;let r=$(t.newValue),{Tether:e}=window;if(!r||!e)return;let a={language:r.language,locale:r.locale,remember:!1};e.setCountry(r.country,a).catch(n=>{i(n,"market from another tab")})}var k=new WeakMap,S=t=>t.split("/").pop()??t;function A(t){try{window.history.replaceState(window.history.state,"",t)}catch{}}function v(t,{variant:r,sellingPlanId:e}){let a=`${r.id}|${e??""}`,n=k.get(t);if(k.set(t,a),!n||n===a)return;let u=s.variantUrl&&document.querySelector(c("product"))===t;if(!d(t,"variant-url")&&!u)return;let o=new URL(window.location.href);o.searchParams.set("variant",S(r.id)),e?o.searchParams.set("selling_plan",S(e)):o.searchParams.delete("selling_plan"),A(o)}export{V as addGroup,_ as onStorage,v as writeVariantUrl};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as T,c as P,d as F,e as h}from"./chunk-27PJJZGC.js";import{a as E,c as w,e as x,f as A,g as U,h as y,i as I}from"./chunk-V74KGEJF.js";import"./chunk-2FSXS55U.js";import"./chunk-HEQILZVR.js";import"./chunk-FCSWQDVV.js";import{b as l}from"./chunk-MPTQYLSM.js";import{d as v}from"./chunk-NEHZ37TU.js";import"./chunk-JOOEXYF3.js";import{b as c,i as f,j as m,k as g,l as C,r as s,s as p,t as b,x as L}from"./chunk-YJLWX2HK.js";import"./chunk-FOMSMIAM.js";var _="{name} ({currency})",j="name";function B(e,t){switch(t){case"name":return e.name;case"code":return e.isoCode;case"currency":return e.currency.isoCode;case"symbol":return e.currency.symbol;default:return""}}var M=(e,t)=>e.replace(/\{(name|code|currency|symbol)\}/g,(o,a)=>B(t,a));function O(e,t,o){let a=e.getAttribute(s("country-label"))||_,r=(e.getAttribute(s("countries"))||"").toUpperCase().split(/[\s,]+/).filter(Boolean),i=r.length?r.map(n=>t.find(u=>u.isoCode===n)).filter(n=>n!==void 0):t;e.replaceChildren(...i.map(n=>new Option(M(a,n),n.isoCode))),e.value=o,e.value!==o&&(e.selectedIndex=-1),e.disabled=i.length<2&&!e.hasAttribute(s("always-enabled"))}function S(e,t,o){let a=(c.country||o.country.isoCode).toUpperCase(),r=[...o.availableCountries].sort((n,u)=>n.name.localeCompare(u.name,c.locale||void 0)),i=r.find(n=>n.isoCode===a)??o.country;e.forEach(n=>{O(n,r,a)}),t.forEach(n=>{n.textContent=B(i,b(n,"country-bind")||j)})}function k(e=document){let t=y(e,`select${p("country-select")}`),o=y(e,p("country-bind"));return!t.length&&!o.length?Promise.resolve():h().then(a=>{S(t,o,a)}).catch(a=>{f(a,"country select")})}var $=e=>Object.values(c.markets?.locales??{}).find(t=>m(t.country)===e);function z(e,t){return e!==void 0?g(e):t?t.language?g(t.language):void 0:c.language}function R(e){v();let t=[];return l.id()&&t.push(l.setBuyerIdentity({countryCode:e},{create:!1}).catch(o=>{f(o,"cart country")})),U(),A.forEach(o=>t.push(o.load())),x.forEach(o=>{o.closest(E())||(w.delete(o),I(o))}),t}async function X(e,{language:t,locale:o,remember:a=!0}={}){let r=m(e);if(!r)throw new C(`Not a country code: ${e}`,{code:"INVALID_COUNTRY"});let i=$(r),n=o||i?.locale,u=n||c.locale,d=z(t,i),D=r!==c.country?.toUpperCase()||d!==c.language||u!==c.locale;if(c.country=r,c.language=d||void 0,c.locale=u,a&&P({country:r,language:d,locale:n}),!D)return k(),l.state;t!==void 0&&F();let N=R(r);return k(),L("country-changed",{country:r,language:c.language}),await Promise.all(N),l.state}export{T as detectMarket,h as getLocalization,k as renderCountrySelects,X as setCountry};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{h as C}from"./chunk-V74KGEJF.js";import"./chunk-2FSXS55U.js";import"./chunk-HEQILZVR.js";import"./chunk-FCSWQDVV.js";import"./chunk-MPTQYLSM.js";import"./chunk-NEHZ37TU.js";import"./chunk-JOOEXYF3.js";import{A as E,C as S,E as L,G as T,J as q,L as d,b as l,g,i as b,p as A,r as y,s as h,t as u,v as c,x as w}from"./chunk-YJLWX2HK.js";import"./chunk-FOMSMIAM.js";var R="query TetherPredictive($q:String!,$limit:Int!){predictiveSearch(query:$q,limit:$limit,limitScope:EACH,types:[PRODUCT,COLLECTION,QUERY]){products{id handle title availableForSale featuredImage{url altText}priceRange{minVariantPrice{amount currencyCode}}}collections{handle title image{url altText}}queries{text}}}",v=6,D=2,F="/search",I=new WeakSet,x=0;function $(r,t,e){return parseInt(u(r,t)||"",10)||e}var k=r=>r.tagName==="A"?r:r.querySelector("a");function f(r,t){c(r,h("search-bind")).forEach(e=>{let i=u(e,"search-bind");i==="image"?E(e,t.image?.url,t.image?.altText||t.title):(i==="title"||i==="price"||i==="text")&&t[i]!=null&&q(e,t[i])})}function U(r,t){let e=k(r);e&&(e.setAttribute("href",t),e.setAttribute("role","option"))}var p=class{constructor(t,e,i){this.root=t,this.input=e,this.results=i,this.limit=$(t,"search-limit",v),this.minCharacters=$(t,"search-min",D),i.id||(x+=1,i.id=`${l.prefix}-search-${x}`),this.resultsId=i.id,this.searchNumber=0,this.timer=void 0,this.lastTerm="",this.cache=new Map,this.setUpAria(),this.listen()}setUpAria(){this.input.setAttribute("role","combobox"),this.input.setAttribute("aria-autocomplete","list"),this.input.setAttribute("aria-controls",this.resultsId),this.input.setAttribute("aria-expanded","false"),this.input.setAttribute("autocomplete","off"),this.results.hasAttribute("role")||this.results.setAttribute("role","listbox"),this.results.hidden=!0}listen(){this.input.addEventListener("input",()=>{let t=this.input.value.trim();t!==this.lastTerm&&(this.lastTerm=t,clearTimeout(this.timer),this.timer=setTimeout(()=>void this.search(t),l.searchDelay))}),this.input.addEventListener("focus",()=>{this.input.value.trim().length>=this.minCharacters&&this.options().length&&this.open(!0)}),this.input.addEventListener("keydown",t=>{this.onKeydown(t)}),document.addEventListener("click",t=>{this.root.contains(t.target)||this.open(!1)})}setState(t){this.root.setAttribute(y("search-state"),t)}open(t){this.root.classList.toggle("is-open",t),this.results.hidden=!t,this.input.setAttribute("aria-expanded",String(t)),t||this.input.removeAttribute("aria-activedescendant")}options(){return c(this.results,'[role="option"]')}async fetchResults(t){let e=this.cache.get(t);if(e)return e;let i=await A(R,{q:t,limit:this.limit});return this.cache.set(t,i.predictiveSearch),i.predictiveSearch}async search(t){this.searchNumber+=1;let e=this.searchNumber;if(t.length<this.minCharacters){this.open(!1),this.setState("idle");return}this.setState("loading");try{let i=await this.fetchResults(t);e===this.searchNumber&&this.render(i,t)}catch(i){if(e!==this.searchNumber){g("a superseded search failed",i);return}this.setState("error"),b(i,"search")}}fillList(t,e,i){c(this.results,h(t)).forEach(n=>{S(n,e,i),d(n,e.length>0)})}queryUrl(t){let e=this.root.getAttribute("action")||F,i=e.includes("?")?"&":"?";return`${e}${i}${this.input.name||"q"}=${encodeURIComponent(t)}`}render(t,e){let i=t?.products??[],n=t?.collections??[],o=(t?.queries??[]).filter(a=>a.text.toLowerCase()!==e.toLowerCase());this.fillList("search-products",i,(a,s)=>{let N=T(s.priceRange.minVariantPrice);f(a,{title:s.title,price:N,image:s.featuredImage}),U(a,L(s.handle,s.id)),a.classList.toggle("is-sold-out",!s.availableForSale)}),this.fillList("search-collections",n,(a,s)=>{f(a,{title:s.title,image:s.image}),U(a,`${(l.collectionPath||"/collection").replace(/\/$/,"")}/${s.handle}`)}),this.fillList("search-queries",o,(a,s)=>{f(a,{text:s.text}),k(a)?.setAttribute("href",this.queryUrl(s.text))});let m=i.length+n.length+o.length;this.showCount(m),this.setState(m?"ready":"empty"),this.open(!0),w("search",{term:e,products:i,collections:n,queries:o},this.root)}showCount(t){c(this.root,h("show")).forEach(e=>{let i=u(e,"show");i==="search-empty"&&d(e,t===0),i==="search-results"&&d(e,t>0)}),this.options().forEach((e,i)=>{e.id||(e.id=`${this.resultsId}-${i}`),e.setAttribute("aria-selected","false")})}highlight(t,e){t.forEach((n,o)=>{n.setAttribute("aria-selected",String(o===e))});let i=t[e];i&&(this.input.setAttribute("aria-activedescendant",i.id),i.scrollIntoView?.({block:"nearest"}))}onKeydown(t){let e=this.options(),i=e.findIndex(n=>n.getAttribute("aria-selected")==="true");t.key==="ArrowDown"&&e.length?(t.preventDefault(),this.open(!0),this.highlight(e,(i+1)%e.length)):t.key==="ArrowUp"&&e.length?(t.preventDefault(),this.highlight(e,i<=0?e.length-1:i-1)):t.key==="Enter"&&i>=0?(t.preventDefault(),e[i]?.click()):t.key==="Escape"&&this.open(!1)}};function _(r=document){for(let t of C(r,h("search"))){if(I.has(t))continue;I.add(t);let e=t.querySelector(h("search-input"))??t.querySelector("input"),i=t.querySelector(h("search-results"));e&&i&&new p(t,e,i)}}export{_ as mountSearch};
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The customer's addresses (`tether-addresses`), with `tether-show="addresses-loading"`, `addresses-empty` and
|
|
3
|
-
* `addresses-error`. The last list rendered is kept, so an edit button can fill the address form from it.
|
|
4
|
-
*/
|
|
5
|
-
import { reportError } from '../config.js';
|
|
6
|
-
import { get, qsa, repeat, sel, setText } from '../dom.js';
|
|
7
|
-
import { addressText, bindsIn, ownValue, showIn } from './markup.js';
|
|
8
|
-
import { addresses } from './operations.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @typedef {object} CustomerAddress An address of account-ops.js's documents, with `isDefault` added.
|
|
12
|
-
* @property {string} id
|
|
13
|
-
* @property {string | null} firstName
|
|
14
|
-
* @property {string | null} lastName
|
|
15
|
-
* @property {string | null} company
|
|
16
|
-
* @property {string | null} address1
|
|
17
|
-
* @property {string | null} address2
|
|
18
|
-
* @property {string | null} city
|
|
19
|
-
* @property {string | null} province
|
|
20
|
-
* @property {string | null} zoneCode
|
|
21
|
-
* @property {string | null} zip
|
|
22
|
-
* @property {string | null} country
|
|
23
|
-
* @property {string | null} countryCode
|
|
24
|
-
* @property {string | null} phoneNumber
|
|
25
|
-
* @property {string[]} formatted
|
|
26
|
-
* @property {boolean} isDefault
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/** @type {readonly CustomerAddress[]} */
|
|
30
|
-
let shown = [];
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {string} id
|
|
34
|
-
* @returns {CustomerAddress | undefined} The address with this id in the list shown last.
|
|
35
|
-
*/
|
|
36
|
-
export const shownAddress = (id) => shown.find((address) => address.id === id);
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {CustomerAddress} address
|
|
40
|
-
* @returns {Record<string, string>} The text of each `tether-address-bind` value.
|
|
41
|
-
*/
|
|
42
|
-
const addressTexts = (address) => ({
|
|
43
|
-
formatted: addressText(address),
|
|
44
|
-
name: [address.firstName, address.lastName].filter(Boolean).join(' '),
|
|
45
|
-
company: address.company || '',
|
|
46
|
-
address1: address.address1 || '',
|
|
47
|
-
address2: address.address2 || '',
|
|
48
|
-
city: address.city || '',
|
|
49
|
-
zip: address.zip || '',
|
|
50
|
-
province: address.province || '',
|
|
51
|
-
country: address.country || '',
|
|
52
|
-
phone: address.phoneNumber || '',
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* @param {HTMLElement} row
|
|
57
|
-
* @param {CustomerAddress} address
|
|
58
|
-
*/
|
|
59
|
-
function fillAddress(row, address) {
|
|
60
|
-
row.dataset['tetherAddress'] = address.id;
|
|
61
|
-
const texts = addressTexts(address);
|
|
62
|
-
bindsIn(row, 'address-bind').forEach((el) => {
|
|
63
|
-
const text = ownValue(texts, get(el, 'address-bind') ?? '');
|
|
64
|
-
if (text !== undefined) setText(el, text);
|
|
65
|
-
});
|
|
66
|
-
showIn(row, 'address-show', { default: address.isDefault, 'not-default': !address.isDefault });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Loads and renders the addresses. A failure is reported and shows `addresses-error`, never "no addresses yet".
|
|
71
|
-
*
|
|
72
|
-
* @param {ParentNode} root
|
|
73
|
-
*/
|
|
74
|
-
export async function renderAddresses(root) {
|
|
75
|
-
const boxes = qsa(root, sel('addresses'));
|
|
76
|
-
if (!boxes.length) return;
|
|
77
|
-
showIn(root, 'show', { 'addresses-loading': true, 'addresses-empty': false });
|
|
78
|
-
/** @type {CustomerAddress[]} */
|
|
79
|
-
let list;
|
|
80
|
-
try {
|
|
81
|
-
list = await addresses();
|
|
82
|
-
} catch (error) {
|
|
83
|
-
reportError(error, 'addresses');
|
|
84
|
-
showIn(root, 'show', { 'addresses-loading': false, 'addresses-error': true });
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
shown = list;
|
|
88
|
-
showIn(root, 'show', { 'addresses-loading': false, 'addresses-empty': list.length === 0, 'addresses-error': false });
|
|
89
|
-
boxes.forEach((box) => {
|
|
90
|
-
repeat(box, list, fillAddress);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser mode (`account.clientId`): the login runs between this browser and Shopify, OAuth with PKCE, and the tokens
|
|
3
|
-
* stay in this browser (sessionStorage, or localStorage with `account.remember`). The cart is linked to the customer,
|
|
4
|
-
* so checkout knows them. No other server sees the customer's data.
|
|
5
|
-
*/
|
|
6
|
-
import { cfg, reportError } from '../config.js';
|
|
7
|
-
import { TetherError } from '../client.js';
|
|
8
|
-
import { emit } from '../dom.js';
|
|
9
|
-
import { Cart } from '../cart.js';
|
|
10
|
-
import { randomToken, toBase64Url } from '../../server/src/shared/base64url.js';
|
|
11
|
-
import {
|
|
12
|
-
isRecord,
|
|
13
|
-
loginCountry,
|
|
14
|
-
loginLanguage,
|
|
15
|
-
navigate,
|
|
16
|
-
pendingLoginKey,
|
|
17
|
-
readJsonIfAllowed,
|
|
18
|
-
readJsonObjectOrEmpty,
|
|
19
|
-
sessionArea,
|
|
20
|
-
sessionKey,
|
|
21
|
-
textOrNull,
|
|
22
|
-
writeJsonIfAllowed,
|
|
23
|
-
} from './shared.js';
|
|
24
|
-
import { answersLogin, discover, requestToken } from './shopify-auth.js';
|
|
25
|
-
import { CUSTOMER_QUERY } from '../../server/src/shared/account-documents.js';
|
|
26
|
-
/** @import { TokenAnswer } from './shopify-auth.js' */
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @typedef {object} BrowserSession The customer's tokens, as kept in storage.
|
|
30
|
-
* @property {string} access The Customer Account API access token.
|
|
31
|
-
* @property {number} expiresAt When `access` is to be refreshed, in milliseconds since the epoch.
|
|
32
|
-
* @property {string | null} refresh
|
|
33
|
-
* @property {string | null} idToken For Shopify's logout.
|
|
34
|
-
*/
|
|
35
|
-
/**
|
|
36
|
-
* @typedef {object} PendingLogin What the callback checks Shopify's answer against.
|
|
37
|
-
* @property {string} state
|
|
38
|
-
* @property {string} nonce
|
|
39
|
-
* @property {string} verifier The PKCE code verifier.
|
|
40
|
-
* @property {string} redirect The callback URL the authorization request named.
|
|
41
|
-
* @property {string} returnTo
|
|
42
|
-
* @property {number} at When the visitor left for Shopify, in milliseconds since the epoch.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
/** An access token is refreshed this long before Shopify says it expires, so a request never carries a dead one. */
|
|
46
|
-
const REFRESH_MARGIN_SECONDS = 120;
|
|
47
|
-
/** A login that comes back later than this is refused: its `state` and verifier have been lying in storage. */
|
|
48
|
-
const LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @typedef {object} AccountCustomer The customer as `CUSTOMER_QUERY` (account-ops.js) reads them.
|
|
52
|
-
* @property {string} [id] Left out in server mode, whose Worker keeps the id to itself.
|
|
53
|
-
* @property {string | null} firstName
|
|
54
|
-
* @property {string | null} lastName
|
|
55
|
-
* @property {string | null} displayName
|
|
56
|
-
* @property {{ emailAddress: string | null } | null} emailAddress
|
|
57
|
-
* @property {{ formatted: string[] | null } | null} defaultAddress
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
/** @type {BrowserSession | null} */
|
|
61
|
-
let session = null;
|
|
62
|
-
/** @type {AccountCustomer | null} The logged-in customer, until a change of their data or the end of the session. */
|
|
63
|
-
let customerProfile = null;
|
|
64
|
-
/** @type {Promise<void> | null} The refresh on its way: one at a time, whoever asks. */
|
|
65
|
-
let refreshing = null;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @param {unknown} value
|
|
69
|
-
* @returns {BrowserSession | null}
|
|
70
|
-
*/
|
|
71
|
-
function toSession(value) {
|
|
72
|
-
if (!isRecord(value)) return null;
|
|
73
|
-
const access = value['access'];
|
|
74
|
-
const expiresAt = value['expiresAt'];
|
|
75
|
-
if (typeof access !== 'string' || typeof expiresAt !== 'number') return null;
|
|
76
|
-
return { access, expiresAt, refresh: textOrNull(value['refresh']), idToken: textOrNull(value['idToken']) };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* @param {unknown} value
|
|
81
|
-
* @returns {PendingLogin | null}
|
|
82
|
-
*/
|
|
83
|
-
function toPendingLogin(value) {
|
|
84
|
-
if (!isRecord(value)) return null;
|
|
85
|
-
const { state, nonce, verifier, redirect, returnTo, at } = value;
|
|
86
|
-
if (typeof state !== 'string' || typeof nonce !== 'string' || typeof verifier !== 'string') return null;
|
|
87
|
-
if (typeof redirect !== 'string' || typeof returnTo !== 'string' || typeof at !== 'number') return null;
|
|
88
|
-
return { state, nonce, verifier, redirect, returnTo, at };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** @returns {BrowserSession | null} The session of this page, else the stored one. */
|
|
92
|
-
const currentSession = () => session ?? toSession(readJsonIfAllowed(sessionArea(), sessionKey()));
|
|
93
|
-
|
|
94
|
-
/** @returns {string | null} The session's access token, which a refresh may have replaced or ended. */
|
|
95
|
-
const accessToken = () => session?.access ?? null;
|
|
96
|
-
|
|
97
|
-
/** @returns {boolean} True while there is an access token that hasn't expired, or a refresh token. */
|
|
98
|
-
export const hasBrowserLogin = () =>
|
|
99
|
-
Boolean(session && (session.refresh || (session.access && session.expiresAt > Date.now())));
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* @returns {string} The configured client id.
|
|
103
|
-
* @throws {TetherError} `NOT_CONFIGURED` without one.
|
|
104
|
-
*/
|
|
105
|
-
function clientId() {
|
|
106
|
-
const id = cfg.account?.clientId;
|
|
107
|
-
if (!id) throw new TetherError('Customer accounts: set account.clientId', { code: 'NOT_CONFIGURED' });
|
|
108
|
-
return id;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** @param {string} text */
|
|
112
|
-
const sha256 = async (text) => toBase64Url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)));
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* To Shopify's login page; the visitor comes back to `account.callbackUrl` (default: this page).
|
|
116
|
-
*
|
|
117
|
-
* @param {{ returnTo: string, locale?: string | undefined }} options
|
|
118
|
-
* @throws {TetherError} `NOT_CONFIGURED` without `account.clientId`.
|
|
119
|
-
*/
|
|
120
|
-
export async function browserLoginRedirect({ returnTo, locale }) {
|
|
121
|
-
const client = clientId();
|
|
122
|
-
const endpoints = await discover();
|
|
123
|
-
/** @type {PendingLogin} */
|
|
124
|
-
const pending = {
|
|
125
|
-
state: randomToken(),
|
|
126
|
-
nonce: randomToken(),
|
|
127
|
-
verifier: randomToken(),
|
|
128
|
-
redirect: cfg.account?.callbackUrl || `${window.location.origin}${window.location.pathname}`,
|
|
129
|
-
returnTo,
|
|
130
|
-
at: Date.now(),
|
|
131
|
-
};
|
|
132
|
-
writeJsonIfAllowed('session', pendingLoginKey(), pending);
|
|
133
|
-
const url = new URL(endpoints.authorizationEndpoint);
|
|
134
|
-
/** @type {Record<string, string>} */
|
|
135
|
-
const params = {
|
|
136
|
-
client_id: client,
|
|
137
|
-
scope: 'openid email customer-account-api:full',
|
|
138
|
-
response_type: 'code',
|
|
139
|
-
redirect_uri: pending.redirect,
|
|
140
|
-
state: pending.state,
|
|
141
|
-
nonce: pending.nonce,
|
|
142
|
-
code_challenge: await sha256(pending.verifier),
|
|
143
|
-
code_challenge_method: 'S256',
|
|
144
|
-
};
|
|
145
|
-
const language = loginLanguage(locale);
|
|
146
|
-
if (language) params['locale'] = language;
|
|
147
|
-
const country = loginCountry();
|
|
148
|
-
if (country) params['region_country'] = country;
|
|
149
|
-
Object.entries(params).forEach(([name, value]) => {
|
|
150
|
-
url.searchParams.set(name, value);
|
|
151
|
-
});
|
|
152
|
-
navigate(url.toString());
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* @param {TokenAnswer} answer
|
|
157
|
-
* @param {BrowserSession | null} previous A refresh answer may leave out the refresh and id tokens: these are kept.
|
|
158
|
-
*/
|
|
159
|
-
function keep(answer, previous) {
|
|
160
|
-
session = {
|
|
161
|
-
access: answer.access,
|
|
162
|
-
expiresAt: Date.now() + Math.max(0, answer.expiresInSeconds - REFRESH_MARGIN_SECONDS) * 1000,
|
|
163
|
-
refresh: answer.refresh ?? previous?.refresh ?? null,
|
|
164
|
-
idToken: answer.idToken ?? previous?.idToken ?? null,
|
|
165
|
-
};
|
|
166
|
-
writeJsonIfAllowed(sessionArea(), sessionKey(), session);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* On the callback page: finishes the login Shopify sends back (`?code=&state=`), and goes on to its `returnTo`.
|
|
171
|
-
* Sends `tether:account-login`, or `tether:account-error` with Shopify's `error`.
|
|
172
|
-
*
|
|
173
|
-
* @returns {Promise<boolean>} True when a login finished.
|
|
174
|
-
* @throws {TetherError} When the login took too long, or Shopify's answer doesn't check out.
|
|
175
|
-
*/
|
|
176
|
-
export async function handleCallback() {
|
|
177
|
-
const params = new URLSearchParams(window.location.search);
|
|
178
|
-
const pending = toPendingLogin(readJsonIfAllowed('session', pendingLoginKey()));
|
|
179
|
-
if (!pending || (!params.has('code') && !params.has('error')) || params.get('state') !== pending.state) return false;
|
|
180
|
-
writeJsonIfAllowed('session', pendingLoginKey(), null);
|
|
181
|
-
const clean = new URL(window.location.href);
|
|
182
|
-
['code', 'state', 'iss', 'error', 'error_description'].forEach((name) => {
|
|
183
|
-
clean.searchParams.delete(name);
|
|
184
|
-
});
|
|
185
|
-
window.history.replaceState(window.history.state, '', clean);
|
|
186
|
-
const error = params.get('error');
|
|
187
|
-
if (error !== null) {
|
|
188
|
-
emit('account-error', { error });
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
if (Date.now() - pending.at > LOGIN_TIMEOUT_MS) {
|
|
192
|
-
throw new TetherError('Customer accounts: the login took too long, please try again');
|
|
193
|
-
}
|
|
194
|
-
const client = clientId();
|
|
195
|
-
const answer = await requestToken({
|
|
196
|
-
grant_type: 'authorization_code',
|
|
197
|
-
client_id: client,
|
|
198
|
-
redirect_uri: pending.redirect,
|
|
199
|
-
code: params.get('code') ?? '',
|
|
200
|
-
code_verifier: pending.verifier,
|
|
201
|
-
});
|
|
202
|
-
const { issuer } = await discover();
|
|
203
|
-
if (!answersLogin(answer.idToken, { nonce: pending.nonce, issuer, client })) {
|
|
204
|
-
throw new TetherError('Customer accounts: the login answer did not check out (nonce/issuer/audience)');
|
|
205
|
-
}
|
|
206
|
-
keep(answer, null);
|
|
207
|
-
await linkCart();
|
|
208
|
-
emit('account-login', {});
|
|
209
|
-
const back = pending.returnTo ? new URL(pending.returnTo, window.location.href) : null;
|
|
210
|
-
if (back?.origin === window.location.origin && back.href !== clean.href) navigate(back.href);
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** Forgets the session here; the next `getToken()` is `null`. */
|
|
215
|
-
function endSession() {
|
|
216
|
-
session = null;
|
|
217
|
-
customerProfile = null;
|
|
218
|
-
writeJsonIfAllowed(sessionArea(), sessionKey(), null);
|
|
219
|
-
Cart.setCustomer({ accessToken: null });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** @param {BrowserSession} expired A session with a refresh token. */
|
|
223
|
-
async function refresh(expired) {
|
|
224
|
-
try {
|
|
225
|
-
const answer = await requestToken({
|
|
226
|
-
grant_type: 'refresh_token',
|
|
227
|
-
client_id: clientId(),
|
|
228
|
-
refresh_token: expired.refresh ?? '',
|
|
229
|
-
});
|
|
230
|
-
keep(answer, expired);
|
|
231
|
-
await linkCart();
|
|
232
|
-
} catch (error) {
|
|
233
|
-
if (error instanceof TetherError && error.code === 'SESSION_ENDED') endSession();
|
|
234
|
-
else throw error;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* A valid access token: refreshed when needed, one refresh at a time.
|
|
240
|
-
*
|
|
241
|
-
* @returns {Promise<string | null>} `null` when nobody is logged in, or Shopify ended the login.
|
|
242
|
-
* @throws {TetherError} When a refresh fails for another reason (the network): the login may still be good.
|
|
243
|
-
*/
|
|
244
|
-
export async function getToken() {
|
|
245
|
-
session = currentSession();
|
|
246
|
-
if (!session) return null;
|
|
247
|
-
if (session.access && session.expiresAt > Date.now()) return session.access;
|
|
248
|
-
if (!session.refresh) {
|
|
249
|
-
endSession();
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
const expired = session;
|
|
253
|
-
refreshing ??= refresh(expired).finally(() => {
|
|
254
|
-
refreshing = null;
|
|
255
|
-
});
|
|
256
|
-
await refreshing;
|
|
257
|
-
return accessToken();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** The customer's checkout is theirs: saved addresses, vaulted cards, the order in their order history. */
|
|
261
|
-
async function linkCart() {
|
|
262
|
-
const token = accessToken();
|
|
263
|
-
Cart.setCustomer({ accessToken: token });
|
|
264
|
-
if (!Cart.id() || !token) return;
|
|
265
|
-
try {
|
|
266
|
-
await Cart.setBuyerIdentity({ customerAccessToken: token }, { create: false });
|
|
267
|
-
} catch (error) {
|
|
268
|
-
reportError(error, 'account cart');
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Unlinks the cart, so the next person on this device doesn't check out as this customer. When Shopify can't be
|
|
274
|
-
* told, the cart is forgotten here instead: the next visitor starts a new one.
|
|
275
|
-
*/
|
|
276
|
-
async function unlinkCart() {
|
|
277
|
-
if (!Cart.id()) return;
|
|
278
|
-
try {
|
|
279
|
-
await Cart.setBuyerIdentity({ customerAccessToken: null }, { create: false });
|
|
280
|
-
} catch (error) {
|
|
281
|
-
reportError(error, 'account cart unlink');
|
|
282
|
-
// The cart id is kept under the `storageKey` option. The cart's own module isn't shared with the parts of the
|
|
283
|
-
// `<script>` build, so the id is removed here.
|
|
284
|
-
writeJsonIfAllowed('local', cfg.storageKey, null);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Logs out here, unlinks the cart, then goes on to Shopify's logout. Sends `tether:account-logout`.
|
|
290
|
-
*
|
|
291
|
-
* @param {string} returnTo Where Shopify sends the visitor afterwards.
|
|
292
|
-
* @returns {Promise<'redirected' | 'stayed'>} `stayed` without an id token, which Shopify's logout needs.
|
|
293
|
-
*/
|
|
294
|
-
export async function browserLogout(returnTo) {
|
|
295
|
-
const idToken = currentSession()?.idToken ?? null;
|
|
296
|
-
await unlinkCart();
|
|
297
|
-
endSession();
|
|
298
|
-
emit('account-logout', {});
|
|
299
|
-
if (!idToken) return 'stayed';
|
|
300
|
-
const url = new URL((await discover()).endSessionEndpoint);
|
|
301
|
-
url.searchParams.set('id_token_hint', idToken);
|
|
302
|
-
url.searchParams.set('post_logout_redirect_uri', returnTo);
|
|
303
|
-
navigate(url.toString());
|
|
304
|
-
return 'redirected';
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Any Customer Account API query, with the customer's token; a token Shopify refuses is refreshed once.
|
|
309
|
-
*
|
|
310
|
-
* @template T The shape of the query's `data`, which the query text decides.
|
|
311
|
-
* @param {string} document
|
|
312
|
-
* @param {Record<string, unknown>} [variables]
|
|
313
|
-
* @returns {Promise<T>}
|
|
314
|
-
* @throws {TetherError} `NOT_LOGGED_IN` without a login, `HTTP_<status>` for a failed request or GraphQL errors.
|
|
315
|
-
*/
|
|
316
|
-
export async function query(document, variables = {}) {
|
|
317
|
-
const token = await getToken();
|
|
318
|
-
if (!token) throw new TetherError('Not logged in', { code: 'NOT_LOGGED_IN' });
|
|
319
|
-
const { graphql } = await discover();
|
|
320
|
-
/** @param {string} accessToken */
|
|
321
|
-
const send = (accessToken) =>
|
|
322
|
-
fetch(graphql, {
|
|
323
|
-
method: 'POST',
|
|
324
|
-
headers: { 'Content-Type': 'application/json', Authorization: accessToken },
|
|
325
|
-
body: JSON.stringify({ query: document, variables }),
|
|
326
|
-
});
|
|
327
|
-
let response = await send(token);
|
|
328
|
-
if (response.status === 401 && session?.refresh) {
|
|
329
|
-
session.expiresAt = 0;
|
|
330
|
-
response = await send((await getToken()) ?? '');
|
|
331
|
-
}
|
|
332
|
-
const json = await readJsonObjectOrEmpty(response);
|
|
333
|
-
const errors = Array.isArray(json['errors']) ? json['errors'] : [];
|
|
334
|
-
if (!response.ok || errors.length) {
|
|
335
|
-
const messages = errors.map((error) => (isRecord(error) ? textOrNull(error['message']) : null)).filter(Boolean);
|
|
336
|
-
throw new TetherError(messages.join(' | ') || `Customer accounts: ${response.status}`, {
|
|
337
|
-
status: response.status,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
// The query text decides the shape of `data`; the caller's `T` states it.
|
|
341
|
-
return /** @type {T} */ (json['data']);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* @returns {Promise<AccountCustomer | null>} The logged-in customer, asked once.
|
|
346
|
-
* @throws {TetherError} As `query`.
|
|
347
|
-
*/
|
|
348
|
-
export async function browserCustomer() {
|
|
349
|
-
if (customerProfile) return customerProfile;
|
|
350
|
-
/** @type {{ customer: AccountCustomer | null }} */
|
|
351
|
-
const data = await query(CUSTOMER_QUERY);
|
|
352
|
-
customerProfile = data.customer;
|
|
353
|
-
return customerProfile;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/** Asks Shopify again on the next `browserCustomer()`, after a change of the customer's data. */
|
|
357
|
-
export function forgetBrowserCustomer() {
|
|
358
|
-
customerProfile = null;
|
|
359
|
-
}
|
package/src/account/markup.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The account markup's shared vocabulary: `tether-*-show` conditions, binds on a template row (the row itself may
|
|
3
|
-
* carry one), and how Shopify's enum values and formatted addresses read on the page.
|
|
4
|
-
*/
|
|
5
|
-
import { get, qsa, sel, show } from '../dom.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Shows or hides each `name` element whose value is one of `conditions`' keys; other values are left alone.
|
|
9
|
-
*
|
|
10
|
-
* @param {ParentNode} root
|
|
11
|
-
* @param {string} name `show`, `order-show`, `address-show`…
|
|
12
|
-
* @param {Readonly<Record<string, boolean>>} conditions
|
|
13
|
-
*/
|
|
14
|
-
export function showIn(root, name, conditions) {
|
|
15
|
-
qsa(root, sel(name)).forEach((el) => {
|
|
16
|
-
const key = get(el, name) ?? '';
|
|
17
|
-
if (Object.hasOwn(conditions, key)) show(el, conditions[key] === true);
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {HTMLElement} row A copy of a repeater's template.
|
|
23
|
-
* @param {string} name
|
|
24
|
-
* @returns {HTMLElement[]} The `name` elements in `row`, and `row` itself when it carries `name`.
|
|
25
|
-
*/
|
|
26
|
-
export const bindsIn = (row, name) => qsa(row, sel(name)).concat(row.matches(sel(name)) ? [row] : []);
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {string | null | undefined} value
|
|
30
|
-
* @returns {string} A Shopify enum as text: `PARTIALLY_FULFILLED` → `partially fulfilled`.
|
|
31
|
-
*/
|
|
32
|
-
export const enumText = (value) => (value ? value.toLowerCase().replace(/_/g, ' ') : '');
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @param {{ formatted?: string[] | null } | null | undefined} address
|
|
36
|
-
* @returns {string} Shopify's formatted address on one line.
|
|
37
|
-
*/
|
|
38
|
-
export const addressText = (address) => (address?.formatted ?? []).join(', ');
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* A bind's value, looked up by the attribute's text: only the table's own keys, never `constructor` and the like.
|
|
42
|
-
*
|
|
43
|
-
* @template T
|
|
44
|
-
* @param {Readonly<Record<string, T>>} values
|
|
45
|
-
* @param {string} key
|
|
46
|
-
* @returns {T | undefined}
|
|
47
|
-
*/
|
|
48
|
-
export const ownValue = (values, key) => (Object.hasOwn(values, key) ? values[key] : undefined);
|