@hanzo/commerce 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/cart.tsx +3 -12
- package/index.ts +1 -1
- package/package.json +1 -1
- package/service/impls/standalone/orders/index.ts +44 -2
- package/service/impls/standalone/standalone-service.ts +26 -14
- package/types/commerce-service.ts +3 -2
- package/util/index.ts +1 -1
- package/util/use-sync-sku-param-w-current-item.ts +81 -0
- package/util/use-sku-and-facet-params.ts +0 -172
package/components/cart.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import React, { type PropsWithChildren
|
|
2
|
+
import React, { type PropsWithChildren } from 'react'
|
|
3
3
|
import { useRouter } from 'next/navigation'
|
|
4
4
|
import { observer } from 'mobx-react-lite'
|
|
5
5
|
|
|
@@ -23,18 +23,9 @@ const Cart: React.FC<PropsWithChildren & {
|
|
|
23
23
|
isMobile=false,
|
|
24
24
|
hideCheckout=false
|
|
25
25
|
}) => {
|
|
26
|
-
const [loadingCheckout, setLoadingCheckout] = useState(false)
|
|
27
26
|
const cmmc = useCommerce()
|
|
28
27
|
const router = useRouter()
|
|
29
28
|
const auth = useAuth()
|
|
30
|
-
|
|
31
|
-
const checkout = async () => {
|
|
32
|
-
setLoadingCheckout(true)
|
|
33
|
-
if (auth.user) {
|
|
34
|
-
await cmmc.createOrder(auth.user.email)
|
|
35
|
-
}
|
|
36
|
-
router.push('/checkout')
|
|
37
|
-
}
|
|
38
29
|
|
|
39
30
|
return (
|
|
40
31
|
<div className={cn('border p-6 rounded-lg', className)}>
|
|
@@ -50,10 +41,10 @@ const Cart: React.FC<PropsWithChildren & {
|
|
|
50
41
|
</div>
|
|
51
42
|
{cmmc.cartItems.length > 0 && !hideCheckout && (
|
|
52
43
|
<>
|
|
53
|
-
{!auth.loggedIn ? (
|
|
44
|
+
{!(auth && auth.loggedIn) ? (
|
|
54
45
|
<Button size='sm' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/login?redirectUrl=checkout')}>Login to checkout</Button>
|
|
55
46
|
) : (
|
|
56
|
-
<Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={checkout}
|
|
47
|
+
<Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/checkout')}>Checkout</Button>
|
|
57
48
|
)}
|
|
58
49
|
</>
|
|
59
50
|
)}
|
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from './types'
|
|
2
2
|
export * from './service/context'
|
|
3
3
|
export type { StandaloneServiceOptions as ServiceOptions } from './service/impls/standalone/standalone-service'
|
|
4
|
-
export {
|
|
4
|
+
export { useSyncSkuParamWithCurrentItem } from './util'
|
package/package.json
CHANGED
|
@@ -23,12 +23,52 @@ const getDBInstance = (name: string): Firestore => {
|
|
|
23
23
|
|
|
24
24
|
interface SavedOrder {
|
|
25
25
|
email: string
|
|
26
|
+
paymentMethod: string
|
|
27
|
+
status: string
|
|
26
28
|
timestamp: FieldValue
|
|
27
29
|
items: ActualLineItemSnapshot[]
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const createOrder = async (
|
|
31
33
|
email: string,
|
|
34
|
+
paymentMethod: string,
|
|
35
|
+
items: ActualLineItemSnapshot[],
|
|
36
|
+
options: {
|
|
37
|
+
dbName: string
|
|
38
|
+
ordersTable: string
|
|
39
|
+
}
|
|
40
|
+
): Promise<{
|
|
41
|
+
success: boolean,
|
|
42
|
+
error: any,
|
|
43
|
+
id?: string
|
|
44
|
+
}> => {
|
|
45
|
+
|
|
46
|
+
let error: any | null = null
|
|
47
|
+
const ordersRef = collection(getDBInstance(options.dbName), options.ordersTable)
|
|
48
|
+
const orderId = `${email}-${new Date().toISOString()}`
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await setDoc(doc(ordersRef, orderId), {
|
|
52
|
+
email,
|
|
53
|
+
paymentMethod,
|
|
54
|
+
status: 'open',
|
|
55
|
+
timestamp: serverTimestamp(),
|
|
56
|
+
items,
|
|
57
|
+
} satisfies SavedOrder)
|
|
58
|
+
return { success: !error, error, id: orderId }
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
console.error('Error writing item document: ', e)
|
|
62
|
+
error = e
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { success: !error, error }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const updateOrder = async (
|
|
69
|
+
orderId: string,
|
|
70
|
+
email: string,
|
|
71
|
+
paymentMethod: string,
|
|
32
72
|
items: ActualLineItemSnapshot[],
|
|
33
73
|
options: {
|
|
34
74
|
dbName: string
|
|
@@ -43,8 +83,10 @@ const createOrder = async (
|
|
|
43
83
|
const ordersRef = collection(getDBInstance(options.dbName), options.ordersTable)
|
|
44
84
|
|
|
45
85
|
try {
|
|
46
|
-
await setDoc(doc(ordersRef,
|
|
86
|
+
await setDoc(doc(ordersRef, orderId), {
|
|
47
87
|
email,
|
|
88
|
+
paymentMethod,
|
|
89
|
+
status: 'open',
|
|
48
90
|
timestamp: serverTimestamp(),
|
|
49
91
|
items,
|
|
50
92
|
} satisfies SavedOrder)
|
|
@@ -57,4 +99,4 @@ const createOrder = async (
|
|
|
57
99
|
return { success: !error, error }
|
|
58
100
|
}
|
|
59
101
|
|
|
60
|
-
export { createOrder }
|
|
102
|
+
export { createOrder, updateOrder }
|
|
@@ -15,7 +15,10 @@ import type {
|
|
|
15
15
|
FacetsDesc
|
|
16
16
|
} from '../../../types'
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
createOrder as createOrderHelper,
|
|
20
|
+
updateOrder as updateOrderHelper
|
|
21
|
+
} from './orders'
|
|
19
22
|
|
|
20
23
|
import ActualLineItem, { type ActualLineItemSnapshot } from './actual-line-item'
|
|
21
24
|
|
|
@@ -84,9 +87,15 @@ class StandaloneService
|
|
|
84
87
|
})
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
async createOrder(email: string): Promise<
|
|
90
|
+
async createOrder(email: string, paymentMethod: string): Promise<string | undefined> {
|
|
88
91
|
const snapshot = this.takeSnapshot()
|
|
89
|
-
createOrderHelper(email, snapshot.items, this._options) // didn't want to have two levels of 'items'
|
|
92
|
+
const order = await createOrderHelper(email, paymentMethod, snapshot.items, this._options) // didn't want to have two levels of 'items'
|
|
93
|
+
return order.id
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async updateOrder(orderId: string, email: string, paymentMethod: string): Promise<void> {
|
|
97
|
+
const snapshot = this.takeSnapshot()
|
|
98
|
+
updateOrderHelper(orderId, email, paymentMethod, snapshot.items, this._options) // didn't want to have two levels of 'items'
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
takeSnapshot = (): StandaloneServiceSnapshot => ({
|
|
@@ -127,13 +136,14 @@ class StandaloneService
|
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (foundItem)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
139
|
+
for( const [categoryId, category] of this._categoryMap.entries()) {
|
|
140
|
+
if (categoriesTried.includes(categoryId)) continue
|
|
141
|
+
const foundItem = category.products.find((p) => (p.sku === skuToFind)) as ActualLineItem | undefined
|
|
142
|
+
if (foundItem) {
|
|
143
|
+
return foundItem as ActualLineItem
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined
|
|
137
147
|
})();
|
|
138
148
|
|
|
139
149
|
return !!this._currentItem
|
|
@@ -197,13 +207,15 @@ class StandaloneService
|
|
|
197
207
|
const keysStr = Object.keys(this._facetsDesc)
|
|
198
208
|
const keysNum = keysStr.map((key) => (parseInt(key)))
|
|
199
209
|
keysNum.forEach((key) => {
|
|
210
|
+
if (partial[key]) {
|
|
211
|
+
// If present, filter out the bad values (the one's that don't exist in the Desc)
|
|
212
|
+
const filtered = partial[key].filter((fv) => (this._facetsDesc[key].find((fvDesc) => (fvDesc.value === fv))))
|
|
213
|
+
result[key] = filtered
|
|
214
|
+
}
|
|
200
215
|
// if not present, assume the facet is "off" and allow all (include all in the set).
|
|
201
|
-
|
|
216
|
+
else {
|
|
202
217
|
result[key] = this._facetsDesc[key].map((fv) => (fv.value))
|
|
203
218
|
}
|
|
204
|
-
// If present, filter out the bad values if any
|
|
205
|
-
const filtered = partial[key].filter((fv) => this._facetsDesc[key].find((fvDesc) => (fvDesc.value === fv)))
|
|
206
|
-
result[key] = filtered
|
|
207
219
|
})
|
|
208
220
|
return result
|
|
209
221
|
}
|
|
@@ -8,7 +8,8 @@ interface CommerceService extends ObsLineItemRef {
|
|
|
8
8
|
get cartTotal(): number
|
|
9
9
|
getCartCategorySubtotal(categoryId: string): number
|
|
10
10
|
|
|
11
|
-
createOrder(email: string): Promise<
|
|
11
|
+
createOrder(email: string, paymentMethod: string): Promise<string | undefined>
|
|
12
|
+
updateOrder(orderId: string, email: string, paymentMethod: string): Promise<void>
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Sets the tokens at each level supplied.
|
|
@@ -29,7 +30,7 @@ interface CommerceService extends ObsLineItemRef {
|
|
|
29
30
|
* "current" is unrelated to what is "specified",
|
|
30
31
|
* ie, facets' values
|
|
31
32
|
* */
|
|
32
|
-
setCurrentItem(sku: string | undefined): boolean //
|
|
33
|
+
setCurrentItem(sku: string | undefined): boolean // valid sku and was set.
|
|
33
34
|
/**
|
|
34
35
|
* For convenience, so widgets can share state.
|
|
35
36
|
* "current" is unrelated to what is "specified",
|
package/util/index.ts
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { useEffect } from 'react'
|
|
3
|
+
import { reaction } from 'mobx'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
useQueryState,
|
|
7
|
+
parseAsString,
|
|
8
|
+
parseAsBoolean,
|
|
9
|
+
} from 'next-usequerystate'
|
|
10
|
+
|
|
11
|
+
import { useCommerce } from '../service/context'
|
|
12
|
+
|
|
13
|
+
const PLEASE_SELECT_FACETS = 'Please select an option from each group above.'
|
|
14
|
+
|
|
15
|
+
const useSyncSkuParamWithCurrentItem = (
|
|
16
|
+
setMessage: (m: string | undefined) => void,
|
|
17
|
+
setLoading?: (l: boolean) => void
|
|
18
|
+
) => {
|
|
19
|
+
|
|
20
|
+
const cmmc = useCommerce()
|
|
21
|
+
|
|
22
|
+
const [skuParam, setSkuParam] = useQueryState('sku',
|
|
23
|
+
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const [addParam, setAddParam] = useQueryState('add',
|
|
27
|
+
parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true })
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
|
|
32
|
+
return reaction(() => ({
|
|
33
|
+
specifiedCat: cmmc.specifiedCategories.length === 1 ? cmmc.specifiedCategories[0] : undefined,
|
|
34
|
+
currentItem: cmmc.currentItem
|
|
35
|
+
}),
|
|
36
|
+
({specifiedCat, currentItem}) => {
|
|
37
|
+
//console.log("REACTION: " + `CAT ID: ${category?.id} SKU: ${item?.sku}`)
|
|
38
|
+
if (currentItem) {
|
|
39
|
+
// if we set the currentItem w sku, then user selects other facets
|
|
40
|
+
if (specifiedCat && currentItem.categoryId != specifiedCat.id ) {
|
|
41
|
+
cmmc.setCurrentItem(specifiedCat.products[0].sku)
|
|
42
|
+
}
|
|
43
|
+
setSkuParam(cmmc.currentItem!.sku)
|
|
44
|
+
}
|
|
45
|
+
else if (specifiedCat) {
|
|
46
|
+
cmmc.setCurrentItem(specifiedCat.products[0].sku)
|
|
47
|
+
setSkuParam(cmmc.currentItem!.sku)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
|
|
54
|
+
const setCurrentCategoryFromSku = (sku: string) => {
|
|
55
|
+
const toks: string[] = sku.split('-')
|
|
56
|
+
cmmc.setFacets({
|
|
57
|
+
1: [toks[1]],
|
|
58
|
+
2: [toks[2]]
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// setCI returns true if it's a recognized sku
|
|
63
|
+
if (skuParam && cmmc.setCurrentItem(skuParam)) {
|
|
64
|
+
if (addParam) {
|
|
65
|
+
if (cmmc.currentItem!.quantity === 0) {
|
|
66
|
+
cmmc.currentItem!.increment()
|
|
67
|
+
}
|
|
68
|
+
setAddParam(false)
|
|
69
|
+
}
|
|
70
|
+
setCurrentCategoryFromSku(skuParam)
|
|
71
|
+
setMessage(undefined)
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
setMessage(PLEASE_SELECT_FACETS)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setLoading && setLoading(false)
|
|
78
|
+
}, [skuParam])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default useSyncSkuParamWithCurrentItem
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
import {
|
|
3
|
-
useEffect,
|
|
4
|
-
useRef,
|
|
5
|
-
useState,
|
|
6
|
-
} from 'react'
|
|
7
|
-
import { autorun, toJS, type IReactionDisposer } from 'mobx'
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
useQueryState,
|
|
11
|
-
parseAsString,
|
|
12
|
-
parseAsBoolean,
|
|
13
|
-
} from 'next-usequerystate'
|
|
14
|
-
|
|
15
|
-
import type { FacetsValue, StringMutator } from '../types'
|
|
16
|
-
import { useCommerce } from '../service/context'
|
|
17
|
-
|
|
18
|
-
const PLEASE_SELECT_FACETS = 'Please select an option from each group above.'
|
|
19
|
-
|
|
20
|
-
interface GetMutator {
|
|
21
|
-
(level: 1 | 2): StringMutator
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const useSkuAndFacetParams = (
|
|
25
|
-
setLoading?: (l: boolean) => void
|
|
26
|
-
) => {
|
|
27
|
-
|
|
28
|
-
const cmmc = useCommerce()
|
|
29
|
-
|
|
30
|
-
const encRef = useRef<{
|
|
31
|
-
usingSkuMode: boolean
|
|
32
|
-
autoRunDisposer: IReactionDisposer | undefined
|
|
33
|
-
}>({
|
|
34
|
-
usingSkuMode: false,
|
|
35
|
-
autoRunDisposer: undefined,
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const [level1, setLevel1] = useQueryState('lev1',
|
|
39
|
-
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
|
|
40
|
-
)
|
|
41
|
-
const [level2, setLevel2] = useQueryState('lev2',
|
|
42
|
-
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
const [skuParam, setSkuParam] = useQueryState('sku',
|
|
46
|
-
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
const [addParam, setAddParam] = useQueryState('add',
|
|
50
|
-
parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true })
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
const [message, setMessage] = useState<string | undefined>(undefined)
|
|
54
|
-
|
|
55
|
-
const directMutator: GetMutator = (level: 1 | 2): StringMutator => {
|
|
56
|
-
|
|
57
|
-
const setLevel = (value: string, level: 1 | 2 ): void => {
|
|
58
|
-
const facets = cmmc.facetsValue
|
|
59
|
-
facets[level] = [value]
|
|
60
|
-
cmmc.setFacets(facets)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const getLevelValueSafe = (level: 1 | 2): string | null => {
|
|
64
|
-
const facets = cmmc.facetsValue
|
|
65
|
-
if (!(level in facets) || facets[level].length === 0 ) {
|
|
66
|
-
return null
|
|
67
|
-
}
|
|
68
|
-
return facets[level][0]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
get: () => (getLevelValueSafe(level)),
|
|
73
|
-
set: (v: string) => {setLevel(v, level)}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const paramsMutator: GetMutator = (level: 1 | 2): StringMutator => {
|
|
78
|
-
return level === 1 ? {
|
|
79
|
-
get: () => (level1),
|
|
80
|
-
set: setLevel1
|
|
81
|
-
} : {
|
|
82
|
-
get: () => (level2),
|
|
83
|
-
set: setLevel2
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
|
|
89
|
-
const setCurrentCategoryFromSku = (sku: string) => {
|
|
90
|
-
const toks: string[] = sku.split('-')
|
|
91
|
-
cmmc.setFacets({
|
|
92
|
-
1: [toks[1]],
|
|
93
|
-
2: [toks[2]]
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (skuParam) {
|
|
98
|
-
// true if a valid sku
|
|
99
|
-
if (cmmc.setCurrentItem(skuParam)) {
|
|
100
|
-
if (addParam && cmmc.currentItem!.quantity === 0) {
|
|
101
|
-
cmmc.currentItem!.increment()
|
|
102
|
-
setAddParam(false)
|
|
103
|
-
}
|
|
104
|
-
// Marks that we've been here so no need to
|
|
105
|
-
// create an autorun() instance twice.
|
|
106
|
-
if (!encRef.current.usingSkuMode) {
|
|
107
|
-
setCurrentCategoryFromSku(skuParam)
|
|
108
|
-
encRef.current.autoRunDisposer = autorun(() => {
|
|
109
|
-
if (cmmc.currentItem && cmmc.currentItem.sku !== skuParam) {
|
|
110
|
-
setSkuParam(cmmc.currentItem.sku)
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
}
|
|
114
|
-
setMessage(undefined)
|
|
115
|
-
encRef.current.usingSkuMode = true
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
setSkuParam('')
|
|
119
|
-
// if sent here w an invalid sku,
|
|
120
|
-
// it will effectively put us in facet params mode
|
|
121
|
-
setMessage('Invalid sku. ' + PLEASE_SELECT_FACETS)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
setLoading && setLoading(false)
|
|
126
|
-
}, [skuParam])
|
|
127
|
-
|
|
128
|
-
// supposed to be when component unmounts
|
|
129
|
-
/*
|
|
130
|
-
useEffect(() => (() => {
|
|
131
|
-
if (encRef.current.autoRunDisposer) {
|
|
132
|
-
//encRef.current.autoRunDisposer() // no idea why this seems to be called prematurely and so many times <shrug>
|
|
133
|
-
}
|
|
134
|
-
}), [])
|
|
135
|
-
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// Level Params Mode
|
|
139
|
-
useEffect(() => {
|
|
140
|
-
|
|
141
|
-
if (encRef.current.usingSkuMode) return
|
|
142
|
-
|
|
143
|
-
const facets: FacetsValue = { }
|
|
144
|
-
if (level1) { facets[1] = [level1] }
|
|
145
|
-
if (level2) { facets[2] = [level2] }
|
|
146
|
-
//console.log(`LEV1: ${level1}, LEV2: ${level2}`)
|
|
147
|
-
if (level1 && level2) {
|
|
148
|
-
const categories = cmmc.setFacets(facets)
|
|
149
|
-
if (categories && categories.length > 0) {
|
|
150
|
-
cmmc.setCurrentItem(categories[0].products[0].sku)
|
|
151
|
-
setMessage(undefined)
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
cmmc.setCurrentItem(undefined)
|
|
155
|
-
setMessage('Unrecognized facets. ' + PLEASE_SELECT_FACETS)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
setMessage(PLEASE_SELECT_FACETS)
|
|
160
|
-
}
|
|
161
|
-
setLoading && setLoading(false)
|
|
162
|
-
}, [level1 , level2])
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
message,
|
|
166
|
-
getMutator: encRef.current.usingSkuMode ?
|
|
167
|
-
directMutator : paramsMutator
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export default useSkuAndFacetParams
|