@occluze/tether 0.6.4 → 0.6.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occluze/tether",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Free, open-source alternative to Smootify and Storesynk (Shopyflow): Shopify products, cart and subscriptions in Webflow with HTML attributes. Optional Shopify to Webflow CMS sync.",
5
5
  "type": "module",
6
6
  "main": "dist/esm/index.js",
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Customer account pages (optional part, loaded by the account part on pages that use them):
3
+ * order detail with returns, profile, addresses, subscriptions. The operations and their input
4
+ * checks are shared with the Worker (server/src/account-ops.js), so both modes do the same.
5
+ */
6
+ import { cfg, reportError } from './config.js';
7
+ import { TetherError } from './client.js';
8
+ import { attr, sel, get, qsa, setText, setImage, repeat, show, formatMoney, emit } from './dom.js';
9
+ import { ops, InputError } from '../server/src/account-ops.js';
10
+
11
+ let ctx = null; // the account part's server(), serverFetch(), query(), customer(), reset()
12
+ let bound = false;
13
+ export function setup(c) { ctx = c; }
14
+
15
+ const https = (u) => { try { return new URL(u).protocol === 'https:'; } catch { return false; } };
16
+ const date = (s) => { try { return new Date(s).toLocaleDateString(cfg.locale || undefined); } catch { return s; } };
17
+ // ---------------------------------------------------------------- order detail, profile, addresses, subscriptions, returns
18
+
19
+ // The same fixed operations in both modes: the Worker's routes in server mode, the Customer
20
+ // Account API directly (with the same documents and input checks) in browser mode.
21
+ const ROUTE = {
22
+ order: (i) => ['GET', `/order?id=${encodeURIComponent(i.id)}`],
23
+ addresses: () => ['GET', '/addresses'],
24
+ subscriptions: () => ['GET', '/subscriptions'],
25
+ profile: () => ['POST', '/profile'],
26
+ saveAddress: () => ['POST', '/address'],
27
+ deleteAddress: () => ['POST', '/address/delete'],
28
+ subscription: () => ['POST', '/subscription'],
29
+ requestReturn: () => ['POST', '/return'],
30
+ };
31
+ async function run(name, input = {}) {
32
+ const { server, serverFetch, query } = ctx;
33
+ if (server()) {
34
+ const [method, path] = ROUTE[name](input);
35
+ const data = await serverFetch(path, method === 'POST' ? { method, body: input } : {});
36
+ if (data.loggedIn === false) throw new TetherError('Not logged in', { code: 'NOT_LOGGED_IN', status: 401 });
37
+ return data;
38
+ }
39
+ try { return await ops[name]((q, v) => query(q, v), input); } catch (err) {
40
+ if (err instanceof InputError) throw new TetherError(err.message, { code: 'INVALID', userErrors: err.userErrors });
41
+ throw err;
42
+ }
43
+ }
44
+ const changed = (what) => { ctx.reset(); emit('account-updated', { what }); };
45
+
46
+ /** One of the customer's orders, in full (lines, totals, addresses, fulfillments, returns). */
47
+ export const order = (id) => run('order', { id }).then((r) => r.order);
48
+ export const updateProfile = ({ firstName, lastName } = {}) => run('profile', { firstName, lastName }).then((r) => { changed('profile'); return r.customer; });
49
+ export const addresses = () => run('addresses').then((r) => r.addresses);
50
+ /** saveAddress({ address1, city, zip, countryCode, … }, { id, isDefault }): creates, or updates with an id. */
51
+ export const saveAddress = (address, { id, isDefault } = {}) => run('saveAddress', { id, address, isDefault }).then((r) => { changed('addresses'); return r.address; });
52
+ export const deleteAddress = (id) => run('deleteAddress', { id }).then((r) => { changed('addresses'); return r.deleted; });
53
+ export const subscriptions = () => run('subscriptions').then((r) => r.subscriptions);
54
+ /** subscriptionAction(id, 'pause' | 'resume' | 'skip' | 'unskip' | 'cancel') */
55
+ export const subscriptionAction = (id, action) => run('subscription', { id, action }).then((r) => { changed('subscriptions'); return r; });
56
+ /** requestReturn(orderId, [{ lineItemId, quantity, reasonId, note }]) */
57
+ export const requestReturn = (orderId, lines) => run('requestReturn', { orderId, lines }).then((r) => { changed('orders'); return r.return; });
58
+
59
+
60
+ const nid = (g) => String(g || '').split('/').pop().split('?')[0];
61
+ /** The order's page: your own (account.orderUrl: '/account/order?id={id}') or Shopify's status page. */
62
+ function orderUrl(o) {
63
+ const t = cfg.account?.orderUrl;
64
+ if (t) return String(t).replace('{id}', encodeURIComponent(nid(o.id)));
65
+ return https(o.statusPageUrl) ? o.statusPageUrl : null;
66
+ }
67
+ const lower = (v) => (v ? String(v).toLowerCase().replace(/_/g, ' ') : '');
68
+ const lines = (a) => (a?.formatted || []).join(', ');
69
+ const showIn = (root, name, conds) => qsa(root, sel(name)).forEach((el) => { const k = get(el, name); if (k in conds) show(el, Boolean(conds[k])); });
70
+ const showGlobal = (root, conds) => showIn(root, 'show', conds);
71
+
72
+ function fillLines(box, items, returnable = null) {
73
+ repeat(box, items, (row, l) => {
74
+ const can = returnable ? returnable.get(l.id) || 0 : 0;
75
+ row.dataset.tetherLine = l.id;
76
+ qsa(row, sel('order-line')).concat(row.matches(sel('order-line')) ? [row] : []).forEach((el) => {
77
+ const k = get(el, 'order-line');
78
+ if (k === 'image') { if (l.image) setImage(el, l.image.url, l.image.altText || l.title); return; }
79
+ const v = { title: l.title || l.name, 'variant-title': l.variantTitle || '', quantity: String(l.quantity), price: l.price ? formatMoney(l.price) : '', total: l.totalPrice ? formatMoney(l.totalPrice) : '', sku: l.sku || '', returnable: String(can) }[k];
80
+ if (v != null) setText(el, v);
81
+ });
82
+ showIn(row, 'order-line-show', { returnable: can > 0 });
83
+ qsa(row, sel('return-quantity')).forEach((input) => { input.max = String(can); input.min = '0'; if (!input.value) input.value = '0'; input.disabled = can < 1; });
84
+ qsa(row, `select${sel('return-reason')}`).forEach((select) => {
85
+ const reasons = l.suggestedReturnReasonDefinitions?.nodes || [];
86
+ if (select.dataset.tetherFilled !== l.id) {
87
+ select.dataset.tetherFilled = l.id;
88
+ reasons.forEach((r) => select.appendChild(new Option(r.name, r.id)));
89
+ }
90
+ });
91
+ });
92
+ }
93
+
94
+ const SCOPE = { order: sel('order') };
95
+ async function renderOrder(el) {
96
+ const raw = get(el, 'order') || '';
97
+ const key = raw.startsWith('url:') ? new URL(window.location.href).searchParams.get(raw.slice(4) || 'id') : raw;
98
+ const state = (s) => el.setAttribute(attr('order-state'), s);
99
+ if (!key) { state('not-found'); showIn(el, 'order-show', { 'not-found': true }); return; }
100
+ state('loading');
101
+ showIn(el, 'order-show', { loading: true, ready: false, 'not-found': false, error: false });
102
+ let o;
103
+ try { o = await order(key); } catch (err) {
104
+ reportError(err, 'order'); state('error'); showIn(el, 'order-show', { loading: false, error: true }); return;
105
+ }
106
+ if (!o) { state('not-found'); showIn(el, 'order-show', { loading: false, 'not-found': true }); return; }
107
+ el.dataset.tetherOrderId = o.id;
108
+ const returnable = new Map((o.returnInformation?.returnableLineItems?.nodes || []).map((r) => [r.lineItem?.id, r.quantity]));
109
+ const ships = o.fulfillments?.nodes || [];
110
+ qsa(el, sel('order-bind')).forEach((b) => {
111
+ const k = get(b, 'order-bind');
112
+ if (k === 'url') { if (https(o.statusPageUrl)) b.setAttribute('href', o.statusPageUrl); return; }
113
+ const v = {
114
+ name: o.name, date: date(o.processedAt), total: formatMoney(o.totalPrice), subtotal: o.subtotal ? formatMoney(o.subtotal) : '',
115
+ shipping: o.totalShipping ? formatMoney(o.totalShipping) : '', tax: o.totalTax ? formatMoney(o.totalTax) : '',
116
+ status: lower(o.financialStatus), fulfillment: lower(o.fulfillmentStatus), 'shipping-address': lines(o.shippingAddress),
117
+ 'billing-address': lines(o.billingAddress), items: (o.lineItems?.nodes || []).map((l) => `${l.quantity}× ${l.title || l.name}`).join(', '),
118
+ cancelled: o.cancelledAt ? date(o.cancelledAt) : '',
119
+ }[k];
120
+ if (v != null) setText(b, v);
121
+ });
122
+ qsa(el, sel('order-lines')).forEach((box) => fillLines(box, o.lineItems?.nodes || [], returnable));
123
+ qsa(el, sel('order-fulfillments')).forEach((box) => repeat(box, ships, (row, f) => {
124
+ const t = f.trackingInformation?.[0] || {};
125
+ qsa(row, sel('fulfillment-bind')).concat(row.matches(sel('fulfillment-bind')) ? [row] : []).forEach((b) => {
126
+ const k = get(b, 'fulfillment-bind');
127
+ if (k === 'url') { if (https(t.url)) b.setAttribute('href', t.url); return; }
128
+ const v = { status: lower(f.latestShipmentStatus || f.status), date: date(f.createdAt), eta: f.estimatedDeliveryAt ? date(f.estimatedDeliveryAt) : '', company: t.company || '', number: t.number || '' }[k];
129
+ if (v != null) setText(b, v);
130
+ });
131
+ }));
132
+ showIn(el, 'order-show', {
133
+ loading: false, ready: true, error: false, 'not-found': false, cancelled: Boolean(o.cancelledAt), shipped: ships.length > 0,
134
+ returnable: [...returnable.values()].some((q) => q > 0), 'has-returns': (o.returns?.nodes || []).length > 0, 'return-requested': el.dataset.tetherReturned === '1',
135
+ });
136
+ state('ready');
137
+ }
138
+
139
+ async function renderProfile(form) {
140
+ const c = await ctx.customer().catch(() => null);
141
+ if (!c) return;
142
+ ['firstName', 'lastName'].forEach((n) => { const i = form.elements?.[n]; if (i && !i.dataset.tetherTouched) i.value = c[n] || ''; });
143
+ }
144
+
145
+ let addressCache = null;
146
+ async function renderAddresses(root) {
147
+ const boxes = qsa(root, sel('addresses'));
148
+ if (!boxes.length) return;
149
+ showGlobal(root, { 'addresses-loading': true, 'addresses-empty': false });
150
+ let list = [];
151
+ try { list = addressCache = await addresses(); } catch (err) { reportError(err, 'addresses'); }
152
+ showGlobal(root, { 'addresses-loading': false, 'addresses-empty': list.length === 0 });
153
+ boxes.forEach((box) => repeat(box, list, (row, a) => {
154
+ row.dataset.tetherAddress = a.id;
155
+ qsa(row, sel('address-bind')).concat(row.matches(sel('address-bind')) ? [row] : []).forEach((b) => {
156
+ const v = { formatted: lines(a), name: [a.firstName, a.lastName].filter(Boolean).join(' '), company: a.company || '', address1: a.address1 || '', address2: a.address2 || '', city: a.city || '', zip: a.zip || '', province: a.province || '', country: a.country || '', phone: a.phoneNumber || '' }[get(b, 'address-bind')];
157
+ if (v != null) setText(b, v);
158
+ });
159
+ showIn(row, 'address-show', { default: a.isDefault, 'not-default': !a.isDefault });
160
+ }));
161
+ }
162
+
163
+ const SUB_ACTIONS = ['pause', 'resume', 'skip', 'unskip', 'cancel'];
164
+ function subConds(c) {
165
+ const next = c.upcomingBillingCycles?.nodes?.[0];
166
+ const st = c.status;
167
+ return {
168
+ active: st === 'ACTIVE', paused: st === 'PAUSED', cancelled: st === 'CANCELLED', expired: st === 'EXPIRED', failed: st === 'FAILED',
169
+ skipped: Boolean(next?.skipped), 'can-pause': st === 'ACTIVE', 'can-resume': st === 'PAUSED' || st === 'FAILED',
170
+ 'can-skip': st === 'ACTIVE' && Boolean(next) && !next.skipped, 'can-unskip': st === 'ACTIVE' && Boolean(next?.skipped), 'can-cancel': st === 'ACTIVE' || st === 'PAUSED' || st === 'FAILED',
171
+ };
172
+ }
173
+ async function renderSubscriptions(root) {
174
+ const boxes = qsa(root, sel('subscriptions'));
175
+ if (!boxes.length) return;
176
+ showGlobal(root, { 'subscriptions-loading': true, 'subscriptions-empty': false });
177
+ let list = [];
178
+ try { list = await subscriptions(); } catch (err) { reportError(err, 'subscriptions'); }
179
+ showGlobal(root, { 'subscriptions-loading': false, 'subscriptions-empty': list.length === 0 });
180
+ boxes.forEach((box) => repeat(box, list, (row, c) => {
181
+ row.dataset.tetherSubscription = c.id;
182
+ row.setAttribute(attr('subscription-status'), lower(c.status));
183
+ const ls = c.lines?.nodes || [];
184
+ const n = c.deliveryPolicy?.intervalCount?.count || 1;
185
+ const unit = lower(c.deliveryPolicy?.interval);
186
+ const total = ls.reduce((sum, l) => sum + parseFloat(l.lineDiscountedPrice?.amount || 0), 0);
187
+ qsa(row, sel('subscription-bind')).forEach((b) => {
188
+ const k = get(b, 'subscription-bind');
189
+ const v = {
190
+ status: lower(c.status), next: c.nextBillingDate ? date(c.nextBillingDate) : '', interval: unit ? (n === 1 ? unit : `${n} ${unit}s`) : '',
191
+ products: ls.map((l) => `${l.quantity}× ${l.title}`).join(', '), total: ls.length ? formatMoney({ amount: String(total), currencyCode: c.currencyCode }) : '',
192
+ delivery: c.deliveryPrice ? formatMoney(c.deliveryPrice) : '',
193
+ }[k];
194
+ if (v != null) setText(b, v);
195
+ });
196
+ qsa(row, sel('subscription-lines')).forEach((lb) => repeat(lb, ls, (lr, l) => {
197
+ qsa(lr, sel('subscription-line')).concat(lr.matches(sel('subscription-line')) ? [lr] : []).forEach((b) => {
198
+ const k = get(b, 'subscription-line');
199
+ if (k === 'image') { if (l.image) setImage(b, l.image.url, l.image.altText || l.title); return; }
200
+ const v = { title: l.title, 'variant-title': l.variantTitle || '', quantity: String(l.quantity), price: formatMoney(l.currentPrice) }[k];
201
+ if (v != null) setText(b, v);
202
+ });
203
+ }));
204
+ const conds = subConds(c);
205
+ showIn(row, 'subscription-show', conds);
206
+ qsa(row, sel('subscription-action')).forEach((btn) => {
207
+ const a = get(btn, 'subscription-action');
208
+ const ok = conds[`can-${a}`];
209
+ btn.disabled = !ok;
210
+ btn.classList.toggle('is-disabled', !ok);
211
+ });
212
+ }));
213
+ }
214
+
215
+ /** Order page, profile form, addresses, subscriptions: only what the page has. */
216
+ export async function renderPages(root = document) {
217
+ if (!bound) { bound = true; bindPages(); }
218
+ await Promise.all([
219
+ ...qsa(root, sel('order')).map((el) => renderOrder(el)),
220
+ ...qsa(root, sel('profile-form')).map((f) => renderProfile(f)),
221
+ renderAddresses(root),
222
+ renderSubscriptions(root),
223
+ ]);
224
+ }
225
+
226
+ const fieldsOf = (form) => Object.fromEntries([...form.elements].filter((i) => i.name && i.type !== 'submit' && i.type !== 'button' && i.type !== 'checkbox').map((i) => [i.name, i.value]));
227
+ async function act(el, fn, { error } = {}) {
228
+ el.classList.add('is-busy'); el.setAttribute('aria-busy', 'true');
229
+ el.setAttribute(attr('form-state'), 'pending');
230
+ qsa(el, sel('form-error')).forEach((e) => setText(e, ''));
231
+ try {
232
+ const out = await fn();
233
+ el.setAttribute(attr('form-state'), 'saved');
234
+ return out;
235
+ } catch (err) {
236
+ el.setAttribute(attr('form-state'), 'error');
237
+ const msg = err?.userErrors?.map((u) => u.message).join(' ') || err?.message || 'Something went wrong';
238
+ qsa(el, sel('form-error')).forEach((e) => setText(e, msg.replace(/^Customer accounts: /, '')));
239
+ if (error) reportError(err, error);
240
+ return undefined;
241
+ } finally { el.classList.remove('is-busy'); el.removeAttribute('aria-busy'); }
242
+ }
243
+
244
+ function bindPages() {
245
+ document.addEventListener('submit', (e) => {
246
+ const f = e.target;
247
+ if (f.matches?.(sel('profile-form'))) {
248
+ e.preventDefault();
249
+ act(f, () => updateProfile(fieldsOf(f)));
250
+ } else if (f.matches?.(sel('address-form'))) {
251
+ e.preventDefault();
252
+ const id = f.dataset.tetherAddressId || undefined;
253
+ const isDefault = Boolean(f.elements?.isDefault?.checked);
254
+ act(f, async () => { await saveAddress(fieldsOf(f), { id, isDefault }); f.reset(); delete f.dataset.tetherAddressId; await renderAddresses(document); });
255
+ }
256
+ });
257
+ document.addEventListener('input', (e) => { if (e.target.closest?.(sel('profile-form'))) e.target.dataset.tetherTouched = '1'; });
258
+ document.addEventListener('click', (e) => {
259
+ const t = e.target;
260
+ const row = t.closest?.('[data-tether-address]');
261
+ const confirmOk = (btn) => { const q = get(btn, 'confirm'); return !q || window.confirm(q); };
262
+ let btn;
263
+ if ((btn = t.closest?.(sel('address-delete'))) && row) {
264
+ e.preventDefault();
265
+ if (confirmOk(btn)) act(row, async () => { await deleteAddress(row.dataset.tetherAddress); await renderAddresses(document); });
266
+ } else if ((btn = t.closest?.(sel('address-default'))) && row) {
267
+ e.preventDefault();
268
+ act(row, async () => { await saveAddress(null, { id: row.dataset.tetherAddress, isDefault: true }); await renderAddresses(document); });
269
+ } else if ((btn = t.closest?.(sel('address-edit'))) && row) {
270
+ e.preventDefault();
271
+ const a = (addressCache || []).find((x) => x.id === row.dataset.tetherAddress);
272
+ const f = document.querySelector(sel('address-form'));
273
+ if (a && f) {
274
+ f.dataset.tetherAddressId = a.id;
275
+ [...f.elements].forEach((i) => { if (i.name && i.name in a) i.value = a[i.name] ?? ''; if (i.name === 'isDefault') i.checked = a.isDefault; });
276
+ f.scrollIntoView?.({ block: 'nearest' });
277
+ }
278
+ } else if ((btn = t.closest?.(sel('address-new')))) {
279
+ const f = document.querySelector(sel('address-form'));
280
+ if (f) { f.reset(); delete f.dataset.tetherAddressId; }
281
+ } else if ((btn = t.closest?.(sel('subscription-action')))) {
282
+ e.preventDefault();
283
+ const sub = btn.closest('[data-tether-subscription]');
284
+ const a = get(btn, 'subscription-action');
285
+ if (!sub || !SUB_ACTIONS.includes(a) || btn.disabled || !confirmOk(btn)) return;
286
+ sub.setAttribute(attr('subscription-state'), 'pending');
287
+ act(sub, async () => { await subscriptionAction(sub.dataset.tetherSubscription, a); await renderSubscriptions(document); }).then(() => {
288
+ sub.setAttribute(attr('subscription-state'), sub.getAttribute(attr('form-state')) === 'error' ? 'error' : 'done');
289
+ });
290
+ } else if ((btn = t.closest?.(sel('order-return')))) {
291
+ e.preventDefault();
292
+ const scope = btn.closest(SCOPE.order);
293
+ if (!scope?.dataset.tetherOrderId) return;
294
+ const req = qsa(scope, '[data-tether-line]').map((r) => ({
295
+ lineItemId: r.dataset.tetherLine,
296
+ quantity: parseInt(r.querySelector(sel('return-quantity'))?.value || '0', 10),
297
+ reasonId: r.querySelector(sel('return-reason'))?.value || undefined,
298
+ note: r.querySelector(sel('return-note'))?.value || undefined,
299
+ })).filter((l) => l.quantity > 0);
300
+ if (!req.length || !confirmOk(btn)) return;
301
+ act(scope, async () => { await requestReturn(scope.dataset.tetherOrderId, req); scope.dataset.tetherReturned = '1'; await renderOrder(scope); });
302
+ }
303
+ });
304
+ }
305
+
package/src/account.js CHANGED
@@ -21,8 +21,9 @@
21
21
  */
22
22
  import { cfg, log, reportError } from './config.js';
23
23
  import { TetherError } from './client.js';
24
- import { sel, get, qsa, setText, setImage, repeat, show, formatMoney, emit } from './dom.js';
24
+ import { attr, sel, get, qsa, setText, setImage, repeat, show, formatMoney, emit } from './dom.js';
25
25
  import { Cart } from './cart.js';
26
+ import { loadPart } from './lazy.js';
26
27
 
27
28
  const KEY = () => `${cfg.prefix}:account:${cfg.domain}`;
28
29
  const PENDING = () => `${cfg.prefix}:account-login:${cfg.domain}`;
@@ -52,7 +53,7 @@ async function serverFetch(path, { method = 'GET', body } = {}) {
52
53
  body: body ? JSON.stringify(body) : undefined,
53
54
  });
54
55
  const data = await res.json().catch(() => ({}));
55
- if (!res.ok && res.status !== 401) throw new TetherError(`Customer accounts: ${data.error || res.status}`, { status: res.status });
56
+ if (!res.ok && res.status !== 401) throw new TetherError(`Customer accounts: ${data.error || res.status}`, { status: res.status, userErrors: data.userErrors });
56
57
  return data;
57
58
  }
58
59
  async function serverMe() {
@@ -227,6 +228,7 @@ export async function orders({ first = 10 } = {}) {
227
228
  return data.customer?.orders?.nodes || [];
228
229
  }
229
230
 
231
+
230
232
  /** Logs out here and at Shopify, unlinks the cart. */
231
233
  export async function logout({ returnTo = cfg.account?.logoutUrl || `${window.location.origin}/` } = {}) {
232
234
  if (server()) {
@@ -261,6 +263,13 @@ export async function render(root = document) {
261
263
  if (k === 'logged-in') show(el, logged);
262
264
  if (k === 'logged-out') show(el, !logged);
263
265
  });
266
+ // Shopify's own pages, for what has to stay there: tether-account-link="subscriptions" / "payment"
267
+ // (account.subscriptionsUrl / paymentUrl), or the account itself.
268
+ qsa(root, sel('account-link')).forEach((a) => {
269
+ const k = get(a, 'account-link');
270
+ const u = (k === 'payment' && (cfg.account?.paymentUrl || cfg.account?.subscriptionsUrl)) || (k === 'subscriptions' && cfg.account?.subscriptionsUrl);
271
+ a.setAttribute('href', u || `https://${cfg.domain}/account`);
272
+ });
264
273
  if (!logged) return;
265
274
  const binds = qsa(root, sel('account-bind'));
266
275
  if (binds.length) {
@@ -286,24 +295,59 @@ export async function render(root = document) {
286
295
  const items = o.lineItems?.nodes || [];
287
296
  qsa(node, sel('order-bind')).forEach((el) => {
288
297
  const k = get(el, 'order-bind');
289
- if (k === 'url') { if (https(o.statusPageUrl)) el.setAttribute('href', o.statusPageUrl); return; }
298
+ if (k === 'url') { const u = orderUrl(o); if (u) el.setAttribute('href', u); return; }
290
299
  if (k === 'image') { const im = items.find((l) => l.image)?.image; if (im) setImage(el, im.url, im.altText || items[0]?.title || ''); return; }
291
300
  const v = { name: o.name, date: date(o.processedAt), total: formatMoney(o.totalPrice), status: o.financialStatus?.toLowerCase(), fulfillment: o.fulfillmentStatus?.toLowerCase(), items: items.map((l) => `${l.quantity}× ${l.title}`).join(', ') }[k];
292
301
  if (v != null) setText(el, v);
293
302
  });
294
303
  // Per product of the order: <div tether-order-lines><div><img tether-order-line="image"> <span tether-order-line="title"></span></div></div>
295
- qsa(node, sel('order-lines')).forEach((box) => repeat(box, items, (row, l) => {
296
- qsa(row, sel('order-line')).concat(row.matches(sel('order-line')) ? [row] : []).forEach((el) => {
297
- const k = get(el, 'order-line');
298
- if (k === 'image') { if (l.image) setImage(el, l.image.url, l.image.altText || l.title); return; }
299
- const v = { title: l.title, 'variant-title': l.variantTitle || '', quantity: String(l.quantity) }[k];
300
- if (v != null) setText(el, v);
301
- });
302
- }));
303
- if (get(node, 'order-bind') === 'url' && https(o.statusPageUrl)) node.setAttribute('href', o.statusPageUrl);
304
+ qsa(node, sel('order-lines')).forEach((box) => fillLines(box, items));
305
+ if (get(node, 'order-bind') === 'url' && orderUrl(o)) node.setAttribute('href', orderUrl(o));
304
306
  }));
305
307
  }
308
+ if (PAGE_ATTRS.some((n) => qsa(root, sel(n)).length)) await (await pages()).renderPages(root);
309
+ }
310
+
311
+
312
+ const nid = (g) => String(g || '').split('/').pop().split('?')[0];
313
+ /** The order's page: your own (account.orderUrl: '/account/order?id={id}') or Shopify's status page. */
314
+ function orderUrl(o) {
315
+ const t = cfg.account?.orderUrl;
316
+ if (t) return String(t).replace('{id}', encodeURIComponent(nid(o.id)));
317
+ return https(o.statusPageUrl) ? o.statusPageUrl : null;
318
+ }
319
+ function fillLines(box, items) {
320
+ repeat(box, items, (row, l) => {
321
+ qsa(row, sel('order-line')).concat(row.matches(sel('order-line')) ? [row] : []).forEach((el) => {
322
+ const k = get(el, 'order-line');
323
+ if (k === 'image') { if (l.image) setImage(el, l.image.url, l.image.altText || l.title); return; }
324
+ const v = { title: l.title, 'variant-title': l.variantTitle || '', quantity: String(l.quantity) }[k];
325
+ if (v != null) setText(el, v);
326
+ });
327
+ });
328
+ }
329
+
330
+ // Order page, profile, addresses, subscriptions, returns: their own part (account-pages), loaded
331
+ // only on pages that use them. It gets this module's login state, so both share one session.
332
+ const PAGE_ATTRS = ['order', 'addresses', 'subscriptions', 'profile-form', 'address-form'];
333
+ let pagesPart = null;
334
+ function pages() {
335
+ pagesPart ||= loadPart('account-pages').then((m) => { m.setup({ server, serverFetch, query, customer, reset: () => { customerCache = null; serverState = null; } }); return m; });
336
+ return pagesPart;
306
337
  }
338
+ const viaPages = (name) => (...a) => pages().then((m) => m[name](...a));
339
+ /** One of the customer's orders, in full (lines, totals, addresses, fulfillments, returns). */
340
+ export const order = viaPages('order');
341
+ export const updateProfile = viaPages('updateProfile');
342
+ export const addresses = viaPages('addresses');
343
+ /** saveAddress({ address1, city, zip, countryCode, … }, { id, isDefault }): creates, or updates with an id. */
344
+ export const saveAddress = viaPages('saveAddress');
345
+ export const deleteAddress = viaPages('deleteAddress');
346
+ export const subscriptions = viaPages('subscriptions');
347
+ /** subscriptionAction(id, 'pause' | 'resume' | 'skip' | 'unskip' | 'cancel') */
348
+ export const subscriptionAction = viaPages('subscriptionAction');
349
+ /** requestReturn(orderId, [{ lineItemId, quantity, reasonId, note }]) */
350
+ export const requestReturn = viaPages('requestReturn');
307
351
 
308
352
  let bound = false;
309
353
  /** Called by the core on mount: finish a login that's coming back, then fill the page. */
package/src/diagnose.js CHANGED
@@ -23,6 +23,7 @@ const SHOW = new Set([
23
23
  'subscription-selected', 'one-time-selected', 'has-savings', 'loading', 'ready', 'error', 'not-found',
24
24
  'cart-empty', 'cart-filled', 'cart-has-subscription', 'cart-no-subscription', 'cart-has-discount', 'cart-error', 'cart-warning',
25
25
  'list-empty', 'list-filled', 'search-empty', 'search-results', 'logged-in', 'logged-out', 'orders-empty', 'orders-loading',
26
+ 'addresses-loading', 'addresses-empty', 'subscriptions-loading', 'subscriptions-empty',
26
27
  ]);
27
28
  const BIND = new Set([
28
29
  'title', 'description', 'description-text', 'vendor', 'type', 'handle', 'published-at', 'price', 'total-price',
@@ -37,7 +38,10 @@ const ATTRS = new Set(`account account-bind account-link account-login account-l
37
38
  country-bind country-label country-select default-plan default-purchase discount discount-code discount-form discount-input
38
39
  discount-remove display error group group-add hide-single language limit line line-attribute line-attributes line-id
39
40
  line-minus line-plan line-plus line-remove line-show list load-more market-pending one-time-label option option-list
40
- option-selected option-value order-bind order-line order-lines orders page-country page-language page-locale page-title plan plan-bind plan-list
41
+ option-selected option-value order order-bind order-line order-lines order-line-show order-show order-state order-fulfillments
42
+ fulfillment-bind order-return return-quantity return-reason return-note orders profile-form address-form addresses address-bind
43
+ address-show address-edit address-new address-delete address-default subscriptions subscription-bind subscription-lines
44
+ 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
41
45
  plan-value product products purchase purchase-mode qty qty-minus qty-plus quantity recommendations reverse search
42
46
  search-bind search-collections search-input search-limit search-min search-products search-queries search-results
43
47
  search-state show soldout-text sort state subscribe-text variant variant-select variant-url max-width max-height crop
package/src/index.js CHANGED
@@ -21,7 +21,7 @@ import { attr, sel, get, qsa, emit, formatMoney, busy, parseJSONAttr, toAttribut
21
21
  import { loadPart, coreModules } from './lazy.js';
22
22
  import { mountedLists, liveLists, pruneLive, within, mountProduct, retryFailed, hydrateFromPage, settleMarket, trackMarket } from './mounting.js';
23
23
 
24
- export const VERSION = '0.6.4';
24
+ export const VERSION = '0.6.5';
25
25
 
26
26
  let initPromise = null;
27
27
  let observer = null;
@@ -51,7 +51,7 @@ export function mount(root = document) {
51
51
  }
52
52
 
53
53
  // Customer accounts: their own optional part, when configured and the page (or a login) needs it.
54
- if ((cfg.account?.clientId || cfg.account?.server) && (within(root, `[${attr('account-login')}],[${attr('account-bind')}],[${attr('orders')}],[${attr('show')}^="logged-"]`).length
54
+ if ((cfg.account?.clientId || cfg.account?.server) && (within(root, ['account-login', 'account-bind', 'account-link', 'orders', 'order', 'addresses', 'subscriptions', 'profile-form', 'address-form'].map((n) => `[${attr(n)}]`).join(',') + `,[${attr('show')}^="logged-"]`).length
55
55
  || /[?&]code=/.test(window.location.search) || hasAccountSession())) {
56
56
  loadPart('account').then((m) => m.mountAccount(root)).catch((err) => reportError(err, 'account'));
57
57
  }
@@ -61,9 +61,6 @@ export function mount(root = document) {
61
61
  loadPart('search').then((m) => m.mountSearch(root)).catch((err) => reportError(err, 'search'));
62
62
  }
63
63
 
64
- qsa(root, sel('account-link')).forEach((a) => {
65
- a.setAttribute('href', `https://${cfg.domain}/account`);
66
- });
67
64
 
68
65
  settleMarket();
69
66
  Cart.render();
package/src/lazy-iife.js CHANGED
@@ -41,3 +41,6 @@ export function loadPart(name) {
41
41
  });
42
42
  return parts[name];
43
43
  }
44
+
45
+ // Parts that load other parts (account -> account-pages) use this same loader.
46
+ coreModules['lazy.js'] = { loadPart, coreModules };
package/src/lazy.js CHANGED
@@ -9,6 +9,7 @@ const PARTS = {
9
9
  search: () => import('./search.js'),
10
10
  extras: () => import('./extras.js'),
11
11
  account: () => import('./account.js'),
12
+ 'account-pages': () => import('./account-pages.js'),
12
13
  analytics: () => import('./analytics.js'),
13
14
  };
14
15
  export const loadPart = (name) => PARTS[name]();
@@ -0,0 +1,3 @@
1
+ import * as part from '../account-pages.js';
2
+
3
+ window.Tether.__parts['account-pages'] = part;
package/types/index.d.ts CHANGED
@@ -131,7 +131,16 @@ export interface TetherConfig {
131
131
  } | null;
132
132
  /** Customer accounts (optional part): the Headless channel's Customer Account API client id, and options. */
133
133
  account?: {
134
- clientId: string;
134
+ /** Browser mode: the Customer Account API client id. */
135
+ clientId?: string;
136
+ /** Server mode (recommended): the Worker's account routes, a URL or a path on this site ('/_tether/account'). */
137
+ server?: string;
138
+ /** Your own order page: tether-order-bind="url" links there, e.g. '/account/order?id={id}' (else Shopify's status page). */
139
+ orderUrl?: string;
140
+ /** Shopify's subscription management page (tether-account-link="subscriptions"); payment methods are changed there too. */
141
+ subscriptionsUrl?: string;
142
+ /** A separate page for payment methods (tether-account-link="payment"); default subscriptionsUrl. */
143
+ paymentUrl?: string;
135
144
  /** The page Shopify sends the visitor back to after login (registered as callback URI). Default: the current page. */
136
145
  callbackUrl?: string;
137
146
  /** Where Shopify sends the visitor after logout (registered as logout URI). Default: the home page. */
@@ -351,6 +360,15 @@ export interface Tether {
351
360
  orders(opts?: { first?: number }): Promise<Array<{ id: string; name: string; processedAt: string; financialStatus?: string; fulfillmentStatus?: string; statusPageUrl: string; totalPrice: Money }>>;
352
361
  query<T = any>(query: string, variables?: Record<string, unknown>): Promise<T>;
353
362
  getToken(): Promise<string | null>;
363
+ /** One order in full (lines, totals, addresses, fulfillments with tracking, returnable lines); null if not the customer's. */
364
+ order(id: string | number): Promise<Record<string, any> | null>;
365
+ updateProfile(fields: { firstName?: string; lastName?: string }): Promise<{ firstName?: string | null; lastName?: string | null }>;
366
+ addresses(): Promise<Array<CustomerAddress & { isDefault: boolean }>>;
367
+ saveAddress(address: Partial<CustomerAddressInput> | null, opts?: { id?: string; isDefault?: boolean }): Promise<CustomerAddress>;
368
+ deleteAddress(id: string): Promise<string>;
369
+ subscriptions(): Promise<Array<Record<string, any>>>;
370
+ subscriptionAction(id: string | number, action: 'pause' | 'resume' | 'skip' | 'unskip' | 'cancel'): Promise<Record<string, any>>;
371
+ requestReturn(orderId: string | number, lines: Array<{ lineItemId: string; quantity: number; reasonId?: string; note?: string }>): Promise<{ id: string; status: string }>;
354
372
  };
355
373
  primeProduct(product: ShopifyProduct): void;
356
374
  normalizeProductKey(raw: string | number): string | null;
@@ -398,3 +416,11 @@ declare global {
398
416
  TETHER_LANGUAGE?: string;
399
417
  }
400
418
  }
419
+
420
+ export interface CustomerAddressInput {
421
+ firstName: string; lastName: string; company: string; address1: string; address2: string;
422
+ city: string; zip: string; zoneCode: string; countryCode: string; phoneNumber: string;
423
+ }
424
+ export interface CustomerAddress extends Partial<CustomerAddressInput> {
425
+ id: string; province?: string | null; country?: string | null; formatted: string[];
426
+ }
@@ -1 +0,0 @@
1
- import{b as f}from"./chunk-L2TRRKYV.js";import"./chunk-FIOZPPC5.js";import"./chunk-AJLPU2JC.js";import{A as R,B as x,D as E,G as j,b as a,e as W,g as I,h as S,o as m,p as k,s as _,u as A,w as X}from"./chunk-K2YEQAIH.js";import"./chunk-VC46IEJQ.js";var O=()=>`${a.prefix}:account:${a.domain}`,G=()=>`${a.prefix}:account-login:${a.domain}`,D=()=>a.account?.remember?window.localStorage:window.sessionStorage,Z=t=>btoa(String.fromCharCode(...new Uint8Array(t))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""),B=()=>Z(crypto.getRandomValues(new Uint8Array(32))),at=async t=>Z(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t))),N=t=>(a.account?.navigate||(e=>window.location.assign(e)))(t),M=(t,e)=>{try{return JSON.parse(t.getItem(e)||"null")}catch{return null}},L=(t,e,n)=>{try{n?t.setItem(e,JSON.stringify(n)):t.removeItem(e)}catch{}},C=null,c=null,J=null,U=null,g=()=>a.account?.server?new URL(String(a.account.server),window.location.href).href.replace(/\/$/,""):"",z=()=>`${a.prefix}:account-hint:${a.domain}`,$=null;async function q(t,{method:e="GET",body:n}={}){let o=await fetch(`${g()}${t}`,{method:e,credentials:"include",headers:e==="POST"?{"Content-Type":"application/json","X-Tether-Account":"1"}:{},body:n?JSON.stringify(n):void 0}),s=await o.json().catch(()=>({}));if(!o.ok&&o.status!==401)throw new S(`Customer accounts: ${s.error||o.status}`,{status:o.status});return s}async function H(){$||($=await q("/me"));try{$.loggedIn?window.localStorage.setItem(z(),"1"):window.localStorage.removeItem(z())}catch{}return $}async function P(){if(C)return C;let t=a.account?.shopDomain||a.domain,e=s=>fetch(`https://${t}/.well-known/${s}`).then(r=>{if(!r.ok)throw new S(`Customer accounts: ${s} answered ${r.status}`,{status:r.status});return r.json()}),[n,o]=await Promise.all([e("openid-configuration"),e("customer-account-api")]);return C={...n,graphql:o.graphql_api},C}var st=()=>a.account?.callbackUrl||`${window.location.origin}${window.location.pathname}`;function tt(){return g()?!!$?.loggedIn:!!(c?.refresh||c?.access&&c.expiresAt>Date.now())}async function ct({returnTo:t=window.location.href,locale:e}={}){if(g()){let d=new URL(`${g()}/login`);d.searchParams.set("returnTo",t);let h=e||a.account.locale||(a.locale||"").split("-")[0];h&&d.searchParams.set("locale",h),a.country&&d.searchParams.set("country",String(a.country).toUpperCase()),N(d.toString());return}if(!a.account?.clientId)throw new S("Customer accounts: set account.clientId",{code:"NOT_CONFIGURED"});let n=await P(),o=B(),s=B(),r=B(),u=st();L(window.sessionStorage,G(),{state:o,nonce:s,verifier:r,redirect:u,returnTo:t,at:Date.now()});let i=new URL(n.authorization_endpoint),w={client_id:a.account.clientId,scope:"openid email customer-account-api:full",response_type:"code",redirect_uri:u,state:o,nonce:s,code_challenge:await at(r),code_challenge_method:"S256"},l=e||a.account.locale||(a.locale||"").split("-")[0];l&&(w.locale=l),a.country&&(w.region_country=String(a.country).toUpperCase()),Object.entries(w).forEach(([d,h])=>i.searchParams.set(d,h)),N(i.toString())}async function et(t){let e=await P(),n=await fetch(a.account.tokenUrl||e.token_endpoint,{method:"POST",body:new URLSearchParams(t)}),o=await n.json().catch(()=>({}));if(!n.ok||!o.access_token)throw new S(`Customer accounts: token ${n.status} ${o.error||""}`.trim(),{status:n.status,code:n.status===400||n.status===401?"SESSION_ENDED":"TOKEN_FAILED"});return o}function it(t){try{return JSON.parse(atob(t.split(".")[1].replace(/-/g,"+").replace(/_/g,"/")))}catch{return null}}function nt(t,e=c){c={access:t.access_token,expiresAt:Date.now()+Math.max(0,(Number(t.expires_in)||3600)-120)*1e3,refresh:t.refresh_token||e?.refresh,idToken:t.id_token||e?.idToken},L(D(),O(),c)}async function lt(){let t=new URLSearchParams(window.location.search),e=M(window.sessionStorage,G());if(!e||!t.has("code")&&!t.has("error")||t.get("state")!==e.state)return!1;L(window.sessionStorage,G(),null);let n=new URL(window.location.href);if(["code","state","iss","error","error_description"].forEach(w=>n.searchParams.delete(w)),window.history.replaceState(window.history.state,"",n),t.has("error"))return A("account-error",{error:t.get("error")}),!1;if(Date.now()-e.at>10*6e4)throw new S("Customer accounts: the login took too long, please try again");let o=await et({grant_type:"authorization_code",client_id:a.account.clientId,redirect_uri:e.redirect,code:t.get("code"),code_verifier:e.verifier}),s=await P(),r=it(o.id_token||""),u=[].concat(r?.aud||[]);if(!r||r.nonce!==e.nonce||r.iss!==s.issuer||!u.includes(a.account.clientId)||r.exp*1e3<Date.now())throw new S("Customer accounts: the login answer did not check out (nonce/issuer/audience)");nt(o,null),await ot(),A("account-login",{});let i=e.returnTo&&new URL(e.returnTo,window.location.href);return i&&i.origin===window.location.origin&&i.href!==n.href&&N(i.href),!0}async function v(){return g()||(c||(c=M(D(),O())),!c)?null:c.access&&c.expiresAt>Date.now()?c.access:c.refresh?(J||(J=(async()=>{try{nt(await et({grant_type:"refresh_token",client_id:a.account.clientId,refresh_token:c.refresh})),await ot()}catch(t){if(t.code==="SESSION_ENDED")F();else throw t}finally{J=null}})()),await J,c?.access||null):(F(),null)}function F(){c=null,U=null,L(D(),O(),null),f.customerAccessToken=null}async function ot(){f.customerAccessToken=c?.access||null,f.id()&&c?.access&&await f.setBuyerIdentity({customerAccessToken:c.access},{create:!1}).catch(t=>I(t,"account cart"))}async function rt(t,e={}){if(g())throw new S("Customer accounts in server mode only answer customer() and orders()",{code:"NOT_ALLOWED"});let n=await v();if(!n)throw new S("Not logged in",{code:"NOT_LOGGED_IN"});let o=await P(),s=i=>fetch(o.graphql,{method:"POST",headers:{"Content-Type":"application/json",Authorization:i},body:JSON.stringify({query:t,variables:e})}),r=await s(n);r.status===401&&c?.refresh&&(c.expiresAt=0,r=await s(await v()));let u=await r.json().catch(()=>({}));if(!r.ok||u.errors?.length)throw new S(u.errors?.map(i=>i.message).join(" | ")||`Customer accounts: ${r.status}`,{status:r.status});return u.data}async function ut(){if(g()){let e=await H();return e.loggedIn?{...e.customer,emailAddress:{emailAddress:e.customer.email},defaultAddress:{formatted:e.customer.address}}:null}return U||(U=(await rt("query TetherCustomer{customer{id firstName lastName displayName emailAddress{emailAddress}defaultAddress{formatted}}}")).customer,U)}async function dt({first:t=10}={}){return g()?(await q(`/orders?first=${t}`)).orders||[]:(await rt("query TetherOrders($first:Int!){customer{orders(first:$first,sortKey:PROCESSED_AT,reverse:true){nodes{id name processedAt financialStatus fulfillmentStatus statusPageUrl totalPrice{amount currencyCode}lineItems(first:20){nodes{title variantTitle quantity image{url altText}}}}}}}",{first:t})).customer?.orders?.nodes||[]}async function ft({returnTo:t=a.account?.logoutUrl||`${window.location.origin}/`}={}){if(g()){let s=await q("/logout",{method:"POST",body:{returnTo:t,cartId:f.id()}});$={loggedIn:!1};try{window.localStorage.removeItem(z())}catch{}A("account-logout",{}),s.logoutUrl?N(s.logoutUrl):K(document);return}let e=c?.idToken||M(D(),O())?.idToken;if(f.id()&&await f.setBuyerIdentity({customerAccessToken:null},{create:!1}).catch(()=>{}),F(),A("account-logout",{}),!e){K(document);return}let n=await P(),o=new URL(n.end_session_endpoint);o.searchParams.set("id_token_hint",e),o.searchParams.set("post_logout_redirect_uri",t),N(o.toString())}var Y=t=>{try{return new URL(t).protocol==="https:"}catch{return!1}},wt=t=>{try{return new Date(t).toLocaleDateString(a.locale||void 0)}catch{return t}};async function K(t=document){let e=g()?!!(await H().catch(()=>({loggedIn:!1}))).loggedIn:tt()||!!await v().catch(()=>null);if(_(t,m("show")).forEach(s=>{let r=k(s,"show");r==="logged-in"&&E(s,e),r==="logged-out"&&E(s,!e)}),!e)return;let n=_(t,m("account-bind"));if(n.length){let s=await ut().catch(r=>(I(r,"account"),null));n.forEach(r=>{let u={"first-name":s?.firstName,"last-name":s?.lastName,name:s?.displayName,email:s?.emailAddress?.emailAddress,address:s?.defaultAddress?.formatted?.join(", ")}[k(r,"account-bind")];u!=null&&x(r,u)})}let o=_(t,m("orders"));if(o.length){let s=(i,w)=>_(t,m("show")).forEach(l=>{let d=k(l,"show");d==="orders-loading"&&E(l,i),d==="orders-empty"&&E(l,w)});s(!0,!1);let r=!1,u=await dt({first:parseInt(k(o[0],"limit")||"10",10)}).catch(i=>(I(i,"orders"),r=!0,[]));s(!1,!r&&u.length===0),o.forEach(i=>j(i,u,(w,l)=>{let d=l.lineItems?.nodes||[];_(w,m("order-bind")).forEach(h=>{let T=k(h,"order-bind");if(T==="url"){Y(l.statusPageUrl)&&h.setAttribute("href",l.statusPageUrl);return}if(T==="image"){let y=d.find(b=>b.image)?.image;y&&R(h,y.url,y.altText||d[0]?.title||"");return}let p={name:l.name,date:wt(l.processedAt),total:X(l.totalPrice),status:l.financialStatus?.toLowerCase(),fulfillment:l.fulfillmentStatus?.toLowerCase(),items:d.map(y=>`${y.quantity}\xD7 ${y.title}`).join(", ")}[T];p!=null&&x(h,p)}),_(w,m("order-lines")).forEach(h=>j(h,d,(T,p)=>{_(T,m("order-line")).concat(T.matches(m("order-line"))?[T]:[]).forEach(y=>{let b=k(y,"order-line");if(b==="image"){p.image&&R(y,p.image.url,p.image.altText||p.title);return}let V={title:p.title,"variant-title":p.variantTitle||"",quantity:String(p.quantity)}[b];V!=null&&x(y,V)})})),k(w,"order-bind")==="url"&&Y(l.statusPageUrl)&&w.setAttribute("href",l.statusPageUrl)}))}}var Q=!1;async function yt(t=document){if(!Q){if(Q=!0,document.addEventListener("click",e=>{let n=e.target;n.closest?.(m("account-login"))?(e.preventDefault(),ct().catch(o=>I(o,"login"))):n.closest?.(m("account-logout"))&&(e.preventDefault(),ft().catch(o=>I(o,"logout")))}),g()){let e=new URL(window.location.href);e.searchParams.get("account")&&(e.searchParams.delete("account"),window.history.replaceState(window.history.state,"",e),A("account-login",{})),f.beforeCheckout=async()=>{if(!(await H()).loggedIn||!f.id())return null;let n=await q("/cart",{method:"POST",body:{cartId:f.id()}}).catch(()=>null);return n?.checkoutUrl?`${n.checkoutUrl}${n.checkoutUrl.includes("?")?"&":"?"}logged_in=true`:null}}else{try{await lt()}catch(e){I(e,"login")}!f.customerAccessToken&&await v().catch(()=>null)&&(f.customerAccessToken=c.access)}W("account",tt()?"logged in":"logged out")}await K(t)}export{ut as customer,v as getToken,lt as handleCallback,tt as isLoggedIn,ct as login,ft as logout,yt as mountAccount,dt as orders,rt as query,K as render};