@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.
- package/README.md +192 -2
- package/dist/account/account.d.ts +54 -0
- package/dist/account/account.js +40 -0
- package/dist/account/index.d.ts +3 -0
- package/dist/account/index.js +2 -0
- package/dist/addresses/api.d.ts +16 -0
- package/dist/addresses/api.js +13 -0
- package/dist/addresses/book.d.ts +40 -0
- package/dist/addresses/book.js +144 -0
- package/dist/addresses/index.d.ts +8 -0
- package/dist/addresses/index.js +5 -0
- package/dist/addresses/rules.d.ts +4 -0
- package/dist/addresses/rules.js +9 -0
- package/dist/addresses/types.d.ts +48 -0
- package/dist/addresses/types.js +6 -0
- package/dist/auth/api.d.ts +43 -0
- package/dist/auth/api.js +22 -0
- package/dist/auth/auth.d.ts +32 -0
- package/dist/auth/auth.js +23 -0
- package/dist/auth/cookies.d.ts +46 -0
- package/dist/auth/cookies.js +156 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/renewal.d.ts +4 -0
- package/dist/auth/renewal.js +37 -0
- package/dist/auth/store.d.ts +70 -0
- package/dist/auth/store.js +284 -0
- package/dist/auth/types.d.ts +105 -0
- package/dist/auth/types.js +5 -0
- package/dist/auth/wake.d.ts +18 -0
- package/dist/auth/wake.js +23 -0
- package/dist/cache.d.ts +49 -0
- package/dist/cache.js +144 -0
- package/dist/canonical.d.ts +7 -0
- package/dist/canonical.js +32 -0
- package/dist/cart/api.d.ts +25 -0
- package/dist/cart/api.js +16 -0
- package/dist/cart/index.d.ts +10 -0
- package/dist/cart/index.js +6 -0
- package/dist/cart/lines.d.ts +15 -0
- package/dist/cart/lines.js +23 -0
- package/dist/cart/storage.d.ts +30 -0
- package/dist/cart/storage.js +82 -0
- package/dist/cart/store.d.ts +58 -0
- package/dist/cart/store.js +218 -0
- package/dist/cart/types.d.ts +194 -0
- package/dist/cart/types.js +11 -0
- package/dist/catalog/cached.d.ts +47 -0
- package/dist/catalog/cached.js +52 -0
- package/dist/catalog/catalog.d.ts +10 -0
- package/dist/catalog/catalog.js +7 -0
- package/dist/catalog/named.d.ts +3 -0
- package/dist/catalog/named.js +7 -0
- package/dist/catalog/products.d.ts +28 -0
- package/dist/catalog/products.js +12 -0
- package/dist/catalog/store-data.d.ts +13 -0
- package/dist/catalog/store-data.js +6 -0
- package/dist/catalog/types.d.ts +430 -0
- package/dist/catalog/types.js +4 -0
- package/dist/checkout/api.d.ts +14 -0
- package/dist/checkout/api.js +7 -0
- package/dist/checkout/errors.d.ts +23 -0
- package/dist/checkout/errors.js +18 -0
- package/dist/checkout/index.d.ts +10 -0
- package/dist/checkout/index.js +6 -0
- package/dist/checkout/return-urls.d.ts +7 -0
- package/dist/checkout/return-urls.js +11 -0
- package/dist/checkout/store.d.ts +98 -0
- package/dist/checkout/store.js +179 -0
- package/dist/checkout/types.d.ts +225 -0
- package/dist/checkout/types.js +28 -0
- package/dist/commerce.d.ts +55 -0
- package/dist/commerce.js +55 -0
- package/dist/customer-resources.d.ts +18 -0
- package/dist/customer-resources.js +40 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +96 -0
- package/dist/http.d.ts +49 -0
- package/dist/http.js +94 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +22 -0
- package/dist/orders/api.d.ts +16 -0
- package/dist/orders/api.js +13 -0
- package/dist/orders/index.d.ts +6 -0
- package/dist/orders/index.js +3 -0
- package/dist/orders/orders.d.ts +19 -0
- package/dist/orders/orders.js +18 -0
- package/dist/orders/types.d.ts +88 -0
- package/dist/orders/types.js +4 -0
- package/dist/resource.d.ts +19 -0
- package/dist/resource.js +15 -0
- package/dist/returns/api.d.ts +27 -0
- package/dist/returns/api.js +19 -0
- package/dist/returns/index.d.ts +6 -0
- package/dist/returns/index.js +3 -0
- package/dist/returns/returns.d.ts +30 -0
- package/dist/returns/returns.js +31 -0
- package/dist/returns/types.d.ts +99 -0
- package/dist/returns/types.js +2 -0
- package/dist/reviews/api.d.ts +19 -0
- package/dist/reviews/api.js +15 -0
- package/dist/reviews/index.d.ts +9 -0
- package/dist/reviews/index.js +5 -0
- package/dist/reviews/reviews.d.ts +23 -0
- package/dist/reviews/reviews.js +37 -0
- package/dist/reviews/rules.d.ts +8 -0
- package/dist/reviews/rules.js +7 -0
- package/dist/reviews/types.d.ts +54 -0
- package/dist/reviews/types.js +10 -0
- package/dist/shop/address-store.d.ts +14 -0
- package/dist/shop/address-store.js +28 -0
- package/dist/shop/address.d.ts +12 -0
- package/dist/shop/address.js +95 -0
- package/dist/shop/index.d.ts +16 -0
- package/dist/shop/index.js +9 -0
- package/dist/shop/question.d.ts +15 -0
- package/dist/shop/question.js +12 -0
- package/dist/shop/range.d.ts +7 -0
- package/dist/shop/range.js +38 -0
- package/dist/shop/sort.d.ts +19 -0
- package/dist/shop/sort.js +18 -0
- package/dist/shop/state.d.ts +47 -0
- package/dist/shop/state.js +51 -0
- package/dist/shop/store.d.ts +26 -0
- package/dist/shop/store.js +47 -0
- package/dist/shop/to-search.d.ts +14 -0
- package/dist/shop/to-search.js +53 -0
- package/dist/store-kit.d.ts +39 -0
- package/dist/store-kit.js +81 -0
- package/dist/wishlist/api.d.ts +12 -0
- package/dist/wishlist/api.js +10 -0
- package/dist/wishlist/index.d.ts +7 -0
- package/dist/wishlist/index.js +4 -0
- package/dist/wishlist/types.d.ts +30 -0
- package/dist/wishlist/types.js +4 -0
- package/dist/wishlist/wishlist.d.ts +45 -0
- package/dist/wishlist/wishlist.js +84 -0
- package/package.json +39 -5
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CustomerResources } from '../customer-resources.js';
|
|
2
|
+
import type { Cache } from '../cache.js';
|
|
3
|
+
import { type Resource, type ResourceIdentity } from '../resource.js';
|
|
4
|
+
import type { ReturnApi } from './api.js';
|
|
5
|
+
import type { CreateGuestReturnInput, CreateReturnInput, GuestReturnSession, GuestVerifyAnswer, GuestVerifyInput, PageQuery, ReturnRequest, ReturnRequestPage } from './types.js';
|
|
6
|
+
export interface Returns {
|
|
7
|
+
list(query?: PageQuery): Resource<ReturnRequestPage> | null;
|
|
8
|
+
get(id: string): Resource<ReturnRequest> | null;
|
|
9
|
+
/** Asks to return some of the items of an order. Throws what the store refuses (an order that is not the customer's: 404). */
|
|
10
|
+
create(input: CreateReturnInput): Promise<ReturnRequest>;
|
|
11
|
+
/** Cancels a request that is still pending (the store refuses any other: 400). */
|
|
12
|
+
cancel(id: string): Promise<ReturnRequest>;
|
|
13
|
+
guest: {
|
|
14
|
+
/** Asks the store to email the link (it answers the same whether the order exists or not). */
|
|
15
|
+
requestLink(input: GuestVerifyInput): Promise<GuestVerifyAnswer>;
|
|
16
|
+
/** The order the link's token opens: a resource, so a page shows it (and what is asked of the same token is asked once). */
|
|
17
|
+
session(token: string): Resource<GuestReturnSession>;
|
|
18
|
+
create(input: CreateGuestReturnInput): Promise<ReturnRequest>;
|
|
19
|
+
cancel(id: string, token: string): Promise<ReturnRequest>;
|
|
20
|
+
};
|
|
21
|
+
/** Forgets what was kept of the customer's requests, so the next question is asked again. */
|
|
22
|
+
refresh(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function createReturns(options: {
|
|
25
|
+
api: ReturnApi;
|
|
26
|
+
customer: CustomerResources;
|
|
27
|
+
cache: Cache;
|
|
28
|
+
identity: ResourceIdentity;
|
|
29
|
+
}): Returns;
|
|
30
|
+
//# sourceMappingURL=returns.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createResourceFactory } from '../resource.js';
|
|
2
|
+
export function createReturns(options) {
|
|
3
|
+
const { api, customer } = options;
|
|
4
|
+
const make = createResourceFactory(options.cache, options.identity);
|
|
5
|
+
const forgetMine = () => { customer.forget('returns.list'); customer.forget('returns.get'); };
|
|
6
|
+
return {
|
|
7
|
+
list: (query) => customer.resource('returns.list', undefined, query ?? {}, (o) => api.list(query, o)),
|
|
8
|
+
get: (id) => customer.resource('returns.get', id, undefined, (o) => api.get(id, o)),
|
|
9
|
+
async create(input) {
|
|
10
|
+
const made = await api.create(input);
|
|
11
|
+
forgetMine();
|
|
12
|
+
return made;
|
|
13
|
+
},
|
|
14
|
+
async cancel(id) {
|
|
15
|
+
try {
|
|
16
|
+
return await api.cancel(id);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
forgetMine();
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
guest: {
|
|
23
|
+
requestLink: (input) => api.verifyGuest(input),
|
|
24
|
+
session: (token) => make('returns.guestSession', token, undefined, (o) => api.guestSession(token, o)),
|
|
25
|
+
create: (input) => api.createGuest(input),
|
|
26
|
+
cancel: (id, token) => api.cancelGuest(id, token),
|
|
27
|
+
},
|
|
28
|
+
refresh: forgetMine,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=returns.js.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { OrderAddress, Page, PageQuery } from '../orders/types.js';
|
|
2
|
+
export type ReturnStatus = 'pending' | 'accepted' | 'rejected' | 'refunded';
|
|
3
|
+
/** One item of a return: which item of the order (`orderItemId` of an `OrderItem`) and how many of it. */
|
|
4
|
+
export interface ReturnItemInput {
|
|
5
|
+
orderItemId: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
}
|
|
8
|
+
/** What a customer sends to return (some of) an order. */
|
|
9
|
+
export interface CreateReturnInput {
|
|
10
|
+
orderId: string;
|
|
11
|
+
items: ReturnItemInput[];
|
|
12
|
+
reason?: string;
|
|
13
|
+
/** Where the store is to collect or send what concerns the return, and who it bills. */
|
|
14
|
+
shippingAddress: OrderAddress;
|
|
15
|
+
billingAddress: OrderAddress;
|
|
16
|
+
}
|
|
17
|
+
/** What a guest sends: the same, with the link's token in place of the order (the token says which). */
|
|
18
|
+
export interface CreateGuestReturnInput extends Omit<CreateReturnInput, 'orderId'> {
|
|
19
|
+
token: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ReturnItem {
|
|
22
|
+
orderItemId: string;
|
|
23
|
+
variantId: string;
|
|
24
|
+
variantName: string;
|
|
25
|
+
coverUrl?: string | null;
|
|
26
|
+
quantity: number;
|
|
27
|
+
unitNet: string;
|
|
28
|
+
unitGross: string;
|
|
29
|
+
unitTax: string;
|
|
30
|
+
totalNet: string;
|
|
31
|
+
totalGross: string;
|
|
32
|
+
totalTax: string;
|
|
33
|
+
appliedTaxRate: number;
|
|
34
|
+
taxBehavior: string;
|
|
35
|
+
skuCode: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface ReturnRequest {
|
|
38
|
+
id: string;
|
|
39
|
+
storeId: string;
|
|
40
|
+
orderId: string;
|
|
41
|
+
orderReference: number | null;
|
|
42
|
+
customerId: string;
|
|
43
|
+
status: ReturnStatus;
|
|
44
|
+
items: ReturnItem[];
|
|
45
|
+
reason: string | null;
|
|
46
|
+
shippingAddress: OrderAddress | null;
|
|
47
|
+
billingAddress: OrderAddress | null;
|
|
48
|
+
customerEmail: string;
|
|
49
|
+
submittedAt: string;
|
|
50
|
+
/** What the store wrote when it answered. */
|
|
51
|
+
merchantNote: string | null;
|
|
52
|
+
processedAt: string | null;
|
|
53
|
+
createdAt: string;
|
|
54
|
+
updatedAt: string;
|
|
55
|
+
}
|
|
56
|
+
export type ReturnRequestPage = Page<ReturnRequest>;
|
|
57
|
+
export type { PageQuery };
|
|
58
|
+
/** How a guest says which order: the address it was bought with, and the order's id OR its reference (the number the customer was given). */
|
|
59
|
+
export interface GuestVerifyInput {
|
|
60
|
+
customerEmail: string;
|
|
61
|
+
orderId?: string;
|
|
62
|
+
reference?: number;
|
|
63
|
+
}
|
|
64
|
+
/** What the store answers to asking for the link: the same whatever the order (it does not tell whether one exists). */
|
|
65
|
+
export interface GuestVerifyAnswer {
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
export interface GuestOrderItem {
|
|
69
|
+
orderItemId: string;
|
|
70
|
+
variantId: string | null;
|
|
71
|
+
coverUrl: string | null;
|
|
72
|
+
name: string;
|
|
73
|
+
skuCode: string | null;
|
|
74
|
+
quantity: number | null;
|
|
75
|
+
unitNet: string | null;
|
|
76
|
+
unitGross: string | null;
|
|
77
|
+
unitTax: string | null;
|
|
78
|
+
totalNet: string | null;
|
|
79
|
+
totalGross: string | null;
|
|
80
|
+
totalTax: string | null;
|
|
81
|
+
appliedTaxRate: number | null;
|
|
82
|
+
taxBehavior: string | null;
|
|
83
|
+
}
|
|
84
|
+
/** What the link's token opens: the order, to choose what to return from. */
|
|
85
|
+
export interface GuestReturnSession {
|
|
86
|
+
success: boolean;
|
|
87
|
+
order: {
|
|
88
|
+
id: string;
|
|
89
|
+
reference: number | null;
|
|
90
|
+
customerEmail: string;
|
|
91
|
+
shippingAddress: OrderAddress | null;
|
|
92
|
+
billingAddress: OrderAddress | null;
|
|
93
|
+
currencyCode: string;
|
|
94
|
+
amount: string | null;
|
|
95
|
+
createdAt: string;
|
|
96
|
+
items: GuestOrderItem[];
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
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 { MyReviewPage, MyReviewsQuery, ReviewAnswer, ReviewDeletedAnswer, ReviewInput, ReviewPatch } from './types.js';
|
|
5
|
+
export interface ReviewApi {
|
|
6
|
+
/** `GET /reviews/by-customer`: the customer's own reviews, a page at a time. */
|
|
7
|
+
mine(input?: MyReviewsQuery, options?: CallOptions & AccessTokenOptions): Promise<MyReviewPage>;
|
|
8
|
+
/**
|
|
9
|
+
* `POST /reviews/product-variants/:variantId`: writes the customer's review of a variant (`201`). Refused with `EMAIL_NOT_VERIFIED` (400: the address is not verified), `REVIEW_ALREADY_EXISTS` (409) and 404
|
|
10
|
+
* for a variant that is not the store's.
|
|
11
|
+
*/
|
|
12
|
+
create(variantId: string, input: ReviewInput, options?: CallOptions & AccessTokenOptions): Promise<ReviewAnswer>;
|
|
13
|
+
/** `PATCH /reviews/:id`: changes what is given of the customer's own review. */
|
|
14
|
+
update(id: string, patch: ReviewPatch, options?: CallOptions & AccessTokenOptions): Promise<ReviewAnswer>;
|
|
15
|
+
/** `DELETE /reviews/:id`: removes the customer's own review. */
|
|
16
|
+
remove(id: string, options?: CallOptions & AccessTokenOptions): Promise<ReviewDeletedAnswer>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createReviewApi(http: HttpClient): ReviewApi;
|
|
19
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The calls of commerce-core that are about what a customer writes of what they bought (`/reviews`), each asking one thing and giving back what the store answers, unchanged (what it writes or removes comes in `data`).
|
|
2
|
+
// All need the JWT of the customer; a store can turn reviews off (403 "Reviews are disabled for this store"). Reading everybody's reviews of a product is the catalog's.
|
|
3
|
+
const bearer = (options) => (options?.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {});
|
|
4
|
+
const part = (id) => encodeURIComponent(id);
|
|
5
|
+
export function createReviewApi(http) {
|
|
6
|
+
return {
|
|
7
|
+
mine: (input, options) => http.request({
|
|
8
|
+
path: '/reviews/by-customer', query: { page: input?.page, limit: input?.limit, sortBy: input?.sortBy, sortDirection: input?.sortDirection, rating: input?.rating, lang: input?.lang }, headers: bearer(options), signal: options?.signal,
|
|
9
|
+
}),
|
|
10
|
+
create: (variantId, input, options) => http.request({ method: 'POST', path: `/reviews/product-variants/${part(variantId)}`, body: input, headers: bearer(options), signal: options?.signal }),
|
|
11
|
+
update: (id, patch, options) => http.request({ method: 'PATCH', path: `/reviews/${part(id)}`, body: patch, headers: bearer(options), signal: options?.signal }),
|
|
12
|
+
remove: (id, options) => http.request({ method: 'DELETE', path: `/reviews/${part(id)}`, headers: bearer(options), signal: options?.signal }),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createReviewApi } from './api.js';
|
|
2
|
+
export type { ReviewApi } from './api.js';
|
|
3
|
+
export { createReviews } from './reviews.js';
|
|
4
|
+
export type { Reviews } from './reviews.js';
|
|
5
|
+
export { reviewAbility } from './rules.js';
|
|
6
|
+
export type { ReviewAbility } from './rules.js';
|
|
7
|
+
export { REVIEW_ERROR_CODES } from './types.js';
|
|
8
|
+
export type { MyReview, MyReviewPage, MyReviewsQuery, CustomerReview, ReviewAnswer, ReviewDeletedAnswer, ReviewInput, ReviewPatch } from './types.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AuthStore } from '../auth/store.js';
|
|
2
|
+
import type { CachedCatalog } from '../catalog/cached.js';
|
|
3
|
+
import type { CustomerResources } from '../customer-resources.js';
|
|
4
|
+
import type { Resource } from '../resource.js';
|
|
5
|
+
import type { ReviewApi } from './api.js';
|
|
6
|
+
import { type MyReviewPage, type MyReviewsQuery, type CustomerReview, type ReviewInput, type ReviewPatch } from './types.js';
|
|
7
|
+
export interface Reviews {
|
|
8
|
+
mine(query?: MyReviewsQuery): Resource<MyReviewPage> | null;
|
|
9
|
+
/** Writes the customer's review of a variant. Throws what the store refuses (`EMAIL_NOT_VERIFIED`, `REVIEW_ALREADY_EXISTS`, a variant that is not the store's) and, for a guest, `LOGIN_REQUIRED`. */
|
|
10
|
+
create(variantId: string, input: ReviewInput): Promise<CustomerReview>;
|
|
11
|
+
update(id: string, patch: ReviewPatch): Promise<CustomerReview>;
|
|
12
|
+
/** Removes the customer's own review; says which. */
|
|
13
|
+
remove(id: string): Promise<{
|
|
14
|
+
id: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function createReviews(options: {
|
|
18
|
+
api: ReviewApi;
|
|
19
|
+
customer: CustomerResources;
|
|
20
|
+
catalog: Pick<CachedCatalog, 'invalidate'>;
|
|
21
|
+
auth: Pick<AuthStore, 'getState'>;
|
|
22
|
+
}): Reviews;
|
|
23
|
+
//# sourceMappingURL=reviews.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CommerceError } from '../errors.js';
|
|
2
|
+
import { REVIEW_ERROR_CODES } from './types.js';
|
|
3
|
+
export function createReviews(options) {
|
|
4
|
+
const { api, customer, catalog, auth } = options;
|
|
5
|
+
const loggedIn = () => auth.getState().status === 'signedIn';
|
|
6
|
+
const login = () => new CommerceError({ kind: 'http', status: 401, code: REVIEW_ERROR_CODES.loginRequired, message: 'Log in to write a review.' });
|
|
7
|
+
/** What was kept that a review changes: the customer's own, and the catalog's reviews of its product (all of them when the product is not known). */
|
|
8
|
+
const forget = (productId) => {
|
|
9
|
+
customer.forget('reviews.mine');
|
|
10
|
+
catalog.invalidate(productId ? { call: 'products.getReviews', subject: productId } : { call: 'products.getReviews' });
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
mine: (query) => customer.resource('reviews.mine', undefined, query ?? {}, (o) => api.mine(query, o)),
|
|
14
|
+
async create(variantId, input) {
|
|
15
|
+
if (!loggedIn())
|
|
16
|
+
throw login();
|
|
17
|
+
const { data } = await api.create(variantId, input);
|
|
18
|
+
forget(data.productId);
|
|
19
|
+
return data;
|
|
20
|
+
},
|
|
21
|
+
async update(id, patch) {
|
|
22
|
+
if (!loggedIn())
|
|
23
|
+
throw login();
|
|
24
|
+
const { data } = await api.update(id, patch);
|
|
25
|
+
forget(data.productId);
|
|
26
|
+
return data;
|
|
27
|
+
},
|
|
28
|
+
async remove(id) {
|
|
29
|
+
if (!loggedIn())
|
|
30
|
+
throw login();
|
|
31
|
+
const { data } = await api.remove(id);
|
|
32
|
+
forget();
|
|
33
|
+
return data;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=reviews.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AuthState } from '../auth/store.js';
|
|
2
|
+
/**
|
|
3
|
+
* Who can write a review, from what the session says: a guest has to log in; a customer whose address is not verified has to verify it (the store refuses with `EMAIL_NOT_VERIFIED`); a customer whose address is verified can.
|
|
4
|
+
* It is a hint for the screen: the store decides (the session may be one verification behind).
|
|
5
|
+
*/
|
|
6
|
+
export type ReviewAbility = 'login' | 'verify' | 'allowed';
|
|
7
|
+
export declare function reviewAbility(auth: Pick<AuthState, 'status' | 'customer'>): ReviewAbility;
|
|
8
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Page, PageQuery } from '../orders/types.js';
|
|
2
|
+
export interface ReviewInput {
|
|
3
|
+
rating: number;
|
|
4
|
+
title: string;
|
|
5
|
+
comment: string;
|
|
6
|
+
}
|
|
7
|
+
/** What is changed of a review: any of it. */
|
|
8
|
+
export type ReviewPatch = Partial<ReviewInput>;
|
|
9
|
+
export interface CustomerReview {
|
|
10
|
+
id: string;
|
|
11
|
+
storeId: string;
|
|
12
|
+
productId: string;
|
|
13
|
+
variantId: string;
|
|
14
|
+
customerId: string;
|
|
15
|
+
rating: number;
|
|
16
|
+
title: string | null;
|
|
17
|
+
comment: string | null;
|
|
18
|
+
/** The reviewer bought what they review. */
|
|
19
|
+
verifiedPurchase: boolean;
|
|
20
|
+
createdAt: string | null;
|
|
21
|
+
updatedAt: string | null;
|
|
22
|
+
/** The reviewer's name: in the customer's list; NOT in what writing or changing a review answers (the store sends the row it kept, which has none). */
|
|
23
|
+
customerName?: string | null;
|
|
24
|
+
}
|
|
25
|
+
/** A review in the customer's own list: with what it is about. */
|
|
26
|
+
export interface MyReview extends CustomerReview {
|
|
27
|
+
productVariantName?: string | null;
|
|
28
|
+
coverUrl?: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface MyReviewsQuery extends PageQuery {
|
|
31
|
+
/** Only the reviews with this rating (1 to 5). */
|
|
32
|
+
rating?: number;
|
|
33
|
+
}
|
|
34
|
+
export type MyReviewPage = Page<MyReview>;
|
|
35
|
+
/** What the store answers to writing a review: the review, in `data`. */
|
|
36
|
+
export interface ReviewAnswer {
|
|
37
|
+
data: CustomerReview;
|
|
38
|
+
}
|
|
39
|
+
/** What it answers to removing one: the review that was removed (its `id` at least; the store sends the whole row). */
|
|
40
|
+
export interface ReviewDeletedAnswer {
|
|
41
|
+
data: {
|
|
42
|
+
id: string;
|
|
43
|
+
} & Partial<CustomerReview>;
|
|
44
|
+
}
|
|
45
|
+
/** The `code`s of what the store refuses of a review. */
|
|
46
|
+
export declare const REVIEW_ERROR_CODES: {
|
|
47
|
+
/** Only a customer whose address is verified can review. */
|
|
48
|
+
readonly emailNotVerified: "EMAIL_NOT_VERIFIED";
|
|
49
|
+
/** One review for each variant. */
|
|
50
|
+
readonly alreadyExists: "REVIEW_ALREADY_EXISTS";
|
|
51
|
+
/** A guest has to log in (from here: the store is not asked). */
|
|
52
|
+
readonly loginRequired: "LOGIN_REQUIRED";
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The `code`s of what the store refuses of a review. */
|
|
2
|
+
export const REVIEW_ERROR_CODES = {
|
|
3
|
+
/** Only a customer whose address is verified can review. */
|
|
4
|
+
emailNotVerified: 'EMAIL_NOT_VERIFIED',
|
|
5
|
+
/** One review for each variant. */
|
|
6
|
+
alreadyExists: 'REVIEW_ALREADY_EXISTS',
|
|
7
|
+
/** A guest has to log in (from here: the store is not asked). */
|
|
8
|
+
loginRequired: 'LOGIN_REQUIRED',
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type AddressOptions } from './address.js';
|
|
2
|
+
import type { ShopStore } from './store.js';
|
|
3
|
+
export interface AddressPort {
|
|
4
|
+
/** The query string of the address now: `''` or `'?a=1&b=2'`. */
|
|
5
|
+
read(): string;
|
|
6
|
+
/** Puts this query string in the address (the page's path stays what it is) and adds a history entry, so "back" goes back to what it was. */
|
|
7
|
+
write(search: string): void;
|
|
8
|
+
/** Called when the address changed whoever changed it (a link, the back button, `write`). Returns what stops it. */
|
|
9
|
+
subscribe(listener: () => void): () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function createAddressShopStore(port: AddressPort, options?: AddressOptions & {
|
|
12
|
+
collectionId?: string;
|
|
13
|
+
}): ShopStore;
|
|
14
|
+
//# sourceMappingURL=address-store.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { canonicalJson } from '../canonical.js';
|
|
2
|
+
import { applyShopQuery, queryToShopState } from './address.js';
|
|
3
|
+
export function createAddressShopStore(port, options = {}) {
|
|
4
|
+
// The state is the SAME object while the address is the same text (what a framework compares to know that nothing changed)
|
|
5
|
+
let cache = null;
|
|
6
|
+
const getState = () => {
|
|
7
|
+
const search = port.read();
|
|
8
|
+
if (cache === null || cache.search !== search) {
|
|
9
|
+
const state = queryToShopState(search, options);
|
|
10
|
+
// Another part of the address changed (`utm_source`), not what is about the shop: the state is the same one
|
|
11
|
+
cache = { search, state: cache !== null && canonicalJson(cache.state) === canonicalJson(state) ? cache.state : state };
|
|
12
|
+
}
|
|
13
|
+
return cache.state;
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
getState,
|
|
17
|
+
subscribe: (listener) => port.subscribe(listener),
|
|
18
|
+
set(next) {
|
|
19
|
+
const current = port.read();
|
|
20
|
+
const written = applyShopQuery(current, next, options);
|
|
21
|
+
// What the address would say of the shop is what it already says (a change that changes nothing, or only what an address cannot carry): no new entry in the history
|
|
22
|
+
if (canonicalJson(queryToShopState(written, options)) === canonicalJson(getState()))
|
|
23
|
+
return;
|
|
24
|
+
port.write(written);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=address-store.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ShopState } from './state.js';
|
|
2
|
+
export interface AddressOptions {
|
|
3
|
+
/** The shop's own number per page: it is not written when it is this (and it is what an address with no `limit` means). */
|
|
4
|
+
limit?: number;
|
|
5
|
+
}
|
|
6
|
+
/** The query string (`?a=1&b=2`, or '' for none) of an address `search` with the choices of `state` in it: ours replaced, the others kept as they were, in their place. */
|
|
7
|
+
export declare function applyShopQuery(search: string, state: ShopState, options?: AddressOptions): string;
|
|
8
|
+
/** The state an address says. `collectionId` (the page's, not the address's) and the shop's own `limit` are what the address does not say; what it says over them wins. Needs no store's data: what a value means is for `toSearch`. */
|
|
9
|
+
export declare function queryToShopState(search: string, options?: AddressOptions & {
|
|
10
|
+
collectionId?: string;
|
|
11
|
+
}): ShopState;
|
|
12
|
+
//# sourceMappingURL=address.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { plainNumber } from './range.js';
|
|
2
|
+
import { DEFAULT_SHOP_LIMIT, emptyShopState } from './state.js';
|
|
3
|
+
// What a customer chose, written in the ADDRESS of the page (`?q=tubo&r.<filter>=10..50&sort=price:asc&page=2`) and read back from it: so a search is a link, "back" goes back through the choices, and a
|
|
4
|
+
// reload keeps them. Pure: it takes and gives the query string, and knows nothing of a router. The rules:
|
|
5
|
+
// - what is the default is not written (page 1, the shop's own limit, nothing chosen): an empty state is an empty query;
|
|
6
|
+
// - only OUR keys are touched: `utm_source`, `lang` and whatever else the address has stays exactly as it was;
|
|
7
|
+
// - an address typed by hand, or an old link, can say anything: what is not valid is ignored, never an error (and `toSearch` corrects what is left). The first of a repeated key counts;
|
|
8
|
+
// - the collection is not here: it is the PAGE (`/shop/<slug>`), and the page tells the state (`setCollection`);
|
|
9
|
+
// - the ids are the store's own (a filter's, a tag value's): long, but stable and not ambiguous (names are not unique and change with the language).
|
|
10
|
+
// The keys: `q` the text · `ignore` 1/0 (searchIgnoresFilters) · `tags` tag value ids, with commas · `r.<filter id>` a range, `min..max`, either side may be empty (`..50`, `10..`) · `sort` `<field>:<asc|desc>`
|
|
11
|
+
// (`price:asc`, `tag:<id>:desc`) · `page` · `limit`.
|
|
12
|
+
const KEYS = ['q', 'ignore', 'tags', 'sort', 'page', 'limit'];
|
|
13
|
+
const RANGE = 'r.';
|
|
14
|
+
const FIELDS = new Set(['createdAt', 'updatedAt', 'alphabetical', 'price', 'match', 'availability']);
|
|
15
|
+
const isOurs = (key) => KEYS.includes(key) || key.startsWith(RANGE);
|
|
16
|
+
function boundText(bound) {
|
|
17
|
+
if (typeof bound === 'number')
|
|
18
|
+
return Number.isFinite(bound) ? plainNumber(bound) : '';
|
|
19
|
+
return typeof bound === 'string' ? bound.trim() : '';
|
|
20
|
+
}
|
|
21
|
+
/** The query string (`?a=1&b=2`, or '' for none) of an address `search` with the choices of `state` in it: ours replaced, the others kept as they were, in their place. */
|
|
22
|
+
export function applyShopQuery(search, state, options = {}) {
|
|
23
|
+
const defaultLimit = options.limit ?? DEFAULT_SHOP_LIMIT;
|
|
24
|
+
const kept = new URLSearchParams(search);
|
|
25
|
+
for (const key of [...new Set([...kept.keys()])])
|
|
26
|
+
if (isOurs(key))
|
|
27
|
+
kept.delete(key);
|
|
28
|
+
const ours = new URLSearchParams();
|
|
29
|
+
if (typeof state.search === 'string' && state.search.trim() !== '')
|
|
30
|
+
ours.set('q', state.search);
|
|
31
|
+
if (typeof state.searchIgnoresFilters === 'boolean')
|
|
32
|
+
ours.set('ignore', state.searchIgnoresFilters ? '1' : '0');
|
|
33
|
+
const tags = [...new Set(state.tagValueIds)].filter((id) => typeof id === 'string' && id !== '' && !id.includes(','));
|
|
34
|
+
if (tags.length > 0)
|
|
35
|
+
ours.set('tags', tags.join(','));
|
|
36
|
+
for (const id of Object.keys(state.ranges).sort()) {
|
|
37
|
+
const range = state.ranges[id];
|
|
38
|
+
const min = boundText(range?.min), max = boundText(range?.max);
|
|
39
|
+
if (id !== '' && (min !== '' || max !== ''))
|
|
40
|
+
ours.set(`${RANGE}${id}`, `${min}..${max}`);
|
|
41
|
+
}
|
|
42
|
+
if (state.sortBy !== undefined && (state.sortDirection === 'asc' || state.sortDirection === 'desc'))
|
|
43
|
+
ours.set('sort', `${state.sortBy}:${state.sortDirection}`);
|
|
44
|
+
if (Number.isInteger(state.page) && state.page > 1)
|
|
45
|
+
ours.set('page', String(state.page));
|
|
46
|
+
if (Number.isInteger(state.limit) && state.limit !== defaultLimit && state.limit > 0)
|
|
47
|
+
ours.set('limit', String(state.limit));
|
|
48
|
+
// What is ours is written so that it can be read: `:` and `,` mean something to us and are safe in a query (what the address already had is not rewritten)
|
|
49
|
+
const text = [kept.toString(), ours.toString().replace(/%3A/gi, ':').replace(/%2C/gi, ',')].filter((part) => part !== '').join('&');
|
|
50
|
+
return text === '' ? '' : `?${text}`;
|
|
51
|
+
}
|
|
52
|
+
const wholeNumber = (text) => (text !== null && /^\d{1,9}$/.test(text) && Number(text) > 0 ? Number(text) : null);
|
|
53
|
+
function parseSort(text) {
|
|
54
|
+
if (text === null)
|
|
55
|
+
return null;
|
|
56
|
+
const cut = text.lastIndexOf(':');
|
|
57
|
+
const field = text.slice(0, cut);
|
|
58
|
+
const direction = text.slice(cut + 1);
|
|
59
|
+
if (cut < 1 || (direction !== 'asc' && direction !== 'desc'))
|
|
60
|
+
return null;
|
|
61
|
+
const sorted = field.startsWith('tag:') ? field.length > 4 : field.startsWith('attribute:') ? field.length > 10 : FIELDS.has(field);
|
|
62
|
+
return sorted ? { sortBy: field, sortDirection: direction } : null;
|
|
63
|
+
}
|
|
64
|
+
/** The state an address says. `collectionId` (the page's, not the address's) and the shop's own `limit` are what the address does not say; what it says over them wins. Needs no store's data: what a value means is for `toSearch`. */
|
|
65
|
+
export function queryToShopState(search, options = {}) {
|
|
66
|
+
const params = new URLSearchParams(search);
|
|
67
|
+
const state = emptyShopState({ limit: wholeNumber(params.get('limit')) ?? options.limit ?? DEFAULT_SHOP_LIMIT, collectionId: options.collectionId });
|
|
68
|
+
const text = params.get('q');
|
|
69
|
+
const ignore = params.get('ignore');
|
|
70
|
+
const tags = (params.get('tags') ?? '').split(',').filter((id) => id !== '');
|
|
71
|
+
const ranges = {};
|
|
72
|
+
for (const [key, value] of params) {
|
|
73
|
+
if (!key.startsWith(RANGE) || key.length === RANGE.length || key.slice(RANGE.length) in ranges)
|
|
74
|
+
continue;
|
|
75
|
+
const at = value.indexOf('..');
|
|
76
|
+
if (at < 0)
|
|
77
|
+
continue;
|
|
78
|
+
const min = value.slice(0, at).trim(), max = value.slice(at + 2).trim();
|
|
79
|
+
if (min === '' && max === '')
|
|
80
|
+
continue;
|
|
81
|
+
ranges[key.slice(RANGE.length)] = { ...(min === '' ? {} : { min }), ...(max === '' ? {} : { max }) };
|
|
82
|
+
}
|
|
83
|
+
const sort = parseSort(params.get('sort'));
|
|
84
|
+
const page = wholeNumber(params.get('page'));
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
...(text !== null && text.trim() !== '' ? { search: text } : {}),
|
|
88
|
+
...(ignore === '1' ? { searchIgnoresFilters: true } : ignore === '0' ? { searchIgnoresFilters: false } : {}),
|
|
89
|
+
tagValueIds: [...new Set(tags)],
|
|
90
|
+
ranges,
|
|
91
|
+
...(sort ?? {}),
|
|
92
|
+
page: page ?? 1,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=address.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { DEFAULT_SHOP_LIMIT, clearFilters, clearSort, emptyShopState, setCollection, setLimit, setPage, setRange, setSearch, setSearchIgnoresFilters, setSort, toggleTagValue } from './state.js';
|
|
2
|
+
export type { RangeBound, RangeValue, ShopState } from './state.js';
|
|
3
|
+
export { rangeFilterId } from './range.js';
|
|
4
|
+
export { sortOptionsOf, validSort } from './sort.js';
|
|
5
|
+
export type { SortOption } from './sort.js';
|
|
6
|
+
export { toSearch } from './to-search.js';
|
|
7
|
+
export type { SearchContext } from './to-search.js';
|
|
8
|
+
export { createShopStore, shopActions } from './store.js';
|
|
9
|
+
export type { ShopActions, ShopStore } from './store.js';
|
|
10
|
+
export { shopQuestion } from './question.js';
|
|
11
|
+
export type { ShopQuestion } from './question.js';
|
|
12
|
+
export { applyShopQuery, queryToShopState } from './address.js';
|
|
13
|
+
export type { AddressOptions } from './address.js';
|
|
14
|
+
export { createAddressShopStore } from './address-store.js';
|
|
15
|
+
export type { AddressPort } from './address-store.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { DEFAULT_SHOP_LIMIT, clearFilters, clearSort, emptyShopState, setCollection, setLimit, setPage, setRange, setSearch, setSearchIgnoresFilters, setSort, toggleTagValue } from './state.js';
|
|
2
|
+
export { rangeFilterId } from './range.js';
|
|
3
|
+
export { sortOptionsOf, validSort } from './sort.js';
|
|
4
|
+
export { toSearch } from './to-search.js';
|
|
5
|
+
export { createShopStore, shopActions } from './store.js';
|
|
6
|
+
export { shopQuestion } from './question.js';
|
|
7
|
+
export { applyShopQuery, queryToShopState } from './address.js';
|
|
8
|
+
export { createAddressShopStore } from './address-store.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CachedCatalog, Resource } from '../catalog/cached.js';
|
|
2
|
+
import type { ProductPage, ProductSearch, StoreData } from '../catalog/types.js';
|
|
3
|
+
import type { ShopState } from './state.js';
|
|
4
|
+
import { type SearchContext } from './to-search.js';
|
|
5
|
+
/** What the shop is asking the catalog right now: the question, and the cache's view of it (a `Resource`, which is what a framework binds to). */
|
|
6
|
+
export interface ShopQuestion {
|
|
7
|
+
question: ProductSearch;
|
|
8
|
+
resource: Resource<ProductPage>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The state of the shop made into a question of the cached catalog. It needs the store's data (its tags, collections and filters say what a state means), so until they are there there is no question:
|
|
12
|
+
* `null`. ONE place does this, so an adapter for another framework only binds what it returns.
|
|
13
|
+
*/
|
|
14
|
+
export declare function shopQuestion(catalog: Pick<CachedCatalog, 'resource'>, storeData: StoreData | null | undefined, state: ShopState, context: SearchContext): ShopQuestion | null;
|
|
15
|
+
//# sourceMappingURL=question.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { toSearch } from './to-search.js';
|
|
2
|
+
/**
|
|
3
|
+
* The state of the shop made into a question of the cached catalog. It needs the store's data (its tags, collections and filters say what a state means), so until they are there there is no question:
|
|
4
|
+
* `null`. ONE place does this, so an adapter for another framework only binds what it returns.
|
|
5
|
+
*/
|
|
6
|
+
export function shopQuestion(catalog, storeData, state, context) {
|
|
7
|
+
if (!storeData)
|
|
8
|
+
return null;
|
|
9
|
+
const question = toSearch(state, storeData, context);
|
|
10
|
+
return { question, resource: catalog.resource.products.search(question) };
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=question.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StoreFilter } from '../catalog/types.js';
|
|
2
|
+
import type { RangeValue } from './state.js';
|
|
3
|
+
/** A number written plainly (no exponent): the catalog reads digits (and so does an address). */
|
|
4
|
+
export declare function plainNumber(n: number): string;
|
|
5
|
+
/** The text of the range of `filter` the customer chose (`<id>[<min>;<max>]`), or null when there is nothing to send (no ends). A minimum above the maximum is turned round (the customer meant the two ends); equal ones are a range. */
|
|
6
|
+
export declare function rangeFilterId(filter: Pick<StoreFilter, 'id' | 'type'>, range: RangeValue): string | null;
|
|
7
|
+
//# sourceMappingURL=range.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const isoDate = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
2
|
+
/** A number written plainly (no exponent): the catalog reads digits (and so does an address). */
|
|
3
|
+
export function plainNumber(n) {
|
|
4
|
+
return n.toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 20 }).replace(/^-0$/, '0');
|
|
5
|
+
}
|
|
6
|
+
function numberBound(bound) {
|
|
7
|
+
const n = typeof bound === 'number' ? bound : typeof bound === 'string' && bound.trim() !== '' ? Number(bound.trim()) : Number.NaN;
|
|
8
|
+
// Beyond what a number can hold exactly it is no price, and writing it would need an exponent
|
|
9
|
+
return Number.isFinite(n) && Math.abs(n) <= Number.MAX_SAFE_INTEGER ? { text: plainNumber(n), value: n } : null;
|
|
10
|
+
}
|
|
11
|
+
function dateBound(bound) {
|
|
12
|
+
if (typeof bound === 'number')
|
|
13
|
+
return Number.isSafeInteger(bound) ? { text: String(bound), value: bound } : null;
|
|
14
|
+
if (typeof bound !== 'string')
|
|
15
|
+
return null;
|
|
16
|
+
const text = bound.trim();
|
|
17
|
+
const parts = isoDate.exec(text);
|
|
18
|
+
if (parts) {
|
|
19
|
+
const [year, month, day] = [Number(parts[1]), Number(parts[2]), Number(parts[3])];
|
|
20
|
+
const at = Date.UTC(year, month - 1, day);
|
|
21
|
+
const real = new Date(at);
|
|
22
|
+
// 2020-02-31 is not a day (the catalog would make it a different one)
|
|
23
|
+
return real.getUTCFullYear() === year && real.getUTCMonth() === month - 1 && real.getUTCDate() === day ? { text, value: at } : null;
|
|
24
|
+
}
|
|
25
|
+
return /^-?\d+$/.test(text) && Number.isSafeInteger(Number(text)) ? { text: String(Number(text)), value: Number(text) } : null;
|
|
26
|
+
}
|
|
27
|
+
/** The text of the range of `filter` the customer chose (`<id>[<min>;<max>]`), or null when there is nothing to send (no ends). A minimum above the maximum is turned round (the customer meant the two ends); equal ones are a range. */
|
|
28
|
+
export function rangeFilterId(filter, range) {
|
|
29
|
+
const read = filter.type === 'date' ? dateBound : numberBound;
|
|
30
|
+
let min = read(range.min);
|
|
31
|
+
let max = read(range.max);
|
|
32
|
+
if (!min && !max)
|
|
33
|
+
return null;
|
|
34
|
+
if (min && max && min.value > max.value)
|
|
35
|
+
[min, max] = [max, min];
|
|
36
|
+
return `${filter.id}[${min?.text ?? ''};${max?.text ?? ''}]`;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=range.js.map
|