@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.
@@ -1,5 +1,5 @@
1
1
  'use client'
2
- import React, { type PropsWithChildren, useState } from 'react'
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} disabled={loadingCheckout}>Checkout</Button>
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 { useSkuAndFacetParams } from './util'
4
+ export { useSyncSkuParamWithCurrentItem } from './util'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/commerce",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Library with shopping cart components.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -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, `${email}-${new Date().toISOString()}`), {
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 { createOrder as createOrderHelper } from './orders'
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<void> {
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
- let foundItem: ActualLineItem | undefined = undefined
132
- this._categoryMap.forEach((category, categoryId) => {
133
- if (foundItem) return
134
- if (categoriesTried.includes(categoryId)) return
135
- foundItem = category.products.find((p) => (p.sku === skuToFind)) as ActualLineItem | undefined
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
- if (!partial[key]) {
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<void>
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 // was valid sku and was set.
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
@@ -25,4 +25,4 @@ export function formatPrice(price: number): string {
25
25
  });
26
26
  }
27
27
 
28
- export { default as useSkuAndFacetParams } from './use-sku-and-facet-params'
28
+ export { default as useSyncSkuParamWithCurrentItem } from './use-sync-sku-param-w-current-item'
@@ -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