4
0
forked from crowetic/commerce

Merge branch 'master' of github.com:okbel/e-comm-example

This commit is contained in:
Belen Curcio 2020-10-22 18:14:43 -03:00
commit 98a1b7e8f0
14 changed files with 185 additions and 84 deletions

View File

@ -26,7 +26,7 @@ const getLoggedInCustomer: CustomersHandlers['getLoggedInCustomer'] = async ({
res, res,
config, config,
}) => { }) => {
const data = await config.fetch<GetLoggedInCustomerQuery>( const { data } = await config.fetch<GetLoggedInCustomerQuery>(
getLoggedInCustomerQuery getLoggedInCustomerQuery
) )
const { customer } = data const { customer } = data

View File

@ -18,51 +18,45 @@ const signup: SignupHandlers['signup'] = async ({
// Passwords must be at least 7 characters and contain both alphabetic // Passwords must be at least 7 characters and contain both alphabetic
// and numeric characters. // and numeric characters.
let result: { data?: any } = {} try {
await config.storeApiFetch('/v3/customers', {
method: 'POST',
body: JSON.stringify([
{
first_name: firstName,
last_name: lastName,
email,
authentication: {
new_password: password,
},
},
]),
})
} catch (error) {
if (error instanceof BigcommerceApiError && error.status === 422) {
const hasEmailError = '0.email' in error.data?.errors
// try { // If there's an error with the email, it most likely means it's duplicated
// result = await config.storeApiFetch('/v3/customers', { if (hasEmailError) {
// method: 'POST', return res.status(400).json({
// body: JSON.stringify([ data: null,
// { errors: [
// first_name: firstName, {
// last_name: lastName, message: 'The email is already in use',
// email, code: 'duplicated_email',
// authentication: { },
// new_password: password, ],
// }, })
// }, }
// ]), }
// })
// } catch (error) {
// if (error instanceof BigcommerceApiError && error.status === 422) {
// const hasEmailError = '0.email' in error.data?.errors
// // If there's an error with the email, it most likely means it's duplicated throw error
// if (hasEmailError) { }
// return res.status(400).json({
// data: null,
// errors: [
// {
// message: 'The email is already in use',
// code: 'duplicated_email',
// },
// ],
// })
// }
// }
// throw error // Login the customer right after creating it
// } await login({ variables: { email, password }, res, config })
console.log('DATA', result.data) res.status(200).json({ data: null })
// TODO: Currently not working, fix this asap.
const loginData = await login({ variables: { email, password }, config })
console.log('LOGIN DATA', loginData)
res.status(200).json({ data: result.data ?? null })
} }
export default signup export default signup

View File

@ -49,9 +49,9 @@ async function getAllProductPaths({
config = getConfig(config) config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is // RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query` // required in case there's a custom `query`
const data = await config.fetch<RecursivePartial<GetAllProductPathsQuery>>( const { data } = await config.fetch<
query RecursivePartial<GetAllProductPathsQuery>
) >(query)
const products = data.site?.products?.edges const products = data.site?.products?.edges
return { return {

View File

@ -111,7 +111,7 @@ async function getAllProducts({
// RecursivePartial forces the method to check for every prop in the data, which is // RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query` // required in case there's a custom `query`
const data = await config.fetch<RecursivePartial<GetAllProductsQuery>>( const { data } = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
query, query,
{ variables } { variables }
) )

View File

@ -71,9 +71,10 @@ async function getProduct({
...vars, ...vars,
path: slug ? `/${slug}/` : vars.path!, path: slug ? `/${slug}/` : vars.path!,
} }
const data = await config.fetch<RecursivePartial<GetProductQuery>>(query, { const { data } = await config.fetch<RecursivePartial<GetProductQuery>>(
variables, query,
}) { variables }
)
const product = data.site?.route?.node const product = data.site?.route?.node
if (product?.__typename === 'Product') { if (product?.__typename === 'Product') {

View File

@ -90,9 +90,10 @@ async function getSiteInfo({
config = getConfig(config) config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is // RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query` // required in case there's a custom `query`
const data = await config.fetch<RecursivePartial<GetSiteInfoQuery>>(query, { const { data } = await config.fetch<RecursivePartial<GetSiteInfoQuery>>(
variables, query,
}) { variables }
)
const categories = data.site?.categoryTree const categories = data.site?.categoryTree
const brands = data.site?.brands?.edges const brands = data.site?.brands?.edges

View File

@ -1,8 +1,10 @@
import type { ServerResponse } from 'http'
import type { import type {
LoginMutation, LoginMutation,
LoginMutationVariables, LoginMutationVariables,
} from 'lib/bigcommerce/schema' } from 'lib/bigcommerce/schema'
import type { RecursivePartial } from '../utils/types' import type { RecursivePartial } from '../utils/types'
import concatHeader from '../utils/concat-cookie'
import { BigcommerceConfig, getConfig } from '..' import { BigcommerceConfig, getConfig } from '..'
export const loginMutation = /* GraphQL */ ` export const loginMutation = /* GraphQL */ `
@ -20,28 +22,41 @@ export type LoginVariables = LoginMutationVariables
async function login(opts: { async function login(opts: {
variables: LoginVariables variables: LoginVariables
config?: BigcommerceConfig config?: BigcommerceConfig
res: ServerResponse
}): Promise<LoginResult> }): Promise<LoginResult>
async function login<T extends { result?: any }, V = any>(opts: { async function login<T extends { result?: any }, V = any>(opts: {
query: string query: string
variables: V variables: V
res: ServerResponse
config?: BigcommerceConfig config?: BigcommerceConfig
}): Promise<LoginResult<T>> }): Promise<LoginResult<T>>
async function login({ async function login({
query = loginMutation, query = loginMutation,
variables, variables,
res: response,
config, config,
}: { }: {
query?: string query?: string
variables: LoginVariables variables: LoginVariables
res: ServerResponse
config?: BigcommerceConfig config?: BigcommerceConfig
}): Promise<LoginResult> { }): Promise<LoginResult> {
config = getConfig(config) config = getConfig(config)
const data = await config.fetch<RecursivePartial<LoginMutation>>(query, { const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>(
variables, query,
}) { variables }
)
const cookie = res.headers.get('Set-Cookie')
if (cookie && typeof cookie === 'string') {
response.setHeader(
'Set-Cookie',
concatHeader(response.getHeader('Set-Cookie'), cookie)!
)
}
return { return {
result: data.login?.result, result: data.login?.result,

View File

@ -0,0 +1,14 @@
type Header = string | number | string[] | undefined
export default function concatHeader(prev: Header, val: Header) {
if (!val) return prev
if (!prev) return val
if (Array.isArray(prev)) return prev.concat(String(val))
prev = String(prev)
if (Array.isArray(val)) return [prev].concat(val)
return [prev, String(val)]
}

View File

@ -1,18 +1,21 @@
import { CommerceAPIFetchOptions } from 'lib/commerce/api' import type { GraphQLFetcher } from 'lib/commerce/api'
import { getConfig } from '..' import { getConfig } from '..'
import log from '@lib/logger' import log from '@lib/logger'
export default async function fetchGraphqlApi<Q, V = any>( const fetchGraphqlApi: GraphQLFetcher = async (
query: string, query: string,
{ variables, preview }: CommerceAPIFetchOptions<V> = {} { variables, preview } = {},
): Promise<Q> { fetchOptions
) => {
// log.warn(query) // log.warn(query)
const config = getConfig() const config = getConfig()
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiToken}`, Authorization: `Bearer ${config.apiToken}`,
...fetchOptions?.headers,
'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
query, query,
@ -20,22 +23,13 @@ export default async function fetchGraphqlApi<Q, V = any>(
}), }),
}) })
// console.log('HEADERS', getRawHeaders(res))
const json = await res.json() const json = await res.json()
if (json.errors) { if (json.errors) {
console.error(json.errors) console.error(json.errors)
throw new Error('Failed to fetch BigCommerce API') throw new Error('Failed to fetch BigCommerce API')
} }
return json.data
return { data: json.data, res }
} }
function getRawHeaders(res: Response) { export default fetchGraphqlApi
const headers: { [key: string]: string } = {}
res.headers.forEach((value, key) => {
headers[key] = value
})
return headers
}

View File

@ -4,6 +4,7 @@ import {
CommerceProvider as CoreCommerceProvider, CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce, useCommerce as useCoreCommerce,
} from 'lib/commerce' } from 'lib/commerce'
import { FetcherError } from '@lib/commerce/utils/errors'
async function getText(res: Response) { async function getText(res: Response) {
try { try {
@ -16,9 +17,9 @@ async function getText(res: Response) {
async function getError(res: Response) { async function getError(res: Response) {
if (res.headers.get('Content-Type')?.includes('application/json')) { if (res.headers.get('Content-Type')?.includes('application/json')) {
const data = await res.json() const data = await res.json()
return data.errors[0] return new FetcherError({ errors: data.errors, status: res.status })
} }
return { message: await getText(res) } return new FetcherError({ message: await getText(res), status: res.status })
} }
export const bigcommerceConfig: CommerceConfig = { export const bigcommerceConfig: CommerceConfig = {

View File

@ -1,5 +1,6 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { HookFetcher } from '@lib/commerce/utils/types' import { CommerceError } from '@lib/commerce/utils/errors'
import type { HookFetcher } from '@lib/commerce/utils/types'
import useCommerceSignup from '@lib/commerce/use-signup' import useCommerceSignup from '@lib/commerce/use-signup'
import type { SignupBody } from './api/customers/signup' import type { SignupBody } from './api/customers/signup'
@ -16,9 +17,10 @@ export const fetcher: HookFetcher<null, SignupBody> = (
fetch fetch
) => { ) => {
if (!(firstName && lastName && email && password)) { if (!(firstName && lastName && email && password)) {
throw new Error( throw new CommerceError({
'A first name, last name, email and password are required to signup' message:
) 'A first name, last name, email and password are required to signup',
})
} }
return fetch({ return fetch({

View File

@ -3,14 +3,29 @@ export interface CommerceAPIConfig {
apiToken: string apiToken: string
cartCookie: string cartCookie: string
cartCookieMaxAge: number cartCookieMaxAge: number
fetch<Q, V = any>( fetch<Data = any, Variables = any>(
query: string, query: string,
queryData?: CommerceAPIFetchOptions<V> queryData?: CommerceAPIFetchOptions<Variables>,
): Promise<Q> fetchOptions?: RequestInit
): Promise<GraphQLFetcherResult<Data>>
} }
export interface CommerceAPIFetchOptions<V> { export type GraphQLFetcher<
variables?: V Data extends GraphQLFetcherResult = GraphQLFetcherResult,
Variables = any
> = (
query: string,
queryData?: CommerceAPIFetchOptions<Variables>,
fetchOptions?: RequestInit
) => Promise<Data>
export interface GraphQLFetcherResult<Data = any> {
data: Data
res: Response
}
export interface CommerceAPIFetchOptions<Variables> {
variables?: Variables
preview?: boolean preview?: boolean
} }

View File

@ -0,0 +1,40 @@
export type ErrorData = {
message: string
code?: string
}
export type ErrorProps = {
code?: string
} & (
| { message: string; errors?: never }
| { message?: never; errors: ErrorData[] }
)
export class CommerceError extends Error {
code?: string
errors: ErrorData[]
constructor({ message, code, errors }: ErrorProps) {
const error: ErrorData = message
? { message, ...(code ? { code } : {}) }
: errors![0]
super(error.message)
this.errors = message ? [error] : errors!
if (error.code) this.code = error.code
}
}
export class FetcherError extends CommerceError {
status: number
constructor(
options: {
status: number
} & ErrorProps
) {
super(options)
this.status = options.status
}
}

View File

@ -1,7 +1,29 @@
import useSignup from '@lib/bigcommerce/use-signup'
import { Layout } from '@components/core' import { Layout } from '@components/core'
import { Logo, Modal, Button } from '@components/ui' import { Logo, Modal, Button } from '@components/ui'
export default function Login() { export default function Login() {
const signup = useSignup()
// TODO: use this method
const handleSignup = async () => {
// TODO: validate the password and email before calling the signup
// Passwords must be at least 7 characters and contain both alphabetic
// and numeric characters.
try {
await signup({
// This account already exists, so it will throw the "duplicated_email" error
email: 'luis@vercel.com',
firstName: 'Luis',
lastName: 'Alvarez',
password: 'luis123',
})
} catch (error) {
if (error.code === 'duplicated_email') {
// TODO: handle duplicated email
}
}
}
return ( return (
<div className="pb-20"> <div className="pb-20">
<Modal close={() => {}}> <Modal close={() => {}}>
@ -22,7 +44,9 @@ export default function Login() {
className="focus:outline-none focus:shadow-outline-gray border-none py-2 px-6 w-full appearance-none transition duration-150 ease-in-out placeholder-accents-5 pr-10" className="focus:outline-none focus:shadow-outline-gray border-none py-2 px-6 w-full appearance-none transition duration-150 ease-in-out placeholder-accents-5 pr-10"
/> />
</div> </div>
<Button variant="slim">Log In</Button> <Button variant="slim" onClick={handleSignup}>
Log In
</Button>
<span className="pt-3 text-center text-sm"> <span className="pt-3 text-center text-sm">
<span className="text-accents-7">Don't have an account?</span> <span className="text-accents-7">Don't have an account?</span>
{` `} {` `}