mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 20:26:49 +00:00
wip: Add subscription endpoint for BC
This commit is contained in:
@@ -8,6 +8,7 @@ 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 } from '@components/common'
|
||||||
import s from './Footer.module.css'
|
import s from './Footer.module.css'
|
||||||
|
import Subscribe from '../Subscribe'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
@@ -64,6 +65,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
</a>
|
</a>
|
||||||
<I18nWidget />
|
<I18nWidget />
|
||||||
</div>
|
</div>
|
||||||
|
<Subscribe />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-6 pb-10 flex flex-col md:flex-row justify-between items-center space-y-4 text-accent-6 text-sm">
|
<div className="pt-6 pb-10 flex flex-col md:flex-row justify-between items-center space-y-4 text-accent-6 text-sm">
|
||||||
|
3
components/common/Subscribe/Subscribe.module.css
Normal file
3
components/common/Subscribe/Subscribe.module.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.root {
|
||||||
|
@apply relative text-sm bg-accent-0 text-base w-full transition-colors duration-150 border border-accent-2;
|
||||||
|
}
|
68
components/common/Subscribe/Subscribe.tsx
Normal file
68
components/common/Subscribe/Subscribe.tsx
Normal file
@@ -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<Props> = ({ 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<EventTarget>) => {
|
||||||
|
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 (
|
||||||
|
|
||||||
|
<form onSubmit={handleSubscribe} className={cn(s.root, className)}>
|
||||||
|
<label className="hidden">
|
||||||
|
Subscribe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className={s.input}
|
||||||
|
placeholder="Your email"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="naked"
|
||||||
|
type="submit"
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
>I subscribe </Button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Subscribe
|
1
components/common/Subscribe/index.ts
Normal file
1
components/common/Subscribe/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './Subscribe'
|
18
framework/bigcommerce/api/endpoints/subscriptions/index.ts
Normal file
18
framework/bigcommerce/api/endpoints/subscriptions/index.ts
Normal file
@@ -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<BigcommerceAPI, SubscriptionsSchema>
|
||||||
|
|
||||||
|
export type SubscriptionsEndpoint = SubscriptionsAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: SubscriptionsEndpoint['handlers'] = { subscriptions }
|
||||||
|
|
||||||
|
const subscriptionsApi = createEndpoint<SubscriptionsAPI>({
|
||||||
|
handler: subscriptionsEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default subscriptionsApi
|
@@ -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
|
@@ -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>
|
||||||
|
|
||||||
|
1
framework/bigcommerce/subscriptions/index.ts
Normal file
1
framework/bigcommerce/subscriptions/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as useSubscribe } from './use-subscribe'
|
38
framework/bigcommerce/subscriptions/use-subscribe.tsx
Normal file
38
framework/bigcommerce/subscriptions/use-subscribe.tsx
Normal file
@@ -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]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
35
framework/commerce/api/endpoints/subscribe.ts
Normal file
35
framework/commerce/api/endpoints/subscribe.ts
Normal 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,
|
||||||
|
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
|
@@ -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<any>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommerceConfig = {
|
export type CommerceConfig = {
|
||||||
|
21
framework/commerce/subscriptions/use-subscriptions.tsx
Normal file
21
framework/commerce/subscriptions/use-subscriptions.tsx
Normal file
@@ -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<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
|
||||||
|
|
4
pages/api/subscribe.ts
Normal file
4
pages/api/subscribe.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import subscriptionsApi from '@framework/api/endpoints/subscriptions'
|
||||||
|
import commerce from '@lib/api/commerce'
|
||||||
|
|
||||||
|
export default subscriptionsApi(commerce)
|
Reference in New Issue
Block a user