Add subscription endpoint for BC

This commit is contained in:
Florian-crg
2021-10-09 11:40:39 +02:00
parent f9644fecef
commit 3e5f54da16
18 changed files with 290 additions and 36 deletions

View File

@@ -6,7 +6,7 @@ import type { Page } from '@commerce/types/page'
import getSlug from '@lib/get-slug' import getSlug from '@lib/get-slug'
import { Github, Vercel } from '@components/icons' import { Github, Vercel } from '@components/icons'
import { Logo, Container } from '@components/ui' import { Logo, Container } from '@components/ui'
import { I18nWidget } from '@components/common' import { I18nWidget, Subscribe } from '@components/common'
import s from './Footer.module.css' import s from './Footer.module.css'
interface Props { interface Props {
@@ -70,6 +70,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
<div> <div>
<span>&copy; 2020 ACME, Inc. All rights reserved.</span> <span>&copy; 2020 ACME, Inc. All rights reserved.</span>
</div> </div>
<Subscribe />
<div className="flex items-center text-primary text-sm"> <div className="flex items-center text-primary text-sm">
<span className="text-primary">Created by</span> <span className="text-primary">Created by</span>
<a <a

View File

@@ -0,0 +1 @@
export { default } from './subscribe'

View File

@@ -0,0 +1,51 @@
import { useState, useEffect } from 'react'
import { validate } from 'email-validator'
import { Button } from '@components/ui'
import useSubscribe from '@framework/subscriptions/use-subscribe'
const Subscribe = () => {
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('')
const subscribe = useSubscribe()
const handleChange = (event) => {
setEmail(event.target.value)
}
const handleSubscribe = async (event) => {
event.preventDefault()
setLoading(true)
try {
await subscribe({
email: email,
})
setLoading(false)
} catch (err) {
console.error(err)
setLoading(false)
}
}
return (
<form onSubmit={handleSubscribe} class="w-full max-w-sm">
<div class="flex items-center border-b border-teal-500 py-2">
<input
class="appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none"
type="email"
placeholder="janedoe@gmail.com"
aria-label="Email"
onChange={handleChange}
/>
<Button variant="slim" className="mt-1 h-8 " loading={loading}>
S'INSCRIRE
</Button>
{/* <button class="flex-shrink-0 bg-teal-500 hover:bg-teal-700 border-teal-500 hover:border-teal-700 text-sm border-4 text-white py-1 px-2 rounded" type="button">
Sign Up
</button> */}
</div>
</form>
)
}
export default Subscribe

View File

@@ -7,3 +7,4 @@ export { default as Searchbar } from './Searchbar'
export { default as UserNav } from './UserNav' export { default as UserNav } from './UserNav'
export { default as Head } from './Head' export { default as Head } from './Head'
export { default as I18nWidget } from './I18nWidget' export { default as I18nWidget } from './I18nWidget'
export { default as Subscribe } from './Subscribe'

View File

@@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import subscriptionsEndpoint from '@commerce/api/endpoints/subscriptions'
import type { SubscriptionsSchema } from '../../../types/subscriptions'
import type { BigcommerceAPI } from '../..'
import subscriptions from './subscriptions'
export type SubscriptionsAPI = GetAPISchema<BigcommerceAPI, SubscriptionsSchema>
export type SubscriptionsEndpoint = SubscriptionsAPI['endpoint']
export const handlers: SubscriptionsEndpoint['handlers'] = { subscriptions }
const subscriptionsApi = createEndpoint<SubscriptionsAPI>({
handler: subscriptionsEndpoint,
handlers,
})
export default subscriptionsApi

View File

@@ -0,0 +1,33 @@
import type { SubscriptionsEndpoint } from '.'
const subscriptions: SubscriptionsEndpoint['handlers']['subscriptions'] = async ({
res,
body: { email, firstName, lastName, source },
config,
}) => {
try {
const { data } = await config.storeApiFetch('/v3/customers/subscribers', {
method: 'POST',
body: {
email,
first_name: firstName,
last_name: lastName,
source
}
})
res.status(200).json({ data: data })
} catch (error) {
console.error(error);
return res.status(400).json({
data: null,
errors: [
{
message: 'Error',
code: 'Error SubscriptionsEndpoint',
},
],
})
}
}
export default subscriptions

View File

@@ -14,6 +14,7 @@ import type { LogoutAPI } from './endpoints/logout'
import type { SignupAPI } from './endpoints/signup' import type { SignupAPI } from './endpoints/signup'
import type { ProductsAPI } from './endpoints/catalog/products' import type { ProductsAPI } from './endpoints/catalog/products'
import type { WishlistAPI } from './endpoints/wishlist' import type { WishlistAPI } from './endpoints/wishlist'
import type { SubscriptionsAPI } from './endpoints/subscriptions'
import login from './operations/login' import login from './operations/login'
import getAllPages from './operations/get-all-pages' import getAllPages from './operations/get-all-pages'
@@ -34,7 +35,7 @@ export interface BigcommerceConfig extends CommerceAPIConfig {
storeChannelId?: string storeChannelId?: string
storeUrl?: string storeUrl?: string
storeApiClientSecret?: string storeApiClientSecret?: string
storeHash?:string storeHash?: string
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T> storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
} }
@@ -81,8 +82,8 @@ const config: BigcommerceConfig = {
storeApiToken: STORE_API_TOKEN, storeApiToken: STORE_API_TOKEN,
storeApiClientId: STORE_API_CLIENT_ID, storeApiClientId: STORE_API_CLIENT_ID,
storeChannelId: STORE_CHANNEL_ID, storeChannelId: STORE_CHANNEL_ID,
storeUrl:STORE_URL, storeUrl: STORE_URL,
storeApiClientSecret:CLIENT_SECRET, storeApiClientSecret: CLIENT_SECRET,
storeHash: STOREFRONT_HASH, storeHash: STOREFRONT_HASH,
storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()), storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()),
} }
@@ -110,6 +111,7 @@ export type APIs =
| SignupAPI | SignupAPI
| ProductsAPI | ProductsAPI
| WishlistAPI | WishlistAPI
| SubscriptionsAPI
export type BigcommerceAPI<P extends Provider = Provider> = CommerceAPI<P> export type BigcommerceAPI<P extends Provider = Provider> = CommerceAPI<P>

View File

@@ -14,6 +14,8 @@ import { handler as useLogin } from './auth/use-login'
import { handler as useLogout } from './auth/use-logout' import { handler as useLogout } from './auth/use-logout'
import { handler as useSignup } from './auth/use-signup' import { handler as useSignup } from './auth/use-signup'
import { handler as useSubscriptions } from './subscriptions/use-subscribe'
import fetcher from './fetcher' import fetcher from './fetcher'
export const bigcommerceProvider = { export const bigcommerceProvider = {
@@ -29,6 +31,7 @@ export const bigcommerceProvider = {
customer: { useCustomer }, customer: { useCustomer },
products: { useSearch }, products: { useSearch },
auth: { useLogin, useLogout, useSignup }, auth: { useLogin, useLogout, useSignup },
subscriptions: { useSubscriptions },
} }
export type BigcommerceProvider = typeof bigcommerceProvider export type BigcommerceProvider = typeof bigcommerceProvider

View File

@@ -0,0 +1 @@
export { default as useSubscribe } from './use-subscribe'

View File

@@ -0,0 +1,38 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useSubscribe, { UseSubscribe } from '@commerce/subscriptions/use-subscribe'
import type { SubscriptionsHook } from '../types/subscriptions'
export default useSubscribe as UseSubscribe<typeof handler>
export const handler: MutationHook<SubscriptionsHook> = {
fetchOptions: {
url: '/api/subscribe',
method: 'POST',
},
async fetcher({ input: { email, firstName, lastName, source }, options, fetch }) {
if (!(email)) {
throw new CommerceError({
message:
'An email is required to subscribe',
})
}
return fetch({
...options,
body: { email, firstName, lastName, source },
})
},
useHook: ({ fetch }) => () => {
return useCallback(
async function subscribe(input) {
const data = await fetch({ input })
return data
},
[fetch]
)
},
}

View File

@@ -0,0 +1 @@
export * from '@commerce/types/subscriptions'

View File

@@ -0,0 +1,35 @@
import type { SubscriptionsSchema } from '../../types/subscriptions'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const subscriptionsEndpoint: GetAPISchema<
any,
SubscriptionsSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
POST: handlers['subscriptions'],
})
) {
return
}
try {
const body = { ...req.body }
return await handlers['subscriptions']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default subscriptionsEndpoint

View File

@@ -11,6 +11,8 @@ import type { WishlistSchema } from '../types/wishlist'
import type { CheckoutSchema } from '../types/checkout' import type { CheckoutSchema } from '../types/checkout'
import type { CustomerCardSchema } from '../types/customer/card' import type { CustomerCardSchema } from '../types/customer/card'
import type { CustomerAddressSchema } from '../types/customer/address' import type { CustomerAddressSchema } from '../types/customer/address'
import type { SubscriptionsSchema } from '../types/subscriptions'
import { import {
defaultOperations, defaultOperations,
OPERATIONS, OPERATIONS,
@@ -29,22 +31,23 @@ export type APISchemas =
| CheckoutSchema | CheckoutSchema
| CustomerCardSchema | CustomerCardSchema
| CustomerAddressSchema | CustomerAddressSchema
| SubscriptionsSchema
export type GetAPISchema< export type GetAPISchema<
C extends CommerceAPI<any>, C extends CommerceAPI<any>,
S extends APISchemas = APISchemas S extends APISchemas = APISchemas
> = { > = {
schema: S schema: S
endpoint: EndpointContext<C, S['endpoint']> endpoint: EndpointContext<C, S['endpoint']>
} }
export type EndpointContext< export type EndpointContext<
C extends CommerceAPI, C extends CommerceAPI,
E extends EndpointSchemaBase E extends EndpointSchemaBase
> = { > = {
handler: Endpoint<C, E> handler: Endpoint<C, E>
handlers: EndpointHandlers<C, E> handlers: EndpointHandlers<C, E>
} }
export type EndpointSchemaBase = { export type EndpointSchemaBase = {
options: {} options: {}
@@ -56,20 +59,20 @@ export type EndpointSchemaBase = {
export type Endpoint< export type Endpoint<
C extends CommerceAPI, C extends CommerceAPI,
E extends EndpointSchemaBase E extends EndpointSchemaBase
> = APIEndpoint<C, EndpointHandlers<C, E>, any, E['options']> > = APIEndpoint<C, EndpointHandlers<C, E>, any, E['options']>
export type EndpointHandlers< export type EndpointHandlers<
C extends CommerceAPI, C extends CommerceAPI,
E extends EndpointSchemaBase E extends EndpointSchemaBase
> = { > = {
[H in keyof E['handlers']]: APIHandler< [H in keyof E['handlers']]: APIHandler<
C, C,
EndpointHandlers<C, E>, EndpointHandlers<C, E>,
E['handlers'][H]['data'], E['handlers'][H]['data'],
E['handlers'][H]['body'], E['handlers'][H]['body'],
E['options'] E['options']
> >
} }
export type APIProvider = { export type APIProvider = {
config: CommerceAPIConfig config: CommerceAPIConfig
@@ -80,7 +83,7 @@ export type CommerceAPI<P extends APIProvider = APIProvider> =
CommerceAPICore<P> & AllOperations<P> CommerceAPICore<P> & AllOperations<P>
export class CommerceAPICore<P extends APIProvider = APIProvider> { export class CommerceAPICore<P extends APIProvider = APIProvider> {
constructor(readonly provider: P) {} constructor(readonly provider: P) { }
getConfig(userConfig: Partial<P['config']> = {}): P['config'] { getConfig(userConfig: Partial<P['config']> = {}): P['config'] {
return Object.entries(userConfig).reduce( return Object.entries(userConfig).reduce(
@@ -139,15 +142,15 @@ export function getEndpoint<
export const createEndpoint = export const createEndpoint =
<API extends GetAPISchema<any, any>>(endpoint: API['endpoint']) => <API extends GetAPISchema<any, any>>(endpoint: API['endpoint']) =>
<P extends APIProvider>( <P extends APIProvider>(
commerce: CommerceAPI<P>, commerce: CommerceAPI<P>,
context?: Partial<API['endpoint']> & { context?: Partial<API['endpoint']> & {
config?: P['config'] config?: P['config']
options?: API['schema']['endpoint']['options'] options?: API['schema']['endpoint']['options']
}
): NextApiHandler => {
return getEndpoint(commerce, { ...endpoint, ...context })
} }
): NextApiHandler => {
return getEndpoint(commerce, { ...endpoint, ...context })
}
export interface CommerceAPIConfig { export interface CommerceAPIConfig {
locale?: string locale?: string
@@ -167,11 +170,11 @@ export interface CommerceAPIConfig {
export type GraphQLFetcher< export type GraphQLFetcher<
Data extends GraphQLFetcherResult = GraphQLFetcherResult, Data extends GraphQLFetcherResult = GraphQLFetcherResult,
Variables = any Variables = any
> = ( > = (
query: string, query: string,
queryData?: CommerceAPIFetchOptions<Variables>, queryData?: CommerceAPIFetchOptions<Variables>,
fetchOptions?: RequestInit fetchOptions?: RequestInit
) => Promise<Data> ) => Promise<Data>
export interface GraphQLFetcherResult<Data = any> { export interface GraphQLFetcherResult<Data = any> {
data: Data data: Data

View File

@@ -16,6 +16,7 @@ import type {
Login, Login,
Logout, Logout,
Checkout, Checkout,
Subscriptions
} from '@commerce/types' } from '@commerce/types'
import type { Fetcher, SWRHook, MutationHook } from './utils/types' import type { Fetcher, SWRHook, MutationHook } from './utils/types'
@@ -62,6 +63,9 @@ export type Provider = CommerceConfig & {
useLogin?: MutationHook<Login.LoginHook> useLogin?: MutationHook<Login.LoginHook>
useLogout?: MutationHook<Logout.LogoutHook> useLogout?: MutationHook<Logout.LogoutHook>
} }
subscriptions?: {
useSubscriptions?: MutationHook<Subscriptions.SubscriptionsHook>
}
} }
export type CommerceConfig = { export type CommerceConfig = {

View File

@@ -0,0 +1,21 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { MutationHook, HookFetcherFn } from '../utils/types'
import type { SubscriptionsHook } from '../types/subscriptions'
import type { Provider } from '..'
export type UseSubscribe<
H extends MutationHook<any> = MutationHook<any>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<any> = mutationFetcher
const fn = (provider: Provider) => provider.subscriptions?.useSubscriptions!
const useSubscribe: any = (...args: any) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useSubscribe

View File

@@ -9,6 +9,7 @@ import * as Product from './product'
import * as Signup from './signup' import * as Signup from './signup'
import * as Site from './site' import * as Site from './site'
import * as Wishlist from './wishlist' import * as Wishlist from './wishlist'
import * as Subscriptions from './subscriptions'
export type { export type {
Cart, Cart,
@@ -22,4 +23,5 @@ export type {
Signup, Signup,
Site, Site,
Wishlist, Wishlist,
Subscriptions
} }

View File

@@ -0,0 +1,35 @@
export type SubscriptionsBody = {
email: string
firstName: string
lastName: string
source: string
}
export type SubscriptionsTypes = {
body: SubscriptionsBody
}
// export type SubscriptionsHook<T extends SubscriptionsTypes = SubscriptionsTypes> = {
// data: null
// body: T['body']
// actionInput: T['body']
// fetcherInput: T['body']
// }
export type SubscriptionsHook<T extends SubscriptionsTypes = SubscriptionsTypes> = {
data: any
input?: any
fetcherInput: any
body: { item: any }
actionInput: any
}
export type SubscriptionsSchema<T extends SubscriptionsTypes = SubscriptionsTypes> = {
endpoint: {
options: {}
handlers: {
subscriptions: SubscriptionsHook<T>
}
}
}

4
pages/api/subscribe.ts Normal file
View File

@@ -0,0 +1,4 @@
import subscriptionsApi from '@framework/api/endpoints/subscriptions'
import commerce from '@lib/api/commerce'
export default subscriptionsApi(commerce)