@faststore/api 1.8.20 → 1.8.23
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 +27 -0
- package/dist/api.cjs.development.js +102 -23
- package/dist/api.cjs.development.js.map +1 -1
- package/dist/api.cjs.production.min.js +1 -1
- package/dist/api.cjs.production.min.js.map +1 -1
- package/dist/api.esm.js +102 -23
- package/dist/api.esm.js.map +1 -1
- package/dist/platforms/vtex/clients/commerce/index.d.ts +6 -0
- package/dist/platforms/vtex/clients/commerce/types/OrderForm.d.ts +8 -1
- package/dist/platforms/vtex/clients/index.d.ts +6 -0
- package/dist/platforms/vtex/index.d.ts +6 -0
- package/dist/platforms/vtex/utils/md5.d.ts +1 -0
- package/package.json +2 -2
- package/src/platforms/vtex/clients/commerce/index.ts +20 -0
- package/src/platforms/vtex/clients/commerce/types/OrderForm.ts +9 -1
- package/src/platforms/vtex/index.ts +7 -0
- package/src/platforms/vtex/resolvers/validateCart.ts +93 -20
- package/src/platforms/vtex/utils/md5.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/api",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.23",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"graphql": "^15.6.0"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "a8958ead23ee3df81fbb78d35f06726e7b290874"
|
|
49
49
|
}
|
|
@@ -103,6 +103,26 @@ export const VtexCommerce = (
|
|
|
103
103
|
}
|
|
104
104
|
)
|
|
105
105
|
},
|
|
106
|
+
setCustomData: ({
|
|
107
|
+
id,
|
|
108
|
+
appId,
|
|
109
|
+
key,
|
|
110
|
+
value,
|
|
111
|
+
}: {
|
|
112
|
+
id: string
|
|
113
|
+
appId: string
|
|
114
|
+
key: string
|
|
115
|
+
value: string
|
|
116
|
+
}): Promise<OrderForm> => {
|
|
117
|
+
return fetchAPI(
|
|
118
|
+
`${base}/api/checkout/pub/orderForm/${id}/customData/${appId}/${key}`,
|
|
119
|
+
{
|
|
120
|
+
...BASE_INIT,
|
|
121
|
+
body: JSON.stringify({ value }),
|
|
122
|
+
method: 'PUT',
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
},
|
|
106
126
|
region: async ({
|
|
107
127
|
postalCode,
|
|
108
128
|
country,
|
|
@@ -136,7 +136,7 @@ export interface OrderForm {
|
|
|
136
136
|
giftRegistryData: any | null
|
|
137
137
|
openTextField: any | null
|
|
138
138
|
invoiceData: any | null
|
|
139
|
-
customData:
|
|
139
|
+
customData: OrderFormCustomData | null
|
|
140
140
|
itemMetadata: {
|
|
141
141
|
items: MetadataItem[]
|
|
142
142
|
}
|
|
@@ -149,6 +149,14 @@ export interface OrderForm {
|
|
|
149
149
|
itemsOrdination: any | null
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
export interface OrderFormCustomData {
|
|
153
|
+
customApps: Array<{
|
|
154
|
+
fields: Record<string, string>
|
|
155
|
+
id: string
|
|
156
|
+
major: number
|
|
157
|
+
}>
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
export interface OrderFormMarketingData {
|
|
153
161
|
utmCampaign?: string
|
|
154
162
|
utmMedium?: string
|
|
@@ -25,6 +25,11 @@ export interface Options {
|
|
|
25
25
|
// Default sales channel to use for fetching products
|
|
26
26
|
channel: string
|
|
27
27
|
hideUnavailableItems: boolean
|
|
28
|
+
flags?: FeatureFlags
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface FeatureFlags {
|
|
32
|
+
enableOrderFormSync?: boolean
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
export interface Context {
|
|
@@ -38,6 +43,7 @@ export interface Context {
|
|
|
38
43
|
* */
|
|
39
44
|
storage: {
|
|
40
45
|
channel: Required<Channel>
|
|
46
|
+
flags: FeatureFlags
|
|
41
47
|
}
|
|
42
48
|
headers: Record<string, string>
|
|
43
49
|
}
|
|
@@ -68,6 +74,7 @@ const Resolvers = {
|
|
|
68
74
|
export const getContextFactory = (options: Options) => (ctx: any): Context => {
|
|
69
75
|
ctx.storage = {
|
|
70
76
|
channel: ChannelMarshal.parse(options.channel),
|
|
77
|
+
flags: options.flags ?? {},
|
|
71
78
|
}
|
|
72
79
|
ctx.clients = getClients(options, ctx)
|
|
73
80
|
ctx.loaders = getLoaders(options, ctx)
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import deepEquals from 'fast-deep-equal'
|
|
2
2
|
|
|
3
|
+
import { md5 } from '../utils/md5'
|
|
3
4
|
import type {
|
|
4
|
-
IStoreOrder,
|
|
5
5
|
IStoreCart,
|
|
6
6
|
IStoreOffer,
|
|
7
|
+
IStoreOrder,
|
|
7
8
|
} from '../../../__generated__/schema'
|
|
8
9
|
import type {
|
|
9
10
|
OrderForm,
|
|
10
|
-
OrderFormItem,
|
|
11
11
|
OrderFormInputItem,
|
|
12
|
+
OrderFormItem,
|
|
12
13
|
} from '../clients/commerce/types/OrderForm'
|
|
13
14
|
import type { Context } from '..'
|
|
14
15
|
|
|
@@ -69,6 +70,70 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
|
|
|
69
70
|
return isSameOrder && orderItemsAreSync
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
const orderFormToCart = (
|
|
74
|
+
form: OrderForm,
|
|
75
|
+
skuLoader: Context['loaders']['skuLoader']
|
|
76
|
+
) => {
|
|
77
|
+
return {
|
|
78
|
+
order: {
|
|
79
|
+
orderNumber: form.orderFormId,
|
|
80
|
+
acceptedOffer: form.items.map((item) => ({
|
|
81
|
+
...item,
|
|
82
|
+
product: skuLoader.load([{ key: 'id', value: item.id }]), // TODO: add channel
|
|
83
|
+
})),
|
|
84
|
+
},
|
|
85
|
+
messages: form.messages.map(({ text, status }) => ({
|
|
86
|
+
text,
|
|
87
|
+
status: status.toUpperCase(),
|
|
88
|
+
})),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const getOrderFormEtag = ({ items }: OrderForm) => md5(JSON.stringify(items))
|
|
93
|
+
|
|
94
|
+
const setOrderFormEtag = async (
|
|
95
|
+
form: OrderForm,
|
|
96
|
+
commerce: Context['clients']['commerce']
|
|
97
|
+
) => {
|
|
98
|
+
try {
|
|
99
|
+
const orderForm = await commerce.checkout.setCustomData({
|
|
100
|
+
id: form.orderFormId,
|
|
101
|
+
appId: 'faststore',
|
|
102
|
+
key: 'cartEtag',
|
|
103
|
+
value: getOrderFormEtag(form),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return orderForm
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(
|
|
109
|
+
'Error while setting custom data to orderForm.\n Make sure to add the following custom app to the orderForm: \n{"fields":["cartEtag"],"id":"faststore","major":1}.\n More info at: https://developers.vtex.com/vtex-rest-api/docs/customizable-fields-with-checkout-api'
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
throw err
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if cartEtag stored on customData is up to date
|
|
118
|
+
* @description If cartEtag is not up to date, this means that
|
|
119
|
+
* another system changed the cart, like Checkout UI or Order Placed
|
|
120
|
+
*/
|
|
121
|
+
const isOrderFormStale = (form: OrderForm) => {
|
|
122
|
+
const faststoreData = form.customData?.customApps.find(
|
|
123
|
+
(app) => app.id === 'faststore'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const oldEtag = faststoreData?.fields?.cartEtag
|
|
127
|
+
|
|
128
|
+
if (oldEtag == null) {
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const newEtag = getOrderFormEtag(form)
|
|
133
|
+
|
|
134
|
+
return newEtag !== oldEtag
|
|
135
|
+
}
|
|
136
|
+
|
|
72
137
|
/**
|
|
73
138
|
* This resolver implements the optimistic cart behavior. The main idea in here
|
|
74
139
|
* is that we receive a cart from the UI (as query params) and we validate it with
|
|
@@ -87,6 +152,7 @@ export const validateCart = async (
|
|
|
87
152
|
{ cart: { order } }: { cart: IStoreCart },
|
|
88
153
|
ctx: Context
|
|
89
154
|
) => {
|
|
155
|
+
const { enableOrderFormSync } = ctx.storage.flags
|
|
90
156
|
const { orderNumber, acceptedOffer } = order
|
|
91
157
|
const {
|
|
92
158
|
clients: { commerce },
|
|
@@ -98,6 +164,19 @@ export const validateCart = async (
|
|
|
98
164
|
id: orderNumber,
|
|
99
165
|
})
|
|
100
166
|
|
|
167
|
+
// Step1.5: Check if another system changed the orderForm with this orderNumber
|
|
168
|
+
// If so, this means the user interacted with this cart elsewhere and expects
|
|
169
|
+
// to see this new cart state instead of what's stored on the user's browser.
|
|
170
|
+
if (enableOrderFormSync === true) {
|
|
171
|
+
const isStale = isOrderFormStale(orderForm)
|
|
172
|
+
|
|
173
|
+
if (isStale === true && orderNumber) {
|
|
174
|
+
const newOrderForm = await setOrderFormEtag(orderForm, commerce)
|
|
175
|
+
|
|
176
|
+
return orderFormToCart(newOrderForm, skuLoader)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
101
180
|
// Step2: Process items from both browser and checkout so they have the same shape
|
|
102
181
|
const browserItemsById = groupById(acceptedOffer)
|
|
103
182
|
const originItemsById = groupById(orderForm.items.map(orderFormItemToOffer))
|
|
@@ -139,28 +218,22 @@ export const validateCart = async (
|
|
|
139
218
|
}
|
|
140
219
|
|
|
141
220
|
// Step4: Apply delta changes to order form
|
|
142
|
-
const updatedOrderForm = await commerce.checkout
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
221
|
+
const updatedOrderForm = await commerce.checkout
|
|
222
|
+
// update orderForm items
|
|
223
|
+
.updateOrderFormItems({
|
|
224
|
+
id: orderForm.orderFormId,
|
|
225
|
+
orderItems: changes,
|
|
226
|
+
})
|
|
227
|
+
// update orderForm etag so we know last time we touched this orderForm
|
|
228
|
+
.then((form) =>
|
|
229
|
+
enableOrderFormSync ? setOrderFormEtag(form, commerce) : form
|
|
230
|
+
)
|
|
146
231
|
|
|
147
232
|
// Step5: If no changes detected before/after updating orderForm, the order is validated
|
|
148
233
|
if (equals(order, updatedOrderForm)) {
|
|
149
234
|
return null
|
|
150
235
|
}
|
|
151
236
|
|
|
152
|
-
// Step6: There were changes, convert orderForm to
|
|
153
|
-
return
|
|
154
|
-
order: {
|
|
155
|
-
orderNumber: updatedOrderForm.orderFormId,
|
|
156
|
-
acceptedOffer: updatedOrderForm.items.map((item) => ({
|
|
157
|
-
...item,
|
|
158
|
-
product: skuLoader.load([{ key: 'id', value: item.id }]), // TODO: add channel
|
|
159
|
-
})),
|
|
160
|
-
},
|
|
161
|
-
messages: updatedOrderForm.messages.map(({ text, status }) => ({
|
|
162
|
-
text,
|
|
163
|
-
status: status.toUpperCase(),
|
|
164
|
-
})),
|
|
165
|
-
}
|
|
237
|
+
// Step6: There were changes, convert orderForm to StoreCart
|
|
238
|
+
return orderFormToCart(updatedOrderForm, skuLoader)
|
|
166
239
|
}
|