swell-js 5.9.3 → 5.10.0

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 CHANGED
@@ -13,7 +13,49 @@ Universal JavaScript client for Swell's Frontend API, providing client-safe acce
13
13
 
14
14
  **About Swell**
15
15
 
16
- [Swell](https://www.swell.is) is a customizable, API-first platform for powering modern B2C/B2B shopping experiences and marketplaces. Build and connect anything using your favorite technologies, and provide admins with an easy to use dashboard.
16
+ [Swell](https://www.swell.is) is an ecommerce platform with subscriptions and B2B pricing built in. Run your store on a Swell theme, or [go headless](https://www.swell.is/headless-commerce) with its APIs.
17
+
18
+ ## Checkout payments
19
+
20
+ Besides `createElements`, `tokenize` and `handleRedirect` (see the documentation), `swell.payment` offers helpers a checkout page needs:
21
+
22
+ - `tokenize({ card: { createIntent: false } })` stores a Stripe card without authorizing it now. `tokenize({ card: { paymentMethod: 'pm_…' } })` authorizes a Stripe card the shopper already has (a saved card, a wallet's payment method) with an intent and 3-D Secure, like a new card, before the order is submitted. Both reuse the cart's intent while it still matches the card and amount.
23
+ - Saferpay card and TWINT (`twint`) redirect to the Saferpay payment page from `tokenize` and come back through `handleRedirect`; iDEAL returns through `handleRedirect({ ideal })` too.
24
+ - Stripe iDEAL needs no element: `tokenize({ ideal: {} })` without `createElements` redirects to the iDEAL page, where the shopper picks the bank, and returns through `handleRedirect({ ideal })`.
25
+ - `handleAsyncRedirect(order)` sends the shopper to the gateway when a submitted order waits for an asynchronous payment.
26
+ - Stripe card and iDEAL can render through the Stripe [Payment Element](https://docs.stripe.com/payments/payment-element) instead of the split elements: pass `paymentElement: true` to both `createElements` and `tokenize`. Stripe draws the whole form (fields, labels, errors) and takes its look from `appearance` ([Appearance API](https://docs.stripe.com/elements/appearance-api)). The element mounts into `#payment-element` unless `elementId` is set; `options` go to `elements.create('payment')` over the defaults `layout: 'tabs'`, `fields.billingDetails: 'never'` (billing details come from the cart) and no Apple Pay / Google Pay. `tokenize` re-reads the cart and updates the amount before `elements.submit()`, so a changed cart total needs no extra call.
27
+
28
+ ```js
29
+ const card = {
30
+ paymentElement: true,
31
+ appearance: {
32
+ theme: 'stripe',
33
+ labels: 'floating',
34
+ variables: { colorPrimary: '#111' },
35
+ },
36
+ elementId: 'card-payment-element',
37
+ onChange: (event) => setComplete(event.complete),
38
+ onSuccess: () => submitOrder(),
39
+ onError: (error) => showError(error.message),
40
+ };
41
+
42
+ await swell.payment.createElements({ card });
43
+ // on "Pay": validates, creates the PaymentMethod and the intent, stores them on the cart
44
+ await swell.payment.tokenize({ card });
45
+
46
+ // iDEAL: the same flag; tokenize redirects to the bank and returns through handleRedirect({ ideal })
47
+ await swell.payment.createElements({
48
+ ideal: {
49
+ paymentElement: true,
50
+ appearance,
51
+ elementId: 'ideal-payment-element',
52
+ },
53
+ });
54
+ ```
55
+
56
+ ## Deprecated
57
+
58
+ - `swell.app` (app components rendered by swell-js) is deprecated and will be removed in the next major version. Apps render their components through [`@swell/apps-sdk/components`](https://www.npmjs.com/package/@swell/apps-sdk) instead; hosted checkout mounts payment components at the place `checkout.payment.methods`.
17
59
 
18
60
  ## Documentation
19
61
 
package/dist/api.mjs CHANGED
@@ -1,21 +1,21 @@
1
- import methods$5 from './card.mjs';
1
+ import methods$6 from './card.mjs';
2
2
  import { getCookie, setCookie } from './cookie.mjs';
3
- import methods$4 from './cache.mjs';
4
- import methods$3 from './cart.mjs';
5
- import methods$6 from './account.mjs';
6
- import { j as isFunction, l as loadScript, e as isObject, v as vaultRequest, k as isEmpty, d as defaultMethods, x as trimEnd, y as utils, z as trimStart, A as trimBoth, b as toSnake, o as base64Encode, i as isServer, t as toCamel } from './index.76eb52e5.mjs';
3
+ import methods$5 from './cache.mjs';
4
+ import methods$4 from './cart.mjs';
5
+ import methods$7 from './account.mjs';
6
+ import { j as isFunction, l as loadScript, e as isObject, v as vaultRequest, k as isEmpty, d as defaultMethods, i as isServer, x as trimEnd, y as utils, z as trimStart, A as trimBoth, b as toSnake, o as base64Encode, t as toCamel } from './index.76eb52e5.mjs';
7
7
  import { Q as stringifyQuery } from './find.ba551a2e.mjs';
8
8
  import 'deepmerge';
9
9
  import 'fast-case';
10
- import methods$7 from './products.mjs';
11
- import methods$8 from './categories.mjs';
12
- import methods$9 from './attributes.mjs';
13
- import methods$a from './subscriptions.mjs';
14
- import methods$b from './content.mjs';
15
- import methods$c from './settings.mjs';
10
+ import methods$8 from './products.mjs';
11
+ import methods$9 from './categories.mjs';
12
+ import methods$a from './attributes.mjs';
13
+ import methods$b from './subscriptions.mjs';
14
+ import methods$c from './content.mjs';
15
+ import methods$d from './settings.mjs';
16
16
  import PaymentController from './payment.mjs';
17
- import methods$d from './locale.mjs';
18
- import methods$e, { isCurrencyCode } from './currency.mjs';
17
+ import methods$e from './locale.mjs';
18
+ import methods$f, { isCurrencyCode } from './currency.mjs';
19
19
  import './round.c68b8be5.mjs';
20
20
 
21
21
  class AppComponent {
@@ -81,7 +81,7 @@ class AppComponent {
81
81
  }
82
82
 
83
83
  async updateCart(data) {
84
- const cart = await methods$3(this.api, this.options).update(data);
84
+ const cart = await methods$4(this.api, this.options).update(data);
85
85
 
86
86
  this.setProps({ cart });
87
87
  }
@@ -122,6 +122,10 @@ class AppComponent {
122
122
  }
123
123
  }
124
124
 
125
+ /**
126
+ * @deprecated Payment components are mounted by `@swell/apps-sdk/components`
127
+ * at the host place `checkout.payment.methods`.
128
+ */
125
129
  class AppPaymentComponent extends AppComponent {
126
130
  constructor(app, component, api, options) {
127
131
  super(app, component, api, options);
@@ -171,6 +175,7 @@ class AppPaymentComponent extends AppComponent {
171
175
  }
172
176
  }
173
177
 
178
+ /** @deprecated Use `@swell/apps-sdk/components` instead. */
174
179
  class App {
175
180
  constructor(app, api, options) {
176
181
  this.app = app;
@@ -279,6 +284,13 @@ class App {
279
284
  }
280
285
  }
281
286
 
287
+ let deprecationWarned = false;
288
+
289
+ /**
290
+ * @deprecated App components are rendered by `@swell/apps-sdk/components`
291
+ * (`createComponents().mount()`); this module will be removed in the next
292
+ * major version.
293
+ */
282
294
  class AppController {
283
295
  #apps = new Map();
284
296
 
@@ -287,7 +299,15 @@ class AppController {
287
299
  this.options = options;
288
300
  }
289
301
 
302
+ /** @deprecated Use `@swell/apps-sdk/components` instead. */
290
303
  async load(appId) {
304
+ if (!deprecationWarned) {
305
+ deprecationWarned = true;
306
+ console.warn(
307
+ 'swell.app is deprecated: render app components with @swell/apps-sdk/components',
308
+ );
309
+ }
310
+
291
311
  const loadedApp = this.#apps.get(appId);
292
312
 
293
313
  if (loadedApp) {
@@ -317,7 +337,7 @@ class AppController {
317
337
  }
318
338
  }
319
339
 
320
- function methods$2(api) {
340
+ function methods$3(api) {
321
341
  const { get, list } = defaultMethods(api, '/invoices', ['list', 'get']);
322
342
  return {
323
343
  get: (id, ...args) => {
@@ -328,7 +348,7 @@ function methods$2(api) {
328
348
  };
329
349
  }
330
350
 
331
- function methods$1(api, opt) {
351
+ function methods$2(api, opt) {
332
352
  return {
333
353
  /**
334
354
  * Get the decoded session as an object of session values
@@ -355,7 +375,7 @@ function methods$1(api, opt) {
355
375
  };
356
376
  }
357
377
 
358
- function methods(api, _opt) {
378
+ function methods$1(api, _opt) {
359
379
  return {
360
380
  /**
361
381
  * Make a request to an app function and greceiveet a response
@@ -424,6 +444,85 @@ function methods(api, _opt) {
424
444
  };
425
445
  }
426
446
 
447
+ const PURPOSES = ['analytics', 'marketing', 'sale_of_data'];
448
+
449
+ /**
450
+ * The page the shopper landed on, which the consent call sends along: the
451
+ * first URL of this document and its referrer. Read when needed, so nothing
452
+ * is kept before the shopper answers.
453
+ *
454
+ * @returns {{url: string, referrer: string} | undefined}
455
+ */
456
+ function getLanding() {
457
+ if (isServer()) {
458
+ return undefined;
459
+ }
460
+
461
+ let url = window.location.href;
462
+
463
+ try {
464
+ const [navigation] = window.performance.getEntriesByType('navigation');
465
+ if (navigation?.name) {
466
+ url = navigation.name;
467
+ }
468
+ } catch {
469
+ // Navigation timing is not available
470
+ }
471
+
472
+ return { url, referrer: window.document.referrer || '' };
473
+ }
474
+
475
+ function methods(api) {
476
+ // Calls run one after another. Consent tools fire several events for one
477
+ // answer, and the first call may set the visitor cookie that the next one
478
+ // has to send: in parallel, each would start a visitor of its own.
479
+ let queue = Promise.resolve();
480
+
481
+ return {
482
+ /**
483
+ * Records the shopper's consent for attribution, from the storefront's
484
+ * consent banner. Purposes left out stay as they are.
485
+ *
486
+ * Calls the storefront's own origin, so that Swell sees the page's
487
+ * cookies and sets the visitor cookie on the storefront's domain.
488
+ *
489
+ * @param {{analytics?: boolean, marketing?: boolean, sale_of_data?: boolean}} consent
490
+ */
491
+ set(consent = {}) {
492
+ if (isServer()) {
493
+ return Promise.reject(new Error('consent.set() runs in the browser'));
494
+ }
495
+
496
+ const data = {};
497
+
498
+ for (const purpose of PURPOSES) {
499
+ // camelCase clients pass saleOfData
500
+ const camel = purpose.replace(/_(\w)/g, (_m, c) => c.toUpperCase());
501
+ const value = consent[purpose] ?? consent[camel];
502
+
503
+ if (typeof value === 'boolean') {
504
+ data[purpose] = value;
505
+ }
506
+ }
507
+
508
+ const result = queue.then(() =>
509
+ api.request(
510
+ 'put',
511
+ '/session/consent',
512
+ { ...data, landing: getLanding() },
513
+ undefined,
514
+ { fullUrl: `${window.location.origin}/api/session/consent` },
515
+ ),
516
+ );
517
+
518
+ // A failed call doesn't hold back the next one
519
+ queue = result.catch(() => undefined);
520
+
521
+ return result;
522
+ },
523
+ };
524
+ }
525
+
427
526
  /**
428
527
  * @template {'snake' | 'camel'} C
429
528
  * @typedef {import('../types').InitOptions<C>} InitOptions<C>
@@ -455,7 +554,7 @@ function swell(initStore = undefined, initKey, initOptions = {}) {
455
554
  const api = {};
456
555
 
457
556
  Object.assign(api, {
458
- version: '5.9.3',
557
+ version: '5.10.0',
459
558
  options,
460
559
  request,
461
560
 
@@ -509,39 +608,41 @@ function swell(initStore = undefined, initKey, initOptions = {}) {
509
608
  return api.request('delete', url, data);
510
609
  },
511
610
 
512
- cache: methods$4(options),
611
+ cache: methods$5(options),
513
612
 
514
- card: methods$5(options),
613
+ card: methods$6(options),
515
614
 
516
- cart: methods$3(api, options),
615
+ cart: methods$4(api, options),
517
616
 
518
- account: methods$6(api),
617
+ account: methods$7(api),
519
618
 
520
619
  app: new AppController(api, options),
521
620
 
522
- products: methods$7(api, options),
621
+ products: methods$8(api, options),
523
622
 
524
- categories: methods$8(api),
623
+ categories: methods$9(api),
525
624
 
526
- attributes: methods$9(api),
625
+ attributes: methods$a(api),
527
626
 
528
- subscriptions: methods$a(api),
627
+ subscriptions: methods$b(api),
529
628
 
530
- invoices: methods$2(api),
629
+ invoices: methods$3(api),
531
630
 
532
- content: methods$b(api, options),
631
+ content: methods$c(api, options),
533
632
 
534
- settings: methods$c(api, options),
633
+ settings: methods$d(api, options),
535
634
 
536
635
  payment: new PaymentController(api, options),
537
636
 
538
- locale: methods$d(api, options),
637
+ locale: methods$e(api, options),
638
+
639
+ currency: methods$f(api, options),
539
640
 
540
- currency: methods$e(api, options),
641
+ session: methods$2(api, options),
541
642
 
542
- session: methods$1(api, options),
643
+ functions: methods$1(api),
543
644
 
544
- functions: methods(api),
645
+ consent: methods(api),
545
646
 
546
647
  utils,
547
648
 
@@ -615,6 +716,13 @@ function swell(initStore = undefined, initKey, initOptions = {}) {
615
716
  reqHeaders['X-Session'] = session;
616
717
  }
617
718
 
719
+ // The storefront's visitor, for attribution: the cookie is on the
720
+ // storefront's domain and doesn't reach the store's API host
721
+ const visitor = allOptions.getCookie('swell-visitor');
722
+ if (visitor) {
723
+ reqHeaders['X-Visitor'] = visitor;
724
+ }
725
+
618
726
  if (locale) {
619
727
  reqHeaders['X-Locale'] = locale;
620
728
  }