@hanzo/commerce 5.0.0 → 5.1.0
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/buy-item/select-category-and-item-widget.tsx +2 -2
- package/components/buy-item/select-category-item-card.tsx +2 -2
- package/components/cart-accordian.tsx +4 -2
- package/components/cart-panel/cart-line-item.tsx +33 -21
- package/components/cart-panel/index.tsx +39 -7
- package/components/cart-panel/promo-code.tsx +98 -0
- package/components/category-item-radio-selector.tsx +2 -2
- package/components/category-item-scroll-selector.tsx +2 -2
- package/components/payment-step-form/card-icon-row.tsx +2 -1
- package/components/payment-step-form/index.tsx +2 -2
- package/components/payment-step-form/methods/bank-transfer.tsx +19 -50
- package/components/payment-step-form/methods/card.tsx +88 -85
- package/components/payment-step-form/methods/crypto.tsx +2 -2
- package/components/product-card.tsx +2 -2
- package/components/select-category-item-panel.tsx +2 -2
- package/index.ts +1 -1
- package/package.json +1 -1
- package/service/context.tsx +1 -0
- package/service/impls/standalone/standalone-service.ts +56 -4
- package/types/checkout.ts +1 -2
- package/types/commerce-service.ts +9 -1
- package/types/index.ts +1 -0
- package/types/promo.ts +10 -0
- package/util/index.ts +3 -2
- package/util/promo-codes.ts +46 -0
|
@@ -7,12 +7,12 @@ import { cn } from '@hanzo/ui/util'
|
|
|
7
7
|
|
|
8
8
|
import type { FacetValueDesc, FacetsValue, LineItem } from '../../types'
|
|
9
9
|
import { useCommerce } from '../../service/context'
|
|
10
|
-
import {
|
|
10
|
+
import { formatCurrencyValue } from '../../util'
|
|
11
11
|
|
|
12
12
|
import FacetTogglesWidget from '../facet-values-widget'
|
|
13
13
|
|
|
14
14
|
const formatItem = (item: LineItem, withQuantity: boolean = false): string => (
|
|
15
|
-
`${item.titleAsOption}, ${
|
|
15
|
+
`${item.titleAsOption}, ${formatCurrencyValue(item.price)}${(withQuantity && item.quantity > 0) ? ` (${item.quantity})` : ''}`
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
const SelectCategoryAndItemWidget: React.FC<{
|
|
@@ -10,7 +10,7 @@ import type { ItemSelector, LineItem } from '../../types'
|
|
|
10
10
|
import AddToCartWidget from '../add-to-cart-widget'
|
|
11
11
|
import CategoryItemRadioSelector from '../category-item-radio-selector'
|
|
12
12
|
import CategoryItemScrollSelector from '../category-item-scroll-selector'
|
|
13
|
-
import {
|
|
13
|
+
import { formatCurrencyValue } from '../../util'
|
|
14
14
|
|
|
15
15
|
const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & ItemSelector & {
|
|
16
16
|
isLoading?: boolean
|
|
@@ -40,7 +40,7 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
|
|
|
40
40
|
const item = category.products[0] as LineItem
|
|
41
41
|
return (
|
|
42
42
|
<div className={cn('flex flex-col justify-center items-center ' + (mobilePicker ? 'h-[180px] ' : 'h-auto min-h-24'), className)}>
|
|
43
|
-
<p className='text-lg text-center font-semibold'>{item.titleAsOption + ', ' +
|
|
43
|
+
<p className='text-lg text-center font-semibold'>{item.titleAsOption + ', ' + formatCurrencyValue(item.price)}</p>
|
|
44
44
|
</div>
|
|
45
45
|
)
|
|
46
46
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
AccordionTrigger,
|
|
12
12
|
} from '@hanzo/ui/primitives'
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { formatCurrencyValue, useCommerce } from '..'
|
|
15
15
|
|
|
16
16
|
import CartPanel from './cart-panel'
|
|
17
17
|
|
|
@@ -35,7 +35,7 @@ const CartAccordian: React.FC<{
|
|
|
35
35
|
</h5>
|
|
36
36
|
</div>
|
|
37
37
|
<div className='flex gap-1 items-center'>
|
|
38
|
-
<h5 className='text-sm sm:text-xl grow truncate'>{
|
|
38
|
+
<h5 className='text-sm sm:text-xl grow truncate'>{formatCurrencyValue(cmmc.promoAppliedCartTotal)}</h5>
|
|
39
39
|
<ChevronRight className="h-5 w-5 -mr-2 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
|
40
40
|
</div>
|
|
41
41
|
</AccordionTrigger>
|
|
@@ -46,6 +46,8 @@ const CartAccordian: React.FC<{
|
|
|
46
46
|
scrollHeightClx='h-[350px]'
|
|
47
47
|
itemClx='mt-2'
|
|
48
48
|
totalClx='sticky px-1 pr-2 border-t -bottom-[1px] bg-background'
|
|
49
|
+
showShipping
|
|
50
|
+
showPromoCode
|
|
49
51
|
/>
|
|
50
52
|
</AccordionContent>
|
|
51
53
|
</AccordionItem>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
1
3
|
import React from 'react'
|
|
2
4
|
import Image from 'next/image'
|
|
3
5
|
import { observer } from 'mobx-react-lite'
|
|
@@ -5,8 +7,9 @@ import { observer } from 'mobx-react-lite'
|
|
|
5
7
|
import { cn } from '@hanzo/ui/util'
|
|
6
8
|
|
|
7
9
|
import type { LineItem } from '../../types'
|
|
8
|
-
import {
|
|
10
|
+
import { formatCurrencyValue } from '../../util'
|
|
9
11
|
import AddToCartWidget from '../add-to-cart-widget'
|
|
12
|
+
import { useCommerce } from '../..'
|
|
10
13
|
|
|
11
14
|
const DEF_IMG_SIZE=40
|
|
12
15
|
|
|
@@ -22,30 +25,39 @@ const CartLineItem: React.FC<{
|
|
|
22
25
|
item: LineItem
|
|
23
26
|
className?: string
|
|
24
27
|
imgSizePx?: number
|
|
25
|
-
|
|
28
|
+
showPromoCode?: boolean
|
|
29
|
+
}> = observer(({
|
|
26
30
|
item,
|
|
27
31
|
className='',
|
|
28
|
-
imgSizePx=DEF_IMG_SIZE
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)}
|
|
37
|
-
<div className='
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
|
|
32
|
+
imgSizePx=DEF_IMG_SIZE,
|
|
33
|
+
showPromoCode
|
|
34
|
+
}) => {
|
|
35
|
+
|
|
36
|
+
const cmmc = useCommerce()
|
|
37
|
+
const promoPrice = showPromoCode ? cmmc.itemPromoPrice(item) : undefined
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={cn('flex flex-col justify-start items-start text-sm font-sans', className)}>
|
|
41
|
+
<div className='flex flex-row justify-between items-center gap-2'>
|
|
42
|
+
{!!item.img ? (
|
|
43
|
+
<Image src={item.img} alt={item.title + ' image'} height={imgSizePx} width={imgSizePx} className=''/>
|
|
44
|
+
) : ( // placeholder so things align
|
|
45
|
+
<div style={{height: imgSizePx, width: imgSizePx}}/>
|
|
46
|
+
)}
|
|
47
|
+
<div className='grow leading-tight'>{renderTitle(item.title)}</div>
|
|
43
48
|
</div>
|
|
44
|
-
<div className='flex flex-row items-center justify-
|
|
45
|
-
|
|
49
|
+
<div className='flex flex-row items-center justify-between w-full'>
|
|
50
|
+
<div className='flex flex-row items-center'>
|
|
51
|
+
<AddToCartWidget className='' item={item} size='xs' buttonClx='h-8 md:h-6' ghost/>
|
|
52
|
+
{item.quantity > 1 && (<span className='pl-2.5'>{'@' + formatCurrencyValue(item.price)}</span>)}
|
|
53
|
+
</div>
|
|
54
|
+
<div className='flex flex-row gap-1 items-center justify-end'>
|
|
55
|
+
<span className={promoPrice ? 'line-through text-muted-2' : ''}>{formatCurrencyValue(item.price * item.quantity)}</span>
|
|
56
|
+
{promoPrice && <span>{formatCurrencyValue(promoPrice * item.quantity)}</span>}
|
|
57
|
+
</div>
|
|
46
58
|
</div>
|
|
47
59
|
</div>
|
|
48
|
-
|
|
49
|
-
)
|
|
60
|
+
)
|
|
61
|
+
})
|
|
50
62
|
|
|
51
63
|
export default CartLineItem
|
|
@@ -6,10 +6,11 @@ import { Button, ScrollArea } from '@hanzo/ui/primitives'
|
|
|
6
6
|
import { cn } from '@hanzo/ui/util'
|
|
7
7
|
|
|
8
8
|
import { useCommerce } from '../../service/context'
|
|
9
|
-
import {
|
|
9
|
+
import { formatCurrencyValue } from '../../util'
|
|
10
10
|
import { sendFBEvent, sendGAEvent } from '../../util/analytics'
|
|
11
11
|
|
|
12
12
|
import CartLineItem from './cart-line-item'
|
|
13
|
+
import PromoCode from './promo-code'
|
|
13
14
|
|
|
14
15
|
const CartPanel: React.FC<PropsWithChildren & {
|
|
15
16
|
/** fix size and scroll after 'scrollAfter items in teh cart. */
|
|
@@ -22,6 +23,8 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
22
23
|
noItemsClx?: string
|
|
23
24
|
totalClx?: string
|
|
24
25
|
buttonClx?: string
|
|
26
|
+
showPromoCode?: boolean
|
|
27
|
+
showShipping?: boolean
|
|
25
28
|
/** if not provided, 'checkout' button will be rendered */
|
|
26
29
|
handleCheckout?: () => void
|
|
27
30
|
}> = observer(({
|
|
@@ -36,6 +39,8 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
36
39
|
noItemsClx='',
|
|
37
40
|
totalClx='',
|
|
38
41
|
buttonClx='',
|
|
42
|
+
showPromoCode=false,
|
|
43
|
+
showShipping=false,
|
|
39
44
|
handleCheckout,
|
|
40
45
|
}) => {
|
|
41
46
|
|
|
@@ -69,19 +74,46 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
69
74
|
handleCheckout && handleCheckout()
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
const Main: React.FC = () => (<>
|
|
77
|
+
const Main: React.FC = observer(() => (<>
|
|
73
78
|
{cmmc.cartEmpty ? (
|
|
74
79
|
<p className={cn('text-center mt-4', noItemsClx)}>No items in cart</p>
|
|
75
80
|
) : (<>
|
|
76
81
|
{cmmc.cartItems.map((item) => (
|
|
77
|
-
<CartLineItem
|
|
82
|
+
<CartLineItem
|
|
83
|
+
key={`mobile-${item.sku}`}
|
|
84
|
+
imgSizePx={imgSizePx}
|
|
85
|
+
item={item}
|
|
86
|
+
className={cn('mb-2', itemClx)}
|
|
87
|
+
showPromoCode={showPromoCode}
|
|
88
|
+
/>
|
|
78
89
|
))}
|
|
79
90
|
</>)}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<
|
|
91
|
+
{showPromoCode && <PromoCode/>}
|
|
92
|
+
{(showShipping || showPromoCode) && (
|
|
93
|
+
<div className='flex flex-col gap-1 py-2 border-t'>
|
|
94
|
+
<p className='flex justify-between'>
|
|
95
|
+
<span className='text-muted-1'>Subtotal</span>
|
|
96
|
+
<span className='font-semibold'>{cmmc.cartTotal === 0 ? '0' : formatCurrencyValue(cmmc.cartTotal)}</span>
|
|
97
|
+
</p>
|
|
98
|
+
{cmmc.promoAppliedCartTotal !== cmmc.cartTotal && (
|
|
99
|
+
<p className='flex justify-between'>
|
|
100
|
+
<span className='text-muted-1'>Promo Discount</span>
|
|
101
|
+
<span className='font-semibold'>-{formatCurrencyValue(cmmc.cartTotal - cmmc.promoAppliedCartTotal)}</span>
|
|
102
|
+
</p>
|
|
103
|
+
)}
|
|
104
|
+
{showShipping && (
|
|
105
|
+
<p className='flex justify-between'>
|
|
106
|
+
<span className='text-muted-1'>Shipping</span>
|
|
107
|
+
<span className='font-semibold'>Free Global Shipping</span>
|
|
108
|
+
</p>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
<p className={cn('border-t py-2 flex justify-between', totalClx)}>
|
|
113
|
+
TOTAL
|
|
114
|
+
<span className='font-semibold'>{formatCurrencyValue(showPromoCode ? cmmc.promoAppliedCartTotal : cmmc.cartTotal)}</span>
|
|
83
115
|
</p>
|
|
84
|
-
</>)
|
|
116
|
+
</>))
|
|
85
117
|
|
|
86
118
|
const scrolling = (): boolean => (cmmc.cartItems.length > scrollAfter)
|
|
87
119
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useSearchParams } from 'next/navigation'
|
|
5
|
+
import { useForm } from 'react-hook-form'
|
|
6
|
+
import * as z from 'zod'
|
|
7
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
8
|
+
import { Check } from 'lucide-react'
|
|
9
|
+
import { observer } from 'mobx-react-lite'
|
|
10
|
+
|
|
11
|
+
import { Button, Form, FormControl, FormField, FormItem, Input } from '@hanzo/ui/primitives'
|
|
12
|
+
|
|
13
|
+
import { useCommerce } from '../..'
|
|
14
|
+
import type { Promo } from '../../types'
|
|
15
|
+
import getPromoFromApi from '../../util/promo-codes'
|
|
16
|
+
|
|
17
|
+
const formSchema = z.object({
|
|
18
|
+
code: z.string().min(1, 'Invalid code'),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const PromoCode = observer(() => {
|
|
22
|
+
const cmmc = useCommerce()
|
|
23
|
+
const searchParams = useSearchParams()
|
|
24
|
+
|
|
25
|
+
const [codeAccepted, setCodeAccepted] = useState<boolean>(false)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const code = searchParams.get('code')
|
|
29
|
+
if (code) {
|
|
30
|
+
getPromoFromApi(code).then((promo?: Promo) => {
|
|
31
|
+
if (promo) {
|
|
32
|
+
form.setValue('code', code)
|
|
33
|
+
setCodeAccepted(true)
|
|
34
|
+
cmmc.setAppliedPromo(promo)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}, [searchParams])
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (cmmc.appliedPromo) {
|
|
42
|
+
form.setValue('code', cmmc.appliedPromo.code)
|
|
43
|
+
setCodeAccepted(true)
|
|
44
|
+
}
|
|
45
|
+
}, [cmmc.appliedPromo])
|
|
46
|
+
|
|
47
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
48
|
+
resolver: zodResolver(formSchema),
|
|
49
|
+
defaultValues: {
|
|
50
|
+
code: '',
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const applyPromoCode = async (values: z.infer<typeof formSchema>) => {
|
|
55
|
+
const { code } = values
|
|
56
|
+
const promo = await getPromoFromApi(code)
|
|
57
|
+
if (!promo) {
|
|
58
|
+
form.setError('code', { message: 'Invalid code' })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
setCodeAccepted(true)
|
|
62
|
+
cmmc.setAppliedPromo(promo)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const removePromoCode = () => {
|
|
66
|
+
cmmc.setAppliedPromo(null)
|
|
67
|
+
setCodeAccepted(false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className='flex flex-col gap-2 border-t py-2'>
|
|
72
|
+
<Form {...form}>
|
|
73
|
+
<form onSubmit={form.handleSubmit(applyPromoCode)}>
|
|
74
|
+
<div className='flex gap-2 items-center'>
|
|
75
|
+
<FormField
|
|
76
|
+
control={form.control}
|
|
77
|
+
name='code'
|
|
78
|
+
render={({ field }) => (
|
|
79
|
+
<FormItem className='w-full'>
|
|
80
|
+
<FormControl>
|
|
81
|
+
<Input {...field} placeholder='Discount or invite code' onInput={removePromoCode}/>
|
|
82
|
+
</FormControl>
|
|
83
|
+
</FormItem>
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
{codeAccepted ? (
|
|
87
|
+
<Check/>
|
|
88
|
+
) : (
|
|
89
|
+
<Button variant='outline' className='!w-fit !min-w-0 font-inter'>Apply</Button>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</form>
|
|
93
|
+
</Form>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export default PromoCode
|
|
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite'
|
|
|
4
4
|
|
|
5
5
|
import { Label, RadioGroup, RadioGroupItem } from '@hanzo/ui/primitives'
|
|
6
6
|
import type { ItemSelector, LineItem, Product } from '../types'
|
|
7
|
-
import {
|
|
7
|
+
import { formatCurrencyValue } from '../util'
|
|
8
8
|
import { cn } from '@hanzo/ui/util'
|
|
9
9
|
|
|
10
10
|
const CategoryItemRadioSelector: React.FC<ItemSelector & {
|
|
@@ -29,7 +29,7 @@ const CategoryItemRadioSelector: React.FC<ItemSelector & {
|
|
|
29
29
|
}) => (
|
|
30
30
|
<div className={cn(className, itemClx)}>
|
|
31
31
|
<RadioGroupItem value={item.sku} id={item.sku} />
|
|
32
|
-
<Label htmlFor={item.sku}>{item.titleAsOption + (showPrice ? (', ' +
|
|
32
|
+
<Label htmlFor={item.sku}>{item.titleAsOption + (showPrice ? (', ' + formatCurrencyValue(item.price)) : '')}</Label>
|
|
33
33
|
</div>
|
|
34
34
|
)
|
|
35
35
|
|
|
@@ -6,7 +6,7 @@ import { cn } from '@hanzo/ui/util'
|
|
|
6
6
|
import { ScrollArea } from '@hanzo/ui/primitives'
|
|
7
7
|
|
|
8
8
|
import type { ItemSelector } from '../types'
|
|
9
|
-
import {
|
|
9
|
+
import { formatCurrencyValue } from '../util'
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
const CategoryItemScrollSelector: React.FC<ItemSelector & {
|
|
@@ -29,7 +29,7 @@ const CategoryItemScrollSelector: React.FC<ItemSelector & {
|
|
|
29
29
|
(iRef.item?.sku === prod.sku) ? 'font-semibold text-accent' : 'text-muted',
|
|
30
30
|
itemClx
|
|
31
31
|
)}>
|
|
32
|
-
{prod.titleAsOption + ', ' +
|
|
32
|
+
{prod.titleAsOption + ', ' + formatCurrencyValue(prod.price)}
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
35
|
))}
|
|
@@ -10,8 +10,9 @@ import Jcb from './card-icons/jcb'
|
|
|
10
10
|
|
|
11
11
|
const PaymentMethods: React.FC = () => {
|
|
12
12
|
return (
|
|
13
|
-
<div className='flex gap-1 items-center text-muted-1'>
|
|
13
|
+
<div className='flex gap-1 items-center text-muted-1 pb-3'>
|
|
14
14
|
<LockKeyhole className='w-4 h-4'/>
|
|
15
|
+
<span className='hidden sm:flex text-sm'>Secure payments with</span>
|
|
15
16
|
<Amex className='w-9 h-5'/>
|
|
16
17
|
<Discover className='w-9 h-5'/>
|
|
17
18
|
<Mastercard className='w-9 h-5'/>
|
|
@@ -60,7 +60,7 @@ const PaymentStepForm: React.FC<CheckoutStepComponentProps> = observer(({
|
|
|
60
60
|
price: item.price,
|
|
61
61
|
quantity: item.quantity
|
|
62
62
|
})),
|
|
63
|
-
value: cmmc.
|
|
63
|
+
value: cmmc.promoAppliedCartTotal,
|
|
64
64
|
currency: 'USD',
|
|
65
65
|
payment_type: paymentInfo.paymentMethod ?? ''
|
|
66
66
|
})
|
|
@@ -69,7 +69,7 @@ const PaymentStepForm: React.FC<CheckoutStepComponentProps> = observer(({
|
|
|
69
69
|
id: item.sku,
|
|
70
70
|
quantity: item.quantity
|
|
71
71
|
})),
|
|
72
|
-
value: cmmc.
|
|
72
|
+
value: cmmc.promoAppliedCartTotal,
|
|
73
73
|
currency: 'USD'
|
|
74
74
|
})
|
|
75
75
|
await cmmc.updateOrderPaymentInfo(id, paymentInfo)
|
|
@@ -3,14 +3,7 @@
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import { Copy } from 'lucide-react'
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
Button,
|
|
8
|
-
Tabs,
|
|
9
|
-
TabsContent,
|
|
10
|
-
TabsList,
|
|
11
|
-
TabsTrigger,
|
|
12
|
-
toast
|
|
13
|
-
} from '@hanzo/ui/primitives'
|
|
6
|
+
import { Button, toast } from '@hanzo/ui/primitives'
|
|
14
7
|
|
|
15
8
|
import type { PaymentMethodComponentProps } from '../../../types'
|
|
16
9
|
|
|
@@ -57,51 +50,27 @@ const PayWithBankTransfer: React.FC<PaymentMethodComponentProps> = ({
|
|
|
57
50
|
})()
|
|
58
51
|
}
|
|
59
52
|
|
|
60
|
-
const groupClx = 'border-2 bg-background border-level-2 md:bg-level-1 md:border-level-3 p-0 h-auto overflow-hidden rounded-md'
|
|
61
|
-
const tabClx = 'text-muted data-[state=active]:text-accent data-[state=active]:bg-level-2 md:data-[state=active]:bg-level-3'
|
|
62
|
-
|
|
63
53
|
return (
|
|
64
54
|
<div className='flex flex-col gap-2 mt-6'>
|
|
65
55
|
<ContactForm form={contactForm}/>
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<InfoField label='Routing Number - Wire' value='026009593' copyValue='026009593'/>
|
|
85
|
-
<InfoField label='Routing Number - SWIFT' value='BOFAUS3N' copyValue='BOFAUS3N'/>
|
|
86
|
-
<InfoField label='Reference' value='Lux' copyValue='Lux'/>
|
|
87
|
-
</div>
|
|
88
|
-
</TabsContent>
|
|
89
|
-
<TabsContent value="eur">
|
|
90
|
-
<div className='flex flex-col gap-4 w-full'>
|
|
91
|
-
<InfoField
|
|
92
|
-
label='Beneficiary Bank'
|
|
93
|
-
value={<div>Clear Junction Limited<br/>4th floor<br/>Imperial House</div>}
|
|
94
|
-
copyValue='Clear Junction Limited, 4th floor, Imperial House'
|
|
95
|
-
/>
|
|
96
|
-
<InfoField label='Account Number (GBP)' value='33781813' copyValue='33781813'/>
|
|
97
|
-
<InfoField label='Account Name' value='AiDLT Global Ltd.' copyValue='AiDLT Global Ltd.'/>
|
|
98
|
-
<InfoField label='SWIFT/BIC' value='CLJUGB21' copyValue='CLJUGB21'/>
|
|
99
|
-
<InfoField label='Sort Code (GBP)' value='041307' copyValue='041307'/>
|
|
100
|
-
<InfoField label='IBAN' value='GB75 CLJU 0413 0733 78181 3' copyValue='GB75 CLJU 0413 0733 78181 3'/>
|
|
101
|
-
<InfoField label='Reference' value='Lux' copyValue='Lux'/>
|
|
102
|
-
</div>
|
|
103
|
-
</TabsContent>
|
|
104
|
-
</Tabs>
|
|
56
|
+
<div className='w-full mx-auto max-w-[50rem]'>
|
|
57
|
+
<div className='flex flex-col gap-4 w-full'>
|
|
58
|
+
<InfoField
|
|
59
|
+
label='Beneficiary Bank'
|
|
60
|
+
value={<div>Bank of America<br/>NA 222 Broadway<br/>New York, New York. 10038</div>}
|
|
61
|
+
copyValue='Bank of America, NA 222 Broadway, New York, New York. 10038'
|
|
62
|
+
/>
|
|
63
|
+
<InfoField
|
|
64
|
+
label='Beneficiary'
|
|
65
|
+
value={<div>Hanzo, Inc<br/>4811 Mastin Street<br/>Merriam, KS 66203</div>}
|
|
66
|
+
copyValue='Hanzo, Inc, 4811 Mastin Street, Merriam, KS 66203'
|
|
67
|
+
/>
|
|
68
|
+
<InfoField label='Routing Number - ACH' value='113000023 / 111000025' copyValue='113000023 / 111000025'/>
|
|
69
|
+
<InfoField label='Routing Number - Wire' value='026009593' copyValue='026009593'/>
|
|
70
|
+
<InfoField label='Routing Number - SWIFT' value='BOFAUS3N' copyValue='BOFAUS3N'/>
|
|
71
|
+
<InfoField label='Reference' value='Lux' copyValue='Lux'/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
105
74
|
<Button onClick={payByBankTransfer} className='mx-auto w-full mt-4'>Continue</Button>
|
|
106
75
|
</div>
|
|
107
76
|
)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import React from 'react'
|
|
2
|
+
import React, { useEffect, useState } from 'react'
|
|
3
3
|
|
|
4
|
-
import type { UseFormReturn } from 'react-hook-form'
|
|
5
4
|
// @ts-ignore
|
|
6
5
|
import { ApplePay, GooglePay, CreditCard, PaymentForm } from 'react-square-web-payments-sdk'
|
|
7
6
|
|
|
8
7
|
import { ChevronRight } from 'lucide-react'
|
|
8
|
+
import { observer } from 'mobx-react-lite'
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
Accordion,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
AccordionTrigger,
|
|
15
15
|
ApplyTypography,
|
|
16
16
|
Button,
|
|
17
|
+
Skeleton,
|
|
17
18
|
buttonVariants
|
|
18
19
|
} from '@hanzo/ui/primitives'
|
|
19
20
|
|
|
@@ -27,7 +28,7 @@ import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
|
|
|
27
28
|
import ContactInfo from '../contact-form'
|
|
28
29
|
import PaymentMethods from '../card-icon-row'
|
|
29
30
|
|
|
30
|
-
const PayWithCard: React.FC<PaymentMethodComponentProps> = ({
|
|
31
|
+
const PayWithCard: React.FC<PaymentMethodComponentProps> = observer(({
|
|
31
32
|
onDone,
|
|
32
33
|
transactionStatus,
|
|
33
34
|
setTransactionStatus,
|
|
@@ -42,9 +43,8 @@ const PayWithCard: React.FC<PaymentMethodComponentProps> = ({
|
|
|
42
43
|
) => {
|
|
43
44
|
contactForm.handleSubmit(async () => {
|
|
44
45
|
setTransactionStatus('paid')
|
|
45
|
-
const res = await processSquareCardPayment(token.token, cmmc.
|
|
46
|
+
const res = await processSquareCardPayment(token.token, cmmc.promoAppliedCartTotal, verifiedBuyer.token)
|
|
46
47
|
if (res) {
|
|
47
|
-
console.log(token)
|
|
48
48
|
await storePaymentInfo({paymentMethod: token.details.method ?? null, processed: res})
|
|
49
49
|
setTransactionStatus('confirmed')
|
|
50
50
|
sendGAEvent('purchase', {
|
|
@@ -66,7 +66,7 @@ const PayWithCard: React.FC<PaymentMethodComponentProps> = ({
|
|
|
66
66
|
quantity: item.quantity
|
|
67
67
|
})),
|
|
68
68
|
num_items: cmmc.cartItems.length,
|
|
69
|
-
value: cmmc.
|
|
69
|
+
value: cmmc.promoAppliedCartTotal,
|
|
70
70
|
currency: 'USD',
|
|
71
71
|
})
|
|
72
72
|
} else {
|
|
@@ -78,7 +78,7 @@ const PayWithCard: React.FC<PaymentMethodComponentProps> = ({
|
|
|
78
78
|
const createVerificationDetails = () => {
|
|
79
79
|
const {name, email} = contactForm.getValues()
|
|
80
80
|
return {
|
|
81
|
-
amount: cmmc.
|
|
81
|
+
amount: cmmc.promoAppliedCartTotal.toFixed(2),
|
|
82
82
|
billingContact: {
|
|
83
83
|
givenName: name,
|
|
84
84
|
email,
|
|
@@ -88,7 +88,7 @@ const PayWithCard: React.FC<PaymentMethodComponentProps> = ({
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
const createPaymentRequest= () => ({
|
|
91
|
+
const createPaymentRequest = () => ({
|
|
92
92
|
countryCode: "US",
|
|
93
93
|
currencyCode: "USD",
|
|
94
94
|
lineItems: cmmc.cartItems.map(item => ({
|
|
@@ -99,11 +99,31 @@ const PayWithCard: React.FC<PaymentMethodComponentProps> = ({
|
|
|
99
99
|
requestBillingContact: false,
|
|
100
100
|
requestShippingContact: false,
|
|
101
101
|
total: {
|
|
102
|
-
amount: cmmc.
|
|
102
|
+
amount: cmmc.promoAppliedCartTotal.toFixed(2),
|
|
103
103
|
label: "Total",
|
|
104
104
|
},
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Reload payment form after checkout value changes (promo code applied, etc.)
|
|
109
|
+
* Reloading is required so that Apple Pay and Google Pay buttons are updated for new cart total.
|
|
110
|
+
*/
|
|
111
|
+
const [loadingPaymentForm, setLoadingPaymentForm] = useState<boolean>(false)
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
setLoadingPaymentForm(true)
|
|
114
|
+
const timeout = setTimeout(() => setLoadingPaymentForm(false), 1000)
|
|
115
|
+
return () => clearTimeout(timeout)
|
|
116
|
+
}, [cmmc.promoAppliedCartTotal])
|
|
117
|
+
|
|
118
|
+
if (loadingPaymentForm) {
|
|
119
|
+
return (
|
|
120
|
+
<div className='flex flex-col gap-2'>
|
|
121
|
+
<Skeleton className='w-full h-10' />
|
|
122
|
+
<Skeleton className='w-full h-10' />
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
107
127
|
return (
|
|
108
128
|
<PaymentForm
|
|
109
129
|
/**
|
|
@@ -143,92 +163,75 @@ const PayWithCard: React.FC<PaymentMethodComponentProps> = ({
|
|
|
143
163
|
<Button onClick={onDone}>Continue</Button>
|
|
144
164
|
</div>
|
|
145
165
|
) : (
|
|
146
|
-
|
|
166
|
+
<div className='flex flex-col gap-1'>
|
|
147
167
|
<GooglePay/>
|
|
148
168
|
<ApplePay/>
|
|
149
169
|
|
|
150
|
-
<div className='flex gap-2 whitespace-nowrap items-center my-
|
|
170
|
+
<div className='flex gap-2 whitespace-nowrap items-center my-1 sm:my-3 text-xs text-muted'>
|
|
151
171
|
<hr className='grow border'/><div className='shrink-0 mx-1'>or</div><hr className='grow border'/>
|
|
152
172
|
</div>
|
|
153
173
|
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
<PaymentMethods />
|
|
175
|
+
|
|
176
|
+
<ContactInfo form={contactForm}/>
|
|
177
|
+
{/* Imitates hanzo/ui Button and Input styles, I was unable to render the
|
|
178
|
+
hanzo/ui button outright and keeping the submit form functionality*/}
|
|
179
|
+
<CreditCard
|
|
180
|
+
style={{
|
|
181
|
+
'.input-container': {
|
|
182
|
+
borderColor: '#404040',
|
|
183
|
+
borderRadius: '6px',
|
|
184
|
+
},
|
|
185
|
+
'.input-container.is-focus': {
|
|
186
|
+
borderColor: '#ffffff',
|
|
187
|
+
},
|
|
188
|
+
'.input-container.is-error': {
|
|
189
|
+
borderColor: '#ff1600',
|
|
190
|
+
},
|
|
191
|
+
'.message-text': {
|
|
192
|
+
color: '#999999',
|
|
193
|
+
},
|
|
194
|
+
'.message-icon': {
|
|
195
|
+
color: '#999999',
|
|
196
|
+
},
|
|
197
|
+
'.message-text.is-error': {
|
|
198
|
+
color: '#ff1600',
|
|
199
|
+
},
|
|
200
|
+
'.message-icon.is-error': {
|
|
201
|
+
color: '#ff1600',
|
|
202
|
+
},
|
|
203
|
+
input: {
|
|
204
|
+
backgroundColor: 'transparent',
|
|
205
|
+
color: '#FFFFFF',
|
|
206
|
+
fontSize: '15px',
|
|
207
|
+
fontFamily: 'helvetica neue, sans-serif',
|
|
208
|
+
},
|
|
209
|
+
'input::placeholder': {
|
|
210
|
+
color: '#999999',
|
|
211
|
+
},
|
|
212
|
+
'input.is-error': {
|
|
213
|
+
color: '#ff1600',
|
|
214
|
+
},
|
|
215
|
+
}}
|
|
216
|
+
render={(Button: any) => (
|
|
217
|
+
<Button className={cn(
|
|
218
|
+
'items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
219
|
+
'focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
|
|
220
|
+
'!bg-primary !text-primary-fg hover:!bg-primary-hover font-nav whitespace-nowrap not-typography h-10 py-2 px-4',
|
|
221
|
+
'!text-sm rounded-md lg:min-w-[220px] sm:min-w-[220px] flex'
|
|
161
222
|
)}>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
{/* Imitates hanzo/ui Button and Input styles, I was unable to render the
|
|
171
|
-
hanzo/ui button outright and keeping the submit form functionality*/}
|
|
172
|
-
<CreditCard
|
|
173
|
-
style={{
|
|
174
|
-
'.input-container': {
|
|
175
|
-
borderColor: '#404040',
|
|
176
|
-
borderRadius: '6px',
|
|
177
|
-
},
|
|
178
|
-
'.input-container.is-focus': {
|
|
179
|
-
borderColor: '#ffffff',
|
|
180
|
-
},
|
|
181
|
-
'.input-container.is-error': {
|
|
182
|
-
borderColor: '#ff1600',
|
|
183
|
-
},
|
|
184
|
-
'.message-text': {
|
|
185
|
-
color: '#999999',
|
|
186
|
-
},
|
|
187
|
-
'.message-icon': {
|
|
188
|
-
color: '#999999',
|
|
189
|
-
},
|
|
190
|
-
'.message-text.is-error': {
|
|
191
|
-
color: '#ff1600',
|
|
192
|
-
},
|
|
193
|
-
'.message-icon.is-error': {
|
|
194
|
-
color: '#ff1600',
|
|
195
|
-
},
|
|
196
|
-
input: {
|
|
197
|
-
backgroundColor: 'transparent',
|
|
198
|
-
color: '#FFFFFF',
|
|
199
|
-
fontSize: '15px',
|
|
200
|
-
fontFamily: 'helvetica neue, sans-serif',
|
|
201
|
-
},
|
|
202
|
-
'input::placeholder': {
|
|
203
|
-
color: '#999999',
|
|
204
|
-
},
|
|
205
|
-
'input.is-error': {
|
|
206
|
-
color: '#ff1600',
|
|
207
|
-
},
|
|
208
|
-
}}
|
|
209
|
-
render={(Button: any) => (
|
|
210
|
-
<Button className={cn(
|
|
211
|
-
'items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
212
|
-
'focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
|
|
213
|
-
'!bg-primary !text-primary-fg hover:!bg-primary-hover font-nav whitespace-nowrap not-typography h-10 py-2 px-4',
|
|
214
|
-
'!text-sm rounded-md lg:min-w-[220px] sm:min-w-[220px] flex'
|
|
215
|
-
)}>
|
|
216
|
-
Pay
|
|
217
|
-
</Button>
|
|
218
|
-
)}
|
|
219
|
-
/>
|
|
220
|
-
{transactionStatus === 'error' && (
|
|
221
|
-
<p className='mx-auto text-destructive'>There was an error processing your payment.</p>
|
|
222
|
-
)}
|
|
223
|
-
</AccordionContent>
|
|
224
|
-
</AccordionItem>
|
|
225
|
-
</Accordion>
|
|
226
|
-
|
|
227
|
-
</>
|
|
223
|
+
Pay
|
|
224
|
+
</Button>
|
|
225
|
+
)}
|
|
226
|
+
/>
|
|
227
|
+
{transactionStatus === 'error' && (
|
|
228
|
+
<p className='mx-auto text-destructive'>There was an error processing your payment.</p>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
228
231
|
)}
|
|
229
232
|
</ApplyTypography>
|
|
230
233
|
</PaymentForm>
|
|
231
234
|
)
|
|
232
|
-
}
|
|
235
|
+
})
|
|
233
236
|
|
|
234
237
|
export default PayWithCard
|
|
@@ -64,7 +64,7 @@ const PayWithCrypto: React.FC<PaymentMethodComponentProps> = observer(({
|
|
|
64
64
|
.then(res => res.json())
|
|
65
65
|
.then((exchangeRate) => {
|
|
66
66
|
const oneUsdInWei = (10**18) / exchangeRate.data.amount
|
|
67
|
-
const usdAmountInWei = oneUsdInWei * cmmc.
|
|
67
|
+
const usdAmountInWei = oneUsdInWei * cmmc.promoAppliedCartTotal
|
|
68
68
|
setAmount(usdAmountInWei)
|
|
69
69
|
setLoadingPrice(false)
|
|
70
70
|
})
|
|
@@ -77,7 +77,7 @@ const PayWithCrypto: React.FC<PaymentMethodComponentProps> = observer(({
|
|
|
77
77
|
const interval = setInterval(fetchPrice, 30000)
|
|
78
78
|
|
|
79
79
|
return () => clearInterval(interval)
|
|
80
|
-
}, [cmmc.
|
|
80
|
+
}, [cmmc.promoAppliedCartTotal])
|
|
81
81
|
|
|
82
82
|
const sendPayment = async (ether: number) => {
|
|
83
83
|
contactForm.handleSubmit(async () => {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import type { ImageDef } from '@hanzo/ui/types'
|
|
14
14
|
import { cn } from '@hanzo/ui/util'
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { formatCurrencyValue } from '../util'
|
|
17
17
|
import type { LineItem } from '../types'
|
|
18
18
|
import { Icons } from './Icons'
|
|
19
19
|
|
|
@@ -66,7 +66,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
66
66
|
<CardContent className='grid gap-2.5 p-4'>
|
|
67
67
|
<CardTitle className='text-sm sm:text-base flex flex-col justify-start items-center line-clap-3'>
|
|
68
68
|
{item.title.split(', ').map((e, i) => (<p key={i}>{e}</p>))}
|
|
69
|
-
<p className='mt-1 font-semibold'>{
|
|
69
|
+
<p className='mt-1 font-semibold'>{formatCurrencyValue(item.price)}</p>
|
|
70
70
|
</CardTitle>
|
|
71
71
|
</CardContent>
|
|
72
72
|
<CardFooter className='p-4 flex flex-row justify-center'>
|
|
@@ -7,7 +7,7 @@ import { cn } from '@hanzo/ui/util'
|
|
|
7
7
|
import { Skeleton } from '@hanzo/ui/primitives'
|
|
8
8
|
|
|
9
9
|
import type { ItemSelector } from '../types'
|
|
10
|
-
import {
|
|
10
|
+
import { formatCurrencyValue } from '../util'
|
|
11
11
|
import { Icons } from './Icons'
|
|
12
12
|
|
|
13
13
|
import AddToCartWidget from './add-to-cart-widget'
|
|
@@ -132,7 +132,7 @@ const SelectCategoryItemPanel: React.FC<
|
|
|
132
132
|
</h3>
|
|
133
133
|
{selectedItemRef.item?.sku ? (
|
|
134
134
|
<h6 className='text-center font-semibold'>
|
|
135
|
-
{(soleOption ? '' : (selectedItemRef.item.titleAsOption + ': ')) +
|
|
135
|
+
{(soleOption ? '' : (selectedItemRef.item.titleAsOption + ': ')) + formatCurrencyValue(selectedItemRef.item.price)}
|
|
136
136
|
</h6>
|
|
137
137
|
) : ''}
|
|
138
138
|
</div>
|
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from './service/context'
|
|
2
2
|
export * from './components'
|
|
3
3
|
export type { StandaloneServiceOptions as ServiceOptions } from './service/impls/standalone/standalone-service'
|
|
4
|
-
export { useSyncSkuParamWithCurrentItem, getFacetValuesMutator,
|
|
4
|
+
export { useSyncSkuParamWithCurrentItem, getFacetValuesMutator, formatCurrencyValue } from './util'
|
package/package.json
CHANGED
package/service/context.tsx
CHANGED
|
@@ -29,6 +29,7 @@ const CommerceServiceProvider: React.FC<PropsWithChildren & {
|
|
|
29
29
|
options
|
|
30
30
|
}) => {
|
|
31
31
|
|
|
32
|
+
// TODO: Inject Promo fixture here
|
|
32
33
|
const serviceRef = useRef<CommerceService>(getServiceSingleton(productsByCategory, rootFacet, options))
|
|
33
34
|
return (
|
|
34
35
|
<CommerceServiceContext.Provider value={serviceRef.current}>
|
|
@@ -14,7 +14,8 @@ import type {
|
|
|
14
14
|
Category,
|
|
15
15
|
LineItem,
|
|
16
16
|
FacetsValue,
|
|
17
|
-
FacetValueDesc
|
|
17
|
+
FacetValueDesc,
|
|
18
|
+
Promo
|
|
18
19
|
} from '../../../types'
|
|
19
20
|
|
|
20
21
|
import {
|
|
@@ -43,6 +44,7 @@ class StandaloneService
|
|
|
43
44
|
private _categoryMap = new Map<string, Category>()
|
|
44
45
|
private _rootFacet: FacetValueDesc
|
|
45
46
|
private _selectedFacets: FacetsValue = {}
|
|
47
|
+
private _promo: Promo | null = null
|
|
46
48
|
|
|
47
49
|
private _options : StandaloneServiceOptions
|
|
48
50
|
private _currentItem: ActualLineItem | undefined = undefined
|
|
@@ -73,23 +75,28 @@ class StandaloneService
|
|
|
73
75
|
makeObservable<
|
|
74
76
|
StandaloneService,
|
|
75
77
|
'_selectedFacets' |
|
|
76
|
-
'_currentItem'
|
|
78
|
+
'_currentItem' |
|
|
79
|
+
'_promo'
|
|
77
80
|
>(this, {
|
|
78
81
|
_selectedFacets : observable.deep,
|
|
79
82
|
_currentItem: observable,
|
|
83
|
+
_promo: observable,
|
|
80
84
|
})
|
|
81
85
|
|
|
82
86
|
makeObservable(this, {
|
|
83
87
|
cartItems: computed,
|
|
84
88
|
cartQuantity: computed,
|
|
85
|
-
cartTotal: computed,
|
|
89
|
+
cartTotal: computed,
|
|
90
|
+
promoAppliedCartTotal: computed,
|
|
86
91
|
cartEmpty: computed,
|
|
87
92
|
specifiedItems: computed,
|
|
88
93
|
specifiedCategories: computed,
|
|
89
94
|
setCurrentItem: action,
|
|
90
95
|
currentItem: computed,
|
|
91
96
|
item: computed,
|
|
92
|
-
facetsValue: computed
|
|
97
|
+
facetsValue: computed,
|
|
98
|
+
appliedPromo: computed,
|
|
99
|
+
setAppliedPromo: action,
|
|
93
100
|
/* NOT setFacets. It implements it's action mechanism */
|
|
94
101
|
})
|
|
95
102
|
}
|
|
@@ -181,6 +188,43 @@ class StandaloneService
|
|
|
181
188
|
)
|
|
182
189
|
}
|
|
183
190
|
|
|
191
|
+
_promoValue_unsafe(value: number): number {
|
|
192
|
+
if (this._promo!.type === 'percent') {
|
|
193
|
+
return value * (1 - this._promo!.value / 100)
|
|
194
|
+
}
|
|
195
|
+
return value - this._promo!.value
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get promoAppliedCartTotal(): number {
|
|
199
|
+
if (!this._promo) {
|
|
200
|
+
return this.cartTotal
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!this._promo.skus) {
|
|
204
|
+
return this._promoValue_unsafe(this.cartTotal)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let total = this.cartItems.reduce(
|
|
208
|
+
(total, item) => {
|
|
209
|
+
const itemPrice = this._promo!.skus!.includes(item.sku) ?
|
|
210
|
+
this._promoValue_unsafe(item.price)
|
|
211
|
+
:
|
|
212
|
+
item.price
|
|
213
|
+
return total + itemPrice * item.quantity
|
|
214
|
+
},
|
|
215
|
+
0
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return total
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
itemPromoPrice(item: LineItem): number | undefined {
|
|
222
|
+
if (this._promo && (!this._promo.skus || this._promo.skus.includes(item.sku) )) {
|
|
223
|
+
return this._promoValue_unsafe(item.price)
|
|
224
|
+
}
|
|
225
|
+
return undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
184
228
|
get cartQuantity(): number {
|
|
185
229
|
return this.cartItems.reduce(
|
|
186
230
|
(total, item) => (total + item.quantity),
|
|
@@ -188,6 +232,14 @@ class StandaloneService
|
|
|
188
232
|
)
|
|
189
233
|
}
|
|
190
234
|
|
|
235
|
+
get appliedPromo(): Promo | null {
|
|
236
|
+
return this._promo
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
setAppliedPromo(promo: Promo | null): void {
|
|
240
|
+
this._promo = promo
|
|
241
|
+
}
|
|
242
|
+
|
|
191
243
|
setCurrentItem(skuToFind: string | undefined): boolean {
|
|
192
244
|
|
|
193
245
|
if (skuToFind === undefined || skuToFind.length === 0) {
|
package/types/checkout.ts
CHANGED
|
@@ -37,12 +37,11 @@ interface PaymentMethodDesc {
|
|
|
37
37
|
Comp: ComponentType<PaymentMethodComponentProps>
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
40
|
export {
|
|
42
41
|
type TransactionStatus,
|
|
43
42
|
type CheckoutStepComponentProps,
|
|
44
43
|
type CheckoutStep,
|
|
45
44
|
type PaymentMethodComponentProps,
|
|
46
45
|
type PaymentMethodDesc,
|
|
47
|
-
type ContactFormType
|
|
46
|
+
type ContactFormType,
|
|
48
47
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LineItem, ObsLineItemRef } from './line-item'
|
|
2
2
|
import type { FacetValueDesc, FacetsValue } from './facet'
|
|
3
3
|
import type Category from './category'
|
|
4
|
+
import type Promo from './promo'
|
|
4
5
|
|
|
5
6
|
interface CommerceService extends ObsLineItemRef {
|
|
6
7
|
|
|
@@ -8,10 +9,17 @@ interface CommerceService extends ObsLineItemRef {
|
|
|
8
9
|
get cartItems(): LineItem[]
|
|
9
10
|
/** Total of all quantities of all items in cart */
|
|
10
11
|
get cartQuantity(): number
|
|
11
|
-
/** Total of all prices
|
|
12
|
+
/** Total of all prices x quantities of items in cart */
|
|
12
13
|
get cartTotal(): number
|
|
13
14
|
|
|
15
|
+
get promoAppliedCartTotal(): number
|
|
16
|
+
|
|
14
17
|
get cartEmpty(): boolean
|
|
18
|
+
|
|
19
|
+
get appliedPromo(): Promo | null
|
|
20
|
+
setAppliedPromo(promo: Promo | null): void
|
|
21
|
+
/** returns the price with promo applied, of undefined if no promo or promo does not apply */
|
|
22
|
+
itemPromoPrice(item: LineItem): number | undefined
|
|
15
23
|
|
|
16
24
|
getCartCategorySubtotal(categoryId: string): number
|
|
17
25
|
|
package/types/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { default as Category } from './category'
|
|
|
3
3
|
export type { default as CommerceService } from './commerce-service'
|
|
4
4
|
export type { default as ItemSelector } from './item-selector'
|
|
5
5
|
export type { default as Product } from './product'
|
|
6
|
+
export type { default as Promo } from './promo'
|
|
6
7
|
|
|
7
8
|
export * from './checkout'
|
|
8
9
|
export * from './line-item'
|
package/types/promo.ts
ADDED
package/util/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StringMutator, CommerceService } from '../types'
|
|
1
|
+
import type { StringMutator, CommerceService, Promo } from '../types'
|
|
2
2
|
|
|
3
3
|
export function toTitleCase(str: string) {
|
|
4
4
|
return str.replace(
|
|
@@ -20,7 +20,7 @@ export function unslugify(str: string) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
export function
|
|
23
|
+
export function formatCurrencyValue(price: number): string {
|
|
24
24
|
const str = price.toLocaleString('en-US', {
|
|
25
25
|
style: 'currency',
|
|
26
26
|
currency: 'USD',
|
|
@@ -28,6 +28,7 @@ export function formatPrice(price: number): string {
|
|
|
28
28
|
return (str.endsWith('.00')) ? str.replace('.00', '') : str
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
export const getFacetValuesMutator = (level: number, cmmc: CommerceService): StringMutator => {
|
|
32
33
|
|
|
33
34
|
const setLevel = (value: string, level: number ): void => {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import type { Promo } from '../types'
|
|
4
|
+
|
|
5
|
+
const PROMO_CODES: Promo[] = [
|
|
6
|
+
{
|
|
7
|
+
type: 'percent',
|
|
8
|
+
code: 'ANGEL',
|
|
9
|
+
value: 20
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
type: 'percent',
|
|
13
|
+
code: 'NASSER',
|
|
14
|
+
value: 20
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: 'percent',
|
|
18
|
+
code: 'DARA',
|
|
19
|
+
value: 20
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 'percent',
|
|
23
|
+
code: 'ROSEY',
|
|
24
|
+
value: 20
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'percent',
|
|
28
|
+
code: 'IYKYK',
|
|
29
|
+
value: 20
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'percent',
|
|
33
|
+
code: 'GENESIS',
|
|
34
|
+
value: 99,
|
|
35
|
+
skus: ['LXM-PS-PS']
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
// TODO: implement a real API call
|
|
40
|
+
const getPromoFromApi = async (code: string) => {
|
|
41
|
+
return PROMO_CODES.find(promo => promo.code === code)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
getPromoFromApi as default
|
|
46
|
+
}
|