Merge branch 'outgrow-reaction-commerce-provider' of github.com:outgrow/commerce into outgrow-reaction-commerce-provider

This commit is contained in:
Loan Laux 2021-05-27 20:19:20 +04:00
commit 94c04bf564
No known key found for this signature in database
GPG Key ID: AF9E9BD6548AD52E
8 changed files with 199 additions and 126 deletions

View File

@ -127,4 +127,3 @@ a {
opacity: 1; opacity: 1;
} }
} }

View File

@ -1,3 +1,3 @@
.fit { .fit {
min-height: calc(100vh - 88px); min-height: calc(100vh - 88px);
} }

View File

@ -49,10 +49,10 @@ const ProductView: FC<Props> = ({ product }) => {
await addItem({ await addItem({
productId: String(product.id), productId: String(product.id),
variantId: String(selectedVariant.sku), variantId: String(selectedVariant.id),
pricing: { pricing: {
amount: selectedVariant.price, amount: selectedVariant.price,
currencyCode: product.price.currencyCode, currencyCode: product.price.currencyCode ?? 'USD',
}, },
}) })
openSidebar() openSidebar()

View File

@ -63,6 +63,7 @@ export type ProductVariant = {
// The variant's depth. If a depth was not explicitly specified on the // The variant's depth. If a depth was not explicitly specified on the
// variant, this will be the product's depth. // variant, this will be the product's depth.
depth?: Measurement depth?: Measurement
options: ProductOption[]
} }
// Shopping cart, a.k.a Checkout // Shopping cart, a.k.a Checkout
@ -109,6 +110,10 @@ export type CartItemBody = {
variantId: string variantId: string
productId?: string productId?: string
quantity?: number quantity?: number
pricing?: {
amount: number
currencyCode: string
}
} }
// Body used by the `getCart` operation handler // Body used by the `getCart` operation handler
@ -167,18 +172,18 @@ export interface Product extends Entity {
slug?: string slug?: string
path?: string path?: string
images: ProductImage[] images: ProductImage[]
variants: ProductVariant2[] variants: ProductVariant[]
price: ProductPrice price: ProductPrice
options: ProductOption[] options: ProductOption[]
sku?: string sku?: string
} }
interface ProductOption extends Entity { export interface ProductOption extends Entity {
displayName: string displayName: string
values: ProductOptionValues[] values: ProductOptionValues[]
} }
interface ProductOptionValues { export interface ProductOptionValues {
label: string label: string
hexColors?: string[] hexColors?: string[]
} }
@ -188,11 +193,6 @@ interface ProductImage {
alt?: string alt?: string
} }
interface ProductVariant2 {
id: string | number
options: ProductOption[]
}
interface ProductPrice { interface ProductPrice {
value: number value: number
currencyCode: 'USD' | 'ARS' | string | undefined currencyCode: 'USD' | 'ARS' | string | undefined

View File

@ -1,6 +1,5 @@
import type { Cart } from '../../../types'
import type { CartHandlers } from '../' import type { CartHandlers } from '../'
import getAnomymousCartQuery from '@framework/utils/queries/get-anonymous-cart' import getAnonymousCartQuery from '@framework/utils/queries/get-anonymous-cart'
import accountCartByAccountIdQuery from '@framework/utils/queries/account-cart-by-account-id' import accountCartByAccountIdQuery from '@framework/utils/queries/account-cart-by-account-id'
import getCartCookie from '@framework/api/utils/get-cart-cookie' import getCartCookie from '@framework/api/utils/get-cart-cookie'
import reconcileCarts from '@framework/api/utils/reconcile-carts' import reconcileCarts from '@framework/api/utils/reconcile-carts'
@ -9,7 +8,7 @@ import {
REACTION_ANONYMOUS_CART_TOKEN_COOKIE, REACTION_ANONYMOUS_CART_TOKEN_COOKIE,
REACTION_CART_ID_COOKIE, REACTION_CART_ID_COOKIE,
REACTION_CUSTOMER_TOKEN_COOKIE, REACTION_CUSTOMER_TOKEN_COOKIE,
} from '@framework/const.ts' } from '@framework/const'
import { normalizeCart } from '@framework/utils' import { normalizeCart } from '@framework/utils'
// Return current cart info // Return current cart info
@ -42,7 +41,7 @@ const getCart: CartHandlers['getCart'] = async ({ req, res, config }) => {
} else if (cartId && anonymousCartToken) { } else if (cartId && anonymousCartToken) {
const { const {
data: { cart: rawAnonymousCart }, data: { cart: rawAnonymousCart },
} = await config.fetch(getAnomymousCartQuery, { } = await config.fetch(getAnonymousCartQuery, {
variables: { variables: {
cartId, cartId,
cartToken: anonymousCartToken, cartToken: anonymousCartToken,

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import { CommerceError, ValidationError } from '@commerce/utils/errors' import { CommerceError } from '@commerce/utils/errors'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
import authenticateMutation from '../utils/mutations/authenticate' import authenticateMutation from '../utils/mutations/authenticate'
import { import {

View File

@ -6,7 +6,10 @@
}, },
"generates": { "generates": {
"./framework/reactioncommerce/schema.d.ts": { "./framework/reactioncommerce/schema.d.ts": {
"plugins": ["typescript", "typescript-operations"] "plugins": ["typescript", "typescript-operations"],
"config": {
"avoidOptionals": true
}
}, },
"./framework/reactioncommerce/schema.graphql": { "./framework/reactioncommerce/schema.graphql": {
"plugins": ["schema-ast"] "plugins": ["schema-ast"]

View File

@ -1,119 +1,168 @@
import { Product, Customer } from '@commerce/types' import {
Product,
Customer,
ProductVariant,
ProductOption,
ProductOptionValues,
} from '@commerce/types'
import { import {
Account, Account,
Cart as ReactionCart, Cart as ReactionCart,
ProductPricingInfo,
CatalogProductVariant, CatalogProductVariant,
CartItemEdge, CartItemEdge,
CatalogItemProduct, CatalogItemProduct,
CatalogProduct, CatalogProduct,
ImageInfo, ImageInfo,
Maybe, CartItem,
} from '../schema' } from '../schema'
import type { Cart, LineItem } from '../types' import type { Cart, LineItem } from '../types'
type ProductOption = { const normalizeProductImages = (images: ImageInfo[], name: string) =>
__typename?: string
id: string
displayName: string
values: any[]
}
const money = ({ displayPrice }: ProductPricingInfo) => {
return {
displayPrice,
}
}
const normalizeProductImages = (images: Maybe<ImageInfo>[], name: string) =>
images.map((image) => ({ images.map((image) => ({
url: image?.URLs?.original || image?.URLs?.medium || '', url: image?.URLs?.original || image?.URLs?.medium || '',
alt: name, alt: name,
})) }))
const normalizeProductOption = ({ id, displayName, values }: ProductOption) => { const normalizeProductOption = (variant: CatalogProductVariant) => {
return { const option = <ProductOption>{
__typename: 'MultipleChoiceOption', __typename: 'MultipleChoiceOption',
id, id: variant._id,
displayName, displayName: variant.attributeLabel,
values: values.map((value) => { values: variant.optionTitle ? [{ label: variant.optionTitle }] : [],
let output: any = {
label: value,
}
if (displayName.toLowerCase() === 'color') {
output = {
...output,
hexColors: [value],
}
}
return output
}),
} }
option.values = option.values.map((value) =>
colorizeProductOptionValue(value, option.displayName)
)
return option
} }
const normalizeProductVariants = (variants: CatalogProductVariant[]) => { function colorizeProductOptionValue(
return variants.map((variant) => { value: ProductOptionValues,
const { _id, options, sku, title, pricing = [], variantId } = variant ?? {} displayName: string
const variantPrice = pricing[0]?.price ?? pricing[0]?.minPrice ?? 0 ): ProductOptionValues {
if (displayName.toLowerCase() === 'color') {
return { value.hexColors = [value.label]
id: _id ?? '', }
name: title, return value
sku: sku ?? variantId,
price: variantPrice,
listPrice: pricing[0]?.compareAtPrice?.amount ?? variantPrice,
requiresShipping: true,
options: options?.length
? options.map((option) => {
return normalizeProductOption({
id: option?._id ?? '',
displayName: option?.attributeLabel ?? '',
values: [option?.optionTitle],
})
})
: [],
}
})
} }
export function groupProductOptionsByAttributeLabel( const normalizeProductVariants = (
options: CatalogProductVariant[] variants: Array<CatalogProductVariant>
) { ): ProductVariant[] => {
return options.reduce((groupedOptions, currentOption) => { return variants.reduce(
const attributeLabelIndex = groupedOptions.findIndex((option) => { (productVariants: ProductVariant[], variant: CatalogProductVariant) => {
return ( if (variantHasOptions(variant)) {
option.displayName.toLowerCase() === productVariants.push(...flatVariantOptions(variant))
currentOption?.attributeLabel.toLowerCase() return productVariants
}
const { sku, title, pricing = [], variantId } = variant ?? {}
const variantPrice = pricing[0]?.price ?? pricing[0]?.minPrice ?? 0
productVariants.push(<ProductVariant>{
id: variantId ?? '',
name: title,
sku: sku ?? variantId,
price: variantPrice,
listPrice: pricing[0]?.compareAtPrice?.amount ?? variantPrice,
requiresShipping: true,
options: [normalizeProductOption(variant)],
})
return productVariants
},
[]
)
}
function groupProductOptionsByAttributeLabel(
variants: CatalogProductVariant[]
): ProductOption[] {
return variants.reduce(
(
groupedOptions: ProductOption[],
currentVariant: CatalogProductVariant
) => {
groupedOptions = mergeVariantOptionsWithExistingOptions(
groupedOptions,
currentVariant
) )
})
if (attributeLabelIndex !== -1) { if (variantHasOptions(currentVariant)) {
groupedOptions[attributeLabelIndex].values = [ (<CatalogProductVariant[]>currentVariant.options).forEach(
...groupedOptions[attributeLabelIndex].values, (variantOption) => {
{ groupedOptions = mergeVariantOptionsWithExistingOptions(
label: currentOption?.optionTitle ?? '', groupedOptions,
hexColors: [currentOption?.optionTitle] ?? '', variantOption
}, )
] }
} else { )
groupedOptions = [ }
...groupedOptions,
normalizeProductOption({
id: currentOption?._id ?? '',
displayName: currentOption?.attributeLabel ?? '',
values: [currentOption?.optionTitle ?? ''],
}),
]
}
return groupedOptions return groupedOptions
}, [] as ProductOption[]) },
[]
)
}
function mergeVariantOptionsWithExistingOptions(
groupedOptions: ProductOption[],
currentVariant: CatalogProductVariant
): ProductOption[] {
const matchingOptionIndex = findCurrentVariantOptionsInGroupedOptions(
groupedOptions,
currentVariant
)
return matchingOptionIndex !== -1
? mergeWithExistingOptions(
groupedOptions,
currentVariant,
matchingOptionIndex
)
: addNewProductOption(groupedOptions, currentVariant)
}
function findCurrentVariantOptionsInGroupedOptions(
groupedOptions: ProductOption[],
currentVariant: CatalogProductVariant
): number {
return groupedOptions.findIndex(
(option) =>
option.displayName.toLowerCase() ===
currentVariant.attributeLabel.toLowerCase()
)
}
function mergeWithExistingOptions(
groupedOptions: ProductOption[],
currentVariant: CatalogProductVariant,
matchingOptionIndex: number
) {
const currentVariantOption = normalizeProductOption(currentVariant)
groupedOptions[matchingOptionIndex].values = [
...groupedOptions[matchingOptionIndex].values,
...currentVariantOption.values,
]
return groupedOptions
}
function addNewProductOption(
groupedOptions: ProductOption[],
currentVariant: CatalogProductVariant
) {
return [...groupedOptions, normalizeProductOption(currentVariant)]
} }
export function normalizeProduct(productNode: CatalogItemProduct): Product { export function normalizeProduct(productNode: CatalogItemProduct): Product {
const product = productNode.product as CatalogProduct const product = productNode.product
if (!product) {
return <Product>{}
}
const { const {
_id, _id,
@ -124,10 +173,8 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product {
sku, sku,
media, media,
pricing, pricing,
vendor,
variants, variants,
...rest } = <CatalogProduct>product
} = product
return { return {
id: productId ?? _id, id: productId ?? _id,
@ -136,17 +183,20 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product {
slug: slug?.replace(/^\/+|\/+$/g, '') ?? '', slug: slug?.replace(/^\/+|\/+$/g, '') ?? '',
path: slug ?? '', path: slug ?? '',
sku: sku ?? '', sku: sku ?? '',
images: media?.length ? normalizeProductImages(media, title ?? '') : [], images: media?.length
? normalizeProductImages(<ImageInfo[]>media, title ?? '')
: [],
vendor: product.vendor, vendor: product.vendor,
price: { price: {
value: pricing[0]?.minPrice ?? 0, value: pricing[0]?.minPrice ?? 0,
currencyCode: pricing[0]?.currency.code, currencyCode: pricing[0]?.currency.code,
}, },
variants: variants?.length ? normalizeProductVariants(variants) : [], variants: !!variants
options: variants?.length ? normalizeProductVariants(<CatalogProductVariant[]>variants)
? groupProductOptionsByAttributeLabel(variants) : [],
options: !!variants
? groupProductOptionsByAttributeLabel(<CatalogProductVariant[]>variants)
: [], : [],
...rest,
} }
} }
@ -157,19 +207,28 @@ export function normalizeCart(cart: ReactionCart): Cart {
email: '', email: '',
createdAt: cart.createdAt, createdAt: cart.createdAt,
currency: { currency: {
code: cart.checkout?.summary?.total?.currency.code, code: cart.checkout?.summary?.total?.currency.code ?? '',
}, },
taxesIncluded: false, taxesIncluded: false,
lineItems: cart.items?.edges?.map(normalizeLineItem), lineItems:
lineItemsSubtotalPrice: +cart.checkout?.summary?.itemTotal?.amount, cart.items?.edges?.map((cartItem) =>
subtotalPrice: +cart.checkout?.summary?.itemTotal?.amount, normalizeLineItem(<CartItemEdge>cartItem)
totalPrice: cart.checkout?.summary?.total?.amount, ) ?? [],
lineItemsSubtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0),
subtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0),
totalPrice: cart.checkout?.summary?.total?.amount ?? 0,
discounts: [], discounts: [],
} }
} }
function normalizeLineItem({ function normalizeLineItem(cartItemEdge: CartItemEdge): LineItem {
node: { const cartItem = cartItemEdge.node
if (!cartItem) {
return <LineItem>{}
}
const {
_id, _id,
compareAtPrice, compareAtPrice,
imageURLs, imageURLs,
@ -179,9 +238,8 @@ function normalizeLineItem({
optionTitle, optionTitle,
variantTitle, variantTitle,
quantity, quantity,
}, } = <CartItem>cartItem
}: CartItemEdge): LineItem {
console.log('imageURLs', imageURLs)
return { return {
id: _id, id: _id,
variantId: String(productConfiguration?.productVariantId), variantId: String(productConfiguration?.productVariantId),
@ -193,11 +251,12 @@ function normalizeLineItem({
sku: String(productConfiguration?.productVariantId), sku: String(productConfiguration?.productVariantId),
name: String(optionTitle || variantTitle), name: String(optionTitle || variantTitle),
image: { image: {
url: imageURLs?.original ?? '/product-img-placeholder.svg', url: imageURLs?.thumbnail ?? '/product-img-placeholder.svg',
}, },
requiresShipping: true, requiresShipping: true,
price: priceWhenAdded?.amount, price: priceWhenAdded?.amount,
listPrice: compareAtPrice?.amount, listPrice: compareAtPrice?.amount ?? 0,
options: [],
}, },
path: '', path: '',
discounts: [], discounts: [],
@ -211,12 +270,25 @@ function normalizeLineItem({
export function normalizeCustomer(viewer: Account): Customer { export function normalizeCustomer(viewer: Account): Customer {
if (!viewer) { if (!viewer) {
return {} return <Customer>{}
} }
return { return <Customer>{
firstName: viewer.firstName ?? '', firstName: viewer.firstName ?? '',
lastName: viewer.lastName ?? '', lastName: viewer.lastName ?? '',
email: viewer.primaryEmailAddress, email: viewer.primaryEmailAddress,
} }
} }
function flatVariantOptions(variant: CatalogProductVariant): ProductVariant[] {
const variantOptions = <CatalogProductVariant[]>variant.options
return normalizeProductVariants(variantOptions).map((variantOption) => {
variantOption.options.push(normalizeProductOption(variant))
return variantOption
})
}
function variantHasOptions(variant: CatalogProductVariant) {
return !!variant.options && variant.options.length != 0
}