@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.
@@ -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={onCheckoutOpen}
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 BagIcon from './icons/bag-icon'
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='grid grid-cols-1 md:grid-cols-2 justify-center h-full overflow-y-auto md:overflow-y-hidden'>
84
- <div className='bg-background flex flex-row items-start justify-end'>
85
- <div className='h-full w-full max-w-[750px] relative flex flex-col items-center justify-start px-6 pt-10 pb-0 md:pb-9'>
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 noCheckout className='border border-muted-2 mt-10 w-full max-w-[550px] hidden md:flex'/>
89
-
90
- <Accordion type="single" collapsible className='flex items-center justify-center py-2 w-full md:hidden'>
91
- <AccordionItem value="cart" className='w-full border-b-0'>
92
- <AccordionTrigger className='!no-underline'>
93
- <div className='flex gap-4 items-center'>
94
- <BagIcon className='w-4 h-4 sm:w-6 sm:h-6'/>
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
- <div className='bg-level-1 flex flex-row items-start justify-start md:overflow-y-auto'>
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
- </div>
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 flex-col sm:flex-row gap-2'>
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-sm sm:text-base'
89
+ className='whitespace-normal h-full text-xs sm:text-base px-1'
69
90
  disabled={transactionStatus === 'paid' || transactionStatus === 'confirmed'}
70
91
  >
71
- PAY WITH CARD
92
+ Pay with card
72
93
  </TabsTrigger>
73
94
  <TabsTrigger
74
95
  value='crypto'
75
- className='whitespace-normal h-full text-sm sm:text-base'
96
+ className='whitespace-normal h-full text-xs sm:text-base px-1'
76
97
  disabled={transactionStatus === 'paid' || transactionStatus === 'confirmed'}
77
98
  >
78
- PAY WITH CRYPTO
99
+ Pay with Wallet
79
100
  </TabsTrigger>
80
101
  <TabsTrigger
81
102
  value='bank'
82
- className='whitespace-normal h-full text-sm sm:text-base'
103
+ className='whitespace-normal h-full text-xs sm:text-base px-1'
83
104
  disabled={transactionStatus === 'paid' || transactionStatus === 'confirmed'}
84
105
  >
85
- BANK TRANSFER
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-6 mt-6'>
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 gap-2 mt-6'>
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-6 sm:my-10 text-sm text-foreground/60'>
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
- buttonProps={{
173
- className: 'font-nav h-10',
174
- css: {
175
- backgroundColor: '#fff',
176
- color: '#000',
177
- padding: 0,
178
- '&:hover': {
179
- backgroundColor: '#ffffffd9',
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 { LoginComponent } from '@hanzo/auth/components'
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 c = useCommerce()
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 * c.cartTotal
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
- }, [c.cartTotal])
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: ethers.parseEther(ether.toString())
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='Select a country' />
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
- const isURL = typeof img === 'string'
25
- if (isURL) {
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.5.0",
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.2.1",
41
- "@hanzo/ui": "^3.0.7",
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.quantity
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
+ }