@scayle/storefront-core 8.45.1 → 8.47.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/CHANGELOG.md +96 -0
- package/dist/helpers/attributeHelpers.d.ts +64 -2
- package/dist/helpers/attributeHelpers.mjs +34 -5
- package/dist/rpc/methods/basket/basket.d.ts +7 -5
- package/dist/rpc/methods/basket/basket.mjs +55 -11
- package/dist/rpc/methods/checkout/checkout.d.ts +2 -0
- package/dist/rpc/methods/checkout/checkout.mjs +7 -3
- package/dist/rpc/methods/checkout/order.d.ts +1 -0
- package/dist/rpc/methods/checkout/order.mjs +3 -0
- package/dist/utils/basket.d.ts +41 -0
- package/dist/utils/basket.mjs +9 -0
- package/dist/utils/user.d.ts +8 -0
- package/dist/utils/user.mjs +15 -2
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,101 @@
|
|
|
1
1
|
# @scayle/storefront-core
|
|
2
2
|
|
|
3
|
+
## 8.47.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Enhanced basket RPC methods to support order custom data functionality.
|
|
8
|
+
|
|
9
|
+
All basket RPC methods now integrate with the `getOrderCustomData` RPC method to fetch and include order custom data in SAPI requests.
|
|
10
|
+
This enables developers to attach order-specific custom metadata, business rules, or additional context to orders throughout the basket lifecycle.
|
|
11
|
+
The existing `orderCustomData` parameter is deprecated and will be removed in the next major version.
|
|
12
|
+
For now, both parameters are supported and merged together, where attributes from the parameter take precedence.
|
|
13
|
+
|
|
14
|
+
On default, this RPC returns an empty object but can be overridden by the developer to return a custom object.
|
|
15
|
+
|
|
16
|
+
Example `getOrderCustomData` RPC:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
export const getOrderCustomData: RpcHandler<Record<string, unknown>> =
|
|
20
|
+
defineRpcHandler<Record<string, unknown>>(async (context: RpcContext) => {
|
|
21
|
+
return {
|
|
22
|
+
customer: {
|
|
23
|
+
groups: context.user?.groups,
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For further information, please refer to the [Overriding Core RPC Methods](https://scayle.dev/en/core-documentation/storefront-guide/storefront-application/technical-foundation/rpc-methods?sourceText=RPC%2520Methods#overriding-core-rpc-methods).
|
|
30
|
+
|
|
31
|
+
- Updated all basket RPC methods to pass the customer access token to the SAPI client, enabling the `X-Customer-Token` header for promotion validation.
|
|
32
|
+
|
|
33
|
+
The customer access token is now automatically included in basket requests when available.
|
|
34
|
+
If the token has expired, it will be omitted from the request to prevent validation errors.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
**Dependencies**
|
|
39
|
+
|
|
40
|
+
- Updated dependency to @scayle/unstorage-scayle-kv-driver@2.0.7
|
|
41
|
+
- Updated dependency to @scayle/storefront-api@18.18.0
|
|
42
|
+
|
|
43
|
+
## 8.46.0
|
|
44
|
+
|
|
45
|
+
### Minor Changes
|
|
46
|
+
|
|
47
|
+
- Added `getAttributeValuesByGroupId` function to the `attributeHelpers` helper for retrieving attribute values by their numeric attribute group ID.
|
|
48
|
+
|
|
49
|
+
This function allows you to retrieve attribute values filtered by a specific attribute group ID, providing a more targeted approach when working with grouped product attributes.
|
|
50
|
+
The existing `getAttributeValues` function has been renamed to `getAttributeValuesByName` to better differentiate between the two lookup methods.
|
|
51
|
+
|
|
52
|
+
### Usage Examples
|
|
53
|
+
|
|
54
|
+
Both functions return an array of values for consistency, making them safe to use in iterations. For single-select attributes, the value is wrapped in an array. For multi-select attributes, the array is returned directly.
|
|
55
|
+
|
|
56
|
+
#### Looking up by name (existing, now renamed):
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { getAttributeValuesByName } from '@scayle/storefront-core'
|
|
60
|
+
|
|
61
|
+
// Single-select attribute
|
|
62
|
+
const sizes = getAttributeValuesByName(product.attributes, 'size')
|
|
63
|
+
// Returns: [{ id: 1, label: 'M', value: 'medium' }]
|
|
64
|
+
|
|
65
|
+
// Multi-select attribute
|
|
66
|
+
const colors = getAttributeValuesByName(product.attributes, 'color')
|
|
67
|
+
// Returns: [{ id: 1, label: 'Red' }, { id: 2, label: 'Blue' }]
|
|
68
|
+
|
|
69
|
+
// Non-existent attribute
|
|
70
|
+
const missing = getAttributeValuesByName(product.attributes, 'doesNotExist')
|
|
71
|
+
// Returns: []
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Looking up by group ID (new):
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { getAttributeValuesByGroupId } from '@scayle/storefront-core'
|
|
78
|
+
|
|
79
|
+
// Look up attribute by its numeric ID from the SCAYLE API
|
|
80
|
+
const promotionValues = getAttributeValuesByGroupId(product.attributes, 42)
|
|
81
|
+
// Returns: [{ id: 123, label: 'Summer Sale', value: 'summer-2024' }]
|
|
82
|
+
|
|
83
|
+
// Non-existent attribute group
|
|
84
|
+
const missing = getAttributeValuesByGroupId(product.attributes, 9999)
|
|
85
|
+
// Returns: []
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### When to Use Each Function
|
|
89
|
+
|
|
90
|
+
- **`getAttributeValuesByName`**: Use when you know the attribute name key (e.g., 'color', 'size'). This is the most common use case in application code.
|
|
91
|
+
- **`getAttributeValuesByGroupId`**: Use when you have the numeric attribute group ID from the SCAYLE API, such as when processing API configurations, handling dynamic attribute mappings, or working with attribute group references.
|
|
92
|
+
|
|
93
|
+
### Patch Changes
|
|
94
|
+
|
|
95
|
+
**Dependencies**
|
|
96
|
+
|
|
97
|
+
- Updated dependency to @scayle/storefront-api@18.17.0
|
|
98
|
+
|
|
3
99
|
## 8.45.1
|
|
4
100
|
|
|
5
101
|
### Patch Changes
|
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
import type { Attributes, Value } from '@scayle/storefront-api';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Returns the first value of an attribute.
|
|
4
|
+
* This function is useful when you need to get the first value of an attribute.
|
|
5
|
+
*
|
|
6
|
+
* This returns undefined, if the attribute doesn't exist or if it doesn't have any values.
|
|
7
|
+
*
|
|
8
|
+
* @param attributes - The product attributes object
|
|
9
|
+
* @param attributeName - The name of the attribute to look up
|
|
10
|
+
*
|
|
11
|
+
* @returns The first value of the attribute, or undefined if the attribute doesn't exist or has no values
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const firstColor = getFirstAttributeValue(product.attributes, 'color')
|
|
16
|
+
* // Returns: { id: 1, label: 'Red', value: 'red' }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare const getFirstAttributeValue: (attributes: Attributes | undefined, attributeName: string) => Value | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Returns all values of an attribute group by its name key.
|
|
22
|
+
* This function always returns an array, making it safe to use in iterations.
|
|
23
|
+
* For single-select attributes, the value is wrapped in an array.
|
|
24
|
+
* For multi-select attributes, the array is returned directly.
|
|
25
|
+
*
|
|
26
|
+
* @param attributes - The product attributes object
|
|
27
|
+
* @param name - The name key of the attribute group to look up (e.g., 'color', 'size')
|
|
28
|
+
*
|
|
29
|
+
* @returns An array of values, or an empty array if the attribute doesn't exist or has no values
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // Single-select attribute
|
|
34
|
+
* const sizes = getAttributeValuesByName(product.attributes, 'size')
|
|
35
|
+
* // Returns: [{ id: 1, label: 'M', value: 'medium' }]
|
|
36
|
+
*
|
|
37
|
+
* // Multi-select attribute
|
|
38
|
+
* const colors = getAttributeValuesByName(product.attributes, 'color')
|
|
39
|
+
* // Returns: [{ id: 1, label: 'Red' }, { id: 2, label: 'Blue' }]
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare const getAttributeValuesByName: (attributes: Attributes | undefined, name: string) => Value[];
|
|
43
|
+
/**
|
|
44
|
+
* Returns all values of an attribute group by its numeric ID.
|
|
45
|
+
*
|
|
46
|
+
* This function searches through all attribute groups to find one matching the given ID.
|
|
47
|
+
* This is useful when you need to look up attributes by their SCAYLE API identifier rather than by name.
|
|
48
|
+
* Like {@link getAttributeValuesByName}, this always returns an array for consistency.
|
|
49
|
+
*
|
|
50
|
+
* @param attributes - The product attributes object
|
|
51
|
+
* @param groupId - The numeric ID of the attribute group to look up
|
|
52
|
+
* @returns An array of values, or an empty array if the attribute doesn't exist or has no values
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // Look up by attribute group ID instead of name
|
|
57
|
+
* const promotionValues = getAttributeValuesByGroupId(product.attributes, 42)
|
|
58
|
+
* // Returns: [{ id: 123, label: 'Summer Sale', value: 'summer-2024' }]
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare const getAttributeValuesByGroupId: (attributes: Attributes | undefined, groupId: number) => Value[];
|
|
3
62
|
/**
|
|
4
63
|
* Retrieves the value or label of the first attribute value for a given attribute name.
|
|
5
64
|
*
|
|
@@ -18,4 +77,7 @@ export declare const getAttributeValue: (attributes: Attributes | undefined, att
|
|
|
18
77
|
* @returns An array of attribute values, filtered to remove any null or undefined values.
|
|
19
78
|
*/
|
|
20
79
|
export declare const getManyAttributeValueTuples: (attributes: Attributes | undefined, attributeNames: string[]) => Value[];
|
|
21
|
-
|
|
80
|
+
/**
|
|
81
|
+
* @deprecated This function will be removed in the next major version. Use {@link getAttributeValuesByName} instead.
|
|
82
|
+
*/
|
|
83
|
+
export { getAttributeValuesByName as getAttributeValueTuples };
|
|
@@ -1,7 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export const getFirstAttributeValue = (attributes, attributeName) => {
|
|
2
|
+
const attribute = attributes && attributes[attributeName];
|
|
3
|
+
if (!attribute || !attribute.values) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
if (attribute.multiSelect) {
|
|
7
|
+
return attribute.values.length > 0 ? attribute.values[0] : void 0;
|
|
8
|
+
}
|
|
9
|
+
return attribute.values;
|
|
10
|
+
};
|
|
11
|
+
export const getAttributeValuesByName = (attributes, name) => {
|
|
12
|
+
const attribute = attributes && attributes[name];
|
|
13
|
+
if (!attribute || !attribute.values) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
if (attribute.multiSelect) {
|
|
17
|
+
return attribute.values;
|
|
18
|
+
}
|
|
19
|
+
return [attribute.values];
|
|
20
|
+
};
|
|
21
|
+
export const getAttributeValuesByGroupId = (attributes, groupId) => {
|
|
22
|
+
if (!attributes) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const attribute = Object.values(attributes).find((attribute2) => attribute2?.id === groupId);
|
|
26
|
+
if (!attribute || !attribute.values) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
if (attribute.multiSelect) {
|
|
30
|
+
return attribute.values;
|
|
31
|
+
}
|
|
32
|
+
return [attribute.values];
|
|
33
|
+
};
|
|
5
34
|
export const getAttributeValue = (attributes, attributeName) => {
|
|
6
35
|
const values = getFirstAttributeValue(attributes, attributeName);
|
|
7
36
|
return values?.value ?? values?.label;
|
|
@@ -9,4 +38,4 @@ export const getAttributeValue = (attributes, attributeName) => {
|
|
|
9
38
|
export const getManyAttributeValueTuples = (attributes, attributeNames) => {
|
|
10
39
|
return attributeNames.map((key) => getFirstAttributeValue(attributes, key)).filter((entry) => !!entry);
|
|
11
40
|
};
|
|
12
|
-
export {
|
|
41
|
+
export { getAttributeValuesByName as getAttributeValueTuples };
|
|
@@ -15,7 +15,7 @@ import { mergeBaskets as mergeBasketFunction } from '../../../utils/user';
|
|
|
15
15
|
* @param params.existingItemHandling How to handle existing items, default to `'AddQuantityToExisting'`.
|
|
16
16
|
* @param params.itemGroup The item group.
|
|
17
17
|
* @param params.with withParams for basket.
|
|
18
|
-
* @param params.orderCustomData
|
|
18
|
+
* @param params.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
|
|
19
19
|
* @param context The RPC context.
|
|
20
20
|
*
|
|
21
21
|
* @returns The updated basket, potentially with errors. It will return
|
|
@@ -36,7 +36,7 @@ export declare const addItemToBasket: RpcHandler<AddOrUpdateItemType & {
|
|
|
36
36
|
* @param params.items An array of items to add.
|
|
37
37
|
* @param params.existingItemHandling How to handle existing items.
|
|
38
38
|
* @param params.with withParams for basket.
|
|
39
|
-
* @param params.orderCustomData
|
|
39
|
+
* @param params.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
|
|
40
40
|
* @param context The RPC context.
|
|
41
41
|
*
|
|
42
42
|
* @returns The updated basket, potentially with errors. It will return
|
|
@@ -55,7 +55,7 @@ export declare const addItemsToBasket: RpcHandler<{
|
|
|
55
55
|
* Retrieves the current basket.
|
|
56
56
|
*
|
|
57
57
|
* @param options The options for retrieving the basket.
|
|
58
|
-
* @param options.orderCustomData
|
|
58
|
+
* @param options.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
|
|
59
59
|
* @param context The RPC context.
|
|
60
60
|
*
|
|
61
61
|
* @returns The basket data. It will return an `ErrorResponse` alternatively
|
|
@@ -72,7 +72,7 @@ export declare const getBasket: RpcHandler<BasketWithOptions & {
|
|
|
72
72
|
* @param options The options for removing the item.
|
|
73
73
|
* @param options.itemKey The key of the item to remove.
|
|
74
74
|
* @param options.with withParams for basket.
|
|
75
|
-
* @param options.orderCustomData
|
|
75
|
+
* @param options.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
|
|
76
76
|
* @param context The RPC context.
|
|
77
77
|
*
|
|
78
78
|
* @returns The updated basket. It will return an `ErrorResponse` alternatively
|
|
@@ -101,7 +101,7 @@ export declare const clearBasket: RpcHandler<boolean>;
|
|
|
101
101
|
* @param params.fromBasketKey The key of the source basket.
|
|
102
102
|
* @param params.toBasketKey The key of the target basket.
|
|
103
103
|
* @param params.with withParams for basket.
|
|
104
|
-
* @param params.orderCustomData
|
|
104
|
+
* @param params.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
|
|
105
105
|
* @param context The RPC context.
|
|
106
106
|
*
|
|
107
107
|
* @returns The new merged basket as result of the merge operation.
|
|
@@ -135,6 +135,7 @@ export interface UpdateBasketItemRequestParameter {
|
|
|
135
135
|
* An object describing which data the returned basket should contain.
|
|
136
136
|
*/
|
|
137
137
|
with?: BasketWithOptions;
|
|
138
|
+
orderCustomData?: Record<string, unknown>;
|
|
138
139
|
}
|
|
139
140
|
/**
|
|
140
141
|
* Applies the provided update data to a specific item in a basket.
|
|
@@ -143,6 +144,7 @@ export interface UpdateBasketItemRequestParameter {
|
|
|
143
144
|
* @param params.basketItemKey The key of the item to be updated.
|
|
144
145
|
* @param params.update The update data that should be applied to the basket item.
|
|
145
146
|
* @param params.with An object describing which data the returned basket should contain.
|
|
147
|
+
* @param params.orderCustomData Custom data for the order.
|
|
146
148
|
* @param context The RPC context.
|
|
147
149
|
*
|
|
148
150
|
* @returns A promise that resolves with the updated basket.
|
|
@@ -6,9 +6,16 @@ import {
|
|
|
6
6
|
HttpStatusMessage,
|
|
7
7
|
MIN_WITH_PARAMS_BASKET
|
|
8
8
|
} from "../../../constants/index.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
mergeBaskets as mergeBasketFunction,
|
|
11
|
+
getValidatedAccessToken
|
|
12
|
+
} from "../../../utils/user.mjs";
|
|
10
13
|
import { unwrap } from "../../../utils/response.mjs";
|
|
11
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
defineRpcHandler,
|
|
16
|
+
mergeOrderCustomData,
|
|
17
|
+
wasAddedWithReducedQuantity
|
|
18
|
+
} from "../../../utils/index.mjs";
|
|
12
19
|
import { mapSAPIFetchErrorToResponse } from "../../../utils/sapi.mjs";
|
|
13
20
|
const SAPI_ERROR_NAME = "SAPI ERROR";
|
|
14
21
|
function getWithParams(params, context) {
|
|
@@ -35,6 +42,7 @@ export const addItemToBasket = defineRpcHandler(async ({
|
|
|
35
42
|
}
|
|
36
43
|
const { sapiClient, basketKey } = context;
|
|
37
44
|
const campaignKey = await context.callRpc?.("getCampaignKey");
|
|
45
|
+
const _orderCustomData = await context.callRpc?.("getOrderCustomData");
|
|
38
46
|
const resolvedWith = getWithParams(
|
|
39
47
|
{ with: _with },
|
|
40
48
|
context
|
|
@@ -60,7 +68,8 @@ export const addItemToBasket = defineRpcHandler(async ({
|
|
|
60
68
|
],
|
|
61
69
|
{
|
|
62
70
|
skipAvailabilityCheck: false,
|
|
63
|
-
orderCustomData
|
|
71
|
+
orderCustomData: mergeOrderCustomData(_orderCustomData, orderCustomData),
|
|
72
|
+
customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
|
|
64
73
|
},
|
|
65
74
|
{
|
|
66
75
|
existingItemHandling
|
|
@@ -97,6 +106,7 @@ export const addItemsToBasket = defineRpcHandler(async (params, context) => {
|
|
|
97
106
|
}
|
|
98
107
|
const { sapiClient, basketKey } = context;
|
|
99
108
|
const campaignKey = await context.callRpc?.("getCampaignKey");
|
|
109
|
+
const _orderCustomData = await context.callRpc?.("getOrderCustomData");
|
|
100
110
|
const resolvedWith = getWithParams(params, context);
|
|
101
111
|
const itemsToBeAddedOrUpdated = params.items.map((item) => ({
|
|
102
112
|
variantId: item.variantId,
|
|
@@ -119,7 +129,11 @@ export const addItemsToBasket = defineRpcHandler(async (params, context) => {
|
|
|
119
129
|
{
|
|
120
130
|
skipAvailabilityCheck: false,
|
|
121
131
|
includeItemsWithoutProductData: resolvedWith?.includeItemsWithoutProductData,
|
|
122
|
-
orderCustomData:
|
|
132
|
+
orderCustomData: mergeOrderCustomData(
|
|
133
|
+
_orderCustomData,
|
|
134
|
+
params.orderCustomData
|
|
135
|
+
),
|
|
136
|
+
customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
|
|
123
137
|
},
|
|
124
138
|
{
|
|
125
139
|
existingItemHandling: params.existingItemHandling || ExistingItemHandling.ADD_QUANTITY_TO_EXISTING
|
|
@@ -159,6 +173,7 @@ export const getBasket = defineRpcHandler(async (options, context) => {
|
|
|
159
173
|
}
|
|
160
174
|
const { sapiClient, basketKey } = context;
|
|
161
175
|
const campaignKey = await context.callRpc?.("getCampaignKey");
|
|
176
|
+
const _orderCustomData = await context.callRpc?.("getOrderCustomData");
|
|
162
177
|
const resolvedWith = getWithParams(
|
|
163
178
|
{ with: options },
|
|
164
179
|
context
|
|
@@ -167,7 +182,11 @@ export const getBasket = defineRpcHandler(async (options, context) => {
|
|
|
167
182
|
with: resolvedWith,
|
|
168
183
|
campaignKey,
|
|
169
184
|
includeItemsWithoutProductData: resolvedWith.includeItemsWithoutProductData,
|
|
170
|
-
orderCustomData:
|
|
185
|
+
orderCustomData: mergeOrderCustomData(
|
|
186
|
+
_orderCustomData,
|
|
187
|
+
options?.orderCustomData
|
|
188
|
+
),
|
|
189
|
+
customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
|
|
171
190
|
});
|
|
172
191
|
if (response.type === "failure") {
|
|
173
192
|
const { statusCode, message } = parseBasketError(response);
|
|
@@ -192,6 +211,7 @@ export const removeItemFromBasket = defineRpcHandler(async (options, context) =>
|
|
|
192
211
|
}
|
|
193
212
|
const { sapiClient, basketKey } = context;
|
|
194
213
|
const campaignKey = await context.callRpc?.("getCampaignKey");
|
|
214
|
+
const _orderCustomData = await context.callRpc?.("getOrderCustomData");
|
|
195
215
|
const resolvedWith = getWithParams(options, context);
|
|
196
216
|
const basket = await sapiClient.basket.deleteItem(
|
|
197
217
|
basketKey,
|
|
@@ -200,7 +220,11 @@ export const removeItemFromBasket = defineRpcHandler(async (options, context) =>
|
|
|
200
220
|
with: resolvedWith,
|
|
201
221
|
campaignKey,
|
|
202
222
|
includeItemsWithoutProductData: resolvedWith?.includeItemsWithoutProductData,
|
|
203
|
-
orderCustomData:
|
|
223
|
+
orderCustomData: mergeOrderCustomData(
|
|
224
|
+
_orderCustomData,
|
|
225
|
+
options.orderCustomData
|
|
226
|
+
),
|
|
227
|
+
customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
|
|
204
228
|
}
|
|
205
229
|
);
|
|
206
230
|
return { basket };
|
|
@@ -226,14 +250,18 @@ export const mergeBaskets = defineRpcHandler(async ({ fromBasketKey, toBasketKey
|
|
|
226
250
|
{ with: _with },
|
|
227
251
|
context
|
|
228
252
|
);
|
|
253
|
+
const _orderCustomData = await context.callRpc?.("getOrderCustomData");
|
|
229
254
|
return await mergeBasketFunction(
|
|
230
255
|
fromBasketKey,
|
|
231
256
|
toBasketKey,
|
|
232
|
-
{
|
|
257
|
+
{
|
|
258
|
+
...resolvedWith,
|
|
259
|
+
orderCustomData: mergeOrderCustomData(_orderCustomData, orderCustomData)
|
|
260
|
+
},
|
|
233
261
|
context
|
|
234
262
|
);
|
|
235
263
|
});
|
|
236
|
-
export const updateBasketItem = defineRpcHandler(async ({ basketItemKey, update, with: _with }, context) => {
|
|
264
|
+
export const updateBasketItem = defineRpcHandler(async ({ basketItemKey, update, with: _with, orderCustomData }, context) => {
|
|
237
265
|
if (!hasSession(context)) {
|
|
238
266
|
return new ErrorResponse(
|
|
239
267
|
HttpStatusCode.BAD_REQUEST,
|
|
@@ -248,11 +276,18 @@ export const updateBasketItem = defineRpcHandler(async ({ basketItemKey, update,
|
|
|
248
276
|
);
|
|
249
277
|
const { quantity, ...updateParams } = update;
|
|
250
278
|
const campaignKey = update.campaignKey ?? await context.callRpc?.("getCampaignKey");
|
|
279
|
+
const _orderCustomData = await context.callRpc?.("getOrderCustomData");
|
|
251
280
|
const result = await sapiClient.basket.updateItem(
|
|
252
281
|
basketKey,
|
|
253
282
|
basketItemKey,
|
|
254
283
|
quantity,
|
|
255
|
-
{
|
|
284
|
+
{
|
|
285
|
+
...updateParams,
|
|
286
|
+
campaignKey,
|
|
287
|
+
with: resolvedWith,
|
|
288
|
+
orderCustomData: mergeOrderCustomData(_orderCustomData, orderCustomData),
|
|
289
|
+
customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
|
|
290
|
+
}
|
|
256
291
|
);
|
|
257
292
|
if (result.type === "success") {
|
|
258
293
|
return { basket: result.basket };
|
|
@@ -287,12 +322,18 @@ export const getApplicablePromotionsByCode = defineRpcHandler(async ({ promotion
|
|
|
287
322
|
context
|
|
288
323
|
);
|
|
289
324
|
const campaignKey = await context.callRpc?.("getCampaignKey");
|
|
325
|
+
const orderCustomData = await context.callRpc?.("getOrderCustomData");
|
|
290
326
|
const result = await mapSAPIFetchErrorToResponse(
|
|
291
327
|
sapiClient.basket.getApplicablePromotionsByCode
|
|
292
328
|
)(
|
|
293
329
|
basketKey,
|
|
294
330
|
promotionCode,
|
|
295
|
-
{
|
|
331
|
+
{
|
|
332
|
+
campaignKey,
|
|
333
|
+
with: resolvedWith,
|
|
334
|
+
orderCustomData,
|
|
335
|
+
customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
|
|
336
|
+
}
|
|
296
337
|
);
|
|
297
338
|
if (result instanceof Response || result.type === "failure") {
|
|
298
339
|
context.log.info("Getting applicable promotion codes failed", {
|
|
@@ -318,6 +359,7 @@ export const updatePromotions = defineRpcHandler(async (params, context) => {
|
|
|
318
359
|
}
|
|
319
360
|
const { basketKey, sapiClient } = context;
|
|
320
361
|
const campaignKey = params.campaignKey ?? await context.callRpc?.("getCampaignKey");
|
|
362
|
+
const orderCustomData = await context.callRpc?.("getOrderCustomData");
|
|
321
363
|
const resolvedWith = getWithParams(
|
|
322
364
|
{ with: params.with },
|
|
323
365
|
context
|
|
@@ -329,7 +371,9 @@ export const updatePromotions = defineRpcHandler(async (params, context) => {
|
|
|
329
371
|
{
|
|
330
372
|
...params,
|
|
331
373
|
campaignKey,
|
|
332
|
-
with: resolvedWith
|
|
374
|
+
with: resolvedWith,
|
|
375
|
+
orderCustomData,
|
|
376
|
+
customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
|
|
333
377
|
}
|
|
334
378
|
);
|
|
335
379
|
if (result instanceof Response || result.type === "failure") {
|
|
@@ -45,6 +45,8 @@ interface CheckoutJwtPayload {
|
|
|
45
45
|
* @param jwtPayload The payload for the JWT. Provides checkout-related data to embed within the token.
|
|
46
46
|
* @param context The RPC context.
|
|
47
47
|
*
|
|
48
|
+
* @see https://scayle.dev/en/core-documentation/storefront/checkout-guide/implementation/webcomponent#jwt-attribute
|
|
49
|
+
*
|
|
48
50
|
* @returns An object containing the access token and checkout JWT.
|
|
49
51
|
* It will return an `ErrorResponse` if no session is found, no access token is present.
|
|
50
52
|
*/
|
|
@@ -26,14 +26,18 @@ export const getCheckoutToken = defineRpcHandler(async (jwtPayload = {}, context
|
|
|
26
26
|
const now = /* @__PURE__ */ new Date();
|
|
27
27
|
const { voucher, customData, preferredCollectionPoint, carrier } = jwtPayload;
|
|
28
28
|
const campaignKey = await context.callRpc?.("getCampaignKey");
|
|
29
|
+
const orderCustomData = await context.callRpc?.("getOrderCustomData");
|
|
29
30
|
const checkoutJwt = await new SignJWT({
|
|
30
31
|
voucher,
|
|
31
|
-
customData,
|
|
32
32
|
preferredCollectionPoint,
|
|
33
33
|
carrier,
|
|
34
34
|
basketId: context.basketKey,
|
|
35
|
-
campaignKey
|
|
36
|
-
|
|
35
|
+
campaignKey,
|
|
36
|
+
customData: {
|
|
37
|
+
...customData,
|
|
38
|
+
...orderCustomData
|
|
39
|
+
}
|
|
40
|
+
}).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"8.47.0"}`).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(secret);
|
|
37
41
|
return {
|
|
38
42
|
accessToken: refreshedAccessToken,
|
|
39
43
|
checkoutJwt
|
package/dist/utils/basket.d.ts
CHANGED
|
@@ -23,3 +23,44 @@ import type { AddOrUpdateItemError } from '@scayle/storefront-api';
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
export declare const wasAddedWithReducedQuantity: (errors?: AddOrUpdateItemError[]) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Merges the two orderCustomData objects when both RPC and parameter values are provided.
|
|
28
|
+
* If an attribute exists in both, the parameter value takes precedence.
|
|
29
|
+
*
|
|
30
|
+
* @param orderCustomDataRpc The order custom data from the RPC context.
|
|
31
|
+
* @param orderCustomDataParam The order custom data from the parameter.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const orderCustomDataRpc = {
|
|
36
|
+
* orderId: '1',
|
|
37
|
+
* }
|
|
38
|
+
* const orderCustomDataParam = {
|
|
39
|
+
* orderId: '2',
|
|
40
|
+
* }
|
|
41
|
+
* const mergedOrderCustomData = mergeOrderCustomData(orderCustomDataRpc, orderCustomDataParam) // { orderId: '2' }
|
|
42
|
+
*
|
|
43
|
+
* const orderCustomDataRpc = {
|
|
44
|
+
* orderId: '1',
|
|
45
|
+
* }
|
|
46
|
+
* const orderCustomDataParam = undefined
|
|
47
|
+
* const mergedOrderCustomData = mergeOrderCustomData(orderCustomDataRpc, orderCustomDataParam) // { orderId: '1' }
|
|
48
|
+
*
|
|
49
|
+
* const orderCustomDataRpc = undefined
|
|
50
|
+
* const orderCustomDataParam = {
|
|
51
|
+
* orderId: '2',
|
|
52
|
+
* }
|
|
53
|
+
* const mergedOrderCustomData = mergeOrderCustomData(orderCustomDataRpc, orderCustomDataParam) // { orderId: '2' }
|
|
54
|
+
*
|
|
55
|
+
* const orderCustomDataRpc = {
|
|
56
|
+
* orderId: '1',
|
|
57
|
+
* }
|
|
58
|
+
* const orderCustomDataParam = {
|
|
59
|
+
* orderName: 'Order 2',
|
|
60
|
+
* }
|
|
61
|
+
* const mergedOrderCustomData = mergeOrderCustomData(orderCustomDataRpc, orderCustomDataParam) // { orderId: '1', orderName: 'Order 2' }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @returns The merged order custom data.
|
|
65
|
+
*/
|
|
66
|
+
export declare const mergeOrderCustomData: (orderCustomDataRpc?: Record<string, unknown>, orderCustomDataParam?: Record<string, unknown>) => Record<string, unknown>;
|
package/dist/utils/basket.mjs
CHANGED
|
@@ -10,3 +10,12 @@ export const wasAddedWithReducedQuantity = (errors) => {
|
|
|
10
10
|
(error) => error.operation === "add" && error.kind === AddToBasketFailureKind.ITEM_ADDED_WITH_REDUCED_QUANTITY || error.operation === "update" && error.kind === UpdateBasketItemFailureKind.ITEM_ADDED_WITH_REDUCED_QUANTITY
|
|
11
11
|
);
|
|
12
12
|
};
|
|
13
|
+
export const mergeOrderCustomData = (orderCustomDataRpc, orderCustomDataParam) => {
|
|
14
|
+
if (!orderCustomDataParam) {
|
|
15
|
+
return orderCustomDataRpc ?? {};
|
|
16
|
+
}
|
|
17
|
+
if (!orderCustomDataRpc) {
|
|
18
|
+
return orderCustomDataParam ?? {};
|
|
19
|
+
}
|
|
20
|
+
return { ...orderCustomDataRpc, ...orderCustomDataParam };
|
|
21
|
+
};
|
package/dist/utils/user.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { BasketResponse, AddManyItemsBasketResponse } from '@scayle/storefront-api';
|
|
2
2
|
import type { BasketWithOptions, Product, RpcContext, Variant } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the access token is expired.
|
|
5
|
+
*
|
|
6
|
+
* @param accessToken The access token to validate.
|
|
7
|
+
*
|
|
8
|
+
* @returns The validated access token or undefined if the access token is not present or expired.
|
|
9
|
+
*/
|
|
10
|
+
export declare const getValidatedAccessToken: (accessToken?: string) => string | undefined;
|
|
3
11
|
/**
|
|
4
12
|
* Merges two baskets. Adds the items from the `fromBasketKey` basket to the `toBasketKey` basket
|
|
5
13
|
* and then clears the `fromBasketKey` basket. Handles existing items by adding quantities.
|
package/dist/utils/user.mjs
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { ExistingItemHandling } from "@scayle/storefront-api";
|
|
2
|
+
import { decodeJwt } from "jose";
|
|
3
|
+
export const getValidatedAccessToken = (accessToken) => {
|
|
4
|
+
if (!accessToken) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const payload = decodeJwt(accessToken);
|
|
8
|
+
if (!payload.exp || payload.exp < Date.now() / 1e3) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
return accessToken;
|
|
12
|
+
};
|
|
2
13
|
export const mergeBaskets = async (fromBasketKey, toBasketKey, withOptions, context) => {
|
|
3
14
|
const { sapiClient } = context;
|
|
4
15
|
const campaignKey = await context.callRpc?.("getCampaignKey");
|
|
5
16
|
const fromOriginBasket = await sapiClient.basket.get(fromBasketKey, {
|
|
6
17
|
with: withOptions,
|
|
7
18
|
campaignKey,
|
|
8
|
-
orderCustomData: withOptions.orderCustomData
|
|
19
|
+
orderCustomData: withOptions.orderCustomData,
|
|
20
|
+
customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
|
|
9
21
|
});
|
|
10
22
|
context.log.debug(`Merging basket ${fromBasketKey} to ${toBasketKey}`);
|
|
11
23
|
if (fromBasketKey === toBasketKey) {
|
|
@@ -31,7 +43,8 @@ export const mergeBaskets = async (fromBasketKey, toBasketKey, withOptions, cont
|
|
|
31
43
|
with: withOptions,
|
|
32
44
|
campaignKey,
|
|
33
45
|
includeItemsWithoutProductData: withOptions?.includeItemsWithoutProductData,
|
|
34
|
-
orderCustomData: withOptions.orderCustomData
|
|
46
|
+
orderCustomData: withOptions.orderCustomData,
|
|
47
|
+
customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
|
|
35
48
|
},
|
|
36
49
|
{
|
|
37
50
|
existingItemHandling: ExistingItemHandling.ADD_QUANTITY_TO_EXISTING
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scayle/storefront-core",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.47.0",
|
|
4
4
|
"description": "Collection of essential utilities to work with the Storefront API",
|
|
5
5
|
"author": "SCAYLE Commerce Engine",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,29 +48,29 @@
|
|
|
48
48
|
"ufo": "^1.5.3",
|
|
49
49
|
"uncrypto": "^0.1.3",
|
|
50
50
|
"utility-types": "^3.11.0",
|
|
51
|
-
"@scayle/storefront-api": "18.
|
|
52
|
-
"@scayle/unstorage-scayle-kv-driver": "2.0.
|
|
51
|
+
"@scayle/storefront-api": "18.18.0",
|
|
52
|
+
"@scayle/unstorage-scayle-kv-driver": "2.0.7"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/crypto-js": "4.2.2",
|
|
56
|
-
"@types/node": "22.18.
|
|
56
|
+
"@types/node": "22.18.13",
|
|
57
57
|
"@types/webpack-env": "1.18.8",
|
|
58
58
|
"@vitest/coverage-v8": "3.2.4",
|
|
59
59
|
"dprint": "0.50.2",
|
|
60
60
|
"eslint-formatter-gitlab": "6.0.1",
|
|
61
|
-
"eslint": "9.
|
|
61
|
+
"eslint": "9.38.0",
|
|
62
62
|
"fishery": "2.3.1",
|
|
63
|
-
"publint": "0.3.
|
|
63
|
+
"publint": "0.3.15",
|
|
64
64
|
"rimraf": "6.0.1",
|
|
65
65
|
"typescript": "5.9.3",
|
|
66
66
|
"unbuild": "3.6.1",
|
|
67
67
|
"unstorage": "1.17.1",
|
|
68
68
|
"vitest": "3.2.4",
|
|
69
|
-
"@scayle/eslint-config-storefront": "4.7.
|
|
69
|
+
"@scayle/eslint-config-storefront": "4.7.11",
|
|
70
70
|
"@scayle/vitest-config-storefront": "1.0.0"
|
|
71
71
|
},
|
|
72
72
|
"volta": {
|
|
73
|
-
"node": "22.
|
|
73
|
+
"node": "22.21.0"
|
|
74
74
|
},
|
|
75
75
|
"scripts": {
|
|
76
76
|
"clean": "rimraf ./dist",
|