Skip to content

Commit bc42d94

Browse files
committed
initial commit
0 parents  commit bc42d94

File tree

14 files changed

+9228
-0
lines changed

14 files changed

+9228
-0
lines changed

.babelrc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"plugins": [
3+
[
4+
"@babel/plugin-proposal-optional-chaining",
5+
{
6+
"loose": true
7+
}
8+
]
9+
],
10+
"presets": [
11+
[
12+
"@babel/preset-env",
13+
{
14+
"targets": {
15+
"node": "current"
16+
}
17+
}
18+
],
19+
"@babel/preset-typescript",
20+
"@babel/preset-react"
21+
]
22+
}

.github/workflows/test.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Test
2+
3+
on: push
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v2
10+
11+
- name: Get yarn cache
12+
id: yarn-cache
13+
run: echo "::set-output name=dir::$(yarn cache dir)"
14+
15+
- uses: actions/cache@v1
16+
with:
17+
path: ${{ steps.yarn-cache.outputs.dir }}
18+
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
19+
restore-keys: |
20+
${{ runner.os }}-yarn-
21+
22+
- uses: actions/setup-node@v1
23+
with:
24+
node-version: 14.x
25+
26+
- name: yarn install
27+
run: yarn
28+
env:
29+
CI: true
30+
31+
- name: Run test
32+
run: yarn test

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
node_modules
3+
dist
4+
.next

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# next-shopify

package.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "next-shopify",
3+
"version": "0.0.1",
4+
"main": "./dist/index.js",
5+
"module": "./dist/index.modern.js",
6+
"types": "./dist/index.d.ts",
7+
"source": "./src/index.tsx",
8+
"license": "MIT",
9+
"files": [
10+
"dist"
11+
],
12+
"scripts": {
13+
"prepublish": "yarn build",
14+
"build": "microbundle --jsx React.createElement --compress --no-sourcemap",
15+
"test": "jest"
16+
},
17+
"dependencies": {},
18+
"peerDependencies": {
19+
"next": "*",
20+
"react": "*",
21+
"react-dom": "*",
22+
"react-query": "*",
23+
"shopify-buy": "*"
24+
},
25+
"devDependencies": {
26+
"@babel/core": "^7.13.10",
27+
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
28+
"@babel/preset-env": "^7.13.10",
29+
"@babel/preset-react": "^7.12.13",
30+
"@babel/preset-typescript": "^7.13.0",
31+
"@testing-library/react": "^11.2.5",
32+
"@types/jest": "^26.0.21",
33+
"@types/next": "^9.0.0",
34+
"@types/react": "^16.9.53",
35+
"@types/shopify-buy": "^2.10.7",
36+
"babel-jest": "^26.6.3",
37+
"jest": "^26.6.3",
38+
"microbundle": "^0.12.3",
39+
"prettier": "^2.2.1",
40+
"react": "^17.0.1",
41+
"react-dom": "^17.0.1",
42+
"react-query": "^3.21.0",
43+
"shopify-buy": "^2.11.0",
44+
"typescript": "^4.0.3"
45+
},
46+
"repository": {
47+
"type": "git",
48+
"url": "https://github.com/basementstudio/next-shopify.git"
49+
},
50+
"prettier": {
51+
"semi": false,
52+
"singleQuote": true,
53+
"trailingComma": "none",
54+
"arrowParens": "avoid"
55+
}
56+
}

src/client.tsx

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React from 'react'
2+
import { useMutation, useQuery, useQueryClient } from 'react-query'
3+
import { useToggleState, ToggleState } from './utils'
4+
5+
type LineItem = {
6+
id: string
7+
title: string
8+
quantity: number
9+
variant: {
10+
image: { src: string; altText?: string }
11+
price: string
12+
selectedOptions: { name: string; value: string }[]
13+
}
14+
}
15+
16+
export type Cart = Omit<any, 'checkoutUrl' | 'lineItems'> & {
17+
webUrl: string
18+
lineItems: LineItem[]
19+
}
20+
21+
type Context = {
22+
cartToggleState: ToggleState
23+
cart: Cart | undefined
24+
onAddLineItem: (vars: {
25+
variantId: string
26+
quantity: number
27+
}) => Promise<void>
28+
onRemoveLineItem: (vars: { variantId: string }) => Promise<void>
29+
onUpdateLineItem: (vars: {
30+
variantId: string
31+
quantity: number
32+
}) => Promise<void>
33+
}
34+
35+
const ShopifyContext = React.createContext<Context | undefined>(undefined)
36+
37+
const getQueryKey = (checkoutId: string | null) => ['checkout', checkoutId]
38+
39+
export const ShopifyContextProvider = ({
40+
children
41+
}: {
42+
children?: React.ReactNode
43+
}) => {
44+
const cartToggleState = useToggleState()
45+
const [localStorageCheckoutId, setLocalStorageCheckoutId] = React.useState<
46+
string | null
47+
>(null)
48+
const queryClient = useQueryClient()
49+
50+
const queryKey = React.useMemo(() => getQueryKey(localStorageCheckoutId), [
51+
localStorageCheckoutId
52+
])
53+
54+
React.useEffect(() => {
55+
const checkoutId = localStorage.getItem('checkout-id')
56+
if (checkoutId) setLocalStorageCheckoutId(checkoutId)
57+
else {
58+
fetch('/api/checkout').then(async res => {
59+
const { checkout } = await res.json()
60+
const checkoutId = checkout.id.toString()
61+
queryClient.setQueryData(['checkout', checkoutId], checkout)
62+
localStorage.setItem('checkout-id', checkoutId)
63+
setLocalStorageCheckoutId(checkoutId)
64+
})
65+
}
66+
}, [queryClient])
67+
68+
const { data: cart } = useQuery<Cart>(queryKey, {
69+
enabled: !!localStorageCheckoutId,
70+
queryFn: async () => {
71+
const res = await fetch(`/api/checkout/${localStorageCheckoutId}`)
72+
const { checkout } = await res.json()
73+
const checkoutId = checkout.id.toString()
74+
if (checkoutId !== localStorageCheckoutId) {
75+
// the checkout was invalid
76+
localStorage.setItem('checkout-id', checkoutId)
77+
setLocalStorageCheckoutId(checkoutId)
78+
queryClient.setQueryData(getQueryKey(checkoutId), checkout)
79+
}
80+
return checkout
81+
}
82+
})
83+
84+
const { mutateAsync: onAddLineItem } = useMutation({
85+
mutationFn: async ({
86+
variantId,
87+
quantity
88+
}: {
89+
variantId: string
90+
quantity: number
91+
}) => {
92+
const res = await fetch(`/api/checkout/${localStorageCheckoutId}`, {
93+
method: 'POST',
94+
body: JSON.stringify({ variantId, quantity }),
95+
headers: {
96+
'content-type': 'application/json'
97+
}
98+
})
99+
const { checkout } = await res.json()
100+
return checkout
101+
},
102+
onSuccess: newCheckout => {
103+
queryClient.setQueryData(queryKey, newCheckout)
104+
},
105+
// Always refetch after error or success:
106+
onSettled: () => {
107+
queryClient.invalidateQueries(queryKey)
108+
}
109+
})
110+
111+
const { mutateAsync: onUpdateLineItem } = useMutation({
112+
mutationFn: async ({
113+
variantId,
114+
quantity
115+
}: {
116+
variantId: string
117+
quantity: number
118+
}) => {
119+
const res = await fetch(`/api/checkout/${localStorageCheckoutId}`, {
120+
method: 'PUT',
121+
body: JSON.stringify({ variantId, quantity, putAction: 'update' }),
122+
headers: {
123+
'content-type': 'application/json'
124+
}
125+
})
126+
const { checkout } = await res.json()
127+
return checkout
128+
},
129+
onSuccess: newCheckout => {
130+
queryClient.setQueryData(queryKey, newCheckout)
131+
},
132+
// Always refetch after error or success:
133+
onSettled: () => {
134+
queryClient.invalidateQueries(queryKey)
135+
}
136+
})
137+
138+
const { mutateAsync: onRemoveLineItem } = useMutation({
139+
mutationFn: async ({ variantId }: { variantId: string }) => {
140+
const res = await fetch(`/api/checkout/${localStorageCheckoutId}`, {
141+
method: 'PUT',
142+
body: JSON.stringify({ variantId, putAction: 'remove' }),
143+
headers: {
144+
'content-type': 'application/json'
145+
}
146+
})
147+
const { checkout } = await res.json()
148+
return checkout
149+
},
150+
onSuccess: newCheckout => {
151+
queryClient.setQueryData(queryKey, newCheckout)
152+
},
153+
// Always refetch after error or success:
154+
onSettled: () => {
155+
queryClient.invalidateQueries(queryKey)
156+
}
157+
})
158+
159+
return (
160+
<ShopifyContext.Provider
161+
value={{
162+
cart,
163+
cartToggleState,
164+
onAddLineItem,
165+
onRemoveLineItem,
166+
onUpdateLineItem
167+
}}
168+
>
169+
{children}
170+
</ShopifyContext.Provider>
171+
)
172+
}
173+
174+
export const useShopify = () => {
175+
const ctx = React.useContext(ShopifyContext)
176+
if (ctx === undefined) {
177+
throw new Error('useShopify must be used below <ShopifyContextProvider />')
178+
}
179+
return ctx
180+
}

src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { ShopifyContextProvider, useShopify, Cart } from './client'
2+
export { handleShopify } from './server'

src/lib/api-responses.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { NextApiResponse } from 'next'
2+
3+
// Some helpers for usual http responses
4+
5+
const formatError = (e: unknown) => {
6+
try {
7+
if (typeof e === 'string' ? { message: e } : e) {
8+
switch (typeof e) {
9+
case 'string':
10+
return { message: e }
11+
default:
12+
case 'object': {
13+
const anyError = e as any
14+
return { message: anyError.message, code: anyError.code }
15+
}
16+
}
17+
}
18+
} catch (error) {
19+
return { message: 'An unknown error ocurred.' }
20+
}
21+
}
22+
23+
function success(res: NextApiResponse, json: { [key: string]: any } = {}) {
24+
return res.status(200).json(json)
25+
}
26+
27+
function badRequest(res: NextApiResponse, error: unknown = 'Bad Request') {
28+
console.error(error)
29+
return res.status(400).json({ error: formatError(error) })
30+
}
31+
32+
function notAuthorized(
33+
res: NextApiResponse,
34+
error: unknown = 'Not Authorized'
35+
) {
36+
console.error(error)
37+
return res.status(401).json({ error: formatError(error) })
38+
}
39+
40+
function notFound(res: NextApiResponse, error: unknown = 'Not Found') {
41+
console.error(error)
42+
return res.status(404).json({ error: formatError(error) })
43+
}
44+
45+
function internalServerError(res: NextApiResponse, error: unknown, code = 500) {
46+
console.error(error)
47+
return res.status(code).json({ error: formatError(error) })
48+
}
49+
50+
export { success, badRequest, notAuthorized, notFound, internalServerError }

src/lib/shopify.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Client from 'shopify-buy'
2+
3+
const domain = process.env.NEXT_PUBLIC_SHOPIFY_DOMAIN
4+
const storefrontAccessToken = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN
5+
6+
if (typeof domain !== 'string' || typeof storefrontAccessToken !== 'string') {
7+
throw new Error(
8+
`domain (${domain}) and storefrontAccessToken (${storefrontAccessToken}) must be strings`
9+
)
10+
}
11+
12+
export const client = Client.buildClient({ domain, storefrontAccessToken })

0 commit comments

Comments
 (0)