@hanzo/commerce 4.5.0 → 4.6.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 +27 -1
- package/components/checkout-panel/payment/index.tsx +21 -0
- package/components/checkout-panel/payment/pay-with-card.tsx +23 -0
- package/components/checkout-panel/payment/pay-with-crypto.tsx +30 -7
- package/components/checkout-panel/shipping-info.tsx +13 -3
- package/components/facet-values-widget/facet-image.tsx +5 -39
- package/package.json +2 -2
- 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,6 +9,7 @@ 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'
|
|
12
13
|
|
|
13
14
|
const CartPanel: React.FC<PropsWithChildren & {
|
|
14
15
|
className?: string
|
|
@@ -31,6 +32,31 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
31
32
|
return <div />
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
const showCheckout = () => {
|
|
36
|
+
sendGAEvent('begin_checkout', {
|
|
37
|
+
currency: 'USD',
|
|
38
|
+
value: cmmc.cartTotal,
|
|
39
|
+
items: cmmc.cartItems.map((item) => ({
|
|
40
|
+
item_id: item.sku,
|
|
41
|
+
item_name: item.title,
|
|
42
|
+
item_category: item.categoryId,
|
|
43
|
+
price: item.price,
|
|
44
|
+
quantity: item.quantity
|
|
45
|
+
})),
|
|
46
|
+
})
|
|
47
|
+
sendFBEvent('InitiateCheckout', {
|
|
48
|
+
content_ids: cmmc.cartItems.map((item) => item.sku),
|
|
49
|
+
contents: cmmc.cartItems.map(item => ({
|
|
50
|
+
id: item.sku,
|
|
51
|
+
quantity: item.quantity
|
|
52
|
+
})),
|
|
53
|
+
num_items: cmmc.cartItems.length,
|
|
54
|
+
value: cmmc.cartTotal,
|
|
55
|
+
currency: 'USD',
|
|
56
|
+
})
|
|
57
|
+
onCheckoutOpen && onCheckoutOpen()
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
return (
|
|
35
61
|
<div className={cn('border p-4 rounded-lg', className)}>
|
|
36
62
|
{children}
|
|
@@ -50,7 +76,7 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
50
76
|
variant='primary'
|
|
51
77
|
rounded='lg'
|
|
52
78
|
className='mt-12 mx-auto w-full'
|
|
53
|
-
onClick={
|
|
79
|
+
onClick={showCheckout}
|
|
54
80
|
>
|
|
55
81
|
Checkout
|
|
56
82
|
</Button>
|
|
@@ -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
|
}
|
|
@@ -13,6 +13,7 @@ import type { TransactionStatus } from '../../../types'
|
|
|
13
13
|
|
|
14
14
|
import PaymentMethods from './payment-methods'
|
|
15
15
|
import ContactInfo from './contact-info'
|
|
16
|
+
import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
|
|
16
17
|
|
|
17
18
|
const PayWithCard: React.FC<{
|
|
18
19
|
setStep: (currentStep: number) => void
|
|
@@ -46,6 +47,28 @@ const PayWithCard: React.FC<{
|
|
|
46
47
|
console.log(token)
|
|
47
48
|
await storePaymentInfo({paymentMethod: token.details.method ?? null, processed: res})
|
|
48
49
|
setTransactionStatus('confirmed')
|
|
50
|
+
sendGAEvent('purchase', {
|
|
51
|
+
transaction_id: res.payment?.id,
|
|
52
|
+
value: res.payment?.amountMoney?.amount,
|
|
53
|
+
currency: res.payment?.amountMoney?.currency,
|
|
54
|
+
items: cmmc.cartItems.map((item) => ({
|
|
55
|
+
item_id: item.sku,
|
|
56
|
+
item_name: item.title,
|
|
57
|
+
item_category: item.categoryId,
|
|
58
|
+
price: item.price,
|
|
59
|
+
quantity: item.quantity
|
|
60
|
+
})),
|
|
61
|
+
})
|
|
62
|
+
sendFBEvent('Purchase', {
|
|
63
|
+
content_ids: cmmc.cartItems.map((item) => item.sku),
|
|
64
|
+
contents: cmmc.cartItems.map(item => ({
|
|
65
|
+
id: item.sku,
|
|
66
|
+
quantity: item.quantity
|
|
67
|
+
})),
|
|
68
|
+
num_items: cmmc.cartItems.length,
|
|
69
|
+
value: cmmc.cartTotal,
|
|
70
|
+
currency: 'USD',
|
|
71
|
+
})
|
|
49
72
|
} else {
|
|
50
73
|
setTransactionStatus('error')
|
|
51
74
|
}
|
|
@@ -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,7 +52,7 @@ 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')
|
|
@@ -85,7 +84,7 @@ const PayWithCrypto: React.FC<{
|
|
|
85
84
|
.then(res => res.json())
|
|
86
85
|
.then((exchangeRate) => {
|
|
87
86
|
const oneUsdInWei = (10**18) / exchangeRate.data.amount
|
|
88
|
-
const usdAmountInWei = oneUsdInWei *
|
|
87
|
+
const usdAmountInWei = oneUsdInWei * cmmc.cartTotal
|
|
89
88
|
setAmount(usdAmountInWei)
|
|
90
89
|
setLoadingPrice(false)
|
|
91
90
|
})
|
|
@@ -98,7 +97,7 @@ const PayWithCrypto: React.FC<{
|
|
|
98
97
|
const interval = setInterval(fetchPrice, 30000)
|
|
99
98
|
|
|
100
99
|
return () => clearInterval(interval)
|
|
101
|
-
}, [
|
|
100
|
+
}, [cmmc.cartTotal])
|
|
102
101
|
|
|
103
102
|
const sendPayment = async (ether: number) => {
|
|
104
103
|
contactForm.handleSubmit(async () => {
|
|
@@ -130,9 +129,10 @@ const PayWithCrypto: React.FC<{
|
|
|
130
129
|
|
|
131
130
|
const signer = await provider.getSigner()
|
|
132
131
|
ethers.getAddress(process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS ?? '')
|
|
132
|
+
const price = ethers.parseEther(ether.toString())
|
|
133
133
|
const tx = await signer.sendTransaction({
|
|
134
134
|
to: process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS,
|
|
135
|
-
value:
|
|
135
|
+
value: price
|
|
136
136
|
})
|
|
137
137
|
console.log({ ether, addr: process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS })
|
|
138
138
|
console.log('tx', tx)
|
|
@@ -150,7 +150,30 @@ const PayWithCrypto: React.FC<{
|
|
|
150
150
|
await storePaymentInfo({
|
|
151
151
|
ether,
|
|
152
152
|
addr: process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS,
|
|
153
|
-
receipt
|
|
153
|
+
receipt,
|
|
154
|
+
paymentMethod: 'crypto'
|
|
155
|
+
})
|
|
156
|
+
sendGAEvent('purchase', {
|
|
157
|
+
transaction_id: tx.hash,
|
|
158
|
+
value: price,
|
|
159
|
+
currency: 'ETH',
|
|
160
|
+
items: cmmc.cartItems.map((item) => ({
|
|
161
|
+
item_id: item.sku,
|
|
162
|
+
item_name: item.title,
|
|
163
|
+
item_category: item.categoryId,
|
|
164
|
+
price: item.price,
|
|
165
|
+
quantity: item.quantity
|
|
166
|
+
})),
|
|
167
|
+
})
|
|
168
|
+
sendFBEvent('Purchase', {
|
|
169
|
+
content_ids: cmmc.cartItems.map((item) => item.sku),
|
|
170
|
+
contents: cmmc.cartItems.map(item => ({
|
|
171
|
+
id: item.sku,
|
|
172
|
+
quantity: item.quantity
|
|
173
|
+
})),
|
|
174
|
+
num_items: cmmc.cartItems.length,
|
|
175
|
+
value: price,
|
|
176
|
+
currency: 'ETH',
|
|
154
177
|
})
|
|
155
178
|
setTransactionStatus('confirmed')
|
|
156
179
|
})
|
|
@@ -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
|
|
|
@@ -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.6.0",
|
|
4
4
|
"description": "Library with shopping cart components.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"square": "^35.0.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@hanzo/auth": "^2.
|
|
40
|
+
"@hanzo/auth": "^2.3.0",
|
|
41
41
|
"@hanzo/ui": "^3.0.7",
|
|
42
42
|
"@hookform/resolvers": "^3.3.4",
|
|
43
43
|
"firebase": "^10.8.0",
|
|
@@ -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
|
+
}
|