From 5b67df2ccf8fc5471c7ea0f4a0cc8816530715bc Mon Sep 17 00:00:00 2001 From: Tiago Gimenes Date: Tue, 31 May 2022 14:16:30 -0300 Subject: [PATCH] add sales channel support --- packages/api/src/__generated__/schema.ts | 59 ++++++++++--- .../platforms/vtex/clients/commerce/index.ts | 29 ++++--- .../vtex/clients/commerce/types/Session.ts | 9 ++ .../src/platforms/vtex/resolvers/mutation.ts | 4 +- .../api/src/platforms/vtex/resolvers/query.ts | 18 ---- .../platforms/vtex/resolvers/updateSession.ts | 26 ------ .../vtex/resolvers/validateSession.ts | 59 +++++++++++++ packages/api/src/typeDefs/index.ts | 2 + packages/api/src/typeDefs/mutation.graphql | 40 +-------- packages/api/src/typeDefs/person.graphql | 22 +++++ packages/api/src/typeDefs/query.graphql | 5 -- packages/api/src/typeDefs/session.graphql | 84 +++++++++++++++++++ packages/api/test/schema.test.ts | 3 +- packages/sdk/src/cart/Optimistic.tsx | 48 +++-------- packages/sdk/src/index.ts | 9 +- packages/sdk/src/session/Provider.tsx | 82 ++++-------------- packages/sdk/src/session/Revalidate.tsx | 35 ++++++++ packages/sdk/src/session/Session.tsx | 73 ++++++++++++++++ packages/sdk/src/session/useSession.ts | 13 ++- packages/sdk/src/utils/useValidation.ts | 57 +++++++++++++ 20 files changed, 452 insertions(+), 225 deletions(-) delete mode 100644 packages/api/src/platforms/vtex/resolvers/updateSession.ts create mode 100644 packages/api/src/platforms/vtex/resolvers/validateSession.ts create mode 100644 packages/api/src/typeDefs/session.graphql create mode 100644 packages/sdk/src/session/Revalidate.tsx create mode 100644 packages/sdk/src/session/Session.tsx create mode 100644 packages/sdk/src/utils/useValidation.ts diff --git a/packages/api/src/__generated__/schema.ts b/packages/api/src/__generated__/schema.ts index da3cb9d948..f188f40f0f 100644 --- a/packages/api/src/__generated__/schema.ts +++ b/packages/api/src/__generated__/schema.ts @@ -18,6 +18,13 @@ export type IStoreCart = { order: IStoreOrder; }; +export type IStoreCurrency = { + /** Currency code, e.g: USD */ + code: Scalars['String']; + /** Currency symbol, e.g: $ */ + symbol: Scalars['String']; +}; + /** Image input. */ export type IStoreImage = { /** Alias for the input image. */ @@ -54,6 +61,18 @@ export type IStoreOrganization = { identifier: Scalars['String']; }; +/** Client profile data. */ +export type IStorePerson = { + /** Client email. */ + email: Scalars['String']; + /** Client last name. */ + familyName: Scalars['String']; + /** Client first name. */ + givenName: Scalars['String']; + /** Client ID. */ + id: Scalars['String']; +}; + /** Product input. Products are variants within product groups, equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on. */ export type IStoreProduct = { /** Custom Product Additional Properties. */ @@ -88,27 +107,34 @@ export type IStoreSession = { /** Session input channel. */ channel?: Maybe; /** Session input country. */ - country?: Maybe; + country: Scalars['String']; + /** Session input currency. */ + currency: IStoreCurrency; + /** Session input locale. */ + locale: Scalars['String']; + /** Session input postal code. */ + person?: Maybe; /** Session input postal code. */ postalCode?: Maybe; }; export type Mutation = { __typename?: 'Mutation'; - /** Update session information. */ - updateSession: StoreSession; /** Returns the order if anything has changed in it, or `null` if the order is valid. */ validateCart?: Maybe; + /** Validate session information. */ + validateSession?: Maybe; }; -export type MutationUpdateSessionArgs = { - session: IStoreSession; +export type MutationValidateCartArgs = { + cart: IStoreCart; }; -export type MutationValidateCartArgs = { - cart: IStoreCart; +export type MutationValidateSessionArgs = { + search: Scalars['String']; + session: IStoreSession; }; export type Query = { @@ -119,8 +145,6 @@ export type Query = { allProducts: StoreProductConnection; /** Collection query. */ collection: StoreCollection; - /** Person query. */ - person?: Maybe; /** Product query. */ product: StoreProduct; /** Search query. */ @@ -282,6 +306,15 @@ export const enum StoreCollectionType { Department = 'Department' }; +/** Currency information. */ +export type StoreCurrency = { + __typename?: 'StoreCurrency'; + /** Currency code, e.g: USD */ + code: Scalars['String']; + /** Currency symbol, e.g: $ */ + symbol: Scalars['String']; +}; + /** Search facet information. */ export type StoreFacet = { __typename?: 'StoreFacet'; @@ -530,7 +563,13 @@ export type StoreSession = { /** Session channel. */ channel?: Maybe; /** Session country. */ - country?: Maybe; + country: Scalars['String']; + /** Session currency. */ + currency: StoreCurrency; + /** Session locale. */ + locale: Scalars['String']; + /** Session postal code. */ + person?: Maybe; /** Session postal code. */ postalCode?: Maybe; }; diff --git a/packages/api/src/platforms/vtex/clients/commerce/index.ts b/packages/api/src/platforms/vtex/clients/commerce/index.ts index 488754ba3e..c6894a2f99 100644 --- a/packages/api/src/platforms/vtex/clients/commerce/index.ts +++ b/packages/api/src/platforms/vtex/clients/commerce/index.ts @@ -135,17 +135,22 @@ export const VtexCommerce = ( ) }, }, - session: (): Promise => - fetchAPI( - `${base}/api/sessions?items=profile.id,profile.email,profile.firstName,profile.lastName`, - { - method: 'POST', - headers: { - 'content-type': 'application/json', - cookie: ctx.headers.cookie, - }, - body: '{}', - } - ), + session: (search: string): Promise => { + const params = new URLSearchParams(search) + + params.set( + 'items', + 'profile.id,profile.email,profile.firstName,profile.lastName,store.channel,store.countryCode,store.cultureInfo,store.currencyCode,store.currencySymbol' + ) + + return fetchAPI(`${base}/api/sessions?${params.toString()}`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + cookie: ctx.headers.cookie, + }, + body: '{}', + }) + }, } } diff --git a/packages/api/src/platforms/vtex/clients/commerce/types/Session.ts b/packages/api/src/platforms/vtex/clients/commerce/types/Session.ts index 371e0124a7..c6eaa5e192 100644 --- a/packages/api/src/platforms/vtex/clients/commerce/types/Session.ts +++ b/packages/api/src/platforms/vtex/clients/commerce/types/Session.ts @@ -5,12 +5,21 @@ export interface Session { export interface Namespaces { profile?: Profile + store?: Store } export interface Value { value: string } +export interface Store { + channel: Value + countryCode: Value + cultureInfo: Value + currencyCode: Value + currencySymbol: Value +} + export interface Profile { id?: Value email?: Value diff --git a/packages/api/src/platforms/vtex/resolvers/mutation.ts b/packages/api/src/platforms/vtex/resolvers/mutation.ts index b22ea22719..ce6d87441a 100644 --- a/packages/api/src/platforms/vtex/resolvers/mutation.ts +++ b/packages/api/src/platforms/vtex/resolvers/mutation.ts @@ -1,7 +1,7 @@ import { validateCart } from './validateCart' -import { updateSession } from './updateSession' +import { validateSession } from './validateSession' export const Mutation = { validateCart, - updateSession, + validateSession, } diff --git a/packages/api/src/platforms/vtex/resolvers/query.ts b/packages/api/src/platforms/vtex/resolvers/query.ts index e30da670da..1083e1efbf 100644 --- a/packages/api/src/platforms/vtex/resolvers/query.ts +++ b/packages/api/src/platforms/vtex/resolvers/query.ts @@ -165,22 +165,4 @@ export const Query = { })), } }, - person: async (_: unknown, __: unknown, ctx: Context) => { - const { - clients: { commerce }, - } = ctx - - const { - namespaces: { profile = null }, - } = await commerce.session() - - return ( - profile && { - id: profile.id?.value ?? '', - email: profile.email?.value ?? '', - givenName: profile.firstName?.value ?? '', - familyName: profile.lastName?.value ?? '', - } - ) - }, } diff --git a/packages/api/src/platforms/vtex/resolvers/updateSession.ts b/packages/api/src/platforms/vtex/resolvers/updateSession.ts deleted file mode 100644 index 2c743ef435..0000000000 --- a/packages/api/src/platforms/vtex/resolvers/updateSession.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Context } from '..' -import type { - MutationUpdateSessionArgs, - StoreSession, -} from '../../../__generated__/schema' -import ChannelMarshal from '../utils/channel' - -export const updateSession = async ( - _: any, - { session }: MutationUpdateSessionArgs, - { clients }: Context -): Promise => { - const channel = ChannelMarshal.parse(session.channel ?? '') - const regionData = await clients.commerce.checkout.region({ - postalCode: String(session.postalCode ?? '').replace(/\D/g, ''), - country: session.country ?? '', - }) - - return { - ...session, - channel: ChannelMarshal.stringify({ - ...channel, - regionId: regionData?.[0]?.id, - }), - } -} diff --git a/packages/api/src/platforms/vtex/resolvers/validateSession.ts b/packages/api/src/platforms/vtex/resolvers/validateSession.ts new file mode 100644 index 0000000000..ff6c4d9644 --- /dev/null +++ b/packages/api/src/platforms/vtex/resolvers/validateSession.ts @@ -0,0 +1,59 @@ +import deepEquals from 'fast-deep-equal' + +import ChannelMarshal from '../utils/channel' +import type { Context } from '..' +import type { + MutationValidateSessionArgs, + StoreSession, +} from '../../../__generated__/schema' + +export const validateSession = async ( + _: any, + { session: oldSession, search }: MutationValidateSessionArgs, + { clients }: Context +): Promise => { + const channel = ChannelMarshal.parse(oldSession.channel ?? '') + const postalCode = String(oldSession.postalCode ?? '').replace(/\D/g, '') + const country = oldSession.country ?? '' + + const params = new URLSearchParams(search) + + params.set('sc', params.get('sc') ?? channel.salesChannel) + + const [regionData, sessionData] = await Promise.all([ + postalCode + ? clients.commerce.checkout.region({ postalCode, country }) + : Promise.resolve(null), + clients.commerce.session(params.toString()).catch(() => null), + ]) + + const profile = sessionData?.namespaces.profile ?? null + const store = sessionData?.namespaces.store ?? null + + const newSession = { + ...oldSession, + currency: { + code: store?.currencyCode.value ?? oldSession.currency.code, + symbol: store?.currencySymbol.value ?? oldSession.currency.symbol, + }, + country: store?.countryCode.value ?? oldSession.country, + channel: ChannelMarshal.stringify({ + salesChannel: store?.channel?.value ?? channel.salesChannel, + regionId: regionData?.[0]?.id ?? channel.regionId, + }), + person: profile?.id + ? { + id: profile.id?.value ?? '', + email: profile.email?.value ?? '', + givenName: profile.firstName?.value ?? '', + familyName: profile.lastName?.value ?? '', + } + : null, + } + + if (deepEquals(oldSession, newSession)) { + return null + } + + return newSession +} diff --git a/packages/api/src/typeDefs/index.ts b/packages/api/src/typeDefs/index.ts index fe39270c43..3ae5c1257b 100644 --- a/packages/api/src/typeDefs/index.ts +++ b/packages/api/src/typeDefs/index.ts @@ -23,6 +23,7 @@ import Status from './status.graphql' import PropertyValue from './propertyValue.graphql' import Person from './person.graphql' import ObjectOrString from './objectOrString.graphql' +import Session from './session.graphql' export const typeDefs = [ Query, @@ -48,6 +49,7 @@ export const typeDefs = [ PropertyValue, Person, ObjectOrString, + Session, ] .map(print) .join('\n') diff --git a/packages/api/src/typeDefs/mutation.graphql b/packages/api/src/typeDefs/mutation.graphql index 0449302faf..62036eb0db 100644 --- a/packages/api/src/typeDefs/mutation.graphql +++ b/packages/api/src/typeDefs/mutation.graphql @@ -1,46 +1,10 @@ -""" -Session information. -""" -type StoreSession { - """ - Session channel. - """ - channel: String - """ - Session country. - """ - country: String - """ - Session postal code. - """ - postalCode: String -} - -""" -Session input. -""" -input IStoreSession { - """ - Session input channel. - """ - channel: String - """ - Session input country. - """ - country: String - """ - Session input postal code. - """ - postalCode: String -} - type Mutation { """ Returns the order if anything has changed in it, or `null` if the order is valid. """ validateCart(cart: IStoreCart!): StoreCart """ - Update session information. + Validate session information. """ - updateSession(session: IStoreSession!): StoreSession! + validateSession(session: IStoreSession!, search: String!): StoreSession } diff --git a/packages/api/src/typeDefs/person.graphql b/packages/api/src/typeDefs/person.graphql index f2d2f427b3..2e974c7909 100644 --- a/packages/api/src/typeDefs/person.graphql +++ b/packages/api/src/typeDefs/person.graphql @@ -19,3 +19,25 @@ type StorePerson { """ familyName: String! } + +""" +Client profile data. +""" +input IStorePerson { + """ + Client ID. + """ + id: String! + """ + Client email. + """ + email: String! + """ + Client first name. + """ + givenName: String! + """ + Client last name. + """ + familyName: String! +} diff --git a/packages/api/src/typeDefs/query.graphql b/packages/api/src/typeDefs/query.graphql index 99d74109d0..fe22bf3c63 100644 --- a/packages/api/src/typeDefs/query.graphql +++ b/packages/api/src/typeDefs/query.graphql @@ -204,9 +204,4 @@ type Query { """ after: String ): StoreCollectionConnection! - - """ - Person query. - """ - person: StorePerson } diff --git a/packages/api/src/typeDefs/session.graphql b/packages/api/src/typeDefs/session.graphql new file mode 100644 index 0000000000..bd7e16183f --- /dev/null +++ b/packages/api/src/typeDefs/session.graphql @@ -0,0 +1,84 @@ +""" +Currency information. +""" +type StoreCurrency { + """ + Currency code, e.g: USD + """ + code: String! + """ + Currency symbol, e.g: $ + """ + symbol: String! +} + +input IStoreCurrency { + """ + Currency code, e.g: USD + """ + code: String! + """ + Currency symbol, e.g: $ + """ + symbol: String! +} + +""" +Session information. +""" +type StoreSession { + """ + Session locale. + """ + locale: String! + """ + Session currency. + """ + currency: StoreCurrency! + """ + Session country. + """ + country: String! + """ + Session channel. + """ + channel: String + """ + Session postal code. + """ + postalCode: String + """ + Session postal code. + """ + person: StorePerson +} + +""" +Session input. +""" +input IStoreSession { + """ + Session input locale. + """ + locale: String! + """ + Session input currency. + """ + currency: IStoreCurrency! + """ + Session input country. + """ + country: String! + """ + Session input channel. + """ + channel: String + """ + Session input postal code. + """ + postalCode: String + """ + Session input postal code. + """ + person: IStorePerson +} diff --git a/packages/api/test/schema.test.ts b/packages/api/test/schema.test.ts index d79f6fd097..ce112217e1 100644 --- a/packages/api/test/schema.test.ts +++ b/packages/api/test/schema.test.ts @@ -52,10 +52,9 @@ const QUERIES = [ 'search', 'allProducts', 'allCollections', - 'person', ] -const MUTATIONS = ['validateCart', 'updateSession'] +const MUTATIONS = ['validateCart', 'validateSession'] let schema: GraphQLSchema diff --git a/packages/sdk/src/cart/Optimistic.tsx b/packages/sdk/src/cart/Optimistic.tsx index 4d91032b94..b3823a6040 100644 --- a/packages/sdk/src/cart/Optimistic.tsx +++ b/packages/sdk/src/cart/Optimistic.tsx @@ -1,7 +1,8 @@ -import React, { createContext, useEffect, useMemo, useState } from 'react' +import React, { createContext, useMemo } from 'react' import type { PropsWithChildren } from 'react' import { useContext } from '../utils/useContext' +import { createUseValidationHook } from '../utils/useValidation' import { Context as CartContext } from './Cart' import type { ContextValue as CartContextValue, Item } from './Cart' @@ -14,49 +15,20 @@ export interface Props { export const Context = createContext(undefined) Context.displayName = 'StoreCartValidatorContext' -const nullable = async () => null - -// Validation queue -let queue = Promise.resolve() +const useValidation = createUseValidationHook() export const OptimisticProvider = ({ children, - onValidateCart = nullable, + onValidateCart, }: PropsWithChildren>) => { const { items, id, setCart } = useContext(CartContext) const cart = useMemo(() => ({ id, items }), [id, items]) - const [isValidating, setIsValidating] = useState(false) - - useEffect(() => { - let cancel = false - - const revalidate = async () => { - if (cancel) { - return - } - - setIsValidating(true) - const newCart = await onValidateCart(cart as Cart) - - if (cancel) { - return - } - - setIsValidating(false) - if (newCart != null) { - setCart(newCart) - } - } - - // Enqueue validation - setTimeout(() => { - queue = queue.then(revalidate) - }, 0) - - return () => { - cancel = true - } - }, [cart, onValidateCart, setCart]) + + const isValidating = useValidation({ + onValidate: onValidateCart, + value: cart as Cart, + setValue: setCart, + }) return {children} } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index b6e459602d..17e6e968e9 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -95,15 +95,12 @@ export type { export { useGlobalUIState } from './ui/useGlobalUIState' // Session -export { - Provider as SessionProvider, - Context as SessionContext, -} from './session/Provider' +export { Provider as SessionProvider } from './session/Provider' export type { Session, Currency as SessionCurrency, - User as SessionUser, -} from './session/Provider' + Person as SessionPerson, +} from './session/Session' export { useSession } from './session/useSession' // Cart diff --git a/packages/sdk/src/session/Provider.tsx b/packages/sdk/src/session/Provider.tsx index 394c898ca1..d1fcd9089a 100644 --- a/packages/sdk/src/session/Provider.tsx +++ b/packages/sdk/src/session/Provider.tsx @@ -1,73 +1,23 @@ -import React, { createContext, useMemo } from 'react' -import type { FC } from 'react' +import React from 'react' +import type { PropsWithChildren } from 'react' -import { useStorage } from '../storage/useStorage' +import { SessionProvider } from './Session' +import { RevalidateProvider } from './Revalidate' +import type { Props as SessionProps } from './Session' +import type { Props as RevalidateProps } from './Revalidate' -export interface Currency { - code: string // Ex: USD - symbol: string // Ex: $ -} - -export interface User { - id: string - email: string - givenName: string - familyName: string -} - -export interface Session { - locale: string // en-US - currency: Currency - country: string // BRA - channel: string | null - postalCode: string | null - user: User | null -} - -export interface ContextValue extends Session { - setSession: (session: Partial) => void -} - -export const Context = createContext(undefined) -Context.displayName = 'StoreSessionContext' +interface Props extends SessionProps, RevalidateProps {} -const baseInitialState: Session = { - currency: { - code: 'USD', - symbol: '$', - }, - country: 'USA', - locale: 'en', - postalCode: null, - channel: null, - user: null, -} - -interface Props { - initialState?: Partial - namespace?: string -} - -export const Provider: FC = ({ +export const Provider = ({ children, + onValidateSession, initialState, - namespace = 'main', -}) => { - const [session, setSession] = useStorage( - `${namespace}::store::session`, - () => ({ - ...baseInitialState, - ...initialState, - }) +}: PropsWithChildren) => { + return ( + + + {children} + + ) - - const value = useMemo( - () => ({ - ...session, - setSession: (data) => setSession({ ...session, ...data }), - }), - [session, setSession] - ) - - return {children} } diff --git a/packages/sdk/src/session/Revalidate.tsx b/packages/sdk/src/session/Revalidate.tsx new file mode 100644 index 0000000000..2029809ae0 --- /dev/null +++ b/packages/sdk/src/session/Revalidate.tsx @@ -0,0 +1,35 @@ +import React, { createContext, useMemo } from 'react' +import type { PropsWithChildren } from 'react' + +import { useContext } from '../utils/useContext' +import { createUseValidationHook } from '../utils/useValidation' +import { Context as SessionContext } from './Session' +import type { Session } from './Session' + +export interface Props { + onValidateSession?: (session: Session) => Promise +} + +export const Context = createContext(undefined) + +const useValidation = createUseValidationHook() + +export const RevalidateProvider = ({ + onValidateSession, + children, +}: PropsWithChildren) => { + const context = useContext(SessionContext) + const session = useMemo(() => { + const { setSession, ...rest } = context + + return rest + }, [context]) + + const isValidating = useValidation({ + onValidate: onValidateSession, + value: session, + setValue: context.setSession, + }) + + return {children} +} diff --git a/packages/sdk/src/session/Session.tsx b/packages/sdk/src/session/Session.tsx new file mode 100644 index 0000000000..dede087435 --- /dev/null +++ b/packages/sdk/src/session/Session.tsx @@ -0,0 +1,73 @@ +import React, { createContext, useMemo } from 'react' +import type { PropsWithChildren } from 'react' + +import { useStorage } from '../storage/useStorage' + +export interface Currency { + code: string // Ex: USD + symbol: string // Ex: $ +} + +export interface Person { + id: string + email: string + givenName: string + familyName: string +} + +export interface Session { + locale: string // en-US + currency: Currency + country: string // BRA + channel: string | null + postalCode: string | null + person: Person | null +} + +export interface ContextValue extends Session { + setSession: (session: Partial) => void +} + +export const Context = createContext(undefined) +Context.displayName = 'StoreSessionContext' + +const baseInitialState: Session = { + currency: { + code: 'USD', + symbol: '$', + }, + country: 'USA', + locale: 'en', + postalCode: null, + channel: null, + person: null, +} + +export interface Props { + initialState?: Partial + namespace?: string +} + +export const SessionProvider = ({ + children, + initialState, + namespace = 'main', +}: PropsWithChildren) => { + const [session, setSession] = useStorage( + `${namespace}::store::session`, + () => ({ + ...baseInitialState, + ...initialState, + }) + ) + + const value = useMemo( + () => ({ + ...session, + setSession: (data) => setSession({ ...session, ...data }), + }), + [session, setSession] + ) + + return {children} +} diff --git a/packages/sdk/src/session/useSession.ts b/packages/sdk/src/session/useSession.ts index c1efbbfd34..22a62a56e6 100644 --- a/packages/sdk/src/session/useSession.ts +++ b/packages/sdk/src/session/useSession.ts @@ -1,4 +1,13 @@ -import { Context } from './Provider' +import { Context as SessionContext } from './Session' +import { Context as ValidationContext } from './Revalidate' import { useContext } from '../utils/useContext' -export const useSession = () => useContext(Context) +export const useSession = () => { + const session = useContext(SessionContext) + const isValidating = useContext(ValidationContext) + + return { + ...session, + isValidating, + } +} diff --git a/packages/sdk/src/utils/useValidation.ts b/packages/sdk/src/utils/useValidation.ts new file mode 100644 index 0000000000..dbf0f90a11 --- /dev/null +++ b/packages/sdk/src/utils/useValidation.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react' + +interface Options { + onValidate?: (value: T) => Promise + value: T + setValue: (value: T) => void +} + +const nullable = async () => null + +export const createUseValidationHook = () => { + // Validation queue + let queue = Promise.resolve() + + const useValidation = ({ + onValidate = nullable, + value, + setValue, + }: Options) => { + const [isValidating, setIsValidating] = useState(true) + + useEffect(() => { + let cancel = false + + const revalidate = async () => { + if (cancel) { + return + } + + setIsValidating(true) + const newValue = await onValidate(value) + + if (cancel) { + return + } + + setIsValidating(false) + if (newValue != null) { + setValue(newValue) + } + } + + // Enqueue validation + setTimeout(() => { + queue = queue.then(revalidate) + }, 0) + + return () => { + cancel = true + } + }, [onValidate, setValue, value]) + + return isValidating + } + + return useValidation +}