From 132012cc8d8f50347616e5d5f046c157939c4d92 Mon Sep 17 00:00:00 2001 From: Florian-crg Date: Wed, 29 Sep 2021 13:58:55 +0200 Subject: [PATCH] wip: Add subscription endpoint for BC --- components/common/Footer/Footer.tsx | 2 + .../common/Subscribe/Subscribe.module.css | 3 + components/common/Subscribe/Subscribe.tsx | 68 +++++++++++++++++++ components/common/Subscribe/index.ts | 1 + .../api/endpoints/subscriptions/index.ts | 18 +++++ .../endpoints/subscriptions/subscriptions.ts | 33 +++++++++ framework/bigcommerce/api/index.ts | 8 ++- framework/bigcommerce/subscriptions/index.ts | 1 + .../subscriptions/use-subscribe.tsx | 38 +++++++++++ framework/commerce/api/endpoints/subscribe.ts | 35 ++++++++++ framework/commerce/index.tsx | 4 ++ .../subscriptions/use-subscriptions.tsx | 21 ++++++ pages/api/subscribe.ts | 4 ++ 13 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 components/common/Subscribe/Subscribe.module.css create mode 100644 components/common/Subscribe/Subscribe.tsx create mode 100644 components/common/Subscribe/index.ts create mode 100644 framework/bigcommerce/api/endpoints/subscriptions/index.ts create mode 100644 framework/bigcommerce/api/endpoints/subscriptions/subscriptions.ts create mode 100644 framework/bigcommerce/subscriptions/index.ts create mode 100644 framework/bigcommerce/subscriptions/use-subscribe.tsx create mode 100644 framework/commerce/api/endpoints/subscribe.ts create mode 100644 framework/commerce/subscriptions/use-subscriptions.tsx create mode 100644 pages/api/subscribe.ts diff --git a/components/common/Footer/Footer.tsx b/components/common/Footer/Footer.tsx index 8cfcc9666..0e4f4b940 100644 --- a/components/common/Footer/Footer.tsx +++ b/components/common/Footer/Footer.tsx @@ -8,6 +8,7 @@ import { Github, Vercel } from '@components/icons' import { Logo, Container } from '@components/ui' import { I18nWidget } from '@components/common' import s from './Footer.module.css' +import Subscribe from '../Subscribe' interface Props { className?: string @@ -64,6 +65,7 @@ const Footer: FC = ({ className, pages }) => { +
diff --git a/components/common/Subscribe/Subscribe.module.css b/components/common/Subscribe/Subscribe.module.css new file mode 100644 index 000000000..00d65de28 --- /dev/null +++ b/components/common/Subscribe/Subscribe.module.css @@ -0,0 +1,3 @@ +.root { + @apply relative text-sm bg-accent-0 text-base w-full transition-colors duration-150 border border-accent-2; +} diff --git a/components/common/Subscribe/Subscribe.tsx b/components/common/Subscribe/Subscribe.tsx new file mode 100644 index 000000000..35dc12bb0 --- /dev/null +++ b/components/common/Subscribe/Subscribe.tsx @@ -0,0 +1,68 @@ +import { FC, useCallback, useEffect, useState } from 'react' +import cn from 'classnames' +import s from './Subscribe.module.css' +import { Button } from '@components/ui' +import { validate } from 'email-validator' +import useSubscribe from '@framework/subscriptions/use-subscribe' + +interface Props { + className?: string +} + +const Subscribe: FC = ({ className }) => { + const [email, setEmail] = useState('') + const [dirty, setDirty] = useState(false) + const [disabled, setDisabled] = useState(false) + const [loading, setLoading] = useState(false) + + const subscribe = useSubscribe() + + const handleValidation = useCallback(() => { + // Unable to send form unless fields are valid. + if (dirty) { + setDisabled(!validate(email)) + } + }, [email, dirty]) + + useEffect(() => { + handleValidation() + }, [handleValidation]) + + const handleSubscribe = async (e: React.SyntheticEvent) => { + e.preventDefault() + + if (!dirty && !disabled) { + setDirty(true) + handleValidation() + } + setLoading(true) + try { + await subscribe({ email }) + setLoading(false) + } catch (error) { + console.error(error); + setLoading(false) + setDisabled(false) + } + } + return ( + +
+ + + +
+ ) +} + +export default Subscribe diff --git a/components/common/Subscribe/index.ts b/components/common/Subscribe/index.ts new file mode 100644 index 000000000..8ec6f9768 --- /dev/null +++ b/components/common/Subscribe/index.ts @@ -0,0 +1 @@ +export { default } from './Subscribe' diff --git a/framework/bigcommerce/api/endpoints/subscriptions/index.ts b/framework/bigcommerce/api/endpoints/subscriptions/index.ts new file mode 100644 index 000000000..bbb472da8 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/subscriptions/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import subscriptionsEndpoint from '@commerce/api/endpoints/subscribe' +import type { SubscriptionsSchema } from '../../../types/subscriptions' +import type { BigcommerceAPI } from '../..' +import subscriptions from './subscriptions' + +export type SubscriptionsAPI = GetAPISchema + +export type SubscriptionsEndpoint = SubscriptionsAPI['endpoint'] + +export const handlers: SubscriptionsEndpoint['handlers'] = { subscriptions } + +const subscriptionsApi = createEndpoint({ + handler: subscriptionsEndpoint, + handlers, +}) + +export default subscriptionsApi diff --git a/framework/bigcommerce/api/endpoints/subscriptions/subscriptions.ts b/framework/bigcommerce/api/endpoints/subscriptions/subscriptions.ts new file mode 100644 index 000000000..415896926 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/subscriptions/subscriptions.ts @@ -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 diff --git a/framework/bigcommerce/api/index.ts b/framework/bigcommerce/api/index.ts index 9dbe400f9..bc62318be 100644 --- a/framework/bigcommerce/api/index.ts +++ b/framework/bigcommerce/api/index.ts @@ -14,6 +14,7 @@ import type { LogoutAPI } from './endpoints/logout' import type { SignupAPI } from './endpoints/signup' import type { ProductsAPI } from './endpoints/catalog/products' import type { WishlistAPI } from './endpoints/wishlist' +import type { SubscriptionsAPI } from './endpoints/subscriptions' import login from './operations/login' import getAllPages from './operations/get-all-pages' @@ -34,7 +35,7 @@ export interface BigcommerceConfig extends CommerceAPIConfig { storeChannelId?: string storeUrl?: string storeApiClientSecret?: string - storeHash?:string + storeHash?: string storeApiFetch(endpoint: string, options?: RequestInit): Promise } @@ -81,8 +82,8 @@ const config: BigcommerceConfig = { storeApiToken: STORE_API_TOKEN, storeApiClientId: STORE_API_CLIENT_ID, storeChannelId: STORE_CHANNEL_ID, - storeUrl:STORE_URL, - storeApiClientSecret:CLIENT_SECRET, + storeUrl: STORE_URL, + storeApiClientSecret: CLIENT_SECRET, storeHash: STOREFRONT_HASH, storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()), } @@ -110,6 +111,7 @@ export type APIs = | SignupAPI | ProductsAPI | WishlistAPI + | SubscriptionsAPI export type BigcommerceAPI

= CommerceAPI

diff --git a/framework/bigcommerce/subscriptions/index.ts b/framework/bigcommerce/subscriptions/index.ts new file mode 100644 index 000000000..14c9d2232 --- /dev/null +++ b/framework/bigcommerce/subscriptions/index.ts @@ -0,0 +1 @@ +export { default as useSubscribe } from './use-subscribe' diff --git a/framework/bigcommerce/subscriptions/use-subscribe.tsx b/framework/bigcommerce/subscriptions/use-subscribe.tsx new file mode 100644 index 000000000..dda7e6176 --- /dev/null +++ b/framework/bigcommerce/subscriptions/use-subscribe.tsx @@ -0,0 +1,38 @@ +import { useCallback } from 'react' +// import type { MutationHook } from '@commerce/utils/types' +import { CommerceError } from '@commerce/utils/errors' +import useSubscriptions from '@commerce/subscriptions/use-subscriptions' +// import type { SubscriptionsHook } from '../types/subscriptions' + + +export default useSubscriptions as any + +export const handler: any = { + fetchOptions: { + url: '/api/subscribe', + method: 'POST', + }, + async fetcher({ input: { email, firstName, lastName, source }, options, fetch }: any) { + if (!(email)) { + throw new CommerceError({ + message: + 'An email is required to subscribe', + }) + } + + return fetch({ + ...options, + body: { email, firstName, lastName, source }, + }) + }, + useHook: ({ fetch }: any) => () => { + + return useCallback( + async function subscribe(input) { + const data = await fetch({ input }) + return data + }, + [fetch] + ) + }, +} \ No newline at end of file diff --git a/framework/commerce/api/endpoints/subscribe.ts b/framework/commerce/api/endpoints/subscribe.ts new file mode 100644 index 000000000..d4afe06d6 --- /dev/null +++ b/framework/commerce/api/endpoints/subscribe.ts @@ -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, + any +>['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 diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index eaa878a9e..672f39bf3 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -16,6 +16,7 @@ import type { Login, Logout, Checkout, + // Subscriptions } from '@commerce/types' import type { Fetcher, SWRHook, MutationHook } from './utils/types' @@ -62,6 +63,9 @@ export type Provider = CommerceConfig & { useLogin?: MutationHook useLogout?: MutationHook } + subscriptions?: { + useSubscriptions?: MutationHook + } } export type CommerceConfig = { diff --git a/framework/commerce/subscriptions/use-subscriptions.tsx b/framework/commerce/subscriptions/use-subscriptions.tsx new file mode 100644 index 000000000..9a9bd596e --- /dev/null +++ b/framework/commerce/subscriptions/use-subscriptions.tsx @@ -0,0 +1,21 @@ +import { useHook, useMutationHook } from '../utils/use-hook' +import { mutationFetcher } from '../utils/default-fetcher' +import type { HookFetcherFn, MutationHook } from '../utils/types' +// import type { SubscriptionsHook } from '../types/subscriptions' +import type { Provider } from '../' + +export type UseSubscribe< + H extends MutationHook = MutationHook + > = ReturnType + +export const fetcher: HookFetcherFn = 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 + diff --git a/pages/api/subscribe.ts b/pages/api/subscribe.ts new file mode 100644 index 000000000..8cf6fe947 --- /dev/null +++ b/pages/api/subscribe.ts @@ -0,0 +1,4 @@ +import subscriptionsApi from '@framework/api/endpoints/subscriptions' +import commerce from '@lib/api/commerce' + +export default subscriptionsApi(commerce)