@faststore/api 1.12.36 → 1.12.38

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.
@@ -26,7 +26,7 @@ export declare const VtexCommerce: ({ account, environment }: Options, ctx: Cont
26
26
  pagetype: (slug: string) => Promise<PortalPagetype>;
27
27
  };
28
28
  products: {
29
- crossselling: ({ type, productId, groupByProduct }: {
29
+ crossselling: ({ type, productId, groupByProduct, }: {
30
30
  type: ValueOf<typeof FACET_CROSS_SELLING_MAP>;
31
31
  productId: string;
32
32
  groupByProduct?: boolean | undefined;
@@ -35,7 +35,7 @@ export declare const VtexCommerce: ({ account, environment }: Options, ctx: Cont
35
35
  };
36
36
  checkout: {
37
37
  simulation: (args: SimulationArgs, { salesChannel }?: SimulationOptions) => Promise<Simulation>;
38
- shippingData: ({ id, body }: {
38
+ shippingData: ({ id, body, }: {
39
39
  id: string;
40
40
  body: unknown;
41
41
  }) => Promise<OrderForm>;
@@ -60,6 +60,7 @@ export declare const VtexCommerce: ({ account, environment }: Options, ctx: Cont
60
60
  address: ({ postalCode, country, }: AddressInput) => Promise<Address>;
61
61
  };
62
62
  session: (search: string) => Promise<Session>;
63
+ getSessionOrder: () => Promise<Session>;
63
64
  subscribeToNewsletter: (data: {
64
65
  name: string;
65
66
  email: string;
@@ -5,6 +5,7 @@ export interface Session {
5
5
  export interface Namespaces {
6
6
  profile?: Profile;
7
7
  store?: Store;
8
+ checkout?: Checkout;
8
9
  }
9
10
  export interface Value {
10
11
  value: string;
@@ -22,3 +23,6 @@ export interface Profile {
22
23
  firstName?: Value;
23
24
  lastName?: Value;
24
25
  }
26
+ export interface Checkout {
27
+ orderFormId?: Value;
28
+ }
@@ -20,7 +20,7 @@ export declare const getClients: (options: Options, ctx: Context) => {
20
20
  pagetype: (slug: string) => Promise<import("./commerce/types/Portal").PortalPagetype>;
21
21
  };
22
22
  products: {
23
- crossselling: ({ type, productId, groupByProduct }: {
23
+ crossselling: ({ type, productId, groupByProduct, }: {
24
24
  type: "whoboughtalsobought" | "whosawalsosaw" | "similars" | "whosawalsobought" | "accessories" | "suggestions";
25
25
  productId: string;
26
26
  groupByProduct?: boolean | undefined;
@@ -29,7 +29,7 @@ export declare const getClients: (options: Options, ctx: Context) => {
29
29
  };
30
30
  checkout: {
31
31
  simulation: (args: import("./commerce/types/Simulation").SimulationArgs, { salesChannel }?: import("./commerce/types/Simulation").SimulationOptions) => Promise<import("./commerce/types/Simulation").Simulation>;
32
- shippingData: ({ id, body }: {
32
+ shippingData: ({ id, body, }: {
33
33
  id: string;
34
34
  body: unknown;
35
35
  }) => Promise<import("./commerce/types/OrderForm").OrderForm>;
@@ -54,6 +54,7 @@ export declare const getClients: (options: Options, ctx: Context) => {
54
54
  address: ({ postalCode, country, }: import("./commerce/types/Address").AddressInput) => Promise<import("./commerce/types/Address").Address>;
55
55
  };
56
56
  session: (search: string) => Promise<import("./commerce/types/Session").Session>;
57
+ getSessionOrder: () => Promise<import("./commerce/types/Session").Session>;
57
58
  subscribeToNewsletter: (data: {
58
59
  name: string;
59
60
  email: string;
@@ -1,5 +1,5 @@
1
- import type { MutationValidateCartArgs } from '../../../__generated__/schema';
2
1
  import type { Context } from '..';
2
+ import type { MutationValidateCartArgs } from '../../../__generated__/schema';
3
3
  /**
4
4
  * This resolver implements the optimistic cart behavior. The main idea in here
5
5
  * is that we receive a cart from the UI (as query params) and we validate it with
@@ -0,0 +1 @@
1
+ export declare const getCookie: (name: string, cookie: string) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/api",
3
- "version": "1.12.36",
3
+ "version": "1.12.38",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -30,7 +30,7 @@
30
30
  "p-limit": "^3.1.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@faststore/shared": "^1.12.35",
33
+ "@faststore/shared": "^1.12.37",
34
34
  "@graphql-codegen/cli": "2.2.0",
35
35
  "@graphql-codegen/typescript": "2.2.2",
36
36
  "concurrently": "^6.2.1",
@@ -45,5 +45,6 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "graphql": "^15.6.0"
48
- }
48
+ },
49
+ "gitHead": "659eb1ff4c96f85004b3edd1f924f5753fc7a275"
49
50
  }
@@ -15,6 +15,7 @@ import type {
15
15
  } from './types/Simulation'
16
16
  import type { Session } from './types/Session'
17
17
  import type { Channel } from '../../utils/channel'
18
+ import { getCookie } from '../../utils/getCookies'
18
19
  import type { SalesChannel } from './types/SalesChannel'
19
20
  import { MasterDataResponse } from './types/Newsletter'
20
21
  import type { Address, AddressInput } from './types/Address'
@@ -51,20 +52,22 @@ export const VtexCommerce = (
51
52
  fetchAPI(`${base}/api/catalog_system/pub/portal/pagetype/${slug}`),
52
53
  },
53
54
  products: {
54
- crossselling: (
55
- { type, productId, groupByProduct = true }: {
56
- type: ValueOf<typeof FACET_CROSS_SELLING_MAP>;
57
- productId: string;
58
- groupByProduct?: boolean;
59
- },
60
- ): Promise<PortalProduct[]> => {
55
+ crossselling: ({
56
+ type,
57
+ productId,
58
+ groupByProduct = true,
59
+ }: {
60
+ type: ValueOf<typeof FACET_CROSS_SELLING_MAP>
61
+ productId: string
62
+ groupByProduct?: boolean
63
+ }): Promise<PortalProduct[]> => {
61
64
  const params = new URLSearchParams({
62
65
  sc: ctx.storage.channel.salesChannel,
63
66
  groupByProduct: groupByProduct.toString(),
64
67
  })
65
68
 
66
69
  return fetchAPI(
67
- `${base}/api/catalog_system/pub/products/crossselling/${type}/${productId}?${params}`,
70
+ `${base}/api/catalog_system/pub/products/crossselling/${type}/${productId}?${params}`
68
71
  )
69
72
  },
70
73
  },
@@ -86,16 +89,20 @@ export const VtexCommerce = (
86
89
  }
87
90
  )
88
91
  },
89
- shippingData: (
90
- { id, body }: { id: string; body: unknown },
91
- ): Promise<OrderForm> => {
92
+ shippingData: ({
93
+ id,
94
+ body,
95
+ }: {
96
+ id: string
97
+ body: unknown
98
+ }): Promise<OrderForm> => {
92
99
  return fetchAPI(
93
100
  `${base}/api/checkout/pub/orderForm/${id}/attachments/shippingData`,
94
101
  {
95
102
  ...BASE_INIT,
96
103
  body: JSON.stringify(body),
97
- },
98
- );
104
+ }
105
+ )
99
106
  },
100
107
  orderForm: ({
101
108
  id,
@@ -159,7 +166,7 @@ export const VtexCommerce = (
159
166
  ...BASE_INIT,
160
167
  body: JSON.stringify({ value }),
161
168
  method: 'PUT',
162
- },
169
+ }
163
170
  )
164
171
  },
165
172
  region: async ({
@@ -189,14 +196,34 @@ export const VtexCommerce = (
189
196
  'items',
190
197
  'profile.id,profile.email,profile.firstName,profile.lastName,store.channel,store.countryCode,store.cultureInfo,store.currencyCode,store.currencySymbol'
191
198
  )
192
-
193
- return fetchAPI(`${base}/api/sessions?${params.toString()}`, {
194
- method: 'POST',
199
+ if (getCookie('vtex_session', ctx.headers.cookie)) {
200
+ // cookie set
201
+ return fetchAPI(`${base}/api/sessions?${params.toString()}`, {
202
+ method: 'GET',
203
+ headers: {
204
+ 'content-type': 'application/json',
205
+ cookie: ctx.headers.cookie,
206
+ },
207
+ })
208
+ } else {
209
+ // cookie unset -> create session
210
+ return fetchAPI(`${base}/api/sessions?${params.toString()}`, {
211
+ method: 'POST',
212
+ headers: {
213
+ 'content-type': 'application/json',
214
+ cookie: ctx.headers.cookie,
215
+ },
216
+ body: '{}',
217
+ })
218
+ }
219
+ },
220
+ getSessionOrder: (): Promise<Session> => {
221
+ return fetchAPI(`${base}/api/sessions?items=checkout.orderFormId`, {
222
+ method: 'GET',
195
223
  headers: {
196
224
  'content-type': 'application/json',
197
225
  cookie: ctx.headers.cookie,
198
226
  },
199
- body: '{}',
200
227
  })
201
228
  },
202
229
  subscribeToNewsletter: (data: {
@@ -6,6 +6,7 @@ export interface Session {
6
6
  export interface Namespaces {
7
7
  profile?: Profile
8
8
  store?: Store
9
+ checkout?: Checkout
9
10
  }
10
11
 
11
12
  export interface Value {
@@ -26,3 +27,7 @@ export interface Profile {
26
27
  firstName?: Value
27
28
  lastName?: Value
28
29
  }
30
+
31
+ export interface Checkout {
32
+ orderFormId?: Value
33
+ }
@@ -1,22 +1,27 @@
1
1
  import deepEquals from 'fast-deep-equal'
2
2
 
3
+ import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
4
+ import { getCookie } from '../utils/getCookies'
3
5
  import { md5 } from '../utils/md5'
4
- import { attachmentToPropertyValue, getPropertyId, VALUE_REFERENCES } from '../utils/propertyValue'
6
+ import {
7
+ attachmentToPropertyValue,
8
+ getPropertyId,
9
+ VALUE_REFERENCES
10
+ } from '../utils/propertyValue'
5
11
 
12
+ import type { Context } from '..'
6
13
  import type {
7
- IStoreSession,
8
14
  IStoreOffer,
9
15
  IStoreOrder,
10
- IStorePropertyValue,
11
- Maybe,
12
- MutationValidateCartArgs,
16
+ IStorePropertyValue, IStoreSession, Maybe,
17
+ MutationValidateCartArgs
13
18
  } from '../../../__generated__/schema'
14
19
  import type {
15
20
  OrderForm,
16
21
  OrderFormInputItem,
17
- OrderFormItem,
22
+ OrderFormItem
18
23
  } from '../clients/commerce/types/OrderForm'
19
- import type { Context } from '..'
24
+
20
25
  type Indexed<T> = T & { index?: number }
21
26
 
22
27
  const isAttachment = (value: IStorePropertyValue) =>
@@ -37,7 +42,7 @@ const getId = (item: IStoreOffer) =>
37
42
 
38
43
  const orderFormItemToOffer = (
39
44
  item: OrderFormItem,
40
- index?: number,
45
+ index?: number
41
46
  ): Indexed<IStoreOffer> => ({
42
47
  listPrice: item.listPrice / 100,
43
48
  price: item.sellingPrice / 100,
@@ -53,7 +58,7 @@ const orderFormItemToOffer = (
53
58
  })
54
59
 
55
60
  const offerToOrderItemInput = (
56
- offer: Indexed<IStoreOffer>,
61
+ offer: Indexed<IStoreOffer>
57
62
  ): OrderFormInputItem => ({
58
63
  quantity: offer.quantity,
59
64
  seller: offer.seller.identifier,
@@ -99,18 +104,17 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
99
104
  }
100
105
 
101
106
  const joinItems = (form: OrderForm) => {
102
- const itemsById = form.items
103
- .reduce((acc, item) => {
104
- const id = getId(orderFormItemToOffer(item))
107
+ const itemsById = form.items.reduce((acc, item) => {
108
+ const id = getId(orderFormItemToOffer(item))
105
109
 
106
- if (!acc[id]) {
107
- acc[id] = []
108
- }
110
+ if (!acc[id]) {
111
+ acc[id] = []
112
+ }
109
113
 
110
- acc[id].push(item)
114
+ acc[id].push(item)
111
115
 
112
- return acc
113
- }, {} as Record<string, OrderFormItem[]>)
116
+ return acc
117
+ }, {} as Record<string, OrderFormItem[]>)
114
118
 
115
119
  return {
116
120
  ...form,
@@ -119,7 +123,7 @@ const joinItems = (form: OrderForm) => {
119
123
  const quantity = items.reduce((acc, i) => acc + i.quantity, 0)
120
124
  const totalPrice = items.reduce(
121
125
  (acc, i) => acc + i.quantity * i.sellingPrice,
122
- 0,
126
+ 0
123
127
  )
124
128
 
125
129
  return {
@@ -133,14 +137,14 @@ const joinItems = (form: OrderForm) => {
133
137
 
134
138
  const orderFormToCart = async (
135
139
  form: OrderForm,
136
- skuLoader: Context['loaders']['skuLoader'],
140
+ skuLoader: Context['loaders']['skuLoader']
137
141
  ) => {
138
142
  return {
139
143
  order: {
140
144
  orderNumber: form.orderFormId,
141
145
  acceptedOffer: form.items.map(async (item) => ({
142
146
  ...item,
143
- product: await skuLoader.load(item.id), // TODO: add channel
147
+ product: await skuLoader.load(item.id),
144
148
  })),
145
149
  },
146
150
  messages: form.messages.map(({ text, status }) => ({
@@ -154,7 +158,7 @@ const getOrderFormEtag = ({ items }: OrderForm) => md5(JSON.stringify(items))
154
158
 
155
159
  const setOrderFormEtag = async (
156
160
  form: OrderForm,
157
- commerce: Context['clients']['commerce'],
161
+ commerce: Context['clients']['commerce']
158
162
  ) => {
159
163
  try {
160
164
  const orderForm = await commerce.checkout.setCustomData({
@@ -167,7 +171,7 @@ const setOrderFormEtag = async (
167
171
  return orderForm
168
172
  } catch (err) {
169
173
  console.error(
170
- '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',
174
+ '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'
171
175
  )
172
176
 
173
177
  throw err
@@ -181,7 +185,7 @@ const setOrderFormEtag = async (
181
185
  */
182
186
  const isOrderFormStale = (form: OrderForm) => {
183
187
  const faststoreData = form.customData?.customApps.find(
184
- (app) => app.id === 'faststore',
188
+ (app) => app.id === 'faststore'
185
189
  )
186
190
 
187
191
  const oldEtag = faststoreData?.fields?.cartEtag
@@ -195,28 +199,42 @@ const isOrderFormStale = (form: OrderForm) => {
195
199
  return newEtag !== oldEtag
196
200
  }
197
201
 
202
+ async function getOrderNumberFromSession(
203
+ headers: Record<string, string> = {},
204
+ commerce: Context['clients']['commerce']
205
+ ) {
206
+
207
+ const cookieSession = getCookie('vtex_session', headers.cookie)
208
+
209
+ if (cookieSession) {
210
+ const { namespaces } = await commerce.getSessionOrder()
211
+ return namespaces.checkout?.orderFormId?.value
212
+ }
213
+ return ;
214
+ }
215
+
198
216
  // Returns the regionalized orderForm
199
217
  const getOrderForm = async (
200
218
  id: string,
201
219
  session: Maybe<IStoreSession> | undefined,
202
- { clients: { commerce } }: Context,
220
+ { clients: { commerce } }: Context
203
221
  ) => {
204
222
  const orderForm = await commerce.checkout.orderForm({
205
223
  id,
206
- });
224
+ })
207
225
 
208
226
  // Stores that are not yet providing the session while validating the cart
209
227
  // should not be able to update the shipping data
210
228
  //
211
229
  // This was causing errors while validating regionalizated carts
212
230
  // because the following code was trying to change the shippingData to an undefined address/session
213
- if(!session) {
231
+ if (!session) {
214
232
  return orderForm
215
233
  }
216
234
 
217
235
  const shouldUpdateShippingData =
218
236
  typeof session.postalCode === 'string' &&
219
- orderForm.shippingData?.address?.postalCode != session.postalCode;
237
+ orderForm.shippingData?.address?.postalCode != session.postalCode
220
238
 
221
239
  if (shouldUpdateShippingData) {
222
240
  return commerce.checkout.shippingData({
@@ -224,11 +242,11 @@ const getOrderForm = async (
224
242
  body: {
225
243
  selectedAddresses: [session],
226
244
  },
227
- });
245
+ })
228
246
  }
229
247
 
230
- return orderForm;
231
- };
248
+ return orderForm
249
+ }
232
250
 
233
251
  /**
234
252
  * This resolver implements the optimistic cart behavior. The main idea in here
@@ -246,84 +264,93 @@ const getOrderForm = async (
246
264
  export const validateCart = async (
247
265
  _: unknown,
248
266
  { cart: { order }, session }: MutationValidateCartArgs,
249
- ctx: Context,
267
+ ctx: Context
250
268
  ) => {
251
- const { enableOrderFormSync } = ctx.storage.flags
252
- const { orderNumber, acceptedOffer } = order
269
+ const { orderNumber: orderNumberFromCart, acceptedOffer } = order
253
270
  const {
254
271
  clients: { commerce },
255
272
  loaders: { skuLoader },
273
+ headers,
256
274
  } = ctx
257
275
 
276
+ const channel = session?.channel
277
+ const locale = session?.locale
278
+
279
+ if (channel) {
280
+ mutateChannelContext(ctx, channel)
281
+ }
282
+
283
+ if (locale) {
284
+ mutateLocaleContext(ctx, locale)
285
+ }
286
+
287
+ const orderNumberFromSession = await getOrderNumberFromSession(
288
+ headers,
289
+ commerce
290
+ )
291
+
292
+ const orderNumber = orderNumberFromSession ?? orderNumberFromCart ?? ''
293
+
258
294
  // Step1: Get OrderForm from VTEX Commerce
259
295
  const orderForm = await getOrderForm(orderNumber, session, ctx)
260
296
 
261
297
  // Step1.5: Check if another system changed the orderForm with this orderNumber
262
298
  // If so, this means the user interacted with this cart elsewhere and expects
263
299
  // to see this new cart state instead of what's stored on the user's browser.
264
- if (enableOrderFormSync === true) {
265
- const isStale = isOrderFormStale(orderForm)
266
-
267
- if (isStale === true && orderNumber) {
268
- const newOrderForm = await setOrderFormEtag(orderForm, commerce).then(
269
- joinItems,
300
+ const isStale = isOrderFormStale(orderForm)
301
+
302
+ if (isStale && orderNumber) {
303
+ const newOrderForm = await setOrderFormEtag(orderForm, commerce).then(
304
+ joinItems
270
305
  )
271
-
272
- return orderFormToCart(newOrderForm, skuLoader)
273
- }
306
+ return orderFormToCart(newOrderForm, skuLoader)
274
307
  }
275
308
 
276
309
  // Step2: Process items from both browser and checkout so they have the same shape
277
310
  const browserItemsById = groupById(acceptedOffer)
278
311
  const originItemsById = groupById(orderForm.items.map(orderFormItemToOffer))
279
- const originItems = Array.from(originItemsById.entries()); // items on the VTEX platform backend
280
- const browserItems = Array.from(browserItemsById.entries()); // items on the user's browser
312
+ const originItems = Array.from(originItemsById.entries()) // items on the VTEX platform backend
313
+ const browserItems = Array.from(browserItemsById.entries()) // items on the user's browser
281
314
 
282
315
  // Step3: Compute delta changes
283
- const { itemsToAdd, itemsToUpdate } = browserItems
284
- .reduce(
285
- (acc, [id, items]) => {
286
- const maybeOriginItem = originItemsById.get(id)
287
-
288
- // Adding new items to cart
289
- if (!maybeOriginItem) {
290
- items.forEach((item) => acc.itemsToAdd.push(item))
291
-
292
- return acc
293
- }
294
-
295
- // Update existing items
296
- const [head, ...tail] = maybeOriginItem
297
- const totalQuantity = items.reduce(
298
- (acc, curr) => acc + curr.quantity,
299
- 0,
300
- )
301
-
302
- // set total quantity to first item
303
- acc.itemsToUpdate.push({
304
- ...head,
305
- quantity: totalQuantity,
306
- })
307
-
308
- // Remove all the rest
309
- tail.forEach((item) =>
310
- acc.itemsToUpdate.push({ ...item, quantity: 0 })
311
- )
316
+ const { itemsToAdd, itemsToUpdate } = browserItems.reduce(
317
+ (acc, [id, items]) => {
318
+ const maybeOriginItem = originItemsById.get(id)
319
+
320
+ // Adding new items to cart
321
+ if (!maybeOriginItem) {
322
+ items.forEach((item) => acc.itemsToAdd.push(item))
312
323
 
313
324
  return acc
314
- },
315
- {
316
- itemsToAdd: [] as IStoreOffer[],
317
- itemsToUpdate: [] as IStoreOffer[],
318
- },
319
- )
325
+ }
326
+
327
+ // Update existing items
328
+ const [head, ...tail] = maybeOriginItem
329
+ const totalQuantity = items.reduce((acc, curr) => acc + curr.quantity, 0)
330
+
331
+ // set total quantity to first item
332
+ acc.itemsToUpdate.push({
333
+ ...head,
334
+ quantity: totalQuantity,
335
+ })
336
+
337
+ // Remove all the rest
338
+ tail.forEach((item) => acc.itemsToUpdate.push({ ...item, quantity: 0 }))
339
+
340
+ return acc
341
+ },
342
+ {
343
+ itemsToAdd: [] as IStoreOffer[],
344
+ itemsToUpdate: [] as IStoreOffer[],
345
+ }
346
+ )
320
347
 
321
348
  const itemsToDelete = originItems
322
349
  .filter(([id]) => !browserItemsById.has(id))
323
350
  .flatMap(([, items]) => items.map((item) => ({ ...item, quantity: 0 })))
324
351
 
325
352
  const changes = [...itemsToAdd, ...itemsToUpdate, ...itemsToDelete].map(
326
- offerToOrderItemInput,
353
+ offerToOrderItemInput
327
354
  )
328
355
 
329
356
  if (changes.length === 0) {
@@ -339,7 +366,7 @@ export const validateCart = async (
339
366
  })
340
367
  // update orderForm etag so we know last time we touched this orderForm
341
368
  .then((form) =>
342
- enableOrderFormSync ? setOrderFormEtag(form, commerce) : form
369
+ setOrderFormEtag(form, commerce)
343
370
  )
344
371
  .then(joinItems)
345
372
 
@@ -0,0 +1,8 @@
1
+ export const getCookie = (name: string, cookie: string): string => {
2
+ const value = `; ${cookie}`
3
+ const parts = value.split(`; ${name}=`)
4
+ if (parts.length === 2) {
5
+ return parts?.pop()?.split(';').shift() ?? ''
6
+ }
7
+ return ''
8
+ }