@webround/commerce 0.0.0-stage → 0.3.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.
Files changed (138) hide show
  1. package/README.md +192 -2
  2. package/dist/account/account.d.ts +54 -0
  3. package/dist/account/account.js +40 -0
  4. package/dist/account/index.d.ts +3 -0
  5. package/dist/account/index.js +2 -0
  6. package/dist/addresses/api.d.ts +16 -0
  7. package/dist/addresses/api.js +13 -0
  8. package/dist/addresses/book.d.ts +40 -0
  9. package/dist/addresses/book.js +144 -0
  10. package/dist/addresses/index.d.ts +8 -0
  11. package/dist/addresses/index.js +5 -0
  12. package/dist/addresses/rules.d.ts +4 -0
  13. package/dist/addresses/rules.js +9 -0
  14. package/dist/addresses/types.d.ts +48 -0
  15. package/dist/addresses/types.js +6 -0
  16. package/dist/auth/api.d.ts +43 -0
  17. package/dist/auth/api.js +22 -0
  18. package/dist/auth/auth.d.ts +32 -0
  19. package/dist/auth/auth.js +23 -0
  20. package/dist/auth/cookies.d.ts +46 -0
  21. package/dist/auth/cookies.js +156 -0
  22. package/dist/auth/index.d.ts +14 -0
  23. package/dist/auth/index.js +8 -0
  24. package/dist/auth/renewal.d.ts +4 -0
  25. package/dist/auth/renewal.js +37 -0
  26. package/dist/auth/store.d.ts +70 -0
  27. package/dist/auth/store.js +284 -0
  28. package/dist/auth/types.d.ts +105 -0
  29. package/dist/auth/types.js +5 -0
  30. package/dist/auth/wake.d.ts +18 -0
  31. package/dist/auth/wake.js +23 -0
  32. package/dist/cache.d.ts +49 -0
  33. package/dist/cache.js +144 -0
  34. package/dist/canonical.d.ts +7 -0
  35. package/dist/canonical.js +32 -0
  36. package/dist/cart/api.d.ts +25 -0
  37. package/dist/cart/api.js +16 -0
  38. package/dist/cart/index.d.ts +10 -0
  39. package/dist/cart/index.js +6 -0
  40. package/dist/cart/lines.d.ts +15 -0
  41. package/dist/cart/lines.js +23 -0
  42. package/dist/cart/storage.d.ts +30 -0
  43. package/dist/cart/storage.js +82 -0
  44. package/dist/cart/store.d.ts +58 -0
  45. package/dist/cart/store.js +218 -0
  46. package/dist/cart/types.d.ts +194 -0
  47. package/dist/cart/types.js +11 -0
  48. package/dist/catalog/cached.d.ts +47 -0
  49. package/dist/catalog/cached.js +52 -0
  50. package/dist/catalog/catalog.d.ts +10 -0
  51. package/dist/catalog/catalog.js +7 -0
  52. package/dist/catalog/named.d.ts +3 -0
  53. package/dist/catalog/named.js +7 -0
  54. package/dist/catalog/products.d.ts +28 -0
  55. package/dist/catalog/products.js +12 -0
  56. package/dist/catalog/store-data.d.ts +13 -0
  57. package/dist/catalog/store-data.js +6 -0
  58. package/dist/catalog/types.d.ts +430 -0
  59. package/dist/catalog/types.js +4 -0
  60. package/dist/checkout/api.d.ts +14 -0
  61. package/dist/checkout/api.js +7 -0
  62. package/dist/checkout/errors.d.ts +23 -0
  63. package/dist/checkout/errors.js +18 -0
  64. package/dist/checkout/index.d.ts +10 -0
  65. package/dist/checkout/index.js +6 -0
  66. package/dist/checkout/return-urls.d.ts +7 -0
  67. package/dist/checkout/return-urls.js +11 -0
  68. package/dist/checkout/store.d.ts +98 -0
  69. package/dist/checkout/store.js +179 -0
  70. package/dist/checkout/types.d.ts +225 -0
  71. package/dist/checkout/types.js +28 -0
  72. package/dist/commerce.d.ts +55 -0
  73. package/dist/commerce.js +55 -0
  74. package/dist/customer-resources.d.ts +18 -0
  75. package/dist/customer-resources.js +40 -0
  76. package/dist/errors.d.ts +65 -0
  77. package/dist/errors.js +96 -0
  78. package/dist/http.d.ts +49 -0
  79. package/dist/http.js +94 -0
  80. package/dist/index.d.ts +32 -0
  81. package/dist/index.js +22 -0
  82. package/dist/orders/api.d.ts +16 -0
  83. package/dist/orders/api.js +13 -0
  84. package/dist/orders/index.d.ts +6 -0
  85. package/dist/orders/index.js +3 -0
  86. package/dist/orders/orders.d.ts +19 -0
  87. package/dist/orders/orders.js +18 -0
  88. package/dist/orders/types.d.ts +88 -0
  89. package/dist/orders/types.js +4 -0
  90. package/dist/resource.d.ts +19 -0
  91. package/dist/resource.js +15 -0
  92. package/dist/returns/api.d.ts +27 -0
  93. package/dist/returns/api.js +19 -0
  94. package/dist/returns/index.d.ts +6 -0
  95. package/dist/returns/index.js +3 -0
  96. package/dist/returns/returns.d.ts +30 -0
  97. package/dist/returns/returns.js +31 -0
  98. package/dist/returns/types.d.ts +99 -0
  99. package/dist/returns/types.js +2 -0
  100. package/dist/reviews/api.d.ts +19 -0
  101. package/dist/reviews/api.js +15 -0
  102. package/dist/reviews/index.d.ts +9 -0
  103. package/dist/reviews/index.js +5 -0
  104. package/dist/reviews/reviews.d.ts +23 -0
  105. package/dist/reviews/reviews.js +37 -0
  106. package/dist/reviews/rules.d.ts +8 -0
  107. package/dist/reviews/rules.js +7 -0
  108. package/dist/reviews/types.d.ts +54 -0
  109. package/dist/reviews/types.js +10 -0
  110. package/dist/shop/address-store.d.ts +14 -0
  111. package/dist/shop/address-store.js +28 -0
  112. package/dist/shop/address.d.ts +12 -0
  113. package/dist/shop/address.js +95 -0
  114. package/dist/shop/index.d.ts +16 -0
  115. package/dist/shop/index.js +9 -0
  116. package/dist/shop/question.d.ts +15 -0
  117. package/dist/shop/question.js +12 -0
  118. package/dist/shop/range.d.ts +7 -0
  119. package/dist/shop/range.js +38 -0
  120. package/dist/shop/sort.d.ts +19 -0
  121. package/dist/shop/sort.js +18 -0
  122. package/dist/shop/state.d.ts +47 -0
  123. package/dist/shop/state.js +51 -0
  124. package/dist/shop/store.d.ts +26 -0
  125. package/dist/shop/store.js +47 -0
  126. package/dist/shop/to-search.d.ts +14 -0
  127. package/dist/shop/to-search.js +53 -0
  128. package/dist/store-kit.d.ts +39 -0
  129. package/dist/store-kit.js +81 -0
  130. package/dist/wishlist/api.d.ts +12 -0
  131. package/dist/wishlist/api.js +10 -0
  132. package/dist/wishlist/index.d.ts +7 -0
  133. package/dist/wishlist/index.js +4 -0
  134. package/dist/wishlist/types.d.ts +30 -0
  135. package/dist/wishlist/types.js +4 -0
  136. package/dist/wishlist/wishlist.d.ts +45 -0
  137. package/dist/wishlist/wishlist.js +84 -0
  138. package/package.json +39 -5
@@ -0,0 +1,19 @@
1
+ import type { ProductSortField, SortDirection, StoreData } from '../catalog/types.js';
2
+ export interface SortOption {
3
+ sortBy: ProductSortField;
4
+ /** Where the order comes from: one every store has (`field`), or a tag or an attribute of this store (then `name` is what it is called). */
5
+ source: 'field' | 'tag' | 'attribute';
6
+ /** The tag's or attribute's label (or name): what the control says. For a `field` the interface says it in its own words. */
7
+ name?: string;
8
+ }
9
+ /** What can be sorted by now. Every option goes both ways (`asc`, `desc`). */
10
+ export declare function sortOptionsOf(store: Pick<StoreData, 'tags' | 'attributes'>): SortOption[];
11
+ /** The order a customer chose, if it is still one that is offered and has its direction; otherwise none (the catalog's own order). */
12
+ export declare function validSort(store: Pick<StoreData, 'tags' | 'attributes'>, chosen: {
13
+ sortBy?: ProductSortField;
14
+ sortDirection?: SortDirection;
15
+ }): {
16
+ sortBy: ProductSortField;
17
+ sortDirection: SortDirection;
18
+ } | null;
19
+ //# sourceMappingURL=sort.d.ts.map
@@ -0,0 +1,18 @@
1
+ // "match" is the best answer to the text; with no text the catalog orders by creation, newest first, so it is always an order
2
+ const FIELDS = ['match', 'createdAt', 'updatedAt', 'alphabetical', 'price', 'availability'];
3
+ /** What can be sorted by now. Every option goes both ways (`asc`, `desc`). */
4
+ export function sortOptionsOf(store) {
5
+ return [
6
+ ...FIELDS.map((sortBy) => ({ sortBy, source: 'field' })),
7
+ ...store.tags.filter((tag) => tag.sortable).map((tag) => ({ sortBy: `tag:${tag.id}`, source: 'tag', name: tag.label ?? tag.name })),
8
+ ...store.attributes.filter((attribute) => attribute.sortable).map((attribute) => ({ sortBy: `attribute:${attribute.id}`, source: 'attribute', name: attribute.label ?? attribute.name })),
9
+ ];
10
+ }
11
+ /** The order a customer chose, if it is still one that is offered and has its direction; otherwise none (the catalog's own order). */
12
+ export function validSort(store, chosen) {
13
+ const { sortBy, sortDirection } = chosen;
14
+ if (sortBy === undefined || (sortDirection !== 'asc' && sortDirection !== 'desc'))
15
+ return null;
16
+ return sortOptionsOf(store).some((option) => option.sortBy === sortBy) ? { sortBy, sortDirection } : null;
17
+ }
18
+ //# sourceMappingURL=sort.js.map
@@ -0,0 +1,47 @@
1
+ import type { ProductSortField, SortDirection } from '../catalog/types.js';
2
+ /** One end of a range: a number (a price, a number attribute), or for a date `YYYY-MM-DD` or a number of milliseconds. A text of digits is read as a number. Empty or missing: no limit on that side. */
3
+ export type RangeBound = number | string | null | undefined;
4
+ export interface RangeValue {
5
+ min?: RangeBound;
6
+ max?: RangeBound;
7
+ }
8
+ export interface ShopState {
9
+ /** The collection the customer is in: its filters apply (see `StoreCollection.filterIds`). */
10
+ collectionId?: string;
11
+ /** What was typed in the search box, as it was typed. */
12
+ search?: string;
13
+ /** With a text, search ALL the products and not only the ones the filters leave (the catalog's `searchIgnoresFilters`). Left unsaid, the catalog decides. */
14
+ searchIgnoresFilters?: boolean;
15
+ /** The tag values chosen: values of one tag are alternatives, values of different tags all have to be there. */
16
+ tagValueIds: readonly string[];
17
+ /** The range chosen for each range filter, by the filter's id. One range per filter. */
18
+ ranges: Readonly<Record<string, RangeValue>>;
19
+ sortBy?: ProductSortField;
20
+ sortDirection?: SortDirection;
21
+ page: number;
22
+ limit: number;
23
+ }
24
+ /** How many products a page has when nobody said: the shop's own choice (the catalog's own default is not repeated here). */
25
+ export declare const DEFAULT_SHOP_LIMIT = 12;
26
+ export declare function emptyShopState(over?: {
27
+ limit?: number;
28
+ collectionId?: string;
29
+ }): ShopState;
30
+ /** `undefined` takes the collection away (the whole store). */
31
+ export declare function setCollection(state: ShopState, collectionId: string | undefined): ShopState;
32
+ export declare function setSearch(state: ShopState, search: string | undefined): ShopState;
33
+ export declare function setSearchIgnoresFilters(state: ShopState, ignores: boolean | undefined): ShopState;
34
+ /** Chooses a tag value, or takes it away if it was chosen. */
35
+ export declare function toggleTagValue(state: ShopState, tagValueId: string): ShopState;
36
+ /** The range of a filter. `null`, `undefined` or one with no ends takes it away. */
37
+ export declare function setRange(state: ShopState, filterId: string, range: RangeValue | null | undefined): ShopState;
38
+ /** Takes away every filter the customer chose (not the collection, the text or the order). */
39
+ export declare function clearFilters(state: ShopState): ShopState;
40
+ /** The order and its direction go together: a sorting with no direction is the catalog's to decide (descending), and a rule never leaves that unsaid. */
41
+ export declare function setSort(state: ShopState, sortBy: ProductSortField, sortDirection: SortDirection): ShopState;
42
+ export declare function clearSort(state: ShopState): ShopState;
43
+ /** The only change that does not go back to page 1. What is not a page (0, 1.5, NaN) is corrected when the question is made. */
44
+ export declare function setPage(state: ShopState, page: number): ShopState;
45
+ /** How many per page: the customer is put back on page 1, for the page they were on is not the same any more. */
46
+ export declare function setLimit(state: ShopState, limit: number): ShopState;
47
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1,51 @@
1
+ /** How many products a page has when nobody said: the shop's own choice (the catalog's own default is not repeated here). */
2
+ export const DEFAULT_SHOP_LIMIT = 12;
3
+ export function emptyShopState(over = {}) {
4
+ return { tagValueIds: [], ranges: {}, page: 1, limit: over.limit ?? DEFAULT_SHOP_LIMIT, ...(over.collectionId === undefined ? {} : { collectionId: over.collectionId }) };
5
+ }
6
+ /** A change of what is looked for: the same state with it, and back on the first page. */
7
+ const looking = (state, changes) => ({ ...state, ...changes, page: 1 });
8
+ /** `undefined` takes the collection away (the whole store). */
9
+ export function setCollection(state, collectionId) {
10
+ const { collectionId: _gone, ...rest } = state;
11
+ return looking(rest, collectionId === undefined ? {} : { collectionId });
12
+ }
13
+ export function setSearch(state, search) {
14
+ const { search: _gone, ...rest } = state;
15
+ return looking(rest, search === undefined || search === '' ? {} : { search });
16
+ }
17
+ export function setSearchIgnoresFilters(state, ignores) {
18
+ const { searchIgnoresFilters: _gone, ...rest } = state;
19
+ return looking(rest, ignores === undefined ? {} : { searchIgnoresFilters: ignores });
20
+ }
21
+ /** Chooses a tag value, or takes it away if it was chosen. */
22
+ export function toggleTagValue(state, tagValueId) {
23
+ return looking(state, { tagValueIds: state.tagValueIds.includes(tagValueId) ? state.tagValueIds.filter((id) => id !== tagValueId) : [...state.tagValueIds, tagValueId] });
24
+ }
25
+ /** The range of a filter. `null`, `undefined` or one with no ends takes it away. */
26
+ export function setRange(state, filterId, range) {
27
+ const { [filterId]: _gone, ...others } = state.ranges;
28
+ const open = (bound) => bound === null || bound === undefined || bound === '';
29
+ return looking(state, { ranges: !range || (open(range.min) && open(range.max)) ? others : { ...others, [filterId]: { ...range } } });
30
+ }
31
+ /** Takes away every filter the customer chose (not the collection, the text or the order). */
32
+ export function clearFilters(state) {
33
+ return looking(state, { tagValueIds: [], ranges: {} });
34
+ }
35
+ /** The order and its direction go together: a sorting with no direction is the catalog's to decide (descending), and a rule never leaves that unsaid. */
36
+ export function setSort(state, sortBy, sortDirection) {
37
+ return looking(state, { sortBy, sortDirection });
38
+ }
39
+ export function clearSort(state) {
40
+ const { sortBy: _by, sortDirection: _direction, ...rest } = state;
41
+ return looking(rest, {});
42
+ }
43
+ /** The only change that does not go back to page 1. What is not a page (0, 1.5, NaN) is corrected when the question is made. */
44
+ export function setPage(state, page) {
45
+ return { ...state, page };
46
+ }
47
+ /** How many per page: the customer is put back on page 1, for the page they were on is not the same any more. */
48
+ export function setLimit(state, limit) {
49
+ return looking(state, { limit });
50
+ }
51
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,26 @@
1
+ import type { ProductSortField, SortDirection } from '../catalog/types.js';
2
+ import { type RangeValue, type ShopState } from './state.js';
3
+ export interface ShopStore {
4
+ getState(): ShopState;
5
+ /** Called whenever the state changed, once for each change. Returns what stops it. */
6
+ subscribe(listener: () => void): () => void;
7
+ /** Replaces the state (a change the customer made, or the address that changed under it). */
8
+ set(next: ShopState): void;
9
+ }
10
+ /** The state in memory, for as long as the store lives. The state is the SAME object until something really changes: a change that leaves it as it was (the same range again) tells nobody. */
11
+ export declare function createShopStore(initial?: ShopState): ShopStore;
12
+ /** What a page's controls call: the changes of `state.ts`, applied to a store. The same functions, not others: what a change means is written once. */
13
+ export interface ShopActions {
14
+ setCollection(collectionId: string | undefined): void;
15
+ setSearch(search: string | undefined): void;
16
+ setSearchIgnoresFilters(ignores: boolean | undefined): void;
17
+ toggleTagValue(tagValueId: string): void;
18
+ setRange(filterId: string, range: RangeValue | null | undefined): void;
19
+ clearFilters(): void;
20
+ setSort(sortBy: ProductSortField, sortDirection: SortDirection): void;
21
+ clearSort(): void;
22
+ setPage(page: number): void;
23
+ setLimit(limit: number): void;
24
+ }
25
+ export declare function shopActions(store: ShopStore): ShopActions;
26
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,47 @@
1
+ import { canonicalJson } from '../canonical.js';
2
+ import { clearFilters, clearSort, emptyShopState, setCollection, setLimit, setPage, setRange, setSearch, setSearchIgnoresFilters, setSort, toggleTagValue, } from './state.js';
3
+ /** The state in memory, for as long as the store lives. The state is the SAME object until something really changes: a change that leaves it as it was (the same range again) tells nobody. */
4
+ export function createShopStore(initial = emptyShopState()) {
5
+ let state = initial;
6
+ let key = canonicalJson(state);
7
+ const listeners = new Set();
8
+ return {
9
+ getState: () => state,
10
+ subscribe(listener) {
11
+ listeners.add(listener);
12
+ return () => { listeners.delete(listener); };
13
+ },
14
+ set(next) {
15
+ const nextKey = canonicalJson(next);
16
+ if (nextKey === key)
17
+ return;
18
+ state = next;
19
+ key = nextKey;
20
+ // A listener that throws changes nothing here: it is thrown where nobody is waiting on it, so it is seen
21
+ for (const listener of [...listeners]) {
22
+ try {
23
+ listener();
24
+ }
25
+ catch (error) {
26
+ setTimeout(() => { throw error; }, 0);
27
+ }
28
+ }
29
+ },
30
+ };
31
+ }
32
+ export function shopActions(store) {
33
+ const apply = (change) => store.set(change(store.getState()));
34
+ return {
35
+ setCollection: (collectionId) => apply((s) => setCollection(s, collectionId)),
36
+ setSearch: (search) => apply((s) => setSearch(s, search)),
37
+ setSearchIgnoresFilters: (ignores) => apply((s) => setSearchIgnoresFilters(s, ignores)),
38
+ toggleTagValue: (tagValueId) => apply((s) => toggleTagValue(s, tagValueId)),
39
+ setRange: (filterId, range) => apply((s) => setRange(s, filterId, range)),
40
+ clearFilters: () => apply(clearFilters),
41
+ setSort: (sortBy, sortDirection) => apply((s) => setSort(s, sortBy, sortDirection)),
42
+ clearSort: () => apply(clearSort),
43
+ setPage: (page) => apply((s) => setPage(s, page)),
44
+ setLimit: (limit) => apply((s) => setLimit(s, limit)),
45
+ };
46
+ }
47
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,14 @@
1
+ import type { ProductLoad, ProductSearch, StoreData } from '../catalog/types.js';
2
+ import { type ShopState } from './state.js';
3
+ export interface SearchContext {
4
+ /** Prices come in this currency. */
5
+ currencyCode: string;
6
+ langCode?: string;
7
+ cadence?: string;
8
+ /** What the products are loaded with: the caller's choice (`include*`, `includeNestedVariants`). */
9
+ load?: ProductLoad & {
10
+ includeNestedVariants?: boolean;
11
+ };
12
+ }
13
+ export declare function toSearch(state: ShopState, store: Pick<StoreData, 'tags' | 'attributes' | 'collections' | 'filters'>, context: SearchContext): ProductSearch;
14
+ //# sourceMappingURL=to-search.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { rangeFilterId } from './range.js';
2
+ import { validSort } from './sort.js';
3
+ import { DEFAULT_SHOP_LIMIT } from './state.js';
4
+ // What is not a number is not sent as one (JSON would make it `null`); how big a page may be, how far a page may go and how long a text may be is the catalog's to say: its 400 is what a question that goes over gets
5
+ const whole = (n, fallback) => (Number.isFinite(n) ? Math.trunc(n) : fallback);
6
+ function findCollection(collections, id) {
7
+ for (const collection of collections) {
8
+ if (collection.id === id)
9
+ return collection;
10
+ const inside = findCollection(collection.children, id);
11
+ if (inside)
12
+ return inside;
13
+ }
14
+ return undefined;
15
+ }
16
+ export function toSearch(state, store, context) {
17
+ // The page and the limit are whole numbers: what they may be is the catalog's to say
18
+ const limit = whole(state.limit, DEFAULT_SHOP_LIMIT);
19
+ const page = whole(state.page, 1);
20
+ // A state can come from outside the program (an address, a stored page): what is not of the type it should be is taken for nothing, so this never throws
21
+ const ranges = typeof state.ranges === 'object' && state.ranges !== null ? state.ranges : {};
22
+ const tagValueIds = Array.isArray(state.tagValueIds) ? state.tagValueIds.filter((id) => typeof id === 'string') : [];
23
+ // A text of spaces is no text, and the catalog refuses an empty one
24
+ const text = typeof state.search === 'string' ? state.search.trim() : '';
25
+ const hasSearch = text !== '';
26
+ // The filters: the ones of the collection (as they are), and the range the customer chose for each of the store's range filters. Ids are the store's own, never changed; one the store does not have is left out
27
+ const fromCollection = typeof state.collectionId === 'string' ? findCollection(store.collections, state.collectionId)?.filterIds ?? [] : [];
28
+ const fromRanges = store.filters.flatMap((filter) => {
29
+ const range = ranges[filter.id];
30
+ const written = typeof range === 'object' && range !== null ? rangeFilterId(filter, range) : null;
31
+ return written === null ? [] : [written];
32
+ });
33
+ const filterIds = [...new Set([...fromCollection, ...fromRanges])];
34
+ // Tag values the store has (a stale one, or one that is not an id, would be refused or do nothing)
35
+ const known = new Set(store.tags.flatMap((tag) => tag.values.map((value) => value.id)));
36
+ const knownTagValueIds = [...new Set(tagValueIds)].filter((id) => known.has(id));
37
+ const sort = validSort(store, state);
38
+ return {
39
+ currencyCode: context.currencyCode,
40
+ page,
41
+ limit,
42
+ ...(context.langCode === undefined ? {} : { langCode: context.langCode }),
43
+ ...(context.cadence === undefined ? {} : { cadence: context.cadence }),
44
+ ...(filterIds.length > 0 ? { filterIds } : {}),
45
+ ...(knownTagValueIds.length > 0 ? { tagValueIds: knownTagValueIds } : {}),
46
+ ...(hasSearch ? { search: text } : {}),
47
+ // It means something only with a text
48
+ ...(hasSearch && typeof state.searchIgnoresFilters === 'boolean' ? { searchIgnoresFilters: state.searchIgnoresFilters } : {}),
49
+ ...(sort ?? {}),
50
+ ...(context.load ?? {}),
51
+ };
52
+ }
53
+ //# sourceMappingURL=to-search.js.map
@@ -0,0 +1,39 @@
1
+ export interface StateHolder<S> {
2
+ get(): S;
3
+ /** Changes some of the state. A change that leaves it as it was tells nobody. A listener that throws changes nothing here: it is thrown where nobody is waiting on it, so it is seen. */
4
+ set(next: Partial<S>): void;
5
+ subscribe(listener: () => void): () => void;
6
+ }
7
+ export declare function createStateHolder<S>(initial: S): StateHolder<S>;
8
+ export interface SerialQueue {
9
+ /** Runs `task` after the ones before it, whatever became of them (a task that fails does not stop the ones after it). What it gives or throws is what comes back. */
10
+ run<T>(task: () => Promise<T>): Promise<T>;
11
+ /** How many tasks there are, the one running included. */
12
+ pending(): number;
13
+ }
14
+ /** Actions one at a time, in the order they were made. `onBusy` is told when the queue goes from empty to not empty and back. */
15
+ export declare function createSerialQueue(onBusy?: (busy: boolean) => void): SerialQueue;
16
+ /** Whose data a store holds: nobody known yet, a guest's, or a customer's. */
17
+ export type Owner = 'unknown' | 'guest' | 'customer';
18
+ export interface AuthFollowing {
19
+ /** Asks the auth store to find out who is logged in, and follows it from now on. Once; the same promise after. */
20
+ start(): Promise<void>;
21
+ /** Stops following. */
22
+ stop(): void;
23
+ }
24
+ /**
25
+ * What a store that has a guest's data and a customer's does with the auth store: logged in is a customer, logged out is a guest, and signing in or a code being asked changes nothing for a guest. `owner`
26
+ * says who the store holds now; `toGuest` and `toCustomer` are told when it changes hands.
27
+ */
28
+ export declare function followAuth(auth: {
29
+ getState(): {
30
+ status: string;
31
+ };
32
+ subscribe(listener: () => void): () => void;
33
+ restore(): Promise<void>;
34
+ }, owner: {
35
+ current(): Owner;
36
+ toGuest(): void;
37
+ toCustomer(): void;
38
+ }): AuthFollowing;
39
+ //# sourceMappingURL=store-kit.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { canonicalJson } from './canonical.js';
2
+ export function createStateHolder(initial) {
3
+ let state = initial;
4
+ let key = canonicalJson(state);
5
+ const listeners = new Set();
6
+ return {
7
+ get: () => state,
8
+ set(next) {
9
+ const merged = { ...state, ...next };
10
+ const mergedKey = canonicalJson(merged);
11
+ if (mergedKey === key)
12
+ return;
13
+ state = merged;
14
+ key = mergedKey;
15
+ for (const listener of [...listeners]) {
16
+ try {
17
+ listener();
18
+ }
19
+ catch (error) {
20
+ setTimeout(() => { throw error; }, 0);
21
+ }
22
+ }
23
+ },
24
+ subscribe(listener) {
25
+ listeners.add(listener);
26
+ return () => { listeners.delete(listener); };
27
+ },
28
+ };
29
+ }
30
+ /** Actions one at a time, in the order they were made. `onBusy` is told when the queue goes from empty to not empty and back. */
31
+ export function createSerialQueue(onBusy) {
32
+ let chain = Promise.resolve();
33
+ let pending = 0;
34
+ return {
35
+ pending: () => pending,
36
+ run(task) {
37
+ if (pending++ === 0)
38
+ onBusy?.(true);
39
+ const result = chain.then(task);
40
+ chain = result.then(() => undefined, () => undefined);
41
+ const done = () => { if (--pending === 0)
42
+ onBusy?.(false); };
43
+ result.then(done, done);
44
+ return result;
45
+ },
46
+ };
47
+ }
48
+ /**
49
+ * What a store that has a guest's data and a customer's does with the auth store: logged in is a customer, logged out is a guest, and signing in or a code being asked changes nothing for a guest. `owner`
50
+ * says who the store holds now; `toGuest` and `toCustomer` are told when it changes hands.
51
+ */
52
+ export function followAuth(auth, owner) {
53
+ let started = null;
54
+ let stopFollowing = null;
55
+ function follow() {
56
+ const { status } = auth.getState();
57
+ if (status === 'signedIn') {
58
+ if (owner.current() !== 'customer')
59
+ owner.toCustomer();
60
+ return;
61
+ }
62
+ if (status === 'signedOut' || (owner.current() === 'unknown' && (status === 'signingIn' || status === 'challenge'))) {
63
+ if (owner.current() !== 'guest')
64
+ owner.toGuest();
65
+ }
66
+ }
67
+ return {
68
+ start() {
69
+ if (started)
70
+ return started;
71
+ stopFollowing = auth.subscribe(follow);
72
+ started = auth.restore().then(follow, follow);
73
+ return started;
74
+ },
75
+ stop() {
76
+ stopFollowing?.();
77
+ stopFollowing = null;
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=store-kit.js.map
@@ -0,0 +1,12 @@
1
+ import type { HttpClient } from '../http.js';
2
+ import type { CallOptions } from '../catalog/types.js';
3
+ import type { AccessTokenOptions } from '../auth/api.js';
4
+ import type { WishlistList, WishlistToggleAnswer } from './types.js';
5
+ export interface WishlistApi {
6
+ /** `GET /store-customer-wishlists`: the wished variants, each with its product. */
7
+ list(options?: CallOptions & AccessTokenOptions): Promise<WishlistList>;
8
+ /** `POST /store-customer-wishlists`: the switch. A variant that is not wished becomes wished; one that is, is removed. */
9
+ toggle(variantId: string, options?: CallOptions & AccessTokenOptions): Promise<WishlistToggleAnswer>;
10
+ }
11
+ export declare function createWishlistApi(http: HttpClient): WishlistApi;
12
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,10 @@
1
+ // The calls of commerce-core that are about a customer's wishlist (`/store-customer-wishlists`), each asking one thing and giving back what the store answers, unchanged. Both need the JWT of the customer. A store can turn the
2
+ // wishlist off (403 "Wishlist is disabled for this store") and keeps a limited number of items for each customer (50 by default: past it, a 429 `dataLimitOf` knows).
3
+ const bearer = (options) => (options?.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {});
4
+ export function createWishlistApi(http) {
5
+ return {
6
+ list: (options) => http.request({ path: '/store-customer-wishlists', headers: bearer(options), signal: options?.signal }),
7
+ toggle: (variantId, options) => http.request({ method: 'POST', path: '/store-customer-wishlists', body: { variantId }, headers: bearer(options), signal: options?.signal }),
8
+ };
9
+ }
10
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1,7 @@
1
+ export { createWishlistApi } from './api.js';
2
+ export type { WishlistApi } from './api.js';
3
+ export { createWishlist, isInWishlist } from './wishlist.js';
4
+ export type { Wishlist, WishlistOptions, WishlistState } from './wishlist.js';
5
+ export { WISHLIST_ERROR_CODES } from './types.js';
6
+ export type { WishlistAsset, WishlistItem, WishlistList, WishlistToggleAnswer, WishlistVariant } from './types.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export { createWishlistApi } from './api.js';
2
+ export { createWishlist, isInWishlist } from './wishlist.js';
3
+ export { WISHLIST_ERROR_CODES } from './types.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,30 @@
1
+ export interface WishlistAsset {
2
+ url: string;
3
+ }
4
+ export interface WishlistVariant {
5
+ id: string;
6
+ name: string;
7
+ assets?: WishlistAsset[];
8
+ }
9
+ /** One wished variant, with its product: the variant is in `product.variants`. */
10
+ export interface WishlistItem {
11
+ product: {
12
+ id: string;
13
+ variants: WishlistVariant[];
14
+ };
15
+ }
16
+ export interface WishlistList {
17
+ data: WishlistItem[];
18
+ total: number;
19
+ }
20
+ /** What the switch did: `wasAdded` says which way it went; `item` is the one that was added. */
21
+ export interface WishlistToggleAnswer {
22
+ success: boolean;
23
+ wasAdded: boolean;
24
+ item?: WishlistItem;
25
+ }
26
+ /** The `code` of the refusal a guest gets (from here: a guest has no wishlist). */
27
+ export declare const WISHLIST_ERROR_CODES: {
28
+ readonly loginRequired: "LOGIN_REQUIRED";
29
+ };
30
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,4 @@
1
+ // What the wishlist calls of commerce-core take and give, as the store says it. A customer keeps variants they want to come back to; there is one call that adds or removes (a switch). A guest has none: it is the customer's.
2
+ /** The `code` of the refusal a guest gets (from here: a guest has no wishlist). */
3
+ export const WISHLIST_ERROR_CODES = { loginRequired: 'LOGIN_REQUIRED' };
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,45 @@
1
+ import { CommerceError } from '../errors.js';
2
+ import type { AuthStore } from '../auth/store.js';
3
+ import { type Owner } from '../store-kit.js';
4
+ import type { WishlistApi } from './api.js';
5
+ import { type WishlistItem } from './types.js';
6
+ export interface WishlistState {
7
+ /** Whose wishlist this is. `unknown` until it is known whether a customer is logged in; a guest's is empty. */
8
+ owner: Owner;
9
+ items: WishlistItem[];
10
+ /** The variants that are in it: what a heart on a product asks. */
11
+ variantIds: string[];
12
+ count: number;
13
+ /** The customer's list was read. */
14
+ loaded: boolean;
15
+ /** `false` once the store said it has the wishlist turned off (403); `null` while that is not known (nothing says it before asking). */
16
+ enabled: boolean | null;
17
+ /** A switch or a read is under way. */
18
+ busy: boolean;
19
+ /** What stopped the list from being read, without being a switch's own failure (those throw): it goes when it works again. */
20
+ error: CommerceError | null;
21
+ }
22
+ export interface Wishlist {
23
+ getState(): WishlistState;
24
+ subscribe(listener: () => void): () => void;
25
+ /** Starts following who is logged in (asks the auth store to find out) and reads the customer's wishlist. Once. */
26
+ start(): Promise<void>;
27
+ /**
28
+ * The switch: adds a variant that is not in the wishlist, removes one that is, and says which. Throws what the store refuses (the wishlist turned off: 403; over what it allows: 429, `dataLimitOf`; a variant that is
29
+ * not the store's) and, for a guest, `LOGIN_REQUIRED`. What is shown goes back to what the store holds.
30
+ */
31
+ toggle(variantId: string): Promise<{
32
+ wasAdded: boolean;
33
+ }>;
34
+ /** Reads the customer's wishlist again. */
35
+ refresh(): Promise<void>;
36
+ dispose(): void;
37
+ }
38
+ export interface WishlistOptions {
39
+ api: WishlistApi;
40
+ auth: Pick<AuthStore, 'getState' | 'subscribe' | 'restore'>;
41
+ }
42
+ export declare function createWishlist(options: WishlistOptions): Wishlist;
43
+ /** Whether a variant is in the wishlist (what a heart asks). */
44
+ export declare const isInWishlist: (state: Pick<WishlistState, "variantIds">, variantId: string) => boolean;
45
+ //# sourceMappingURL=wishlist.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { CommerceError, toCommerceError } from '../errors.js';
2
+ import { createSerialQueue, createStateHolder, followAuth } from '../store-kit.js';
3
+ import { WISHLIST_ERROR_CODES } from './types.js';
4
+ const idsOf = (items) => items.flatMap((i) => i.product.variants.map((v) => v.id));
5
+ /** Whether the store said the wishlist is off (its 403): a refusal that is not about the customer. */
6
+ const isDisabled = (error) => error.status === 403 && /disabled/i.test(error.message);
7
+ export function createWishlist(options) {
8
+ const { api, auth } = options;
9
+ const holder = createStateHolder({ owner: 'unknown', items: [], variantIds: [], count: 0, loaded: false, enabled: null, busy: false, error: null });
10
+ const set = holder.set;
11
+ const queue = createSerialQueue((busy) => set({ busy }));
12
+ // Counts the times the wishlist changed hands: an answer that comes after that is about a wishlist that is not this one any more
13
+ let epoch = 0;
14
+ async function loadCustomer() {
15
+ const mine = epoch;
16
+ try {
17
+ const list = await api.list();
18
+ if (epoch !== mine)
19
+ return;
20
+ set({ items: list.data, variantIds: idsOf(list.data), count: list.data.length, loaded: true, enabled: true, error: null });
21
+ }
22
+ catch (error) {
23
+ if (epoch !== mine)
24
+ return;
25
+ const failure = toCommerceError(error);
26
+ set(isDisabled(failure) ? { items: [], variantIds: [], count: 0, loaded: true, enabled: false, error: null } : { error: failure });
27
+ }
28
+ }
29
+ function toGuest() {
30
+ epoch++;
31
+ set({ owner: 'guest', items: [], variantIds: [], count: 0, loaded: true, error: null });
32
+ }
33
+ function toCustomer() {
34
+ epoch++;
35
+ set({ owner: 'customer', items: [], variantIds: [], count: 0, loaded: false, error: null });
36
+ void queue.run(loadCustomer);
37
+ }
38
+ const following = followAuth(auth, { current: () => holder.get().owner, toGuest, toCustomer });
39
+ return {
40
+ getState: holder.get,
41
+ subscribe: holder.subscribe,
42
+ start: () => following.start(),
43
+ async toggle(variantId) {
44
+ if (holder.get().owner === 'unknown')
45
+ await following.start();
46
+ if (holder.get().owner !== 'customer')
47
+ throw new CommerceError({ kind: 'http', status: 401, code: WISHLIST_ERROR_CODES.loginRequired, message: 'Log in to use the wishlist.' });
48
+ const mine = epoch;
49
+ // What a heart shows changes at once: the store settles it
50
+ const before = holder.get().variantIds;
51
+ const next = before.includes(variantId) ? before.filter((id) => id !== variantId) : [...before, variantId];
52
+ set({ variantIds: next, count: next.length });
53
+ return queue.run(async () => {
54
+ try {
55
+ const answer = await api.toggle(variantId);
56
+ return { wasAdded: answer.wasAdded };
57
+ }
58
+ catch (error) {
59
+ const failure = toCommerceError(error);
60
+ if (epoch === mine) {
61
+ // What was shown at once was not so: it goes back now, whether or not the read that follows can be made
62
+ set(isDisabled(failure) ? { variantIds: before, count: before.length, enabled: false } : { variantIds: before, count: before.length });
63
+ }
64
+ throw failure;
65
+ }
66
+ finally {
67
+ // What is shown is what the store holds, a refusal included
68
+ if (epoch === mine && queue.pending() === 1)
69
+ await loadCustomer();
70
+ }
71
+ });
72
+ },
73
+ async refresh() {
74
+ if (holder.get().owner === 'unknown')
75
+ await following.start();
76
+ if (holder.get().owner === 'customer')
77
+ await queue.run(loadCustomer);
78
+ },
79
+ dispose: following.stop,
80
+ };
81
+ }
82
+ /** Whether a variant is in the wishlist (what a heart asks). */
83
+ export const isInWishlist = (state, variantId) => state.variantIds.includes(variantId);
84
+ //# sourceMappingURL=wishlist.js.map