From 13f35d5bb1a21c6cb72aa9512ae586180aac27e1 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Fri, 27 Sep 2024 16:10:27 -0300 Subject: [PATCH 1/6] feat(vnda): create addItems action --- vnda/actions/cart/addItems.ts | 46 +++++++++++++++++ vnda/manifest.gen.ts | 18 ++++--- vnda/utils/openapi/vnda.openapi.gen.ts | 26 +++++++--- vnda/utils/openapi/vnda.openapi.json | 70 +++++++++----------------- 4 files changed, 99 insertions(+), 61 deletions(-) create mode 100644 vnda/actions/cart/addItems.ts diff --git a/vnda/actions/cart/addItems.ts b/vnda/actions/cart/addItems.ts new file mode 100644 index 000000000..7cd31f58b --- /dev/null +++ b/vnda/actions/cart/addItems.ts @@ -0,0 +1,46 @@ +import { HttpError } from "../../../utils/http.ts"; +import cartLoader, { Cart } from "../../loaders/cart.ts"; +import { AppContext } from "../../mod.ts"; +import { getCartCookie } from "../../utils/cart.ts"; + +interface Item { + itemId: string; + quantity: number; + attributes?: Record; +} + +export interface Props { + items: Item[]; +} + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { api } = ctx; + const { items } = props; + const cartId = getCartCookie(req.headers); + + if (!cartId) { + throw new HttpError(400, "Missing cart cookie"); + } + + await api["POST /api/v2/carts/:cartId/items/bulk"]({ cartId }, { + body: { + items: items.map((item) => ({ + sku: item.itemId, + quantity: item.quantity, + customizations: item.attributes + ? Object.entries(item.attributes).map(([key, value]) => ({ + [key]: value, + })) + : undefined, + })), + }, + }); + + return cartLoader({}, req, ctx); +}; + +export default action; diff --git a/vnda/manifest.gen.ts b/vnda/manifest.gen.ts index fac2ad1fb..cd5093019 100644 --- a/vnda/manifest.gen.ts +++ b/vnda/manifest.gen.ts @@ -3,10 +3,11 @@ // This file is automatically updated during development when running `dev.ts`. import * as $$$$$$$$$0 from "./actions/cart/addItem.ts"; -import * as $$$$$$$$$1 from "./actions/cart/simulation.ts"; -import * as $$$$$$$$$2 from "./actions/cart/updateCart.ts"; -import * as $$$$$$$$$3 from "./actions/cart/updateItem.ts"; -import * as $$$$$$$$$4 from "./actions/notifyme.ts"; +import * as $$$$$$$$$1 from "./actions/cart/addItems.ts"; +import * as $$$$$$$$$2 from "./actions/cart/simulation.ts"; +import * as $$$$$$$$$3 from "./actions/cart/updateCart.ts"; +import * as $$$$$$$$$4 from "./actions/cart/updateItem.ts"; +import * as $$$$$$$$$5 from "./actions/notifyme.ts"; import * as $$$$0 from "./handlers/sitemap.ts"; import * as $$$0 from "./loaders/cart.ts"; import * as $$$1 from "./loaders/extensions/price/list.ts"; @@ -33,10 +34,11 @@ const manifest = { }, "actions": { "vnda/actions/cart/addItem.ts": $$$$$$$$$0, - "vnda/actions/cart/simulation.ts": $$$$$$$$$1, - "vnda/actions/cart/updateCart.ts": $$$$$$$$$2, - "vnda/actions/cart/updateItem.ts": $$$$$$$$$3, - "vnda/actions/notifyme.ts": $$$$$$$$$4, + "vnda/actions/cart/addItems.ts": $$$$$$$$$1, + "vnda/actions/cart/simulation.ts": $$$$$$$$$2, + "vnda/actions/cart/updateCart.ts": $$$$$$$$$3, + "vnda/actions/cart/updateItem.ts": $$$$$$$$$4, + "vnda/actions/notifyme.ts": $$$$$$$$$5, }, "name": "vnda", "baseUrl": import.meta.url, diff --git a/vnda/utils/openapi/vnda.openapi.gen.ts b/vnda/utils/openapi/vnda.openapi.gen.ts index c17137bac..6bdf30999 100644 --- a/vnda/utils/openapi/vnda.openapi.gen.ts +++ b/vnda/utils/openapi/vnda.openapi.gen.ts @@ -1036,16 +1036,30 @@ store_coupon_code?: string */ "POST /api/v2/carts/:cartId/items/bulk": { body: { +/** + * Itens do carrinho + */ +items?: { +/** + * Código SKU da variante do produto + */ sku: string +/** + * Unidades do produto + */ quantity: number -extra?: { - -} -place_id?: number /** - * Itens do carrinho + * [Personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) do produto */ -items?: any[][] +customizations?: { +/** + * Adicione a customização de acordo com a [personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) incluídas no Admin da loja. + * Se por exemplo a customização do produto é a cor, o parâmetro para a requisição deve ser `Color` ao invés de `CUstomization`. + * Saiba mais sobre como utilizar esse parâmetro pelo exemplo de requsição localizado na seção de **Request Example** (ao lado do código da requisição). + */ +Customization?: string +}[] +}[] minItems?: 0 } response: CartItem[] diff --git a/vnda/utils/openapi/vnda.openapi.json b/vnda/utils/openapi/vnda.openapi.json index b6f698b99..cb8536cc3 100644 --- a/vnda/utils/openapi/vnda.openapi.json +++ b/vnda/utils/openapi/vnda.openapi.json @@ -5109,27 +5109,11 @@ "schema": { "type": "object", "properties": { - "sku": { - "type": "string" - }, - "quantity": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true - }, - "extra": { - "type": "object" - }, - "place_id": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true - }, "items": { "type": "array", "description": "Itens do carrinho", "items": { - "type": "array", + "type": "object", "properties": { "sku": { "type": "string", @@ -5156,39 +5140,31 @@ "sku", "quantity" ] - }, - "example": { - "itemcustomizado": { - "items": { - "value": [ - { - "sku": "variante.sku1", - "quantity": 1, - "customizations": [ - { - "Color": "Black" - } - ] - }, - { - "sku": "variante.sku2", - "quantity": 10, - "customizations": [ - { - "Color": "Red" - } - ] - } - ] - } - } } } }, - "required": [ - "sku", - "quantity" - ] + "example": { + "items": [ + { + "sku": "teste", + "quantity": 1, + "customizations": [ + { + "Color": "Black" + } + ] + }, + { + "sku": "variante.sku2", + "quantity": 10, + "customizations": [ + { + "Color": "Red" + } + ] + } + ] + } } } } From dafa914118eaea068d00c08b2ba105f877ece9cb Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Tue, 1 Oct 2024 15:28:15 -0300 Subject: [PATCH 2/6] feat(vnda): adds user integration --- vnda/hooks/context.ts | 11 ++++++++--- vnda/hooks/useUser.ts | 7 +++++++ vnda/loaders/user.ts | 37 +++++++++++++++++++++++++++++++++++++ vnda/manifest.gen.ts | 2 ++ vnda/utils/user.ts | 23 +++++++++++++++++++++++ 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 vnda/hooks/useUser.ts create mode 100644 vnda/loaders/user.ts create mode 100644 vnda/utils/user.ts diff --git a/vnda/hooks/context.ts b/vnda/hooks/context.ts index 6e0ac94d0..f56e035c6 100644 --- a/vnda/hooks/context.ts +++ b/vnda/hooks/context.ts @@ -1,15 +1,18 @@ import { IS_BROWSER } from "$fresh/runtime.ts"; import { signal } from "@preact/signals"; import { invoke } from "../runtime.ts"; +import type { Person } from "../../commerce/types.ts"; import type { Cart } from "../loaders/cart.ts"; export interface Context { + user: Person | null; cart: Cart; } const loading = signal(true); const context = { - cart: signal(null), + user: IS_BROWSER && signal(null) || { value: null }, + cart: IS_BROWSER && signal(null) || { value: null }, }; let queue = Promise.resolve(); @@ -24,13 +27,14 @@ const enqueue = ( queue = queue.then(async () => { try { - const { cart } = await cb(controller.signal); + const { user, cart } = await cb(controller.signal); if (controller.signal.aborted) { throw { name: "AbortError" }; } - context.cart.value = { ...context.cart.value, ...cart }; + context.cart.value = cart || context.cart.value; + context.user.value = user || context.user.value; loading.value = false; } catch (error) { @@ -49,6 +53,7 @@ const enqueue = ( const load = (signal: AbortSignal) => invoke({ cart: invoke.vnda.loaders.cart(), + user: invoke.shopify.loaders.user(), }, { signal }); if (IS_BROWSER) { diff --git a/vnda/hooks/useUser.ts b/vnda/hooks/useUser.ts new file mode 100644 index 000000000..ef405c564 --- /dev/null +++ b/vnda/hooks/useUser.ts @@ -0,0 +1,7 @@ +import { state as storeState } from "./context.ts"; + +const { user, loading } = storeState; + +const state = { user, loading }; + +export const useUser = () => state; diff --git a/vnda/loaders/user.ts b/vnda/loaders/user.ts new file mode 100644 index 000000000..7a117cdfd --- /dev/null +++ b/vnda/loaders/user.ts @@ -0,0 +1,37 @@ +import { Person } from "../../commerce/types.ts"; +import type { AppContext } from "../mod.ts"; +import { getUserCookie } from "../utils/user.ts"; + +/** + * @title VNDA Integration + * @description User loader + */ +const loader = async ( + _props: unknown, + req: Request, + ctx: AppContext, +): Promise => { + const { api } = ctx; + + const userAccessToken = getUserCookie(req.headers); + + if (!userAccessToken) return null; + + try { + const user = await api["GET /api/v2/clients/:id"]({ id: userAccessToken }) + .then((res) => res.json()); + + if (!user) return null; + + return { + "@id": String(user.id), + email: user.email ?? "", + givenName: user.first_name ?? "", + familyName: user.last_name ?? "", + }; + } catch { + return null; + } +}; + +export default loader; diff --git a/vnda/manifest.gen.ts b/vnda/manifest.gen.ts index cd5093019..2e9e52702 100644 --- a/vnda/manifest.gen.ts +++ b/vnda/manifest.gen.ts @@ -17,6 +17,7 @@ import * as $$$4 from "./loaders/productDetailsPageVideo.ts"; import * as $$$5 from "./loaders/productList.ts"; import * as $$$6 from "./loaders/productListingPage.ts"; import * as $$$7 from "./loaders/proxy.ts"; +import * as $$$8 from "./loaders/user.ts"; const manifest = { "loaders": { @@ -28,6 +29,7 @@ const manifest = { "vnda/loaders/productList.ts": $$$5, "vnda/loaders/productListingPage.ts": $$$6, "vnda/loaders/proxy.ts": $$$7, + "vnda/loaders/user.ts": $$$8, }, "handlers": { "vnda/handlers/sitemap.ts": $$$$0, diff --git a/vnda/utils/user.ts b/vnda/utils/user.ts new file mode 100644 index 000000000..707a2047f --- /dev/null +++ b/vnda/utils/user.ts @@ -0,0 +1,23 @@ +import { getCookies, setCookie } from "std/http/cookie.ts"; + +const CUSTOMER_COOKIE = "client_id"; + +const ONE_WEEK_MS = 7 * 24 * 3600 * 1_000; + +export const getUserCookie = (headers: Headers): string | undefined => { + const cookies = getCookies(headers); + + return cookies[CUSTOMER_COOKIE]; +}; + +export const setUserCookie = (headers: Headers, accessToken: string) => { + setCookie(headers, { + name: CUSTOMER_COOKIE, + value: accessToken, + path: "/", + expires: new Date(Date.now() + ONE_WEEK_MS), + httpOnly: true, + secure: true, + sameSite: "Lax", + }); +}; From d96ff93da92319b884978c4f10a582159cade042 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Tue, 1 Oct 2024 15:31:59 -0300 Subject: [PATCH 3/6] fix(vnda): fix the call to invoke --- vnda/hooks/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vnda/hooks/context.ts b/vnda/hooks/context.ts index f56e035c6..1b52201b6 100644 --- a/vnda/hooks/context.ts +++ b/vnda/hooks/context.ts @@ -53,7 +53,7 @@ const enqueue = ( const load = (signal: AbortSignal) => invoke({ cart: invoke.vnda.loaders.cart(), - user: invoke.shopify.loaders.user(), + user: invoke.vnda.loaders.user(), }, { signal }); if (IS_BROWSER) { From ccec31dd6fe1f531696b064617c856c3dfb95601 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Tue, 1 Oct 2024 16:22:26 -0300 Subject: [PATCH 4/6] fix(vnda): add correct type to Cart signal --- vnda/hooks/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vnda/hooks/context.ts b/vnda/hooks/context.ts index 1b52201b6..b5041d0db 100644 --- a/vnda/hooks/context.ts +++ b/vnda/hooks/context.ts @@ -12,7 +12,7 @@ export interface Context { const loading = signal(true); const context = { user: IS_BROWSER && signal(null) || { value: null }, - cart: IS_BROWSER && signal(null) || { value: null }, + cart: IS_BROWSER && signal(null) || { value: null }, }; let queue = Promise.resolve(); From d03ee0ba4068201f7813f6fe0db1e1703c8946ae Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Tue, 1 Oct 2024 18:12:59 -0300 Subject: [PATCH 5/6] fix(vnda): AUTH COOKIE sounds better --- vnda/utils/user.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vnda/utils/user.ts b/vnda/utils/user.ts index 707a2047f..7b4d0dc60 100644 --- a/vnda/utils/user.ts +++ b/vnda/utils/user.ts @@ -1,18 +1,18 @@ import { getCookies, setCookie } from "std/http/cookie.ts"; -const CUSTOMER_COOKIE = "client_id"; +const AUTH_COOKIE = "client_id"; const ONE_WEEK_MS = 7 * 24 * 3600 * 1_000; export const getUserCookie = (headers: Headers): string | undefined => { const cookies = getCookies(headers); - return cookies[CUSTOMER_COOKIE]; + return cookies[AUTH_COOKIE]; }; export const setUserCookie = (headers: Headers, accessToken: string) => { setCookie(headers, { - name: CUSTOMER_COOKIE, + name: AUTH_COOKIE, value: accessToken, path: "/", expires: new Date(Date.now() + ONE_WEEK_MS), From 9d0abd15e62a3090922f3e439249c2ddfbbc36ac Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Thu, 10 Oct 2024 23:17:16 -0300 Subject: [PATCH 6/6] fix: remove setUserCookie function --- vnda/utils/user.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/vnda/utils/user.ts b/vnda/utils/user.ts index 7b4d0dc60..f763dc7e2 100644 --- a/vnda/utils/user.ts +++ b/vnda/utils/user.ts @@ -1,23 +1,9 @@ -import { getCookies, setCookie } from "std/http/cookie.ts"; +import { getCookies } from "std/http/cookie.ts"; const AUTH_COOKIE = "client_id"; -const ONE_WEEK_MS = 7 * 24 * 3600 * 1_000; - export const getUserCookie = (headers: Headers): string | undefined => { const cookies = getCookies(headers); return cookies[AUTH_COOKIE]; }; - -export const setUserCookie = (headers: Headers, accessToken: string) => { - setCookie(headers, { - name: AUTH_COOKIE, - value: accessToken, - path: "/", - expires: new Date(Date.now() + ONE_WEEK_MS), - httpOnly: true, - secure: true, - sameSite: "Lax", - }); -};