@hanzo/commerce 4.5.0 → 4.7.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/add-to-cart-widget.tsx +33 -1
- package/components/cart-panel/index.tsx +31 -1
- package/components/cart-panel/products-carousel.tsx +60 -0
- package/components/checkout-panel/cart-accordian.tsx +39 -0
- package/components/checkout-panel/index.tsx +18 -29
- package/components/checkout-panel/payment/contact-info.tsx +3 -5
- package/components/checkout-panel/payment/index.tsx +27 -6
- package/components/checkout-panel/payment/pay-with-bank-transfer.tsx +3 -3
- package/components/checkout-panel/payment/pay-with-card.tsx +44 -16
- package/components/checkout-panel/payment/pay-with-crypto.tsx +30 -19
- package/components/checkout-panel/payment/payment-methods/index.tsx +1 -1
- package/components/checkout-panel/shipping-info.tsx +20 -16
- package/components/facet-values-widget/facet-image.tsx +5 -39
- package/package.json +5 -3
- package/service/impls/standalone/actual-line-item.ts +7 -1
- package/types/product.ts +4 -0
- package/util/analytics.ts +21 -0
|
@@ -7,6 +7,7 @@ import { cn } from '@hanzo/ui/util'
|
|
|
7
7
|
|
|
8
8
|
import { Icons } from './Icons'
|
|
9
9
|
import type { LineItem } from '../types'
|
|
10
|
+
import { sendFBEvent, sendGAEvent } from '../util/analytics'
|
|
10
11
|
|
|
11
12
|
const AddToCartWidget: React.FC<{
|
|
12
13
|
item: LineItem
|
|
@@ -50,7 +51,27 @@ const AddToCartWidget: React.FC<{
|
|
|
50
51
|
else {
|
|
51
52
|
toast(`Changed quantity to ${old + 1} for ${item.title}.`)
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
sendGAEvent('add_to_cart', {
|
|
55
|
+
items: [{
|
|
56
|
+
item_id: item.sku,
|
|
57
|
+
item_name: item.title,
|
|
58
|
+
item_category: item.categoryId,
|
|
59
|
+
price: item.price,
|
|
60
|
+
quantity: item.quantity
|
|
61
|
+
}],
|
|
62
|
+
value: item.price,
|
|
63
|
+
currency: 'USD',
|
|
64
|
+
})
|
|
65
|
+
sendFBEvent('AddToCart', {
|
|
66
|
+
content_ids: [item.sku],
|
|
67
|
+
contents: [{
|
|
68
|
+
id: item.sku,
|
|
69
|
+
quantity: item.quantity
|
|
70
|
+
}],
|
|
71
|
+
content_name: item.title,
|
|
72
|
+
value: item.price,
|
|
73
|
+
currency: 'USD',
|
|
74
|
+
})
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
const dec = () => {
|
|
@@ -65,6 +86,17 @@ const AddToCartWidget: React.FC<{
|
|
|
65
86
|
else {
|
|
66
87
|
toast(`Changed quantity to ${old - 1} for ${item.title}.`)
|
|
67
88
|
}
|
|
89
|
+
sendGAEvent('remove_from_cart', {
|
|
90
|
+
items: [{
|
|
91
|
+
item_id: item.sku,
|
|
92
|
+
item_name: item.title,
|
|
93
|
+
item_category: item.categoryId,
|
|
94
|
+
price: item.price,
|
|
95
|
+
quantity: item.quantity
|
|
96
|
+
}],
|
|
97
|
+
value: item.price,
|
|
98
|
+
currency: 'USD',
|
|
99
|
+
})
|
|
68
100
|
}
|
|
69
101
|
|
|
70
102
|
return ( item.isInCart ? (
|
|
@@ -9,11 +9,14 @@ import { useCommerce } from '../../service/context'
|
|
|
9
9
|
import { formatPrice } from '../../util'
|
|
10
10
|
|
|
11
11
|
import CartLineItem from './cart-line-item'
|
|
12
|
+
import { sendFBEvent, sendGAEvent } from '../../util/analytics'
|
|
13
|
+
import ProductsCarousel from './products-carousel'
|
|
12
14
|
|
|
13
15
|
const CartPanel: React.FC<PropsWithChildren & {
|
|
14
16
|
className?: string
|
|
15
17
|
isMobile?: boolean,
|
|
16
18
|
noCheckout?: boolean
|
|
19
|
+
showProductsCarousel?: boolean
|
|
17
20
|
onCheckoutOpen?: () => void
|
|
18
21
|
}> = observer(({
|
|
19
22
|
/** Children is the heading area. */
|
|
@@ -21,6 +24,7 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
21
24
|
className='',
|
|
22
25
|
isMobile=false,
|
|
23
26
|
noCheckout=false,
|
|
27
|
+
showProductsCarousel=false,
|
|
24
28
|
onCheckoutOpen,
|
|
25
29
|
}) => {
|
|
26
30
|
/* TODO: onCheckoutOpen is a hackish way fix a bug with multiple dialog opened at the same time.
|
|
@@ -31,9 +35,35 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
31
35
|
return <div />
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
const showCheckout = () => {
|
|
39
|
+
sendGAEvent('begin_checkout', {
|
|
40
|
+
currency: 'USD',
|
|
41
|
+
value: cmmc.cartTotal,
|
|
42
|
+
items: cmmc.cartItems.map((item) => ({
|
|
43
|
+
item_id: item.sku,
|
|
44
|
+
item_name: item.title,
|
|
45
|
+
item_category: item.categoryId,
|
|
46
|
+
price: item.price,
|
|
47
|
+
quantity: item.quantity
|
|
48
|
+
})),
|
|
49
|
+
})
|
|
50
|
+
sendFBEvent('InitiateCheckout', {
|
|
51
|
+
content_ids: cmmc.cartItems.map((item) => item.sku),
|
|
52
|
+
contents: cmmc.cartItems.map(item => ({
|
|
53
|
+
id: item.sku,
|
|
54
|
+
quantity: item.quantity
|
|
55
|
+
})),
|
|
56
|
+
num_items: cmmc.cartItems.length,
|
|
57
|
+
value: cmmc.cartTotal,
|
|
58
|
+
currency: 'USD',
|
|
59
|
+
})
|
|
60
|
+
onCheckoutOpen && onCheckoutOpen()
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
return (
|
|
35
64
|
<div className={cn('border p-4 rounded-lg', className)}>
|
|
36
65
|
{children}
|
|
66
|
+
{showProductsCarousel && <ProductsCarousel items={cmmc.cartItems}/>}
|
|
37
67
|
<div className='mt-2 w-full'>
|
|
38
68
|
{cmmc.cartEmpty ? (
|
|
39
69
|
<p className='text-center mt-4'>No items in cart</p>
|
|
@@ -50,7 +80,7 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
50
80
|
variant='primary'
|
|
51
81
|
rounded='lg'
|
|
52
82
|
className='mt-12 mx-auto w-full'
|
|
53
|
-
onClick={
|
|
83
|
+
onClick={showCheckout}
|
|
54
84
|
>
|
|
55
85
|
Checkout
|
|
56
86
|
</Button>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Spline from '@splinetool/react-spline'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Carousel,
|
|
5
|
+
CarouselContent,
|
|
6
|
+
CarouselItem,
|
|
7
|
+
CarouselPrevious,
|
|
8
|
+
CarouselNext
|
|
9
|
+
} from '@hanzo/ui/primitives'
|
|
10
|
+
import {
|
|
11
|
+
VideoBlockComponent,
|
|
12
|
+
ImageBlockComponent,
|
|
13
|
+
type ImageBlock,
|
|
14
|
+
type Block,
|
|
15
|
+
type VideoBlock
|
|
16
|
+
} from '@hanzo/ui/blocks'
|
|
17
|
+
|
|
18
|
+
import type { LineItem } from '../../types'
|
|
19
|
+
|
|
20
|
+
// Carousel content hierarchy: 3D > MP4 > Image
|
|
21
|
+
const ProductsCarousel: React.FC<{
|
|
22
|
+
items: LineItem[]
|
|
23
|
+
}> = ({
|
|
24
|
+
items,
|
|
25
|
+
}) => (
|
|
26
|
+
<Carousel options={{ loop: true }} className='w-full max-w-sm mx-auto px-2' >
|
|
27
|
+
<CarouselContent>
|
|
28
|
+
{items.map(({title, img, video, animation}, index) => (
|
|
29
|
+
<CarouselItem key={index}>
|
|
30
|
+
<div className='flex aspect-square items-center justify-center p-6'>
|
|
31
|
+
{animation ? (
|
|
32
|
+
<Spline
|
|
33
|
+
scene={animation}
|
|
34
|
+
className='!aspect-[12/10] pointer-events-none !w-auto !h-auto'
|
|
35
|
+
/>
|
|
36
|
+
) : video ? (
|
|
37
|
+
<VideoBlockComponent
|
|
38
|
+
block={{blockType: 'video', ...video} satisfies VideoBlock as Block}
|
|
39
|
+
/>
|
|
40
|
+
) : (
|
|
41
|
+
<ImageBlockComponent
|
|
42
|
+
block={{
|
|
43
|
+
blockType: 'image',
|
|
44
|
+
src: img ?? '',
|
|
45
|
+
alt: title + ' image',
|
|
46
|
+
dim: { w: 250, h: 250 }
|
|
47
|
+
} satisfies ImageBlock as Block}
|
|
48
|
+
className='m-auto'
|
|
49
|
+
/>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</CarouselItem>
|
|
53
|
+
))}
|
|
54
|
+
</CarouselContent>
|
|
55
|
+
<CarouselPrevious />
|
|
56
|
+
<CarouselNext />
|
|
57
|
+
</Carousel>
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
export default ProductsCarousel
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { observer } from 'mobx-react-lite'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Accordion,
|
|
7
|
+
AccordionContent,
|
|
8
|
+
AccordionItem,
|
|
9
|
+
AccordionTrigger,
|
|
10
|
+
} from '@hanzo/ui/primitives'
|
|
11
|
+
|
|
12
|
+
import { formatPrice, useCommerce } from '../..'
|
|
13
|
+
|
|
14
|
+
import CartPanel from '../cart-panel'
|
|
15
|
+
import BagIcon from './icons/bag-icon'
|
|
16
|
+
|
|
17
|
+
const CartAccordian: React.FC<{className?: string}> = observer(({
|
|
18
|
+
className=''
|
|
19
|
+
}) => {
|
|
20
|
+
|
|
21
|
+
const cmmc = useCommerce()
|
|
22
|
+
return (
|
|
23
|
+
<Accordion type="single" collapsible className={className}>
|
|
24
|
+
<AccordionItem value="cart" className='w-full border-b-0'>
|
|
25
|
+
<AccordionTrigger className='!no-underline py-1'>
|
|
26
|
+
<div className='flex gap-4 items-center'>
|
|
27
|
+
<BagIcon className='w-4 h-4 sm:w-6 sm:h-6'/>
|
|
28
|
+
<h5 className='text-sm sm:text-xl truncate'>Order Summary {formatPrice(cmmc.cartTotal)}</h5>
|
|
29
|
+
</div>
|
|
30
|
+
</AccordionTrigger>
|
|
31
|
+
<AccordionContent>
|
|
32
|
+
<CartPanel noCheckout className='border-none w-full'/>
|
|
33
|
+
</AccordionContent>
|
|
34
|
+
</AccordionItem>
|
|
35
|
+
</Accordion>
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export default CartAccordian
|
|
@@ -4,24 +4,22 @@ import { useState } from 'react'
|
|
|
4
4
|
import { observer } from 'mobx-react-lite'
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
Accordion,
|
|
8
|
-
AccordionContent,
|
|
9
|
-
AccordionItem,
|
|
10
|
-
AccordionTrigger,
|
|
11
7
|
Dialog,
|
|
12
8
|
DialogPortal,
|
|
9
|
+
ScrollArea,
|
|
13
10
|
} from '@hanzo/ui/primitives'
|
|
14
11
|
import { cn } from '@hanzo/ui/util'
|
|
15
12
|
import { AuthWidget } from '@hanzo/auth/components'
|
|
16
13
|
|
|
14
|
+
import { useCommerce } from '../..'
|
|
15
|
+
|
|
17
16
|
import ShippingInfo from './shipping-info'
|
|
18
17
|
import ThankYou from './thank-you'
|
|
19
18
|
import CartPanel from '../cart-panel'
|
|
20
19
|
import Payment from './payment'
|
|
21
20
|
import CloseButton from './close-button'
|
|
22
21
|
import StepIndicator from './step-indicator'
|
|
23
|
-
import
|
|
24
|
-
import { formatPrice, useCommerce } from '../..'
|
|
22
|
+
import CartAccordian from './cart-accordian'
|
|
25
23
|
|
|
26
24
|
const CheckoutPanel: React.FC<{
|
|
27
25
|
open: boolean
|
|
@@ -72,7 +70,7 @@ const CheckoutPanel: React.FC<{
|
|
|
72
70
|
return (
|
|
73
71
|
<Dialog open={open}>
|
|
74
72
|
<DialogPortal>
|
|
75
|
-
<div id='PORTAL_OUTER'
|
|
73
|
+
<div /* id='PORTAL_OUTER' */
|
|
76
74
|
className={cn(
|
|
77
75
|
'fixed top-0 shadow-lg ',
|
|
78
76
|
'animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10',
|
|
@@ -80,35 +78,26 @@ const CheckoutPanel: React.FC<{
|
|
|
80
78
|
'!max-w-none w-full h-full min-h-screen bg-transparent backdrop-blur-sm z-50'
|
|
81
79
|
)}
|
|
82
80
|
>
|
|
83
|
-
<div id='GRID' className='
|
|
84
|
-
<div className='bg-background flex flex-row items-start justify-end'>
|
|
85
|
-
<
|
|
81
|
+
<div /* id='GRID' */ className='flex flex-col md:flex-row justify-center h-full overflow-y-auto md:overflow-y-hidden'>
|
|
82
|
+
<div className='w-full bg-background flex flex-row items-start justify-end'>
|
|
83
|
+
<ScrollArea className='h-full w-full max-w-[750px] relative flex flex-col items-center justify-start px-6 pt-12 pb-0 md:pb-9'>
|
|
86
84
|
<CloseButton onClose={onClose} className='absolute top-3 left-3 w-auto h-auto rounded-full bg-level-1 hover:bg-level-2 hover:border-muted p-2' />
|
|
87
85
|
<AuthWidget hideLogin className='flex md:hidden absolute top-3 right-3'/>
|
|
88
|
-
<CartPanel
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<h5 className='text-sm sm:text-xl truncate'>Order Summary {formatPrice(cmmc.cartTotal)}</h5>
|
|
96
|
-
</div>
|
|
97
|
-
</AccordionTrigger>
|
|
98
|
-
<AccordionContent>
|
|
99
|
-
<CartPanel noCheckout className='border-none w-full'/>
|
|
100
|
-
</AccordionContent>
|
|
101
|
-
</AccordionItem>
|
|
102
|
-
</Accordion>
|
|
103
|
-
</div>
|
|
86
|
+
<CartPanel
|
|
87
|
+
className='hidden md:flex border-none mt-10 w-full max-w-[550px] flex-col'
|
|
88
|
+
noCheckout
|
|
89
|
+
showProductsCarousel
|
|
90
|
+
/>
|
|
91
|
+
<CartAccordian className='md:hidden flex items-center justify-center py-2 w-full' />
|
|
92
|
+
</ScrollArea>
|
|
104
93
|
</div>
|
|
105
|
-
<
|
|
106
|
-
<div className='h-full w-full max-w-[750px] relative flex flex-col gap-8 sm:gap-14 px-8 pb-6 pt-6 md:pt-14'>
|
|
94
|
+
<ScrollArea className='w-full h-full bg-level-1 flex flex-row items-start justify-start md:overflow-y-auto'>
|
|
95
|
+
<div className='h-full w-full max-w-[750px] relative flex flex-col gap-8 sm:gap-14 px-4 md:px-8 pb-6 pt-6 md:pt-14'>
|
|
107
96
|
<AuthWidget hideLogin className='hidden md:flex absolute top-4 right-4 '/>
|
|
108
97
|
<StepIndicator steps={steps} currentStep={step} className='flex gap-2 mx-auto items-center text-xxs sm:text-base' />
|
|
109
98
|
{steps[step].element}
|
|
110
99
|
</div>
|
|
111
|
-
</
|
|
100
|
+
</ScrollArea>
|
|
112
101
|
</div>
|
|
113
102
|
</div>
|
|
114
103
|
</DialogPortal>
|
|
@@ -23,15 +23,14 @@ const ContactInfo: React.FC<{
|
|
|
23
23
|
return (
|
|
24
24
|
<Form {...form}>
|
|
25
25
|
<form className='text-left'>
|
|
26
|
-
<div className='flex
|
|
26
|
+
<div className='flex gap-1 sm:gap-2'>
|
|
27
27
|
<FormField
|
|
28
28
|
control={form.control}
|
|
29
29
|
name='name'
|
|
30
30
|
render={({ field }) => (
|
|
31
31
|
<FormItem className='space-y-1 w-full'>
|
|
32
|
-
<FormLabel>Full name</FormLabel>
|
|
33
32
|
<FormControl>
|
|
34
|
-
<Input {...field} className='border-muted-4'/>
|
|
33
|
+
<Input {...field} className='border-muted-4' placeholder='Full name'/>
|
|
35
34
|
</FormControl>
|
|
36
35
|
<FormMessage />
|
|
37
36
|
</FormItem>
|
|
@@ -42,9 +41,8 @@ const ContactInfo: React.FC<{
|
|
|
42
41
|
name='email'
|
|
43
42
|
render={({ field }) => (
|
|
44
43
|
<FormItem className='space-y-1 w-full'>
|
|
45
|
-
<FormLabel>Email</FormLabel>
|
|
46
44
|
<FormControl>
|
|
47
|
-
<Input {...field} className='border-muted-4'/>
|
|
45
|
+
<Input {...field} className='border-muted-4' placeholder='Email'/>
|
|
48
46
|
</FormControl>
|
|
49
47
|
<FormMessage />
|
|
50
48
|
</FormItem>
|
|
@@ -14,6 +14,7 @@ import PayWithBankTransfer from './pay-with-bank-transfer'
|
|
|
14
14
|
import PayWithCard from './pay-with-card'
|
|
15
15
|
import { useCommerce } from '../../../service/context'
|
|
16
16
|
import type { TransactionStatus } from '../../../types'
|
|
17
|
+
import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
|
|
17
18
|
|
|
18
19
|
const contactFormSchema = z.object({
|
|
19
20
|
name: z.string().min(1, 'Enter your full name.'),
|
|
@@ -56,6 +57,26 @@ const Payment: React.FC<{
|
|
|
56
57
|
setOrderId(id)
|
|
57
58
|
}
|
|
58
59
|
if (id) {
|
|
60
|
+
sendGAEvent('add_payment_info', {
|
|
61
|
+
items: cmmc.cartItems.map((item) => ({
|
|
62
|
+
item_id: item.sku,
|
|
63
|
+
item_name: item.title,
|
|
64
|
+
item_category: item.categoryId,
|
|
65
|
+
price: item.price,
|
|
66
|
+
quantity: item.quantity
|
|
67
|
+
})),
|
|
68
|
+
value: cmmc.cartTotal,
|
|
69
|
+
currency: 'USD',
|
|
70
|
+
payment_type: paymentInfo.paymentMethod ?? ''
|
|
71
|
+
})
|
|
72
|
+
sendFBEvent('AddPaymentInfo', {
|
|
73
|
+
contents: cmmc.cartItems.map(item => ({
|
|
74
|
+
id: item.sku,
|
|
75
|
+
quantity: item.quantity
|
|
76
|
+
})),
|
|
77
|
+
value: cmmc.cartTotal,
|
|
78
|
+
currency: 'USD'
|
|
79
|
+
})
|
|
59
80
|
await cmmc.updateOrderPaymentInfo(id, paymentInfo)
|
|
60
81
|
}
|
|
61
82
|
}
|
|
@@ -65,24 +86,24 @@ const Payment: React.FC<{
|
|
|
65
86
|
<TabsList className='grid w-full grid-cols-3 mx-auto bg-level-2 h-auto'>
|
|
66
87
|
<TabsTrigger
|
|
67
88
|
value='card'
|
|
68
|
-
className='whitespace-normal h-full text-
|
|
89
|
+
className='whitespace-normal h-full text-xs sm:text-base px-1'
|
|
69
90
|
disabled={transactionStatus === 'paid' || transactionStatus === 'confirmed'}
|
|
70
91
|
>
|
|
71
|
-
|
|
92
|
+
Pay with card
|
|
72
93
|
</TabsTrigger>
|
|
73
94
|
<TabsTrigger
|
|
74
95
|
value='crypto'
|
|
75
|
-
className='whitespace-normal h-full text-
|
|
96
|
+
className='whitespace-normal h-full text-xs sm:text-base px-1'
|
|
76
97
|
disabled={transactionStatus === 'paid' || transactionStatus === 'confirmed'}
|
|
77
98
|
>
|
|
78
|
-
|
|
99
|
+
Pay with Wallet
|
|
79
100
|
</TabsTrigger>
|
|
80
101
|
<TabsTrigger
|
|
81
102
|
value='bank'
|
|
82
|
-
className='whitespace-normal h-full text-
|
|
103
|
+
className='whitespace-normal h-full text-xs sm:text-base px-1'
|
|
83
104
|
disabled={transactionStatus === 'paid' || transactionStatus === 'confirmed'}
|
|
84
105
|
>
|
|
85
|
-
|
|
106
|
+
Pay with Wire
|
|
86
107
|
</TabsTrigger>
|
|
87
108
|
</TabsList>
|
|
88
109
|
<TabsContent value='card'>
|
|
@@ -18,13 +18,13 @@ const InfoField: React.FC<{
|
|
|
18
18
|
}) => {
|
|
19
19
|
const copyToClipboard = (label: string, text: string) => {
|
|
20
20
|
navigator.clipboard.writeText(text)
|
|
21
|
-
toast(`${label} copied to clipboard
|
|
21
|
+
toast(`${label} copied to clipboard.`)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
25
|
<div className='flex flex-col gap-1'>
|
|
26
26
|
<p className='text-xs'>{label}</p>
|
|
27
|
-
<div className='flex items-center justify-between text-lg border rounded-lg py-2 px-4'>
|
|
27
|
+
<div className='flex items-center justify-between sm:text-lg border rounded-lg py-2 px-4'>
|
|
28
28
|
{value}
|
|
29
29
|
<Button variant='ghost' size='icon' onClick={() => copyToClipboard(label, copyValue)}>
|
|
30
30
|
<Copy className='h-4 w-4'/>
|
|
@@ -57,7 +57,7 @@ const PayWithBankTransfer: React.FC<{
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
return (
|
|
60
|
-
<div className='flex flex-col gap-
|
|
60
|
+
<div className='flex flex-col gap-2 mt-6'>
|
|
61
61
|
<ContactInfo form={contactForm}/>
|
|
62
62
|
<Tabs defaultValue="usd" className='w-full mx-auto max-w-[50rem]'>
|
|
63
63
|
<TabsList className="grid w-full grid-cols-2 max-w-[15rem] mx-auto bg-level-2">
|
|
@@ -6,6 +6,7 @@ import type { UseFormReturn } from 'react-hook-form'
|
|
|
6
6
|
import { ApplePay, GooglePay, CreditCard, PaymentForm } from 'react-square-web-payments-sdk'
|
|
7
7
|
|
|
8
8
|
import { ApplyTypography, Button } from '@hanzo/ui/primitives'
|
|
9
|
+
import { cn } from '@hanzo/ui/util'
|
|
9
10
|
|
|
10
11
|
import { processSquareCardPayment } from '../../../util'
|
|
11
12
|
import { useCommerce } from '../../../service/context'
|
|
@@ -13,6 +14,7 @@ import type { TransactionStatus } from '../../../types'
|
|
|
13
14
|
|
|
14
15
|
import PaymentMethods from './payment-methods'
|
|
15
16
|
import ContactInfo from './contact-info'
|
|
17
|
+
import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
|
|
16
18
|
|
|
17
19
|
const PayWithCard: React.FC<{
|
|
18
20
|
setStep: (currentStep: number) => void
|
|
@@ -46,6 +48,28 @@ const PayWithCard: React.FC<{
|
|
|
46
48
|
console.log(token)
|
|
47
49
|
await storePaymentInfo({paymentMethod: token.details.method ?? null, processed: res})
|
|
48
50
|
setTransactionStatus('confirmed')
|
|
51
|
+
sendGAEvent('purchase', {
|
|
52
|
+
transaction_id: res.payment?.id,
|
|
53
|
+
value: res.payment?.amountMoney?.amount,
|
|
54
|
+
currency: res.payment?.amountMoney?.currency,
|
|
55
|
+
items: cmmc.cartItems.map((item) => ({
|
|
56
|
+
item_id: item.sku,
|
|
57
|
+
item_name: item.title,
|
|
58
|
+
item_category: item.categoryId,
|
|
59
|
+
price: item.price,
|
|
60
|
+
quantity: item.quantity
|
|
61
|
+
})),
|
|
62
|
+
})
|
|
63
|
+
sendFBEvent('Purchase', {
|
|
64
|
+
content_ids: cmmc.cartItems.map((item) => item.sku),
|
|
65
|
+
contents: cmmc.cartItems.map(item => ({
|
|
66
|
+
id: item.sku,
|
|
67
|
+
quantity: item.quantity
|
|
68
|
+
})),
|
|
69
|
+
num_items: cmmc.cartItems.length,
|
|
70
|
+
value: cmmc.cartTotal,
|
|
71
|
+
currency: 'USD',
|
|
72
|
+
})
|
|
49
73
|
} else {
|
|
50
74
|
setTransactionStatus('error')
|
|
51
75
|
}
|
|
@@ -110,7 +134,7 @@ const PayWithCard: React.FC<{
|
|
|
110
134
|
*/
|
|
111
135
|
locationId={process.env.NEXT_PUBLIC_SQUARE_LOCATION_ID}
|
|
112
136
|
>
|
|
113
|
-
<ApplyTypography className='flex flex-col
|
|
137
|
+
<ApplyTypography className='flex flex-col mt-6'>
|
|
114
138
|
{transactionStatus === 'paid' ? (
|
|
115
139
|
<h6 className='mx-auto font-nav'>Processing your payment...</h6>
|
|
116
140
|
) : transactionStatus === 'confirmed' ? (
|
|
@@ -124,13 +148,11 @@ const PayWithCard: React.FC<{
|
|
|
124
148
|
<GooglePay/>
|
|
125
149
|
<ApplePay/>
|
|
126
150
|
|
|
127
|
-
<div className='flex gap-2 whitespace-nowrap items-center my-
|
|
151
|
+
<div className='flex gap-2 whitespace-nowrap items-center my-1 sm:my-2 text-xs text-foreground/60'>
|
|
128
152
|
<hr className='bg-foreground/60 w-full border'/> or pay with card <hr className='bg-foreground/60 w-full border'/>
|
|
129
153
|
</div>
|
|
130
154
|
|
|
131
155
|
<ContactInfo form={contactForm}/>
|
|
132
|
-
|
|
133
|
-
<PaymentMethods/>
|
|
134
156
|
|
|
135
157
|
{/* Imitates hanzo/ui Button and Input styles, I was unable to render the
|
|
136
158
|
hanzo/ui button outright and keeping the submit form functionality*/}
|
|
@@ -161,6 +183,8 @@ const PayWithCard: React.FC<{
|
|
|
161
183
|
input: {
|
|
162
184
|
backgroundColor: '#1f1f1f',
|
|
163
185
|
color: '#FFFFFF',
|
|
186
|
+
fontSize: '15px',
|
|
187
|
+
fontFamily: 'helvetica neue, sans-serif',
|
|
164
188
|
},
|
|
165
189
|
'input::placeholder': {
|
|
166
190
|
color: '#999999',
|
|
@@ -169,18 +193,22 @@ const PayWithCard: React.FC<{
|
|
|
169
193
|
color: '#ff1600',
|
|
170
194
|
},
|
|
171
195
|
}}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
render={(Button: any) => (<>
|
|
197
|
+
<PaymentMethods/>
|
|
198
|
+
<Button className={cn(
|
|
199
|
+
'items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
200
|
+
'focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
|
|
201
|
+
'!bg-primary !text-primary-fg hover:!bg-primary-hover font-nav whitespace-nowrap not-typography h-10 py-2 px-4',
|
|
202
|
+
'!text-sm rounded-md lg:min-w-[220px] sm:min-w-[220px] flex'
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
205
|
+
Pay
|
|
206
|
+
</Button>
|
|
207
|
+
</>)}
|
|
208
|
+
>
|
|
209
|
+
|
|
210
|
+
</CreditCard>
|
|
211
|
+
|
|
184
212
|
{transactionStatus === 'error' && (
|
|
185
213
|
<p className='mx-auto text-destructive'>There was an error processing your payment.</p>
|
|
186
214
|
)}
|
|
@@ -19,14 +19,13 @@ import {
|
|
|
19
19
|
} from '@hanzo/ui/primitives'
|
|
20
20
|
|
|
21
21
|
import { useAuth } from '@hanzo/auth/service'
|
|
22
|
-
import { Ethereum as EthIconFromAuth } from '@hanzo/auth/icons'
|
|
23
22
|
|
|
24
23
|
import Eth from '../icons/eth'
|
|
25
24
|
import { useCommerce } from '../../../service/context'
|
|
26
25
|
import type { TransactionStatus } from '../../../types'
|
|
27
26
|
import ContactInfo from './contact-info'
|
|
28
27
|
import type { UseFormReturn } from 'react-hook-form'
|
|
29
|
-
import {
|
|
28
|
+
import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
|
|
30
29
|
|
|
31
30
|
declare global {
|
|
32
31
|
interface Window{
|
|
@@ -53,12 +52,11 @@ const PayWithCrypto: React.FC<{
|
|
|
53
52
|
storePaymentInfo,
|
|
54
53
|
contactForm
|
|
55
54
|
}) => {
|
|
56
|
-
const
|
|
55
|
+
const cmmc = useCommerce()
|
|
57
56
|
const auth = useAuth()
|
|
58
57
|
const [loadingPrice, setLoadingPrice] = useState(false)
|
|
59
58
|
//const [selectedToken, setSelectedToken] = useState('eth')
|
|
60
59
|
const [amount, setAmount] = useState<number>()
|
|
61
|
-
const [availableAmount, setAvailableAmount] = useState<number>()
|
|
62
60
|
const [provider, setProvider] = useState<ethers.BrowserProvider>()
|
|
63
61
|
|
|
64
62
|
//const selectedToken = 'eth'
|
|
@@ -69,11 +67,6 @@ const PayWithCrypto: React.FC<{
|
|
|
69
67
|
return autorun(() => {
|
|
70
68
|
const newProvider = new ethers.BrowserProvider(window.ethereum)
|
|
71
69
|
setProvider(newProvider)
|
|
72
|
-
if (auth.user?.walletAddress) {
|
|
73
|
-
newProvider.getBalance(auth.user?.walletAddress).then((balance: any) => {
|
|
74
|
-
setAvailableAmount(Number(balance)/(10**18))
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
70
|
})
|
|
78
71
|
}, [])
|
|
79
72
|
|
|
@@ -85,7 +78,7 @@ const PayWithCrypto: React.FC<{
|
|
|
85
78
|
.then(res => res.json())
|
|
86
79
|
.then((exchangeRate) => {
|
|
87
80
|
const oneUsdInWei = (10**18) / exchangeRate.data.amount
|
|
88
|
-
const usdAmountInWei = oneUsdInWei *
|
|
81
|
+
const usdAmountInWei = oneUsdInWei * cmmc.cartTotal
|
|
89
82
|
setAmount(usdAmountInWei)
|
|
90
83
|
setLoadingPrice(false)
|
|
91
84
|
})
|
|
@@ -98,7 +91,7 @@ const PayWithCrypto: React.FC<{
|
|
|
98
91
|
const interval = setInterval(fetchPrice, 30000)
|
|
99
92
|
|
|
100
93
|
return () => clearInterval(interval)
|
|
101
|
-
}, [
|
|
94
|
+
}, [cmmc.cartTotal])
|
|
102
95
|
|
|
103
96
|
const sendPayment = async (ether: number) => {
|
|
104
97
|
contactForm.handleSubmit(async () => {
|
|
@@ -110,11 +103,6 @@ const PayWithCrypto: React.FC<{
|
|
|
110
103
|
})
|
|
111
104
|
const newProvider = new ethers.BrowserProvider(window.ethereum)
|
|
112
105
|
setProvider(newProvider)
|
|
113
|
-
if (auth.user?.walletAddress) {
|
|
114
|
-
newProvider.getBalance(auth.user?.walletAddress).then((balance: any) => {
|
|
115
|
-
setAvailableAmount(Number(balance)/(10**18))
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
106
|
} catch (err) {
|
|
119
107
|
toast('Please switch your wallet to the Ethereum network.')
|
|
120
108
|
return
|
|
@@ -130,9 +118,10 @@ const PayWithCrypto: React.FC<{
|
|
|
130
118
|
|
|
131
119
|
const signer = await provider.getSigner()
|
|
132
120
|
ethers.getAddress(process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS ?? '')
|
|
121
|
+
const price = ethers.parseEther(ether.toString())
|
|
133
122
|
const tx = await signer.sendTransaction({
|
|
134
123
|
to: process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS,
|
|
135
|
-
value:
|
|
124
|
+
value: price
|
|
136
125
|
})
|
|
137
126
|
console.log({ ether, addr: process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS })
|
|
138
127
|
console.log('tx', tx)
|
|
@@ -150,7 +139,30 @@ const PayWithCrypto: React.FC<{
|
|
|
150
139
|
await storePaymentInfo({
|
|
151
140
|
ether,
|
|
152
141
|
addr: process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS,
|
|
153
|
-
receipt
|
|
142
|
+
receipt,
|
|
143
|
+
paymentMethod: 'crypto'
|
|
144
|
+
})
|
|
145
|
+
sendGAEvent('purchase', {
|
|
146
|
+
transaction_id: tx.hash,
|
|
147
|
+
value: price,
|
|
148
|
+
currency: 'ETH',
|
|
149
|
+
items: cmmc.cartItems.map((item) => ({
|
|
150
|
+
item_id: item.sku,
|
|
151
|
+
item_name: item.title,
|
|
152
|
+
item_category: item.categoryId,
|
|
153
|
+
price: item.price,
|
|
154
|
+
quantity: item.quantity
|
|
155
|
+
})),
|
|
156
|
+
})
|
|
157
|
+
sendFBEvent('Purchase', {
|
|
158
|
+
content_ids: cmmc.cartItems.map((item) => item.sku),
|
|
159
|
+
contents: cmmc.cartItems.map(item => ({
|
|
160
|
+
id: item.sku,
|
|
161
|
+
quantity: item.quantity
|
|
162
|
+
})),
|
|
163
|
+
num_items: cmmc.cartItems.length,
|
|
164
|
+
value: price,
|
|
165
|
+
currency: 'ETH',
|
|
154
166
|
})
|
|
155
167
|
setTransactionStatus('confirmed')
|
|
156
168
|
})
|
|
@@ -202,7 +214,6 @@ const PayWithCrypto: React.FC<{
|
|
|
202
214
|
</div>
|
|
203
215
|
</div>
|
|
204
216
|
</div>
|
|
205
|
-
<div>Available funds in your wallet: {availableAmount} ETH</div>
|
|
206
217
|
|
|
207
218
|
{transactionStatus === 'error' ? (
|
|
208
219
|
<h4 className='text-destructive'>There was an error while confirming the transaction.</h4>
|
|
@@ -8,7 +8,7 @@ import Jcb from './jcb'
|
|
|
8
8
|
|
|
9
9
|
const PaymentMethods: React.FC = () => {
|
|
10
10
|
return (
|
|
11
|
-
<div className='flex gap-1 items-center'>
|
|
11
|
+
<div className='flex gap-1 items-center text-muted-1'>
|
|
12
12
|
<LockKeyhole className='w-4 h-4'/>
|
|
13
13
|
<span className='hidden sm:flex text-sm'>Secure payments with</span>
|
|
14
14
|
<Amex className='w-9 h-5'/>
|
|
@@ -21,11 +21,10 @@ import {
|
|
|
21
21
|
SelectValue
|
|
22
22
|
} from '@hanzo/ui/primitives'
|
|
23
23
|
|
|
24
|
-
import { useAuth } from '@hanzo/auth/service'
|
|
25
|
-
|
|
26
24
|
import { useCommerce } from '../..'
|
|
27
25
|
|
|
28
26
|
import { countries } from './countries'
|
|
27
|
+
import { sendGAEvent } from '../../util/analytics'
|
|
29
28
|
|
|
30
29
|
const shippingFormSchema = z.object({
|
|
31
30
|
addressLine1: z.string().min(2, 'Address must be at least 2 characters.'),
|
|
@@ -43,7 +42,6 @@ const ShippingInfo: React.FC<{
|
|
|
43
42
|
orderId,
|
|
44
43
|
setStep
|
|
45
44
|
}) => {
|
|
46
|
-
const auth = useAuth()
|
|
47
45
|
const cmmc = useCommerce()
|
|
48
46
|
|
|
49
47
|
const shippingForm = useForm<z.infer<typeof shippingFormSchema>>({
|
|
@@ -62,6 +60,18 @@ const ShippingInfo: React.FC<{
|
|
|
62
60
|
if (orderId) {
|
|
63
61
|
await cmmc.updateOrderShippingInfo(orderId, values)
|
|
64
62
|
}
|
|
63
|
+
sendGAEvent('add_shipping_info', {
|
|
64
|
+
items: cmmc.cartItems.map((item) => ({
|
|
65
|
+
item_id: item.sku,
|
|
66
|
+
item_name: item.title,
|
|
67
|
+
item_category: item.categoryId,
|
|
68
|
+
price: item.price,
|
|
69
|
+
quantity: item.quantity
|
|
70
|
+
})),
|
|
71
|
+
num_items: cmmc.cartItems.length,
|
|
72
|
+
value: cmmc.cartTotal,
|
|
73
|
+
currency: 'USD',
|
|
74
|
+
})
|
|
65
75
|
setStep(3)
|
|
66
76
|
}
|
|
67
77
|
|
|
@@ -74,9 +84,8 @@ const ShippingInfo: React.FC<{
|
|
|
74
84
|
name='addressLine1'
|
|
75
85
|
render={({ field }) => (
|
|
76
86
|
<FormItem className='space-y-1 w-full'>
|
|
77
|
-
<FormLabel>Address line 1</FormLabel>
|
|
78
87
|
<FormControl>
|
|
79
|
-
<Input {...field} className='border-muted-4'/>
|
|
88
|
+
<Input {...field} className='border-muted-4' placeholder='Address line 1'/>
|
|
80
89
|
</FormControl>
|
|
81
90
|
<FormMessage />
|
|
82
91
|
</FormItem>
|
|
@@ -87,9 +96,8 @@ const ShippingInfo: React.FC<{
|
|
|
87
96
|
name='addressLine2'
|
|
88
97
|
render={({ field }) => (
|
|
89
98
|
<FormItem className='space-y-1 w-full'>
|
|
90
|
-
<FormLabel>Address line 2 (optional)</FormLabel>
|
|
91
99
|
<FormControl>
|
|
92
|
-
<Input {...field} className='border-muted-4'/>
|
|
100
|
+
<Input {...field} className='border-muted-4' placeholder='Address line 2 (optional)'/>
|
|
93
101
|
</FormControl>
|
|
94
102
|
<FormMessage />
|
|
95
103
|
</FormItem>
|
|
@@ -101,9 +109,8 @@ const ShippingInfo: React.FC<{
|
|
|
101
109
|
name='zipCode'
|
|
102
110
|
render={({ field }) => (
|
|
103
111
|
<FormItem className='space-y-1 w-full'>
|
|
104
|
-
<FormLabel>Zip code</FormLabel>
|
|
105
112
|
<FormControl>
|
|
106
|
-
<Input {...field} className='border-muted-4'/>
|
|
113
|
+
<Input {...field} className='border-muted-4' placeholder='Zip code'/>
|
|
107
114
|
</FormControl>
|
|
108
115
|
<FormMessage />
|
|
109
116
|
</FormItem>
|
|
@@ -114,9 +121,8 @@ const ShippingInfo: React.FC<{
|
|
|
114
121
|
name='city'
|
|
115
122
|
render={({ field }) => (
|
|
116
123
|
<FormItem className='space-y-1 w-full'>
|
|
117
|
-
<FormLabel>City</FormLabel>
|
|
118
124
|
<FormControl>
|
|
119
|
-
<Input {...field} className='border-muted-4'/>
|
|
125
|
+
<Input {...field} className='border-muted-4' placeholder='City'/>
|
|
120
126
|
</FormControl>
|
|
121
127
|
<FormMessage />
|
|
122
128
|
</FormItem>
|
|
@@ -129,9 +135,8 @@ const ShippingInfo: React.FC<{
|
|
|
129
135
|
name='state'
|
|
130
136
|
render={({ field }) => (
|
|
131
137
|
<FormItem className='space-y-1 w-full'>
|
|
132
|
-
<FormLabel>State (Optional)</FormLabel>
|
|
133
138
|
<FormControl>
|
|
134
|
-
<Input {...field} className='border-muted-4'/>
|
|
139
|
+
<Input {...field} className='border-muted-4' placeholder='State (optional)'/>
|
|
135
140
|
</FormControl>
|
|
136
141
|
<FormMessage />
|
|
137
142
|
</FormItem>
|
|
@@ -142,11 +147,10 @@ const ShippingInfo: React.FC<{
|
|
|
142
147
|
name='country'
|
|
143
148
|
render={({ field }) => (
|
|
144
149
|
<FormItem className='space-y-1 w-full'>
|
|
145
|
-
<FormLabel>Country</FormLabel>
|
|
146
150
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
147
151
|
<FormControl>
|
|
148
|
-
<SelectTrigger>
|
|
149
|
-
<SelectValue placeholder='
|
|
152
|
+
<SelectTrigger className='bg-level-1 border-muted-4'>
|
|
153
|
+
<SelectValue placeholder='Country' />
|
|
150
154
|
</SelectTrigger>
|
|
151
155
|
</FormControl>
|
|
152
156
|
<SelectContent>
|
|
@@ -4,25 +4,25 @@ import Image from 'next/image'
|
|
|
4
4
|
import type { FacetValueDesc } from '../../types'
|
|
5
5
|
|
|
6
6
|
const ICON_SIZE = 20
|
|
7
|
-
const SVG_MULT = 1.5
|
|
8
7
|
|
|
9
8
|
const FacetImage: React.FC<{
|
|
10
9
|
facetValueDesc: FacetValueDesc
|
|
11
10
|
}> = ({
|
|
12
11
|
facetValueDesc
|
|
13
12
|
}) => {
|
|
13
|
+
|
|
14
14
|
const {
|
|
15
15
|
img,
|
|
16
16
|
imgAR: ar,
|
|
17
17
|
label
|
|
18
18
|
} = facetValueDesc
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
if (!img) {
|
|
22
21
|
return null
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
|
|
24
|
+
// url
|
|
25
|
+
if (typeof img === 'string') {
|
|
26
26
|
return (
|
|
27
27
|
<Image
|
|
28
28
|
src={img as string}
|
|
@@ -33,42 +33,8 @@ const FacetImage: React.FC<{
|
|
|
33
33
|
/>
|
|
34
34
|
)
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
// ReactNode
|
|
37
37
|
return img as React.ReactNode
|
|
38
|
-
|
|
39
|
-
// Otherwise, assume it's a ReactNode of an imported SVG
|
|
40
|
-
|
|
41
|
-
// If it's not square, center it in the appropriate dimension
|
|
42
|
-
/*
|
|
43
|
-
const svgStyle: any = { position: 'relative' }
|
|
44
|
-
if (ar) {
|
|
45
|
-
if (ar < 1) {
|
|
46
|
-
const w = ICON_SIZE * SVG_MULT
|
|
47
|
-
svgStyle.top = (( 1 / ar - 1) * w) / 2
|
|
48
|
-
}
|
|
49
|
-
else if (ar > 1) {
|
|
50
|
-
const h = ICON_SIZE * SVG_MULT
|
|
51
|
-
svgStyle.left = (((1 - ar) * h) / 2)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<span
|
|
57
|
-
className='flex justify-center items-center overflow-hidden '
|
|
58
|
-
style={{
|
|
59
|
-
color: 'inherit',
|
|
60
|
-
width: ICON_SIZE * SVG_MULT,
|
|
61
|
-
height: ICON_SIZE * SVG_MULT,
|
|
62
|
-
}}
|
|
63
|
-
>
|
|
64
|
-
{React.cloneElement(facetValueDesc.img as React.ReactElement<any>, {
|
|
65
|
-
width: (facetValueDesc.imgAR ?? 1) * ICON_SIZE * SVG_MULT,
|
|
66
|
-
height: SVG_MULT * ICON_SIZE,
|
|
67
|
-
style: svgStyle
|
|
68
|
-
})}
|
|
69
|
-
</span>
|
|
70
|
-
)
|
|
71
|
-
*/
|
|
72
38
|
}
|
|
73
39
|
|
|
74
40
|
export default FacetImage
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/commerce",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "Library with shopping cart components.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -31,14 +31,16 @@
|
|
|
31
31
|
"./types": "./types/index.ts"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@splinetool/react-spline": "^2.2.6",
|
|
35
|
+
"@splinetool/runtime": "^1.0.75",
|
|
34
36
|
"ethers": "^6.11.1",
|
|
35
37
|
"next-usequerystate": "^1.17.0",
|
|
36
38
|
"react-square-web-payments-sdk": "^3.2.1",
|
|
37
39
|
"square": "^35.0.0"
|
|
38
40
|
},
|
|
39
41
|
"peerDependencies": {
|
|
40
|
-
"@hanzo/auth": "^2.
|
|
41
|
-
"@hanzo/ui": "^3.0.
|
|
42
|
+
"@hanzo/auth": "^2.3.1",
|
|
43
|
+
"@hanzo/ui": "^3.0.9",
|
|
42
44
|
"@hookform/resolvers": "^3.3.4",
|
|
43
45
|
"firebase": "^10.8.0",
|
|
44
46
|
"lucide-react": "^0.307.0",
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
observable,
|
|
6
6
|
} from 'mobx'
|
|
7
7
|
|
|
8
|
+
import type { VideoDef } from '@hanzo/ui/types'
|
|
9
|
+
|
|
8
10
|
import type { Product, LineItem } from '../../../types'
|
|
9
11
|
|
|
10
12
|
interface ActualLineItemSnapshot {
|
|
@@ -30,6 +32,8 @@ class ActualLineItem
|
|
|
30
32
|
desc?: string
|
|
31
33
|
price: number
|
|
32
34
|
img?: string
|
|
35
|
+
video?: VideoDef
|
|
36
|
+
animation?: string
|
|
33
37
|
timeAdded: number = 0 // timeAdded of being added to cart
|
|
34
38
|
|
|
35
39
|
constructor(prod: Product, snap?: ActualLineItemSnapshot) {
|
|
@@ -41,10 +45,12 @@ class ActualLineItem
|
|
|
41
45
|
this.desc = prod.desc
|
|
42
46
|
this.price = prod.price
|
|
43
47
|
this.img = prod.img
|
|
48
|
+
this.video = prod.video
|
|
49
|
+
this.animation = prod.animation
|
|
44
50
|
|
|
45
51
|
if (snap) {
|
|
46
52
|
this.qu = snap.quantity
|
|
47
|
-
this.timeAdded = snap.
|
|
53
|
+
this.timeAdded = snap.timeAdded
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
makeObservable(this, {
|
package/types/product.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { VideoDef } from '@hanzo/ui/types'
|
|
2
|
+
|
|
1
3
|
interface Product {
|
|
2
4
|
id: string // DB index // not a logical aspect of our domain. may not be necessary at all
|
|
3
5
|
sku: string // human visible on orders etc.
|
|
@@ -8,6 +10,8 @@ interface Product {
|
|
|
8
10
|
desc?: string
|
|
9
11
|
price: number
|
|
10
12
|
img?: string // if undefined: (category's img exists) ? (use it) : (use generic placeholder)
|
|
13
|
+
animation?: string // spline scene url
|
|
14
|
+
video?: VideoDef
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
fbq: Function;
|
|
4
|
+
gtag: Function;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// https://developers.facebook.com/docs/meta-pixel/reference
|
|
9
|
+
const sendFBEvent = (name: string, options = {}) => {
|
|
10
|
+
window.fbq('track', name, options)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?client_type=gtag
|
|
14
|
+
const sendGAEvent = (name: string, options = {}) => {
|
|
15
|
+
window.gtag('event', name, options)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
sendFBEvent,
|
|
20
|
+
sendGAEvent,
|
|
21
|
+
}
|