@hanzo/commerce 1.1.10 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,15 @@
1
1
  'use client'
2
- import React, { type PropsWithChildren } from 'react'
3
- import { useRouter } from 'next/navigation'
2
+ import React, { useState, type PropsWithChildren } from 'react'
4
3
  import { observer } from 'mobx-react-lite'
5
4
 
6
5
  import { Button } from '@hanzo/ui/primitives'
7
6
  import { cn } from '@hanzo/ui/util'
8
- import { useAuth } from '@hanzo/auth/service'
9
7
 
10
8
  import { useCommerce } from '../service/context'
11
9
  import { formatPrice } from '../util'
12
10
 
13
11
  import CartLineItem from './cart-line-item'
12
+ import { Checkout } from '.'
14
13
 
15
14
  const Cart: React.FC<PropsWithChildren & {
16
15
  className?: string
@@ -23,31 +22,29 @@ const Cart: React.FC<PropsWithChildren & {
23
22
  isMobile=false,
24
23
  hideCheckout=false
25
24
  }) => {
25
+ const [isCheckoutOpen, setIsCheckoutOpen] = useState(false)
26
26
  const cmmc = useCommerce()
27
- const router = useRouter()
28
- const auth = useAuth()
29
-
27
+
30
28
  return (
31
29
  <div className={cn('border p-4 rounded-lg', className)}>
32
30
  {children}
33
- <div className='mt-2'>
31
+ <div className='mt-2 w-full'>
34
32
  {!!children && <div className='h-[1px] w-pr-80 mb-4 mx-auto bg-muted-3'/>}
35
- {cmmc.cartItems.length === 0 ? (
36
- <p className='text-center mt-4'>No items in cart</p>
37
- ) : (<>
38
- {cmmc.cartItems.map((item, i) => (
39
- <CartLineItem isMobile={isMobile} item={item} key={item.sku} className='mb-2 md:mb-3'/>
40
- ))}
41
- <p className='mt-6 text-right border-t pt-1'>TOTAL: <span className='font-semibold'>{cmmc.cartTotal === 0 ? '0' : formatPrice(cmmc.cartTotal)}</span></p>
42
- </>)}
33
+ {cmmc.cartItems.length === 0 ? (
34
+ <p className='text-center mt-4'>No items in cart</p>
35
+ ) : (<>
36
+ {cmmc.cartItems.map((item, i) => (
37
+ <CartLineItem isMobile={isMobile} item={item} key={item.sku} className='mb-2 md:mb-3'/>
38
+ ))}
39
+ <p className='mt-6 text-right border-t pt-1'>TOTAL: <span className='font-semibold'>{cmmc.cartTotal === 0 ? '0' : formatPrice(cmmc.cartTotal)}</span></p>
40
+ </>)}
43
41
  </div>
44
- {cmmc.cartItems.length > 0 && !hideCheckout && (
42
+ {!hideCheckout && (
45
43
  <>
46
- {!(auth && auth.loggedIn) ? (
47
- <Button size='sm' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/login?redirectUrl=checkout')}>Login to checkout</Button>
48
- ) : (
49
- <Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/checkout')}>Checkout</Button>
44
+ {cmmc.cartItems.length > 0 && (
45
+ <Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => setIsCheckoutOpen(!isCheckoutOpen)}>Checkout</Button>
50
46
  )}
47
+ {isCheckoutOpen && <Checkout toggleCheckout={() => setIsCheckoutOpen(!isCheckoutOpen)}/>}
51
48
  </>
52
49
  )}
53
50
  </div>
@@ -0,0 +1,142 @@
1
+ 'use client'
2
+
3
+ import { ApplyTypography, Button } from '@hanzo/ui/primitives'
4
+ import { BookUser, Lock, User } from 'lucide-react'
5
+ import { EnhHeadingBlockComponent, type EnhHeadingBlock } from '@hanzo/ui/blocks'
6
+ import type { UseFormReturn } from 'react-hook-form'
7
+
8
+ const ConfirmOrder: React.FC<{
9
+ paymentForm: UseFormReturn<{
10
+ name: string;
11
+ email: string;
12
+ cardName: string;
13
+ cardNumber: string;
14
+ cardExpiryDate: string;
15
+ cardCvc: string;
16
+ }, any, {
17
+ name: string;
18
+ email: string;
19
+ cardName: string;
20
+ cardNumber: string;
21
+ cardExpiryDate: string;
22
+ cardCvc: string;
23
+ }>,
24
+ shippingForm: UseFormReturn<{
25
+ firstName: string;
26
+ lastName: string;
27
+ addressLine1: string;
28
+ zipCode: string;
29
+ city: string;
30
+ country: string;
31
+ addressLine2?: string | undefined;
32
+ state?: string | undefined;
33
+ }, any, {
34
+ firstName: string;
35
+ lastName: string;
36
+ addressLine1: string;
37
+ zipCode: string;
38
+ city: string;
39
+ country: string;
40
+ addressLine2?: string | undefined;
41
+ state?: string | undefined;
42
+ }>,
43
+ setStep: (step: number) => void,
44
+ onPayClick: () => void
45
+ }> = ({
46
+ paymentForm,
47
+ shippingForm,
48
+ setStep,
49
+ onPayClick
50
+ }) => {
51
+
52
+ const paymentFormValues = paymentForm.getValues()
53
+ const shippingFormValues = shippingForm.getValues()
54
+
55
+ return (
56
+ <div className='flex flex-col gap-6'>
57
+ <div className='flex gap-4 items-center'>
58
+ <User />
59
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
60
+ heading: { text: `Contact Details`, level: 4 },
61
+ } as EnhHeadingBlock}/>
62
+ </div>
63
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
64
+ heading: { text: 'Contact name', level: 6 },
65
+ byline: { text: paymentFormValues.name, level: 0 },
66
+ } as EnhHeadingBlock}/>
67
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
68
+ heading: { text: 'Contact email', level: 6 },
69
+ byline: { text: paymentFormValues.email, level: 0 },
70
+ } as EnhHeadingBlock}/>
71
+
72
+ <div className='flex gap-4 items-center'>
73
+ <ApplyTypography>
74
+ <h4 className='flex gap-4 items-center'><Lock /> Payment Info</h4>
75
+ <p>Guaranteed safe & secure checkout.</p>
76
+ </ApplyTypography>
77
+ </div>
78
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
79
+ heading: { text: 'Name on card', level: 6 },
80
+ byline: { text: paymentFormValues.cardName, level: 0 },
81
+ } as EnhHeadingBlock}/>
82
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
83
+ heading: { text: 'Card number', level: 6 },
84
+ byline: { text: paymentFormValues.cardNumber, level: 0 },
85
+ } as EnhHeadingBlock}/>
86
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
87
+ heading: { text: 'Card expiry date', level: 6 },
88
+ byline: { text: paymentFormValues.cardExpiryDate, level: 0 },
89
+ } as EnhHeadingBlock}/>
90
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
91
+ heading: { text: 'Card CVC', level: 6 },
92
+ byline: { text: paymentFormValues.cardCvc, level: 0 },
93
+ } as EnhHeadingBlock}/>
94
+
95
+ <div className='flex gap-4 items-center'>
96
+ <BookUser />
97
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
98
+ heading: { text: `Shipping address`, level: 4 },
99
+ } as EnhHeadingBlock}/>
100
+ </div>
101
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
102
+ heading: { text: 'First name', level: 6 },
103
+ byline: { text: shippingFormValues.firstName, level: 0 },
104
+ } as EnhHeadingBlock}/>
105
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
106
+ heading: { text: 'Last name', level: 6 },
107
+ byline: { text: shippingFormValues.lastName, level: 0 },
108
+ } as EnhHeadingBlock}/>
109
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
110
+ heading: { text: 'Address line 1', level: 6 },
111
+ byline: { text: shippingFormValues.addressLine1, level: 0 },
112
+ } as EnhHeadingBlock}/>
113
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
114
+ heading: { text: 'Address line 2 (optional)', level: 6 },
115
+ byline: { text: shippingFormValues.addressLine2, level: 0 },
116
+ } as EnhHeadingBlock}/>
117
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
118
+ heading: { text: 'Zip code', level: 6 },
119
+ byline: { text: shippingFormValues.zipCode, level: 0 },
120
+ } as EnhHeadingBlock}/>
121
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
122
+ heading: { text: 'City', level: 6 },
123
+ byline: { text: shippingFormValues.city, level: 0 },
124
+ } as EnhHeadingBlock}/>
125
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
126
+ heading: { text: 'State (optional)', level: 6 },
127
+ byline: { text: shippingFormValues.state, level: 0 },
128
+ } as EnhHeadingBlock}/>
129
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
130
+ heading: { text: 'Country', level: 6 },
131
+ byline: { text: shippingFormValues.country, level: 0 },
132
+ } as EnhHeadingBlock}/>
133
+
134
+ <div className='flex justify-between mt-8'>
135
+ <Button variant='outline' onClick={() => setStep(1)}>Back</Button>
136
+ <Button onClick={onPayClick}>Pay</Button>
137
+ </div>
138
+ </div>
139
+ )
140
+ }
141
+
142
+ export default ConfirmOrder
@@ -0,0 +1,89 @@
1
+ 'use client'
2
+
3
+ import {
4
+ Form,
5
+ FormControl,
6
+ FormField,
7
+ FormItem,
8
+ FormLabel,
9
+ FormMessage,
10
+ } from '@hanzo/ui/primitives/form'
11
+ import { Input, Button } from '@hanzo/ui/primitives'
12
+ import { User } from 'lucide-react'
13
+ import { EnhHeadingBlockComponent, type EnhHeadingBlock } from '@hanzo/ui/blocks'
14
+ import type { UseFormReturn } from 'react-hook-form'
15
+
16
+ const ContactInfo: React.FC<{
17
+ form: UseFormReturn<{
18
+ name: string;
19
+ email: string;
20
+ }, any, {
21
+ name: string;
22
+ email: string;
23
+ }>,
24
+ selectPaymentMethod: (method: 'crypto' | 'bank') => Promise<void>,
25
+ }> = ({
26
+ form,
27
+ selectPaymentMethod,
28
+ }) => {
29
+ return (
30
+ <Form {...form}>
31
+ <form className='text-left'>
32
+ <div className='flex flex-col gap-4'>
33
+ <div className='flex gap-4 items-center'>
34
+ <User />
35
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
36
+ heading: { text: `Contact Info`, level: 4 },
37
+ } as EnhHeadingBlock}/>
38
+ </div>
39
+ <div className='flex flex-col sm:flex-row gap-4'>
40
+ <FormField
41
+ control={form.control}
42
+ name='name'
43
+ render={({ field }) => (
44
+ <FormItem className='space-y-1 w-full'>
45
+ <FormLabel>Name</FormLabel>
46
+ <FormControl>
47
+ <Input {...field} />
48
+ </FormControl>
49
+ <FormMessage />
50
+ </FormItem>
51
+ )}
52
+ />
53
+ <FormField
54
+ control={form.control}
55
+ name='email'
56
+ render={({ field }) => (
57
+ <FormItem className='space-y-1 w-full'>
58
+ <FormLabel>Email</FormLabel>
59
+ <FormControl>
60
+ <Input {...field} />
61
+ </FormControl>
62
+ <FormMessage />
63
+ </FormItem>
64
+ )}
65
+ />
66
+ </div>
67
+ <div className='flex flex-col sm:flex-row gap-4 items-center'>
68
+ <Button
69
+ type='button'
70
+ onClick={() => selectPaymentMethod('crypto')}
71
+ className='mx-auto w-full'
72
+ >
73
+ Pay with crypto
74
+ </Button>
75
+ <Button
76
+ type='button'
77
+ onClick={() => selectPaymentMethod('bank')}
78
+ className='mx-auto w-full'
79
+ >
80
+ Bank transfer
81
+ </Button>
82
+ </div>
83
+ </div>
84
+ </form>
85
+ </Form>
86
+ )
87
+ }
88
+
89
+ export default ContactInfo
@@ -0,0 +1,196 @@
1
+ //https://stefangabos.github.io/world_countries/
2
+ export const countries = [
3
+ {"id":4,"alpha2":"af","alpha3":"afg","name":"Afghanistan"},
4
+ {"id":8,"alpha2":"al","alpha3":"alb","name":"Albania"},
5
+ {"id":12,"alpha2":"dz","alpha3":"dza","name":"Algeria"},
6
+ {"id":20,"alpha2":"ad","alpha3":"and","name":"Andorra"},
7
+ {"id":24,"alpha2":"ao","alpha3":"ago","name":"Angola"},
8
+ {"id":28,"alpha2":"ag","alpha3":"atg","name":"Antigua and Barbuda"},
9
+ {"id":32,"alpha2":"ar","alpha3":"arg","name":"Argentina"},
10
+ {"id":51,"alpha2":"am","alpha3":"arm","name":"Armenia"},
11
+ {"id":36,"alpha2":"au","alpha3":"aus","name":"Australia"},
12
+ {"id":40,"alpha2":"at","alpha3":"aut","name":"Austria"},
13
+ {"id":31,"alpha2":"az","alpha3":"aze","name":"Azerbaijan"},
14
+ {"id":44,"alpha2":"bs","alpha3":"bhs","name":"Bahamas"},
15
+ {"id":48,"alpha2":"bh","alpha3":"bhr","name":"Bahrain"},
16
+ {"id":50,"alpha2":"bd","alpha3":"bgd","name":"Bangladesh"},
17
+ {"id":52,"alpha2":"bb","alpha3":"brb","name":"Barbados"},
18
+ {"id":112,"alpha2":"by","alpha3":"blr","name":"Belarus"},
19
+ {"id":56,"alpha2":"be","alpha3":"bel","name":"Belgium"},
20
+ {"id":84,"alpha2":"bz","alpha3":"blz","name":"Belize"},
21
+ {"id":204,"alpha2":"bj","alpha3":"ben","name":"Benin"},
22
+ {"id":64,"alpha2":"bt","alpha3":"btn","name":"Bhutan"},
23
+ {"id":68,"alpha2":"bo","alpha3":"bol","name":"Bolivia"},
24
+ {"id":70,"alpha2":"ba","alpha3":"bih","name":"Bosnia and Herzegovina"},
25
+ {"id":72,"alpha2":"bw","alpha3":"bwa","name":"Botswana"},
26
+ {"id":76,"alpha2":"br","alpha3":"bra","name":"Brazil"},
27
+ {"id":96,"alpha2":"bn","alpha3":"brn","name":"Brunei Darussalam"},
28
+ {"id":100,"alpha2":"bg","alpha3":"bgr","name":"Bulgaria"},
29
+ {"id":854,"alpha2":"bf","alpha3":"bfa","name":"Burkina Faso"},
30
+ {"id":108,"alpha2":"bi","alpha3":"bdi","name":"Burundi"},
31
+ {"id":132,"alpha2":"cv","alpha3":"cpv","name":"Cabo Verde"},
32
+ {"id":116,"alpha2":"kh","alpha3":"khm","name":"Cambodia"},
33
+ {"id":120,"alpha2":"cm","alpha3":"cmr","name":"Cameroon"},
34
+ {"id":124,"alpha2":"ca","alpha3":"can","name":"Canada"},
35
+ {"id":140,"alpha2":"cf","alpha3":"caf","name":"Central African Republic"},
36
+ {"id":148,"alpha2":"td","alpha3":"tcd","name":"Chad"},
37
+ {"id":152,"alpha2":"cl","alpha3":"chl","name":"Chile"},
38
+ {"id":156,"alpha2":"cn","alpha3":"chn","name":"China"},
39
+ {"id":170,"alpha2":"co","alpha3":"col","name":"Colombia"},
40
+ {"id":174,"alpha2":"km","alpha3":"com","name":"Comoros"},
41
+ {"id":178,"alpha2":"cg","alpha3":"cog","name":"Congo"},
42
+ {"id":188,"alpha2":"cr","alpha3":"cri","name":"Costa Rica"},
43
+ {"id":384,"alpha2":"ci","alpha3":"civ","name":"Côte d'Ivoire"},
44
+ {"id":191,"alpha2":"hr","alpha3":"hrv","name":"Croatia"},
45
+ {"id":192,"alpha2":"cu","alpha3":"cub","name":"Cuba"},
46
+ {"id":196,"alpha2":"cy","alpha3":"cyp","name":"Cyprus"},
47
+ {"id":203,"alpha2":"cz","alpha3":"cze","name":"Czechia"},
48
+ {"id":180,"alpha2":"cd","alpha3":"cod","name":"Democratic Republic of the Congo"},
49
+ {"id":208,"alpha2":"dk","alpha3":"dnk","name":"Denmark"},
50
+ {"id":262,"alpha2":"dj","alpha3":"dji","name":"Djibouti"},
51
+ {"id":212,"alpha2":"dm","alpha3":"dma","name":"Dominica"},
52
+ {"id":214,"alpha2":"do","alpha3":"dom","name":"Dominican Republic"},
53
+ {"id":218,"alpha2":"ec","alpha3":"ecu","name":"Ecuador"},
54
+ {"id":818,"alpha2":"eg","alpha3":"egy","name":"Egypt"},
55
+ {"id":222,"alpha2":"sv","alpha3":"slv","name":"El Salvador"},
56
+ {"id":226,"alpha2":"gq","alpha3":"gnq","name":"Equatorial Guinea"},
57
+ {"id":232,"alpha2":"er","alpha3":"eri","name":"Eritrea"},
58
+ {"id":233,"alpha2":"ee","alpha3":"est","name":"Estonia"},
59
+ {"id":748,"alpha2":"sz","alpha3":"swz","name":"Eswatini"},
60
+ {"id":231,"alpha2":"et","alpha3":"eth","name":"Ethiopia"},
61
+ {"id":242,"alpha2":"fj","alpha3":"fji","name":"Fiji"},
62
+ {"id":246,"alpha2":"fi","alpha3":"fin","name":"Finland"},
63
+ {"id":250,"alpha2":"fr","alpha3":"fra","name":"France"},
64
+ {"id":266,"alpha2":"ga","alpha3":"gab","name":"Gabon"},
65
+ {"id":270,"alpha2":"gm","alpha3":"gmb","name":"Gambia"},
66
+ {"id":268,"alpha2":"ge","alpha3":"geo","name":"Georgia"},
67
+ {"id":276,"alpha2":"de","alpha3":"deu","name":"Germany"},
68
+ {"id":288,"alpha2":"gh","alpha3":"gha","name":"Ghana"},
69
+ {"id":300,"alpha2":"gr","alpha3":"grc","name":"Greece"},
70
+ {"id":308,"alpha2":"gd","alpha3":"grd","name":"Grenada"},
71
+ {"id":320,"alpha2":"gt","alpha3":"gtm","name":"Guatemala"},
72
+ {"id":324,"alpha2":"gn","alpha3":"gin","name":"Guinea"},
73
+ {"id":624,"alpha2":"gw","alpha3":"gnb","name":"Guinea-Bissau"},
74
+ {"id":328,"alpha2":"gy","alpha3":"guy","name":"Guyana"},
75
+ {"id":332,"alpha2":"ht","alpha3":"hti","name":"Haiti"},
76
+ {"id":340,"alpha2":"hn","alpha3":"hnd","name":"Honduras"},
77
+ {"id":348,"alpha2":"hu","alpha3":"hun","name":"Hungary"},
78
+ {"id":352,"alpha2":"is","alpha3":"isl","name":"Iceland"},
79
+ {"id":356,"alpha2":"in","alpha3":"ind","name":"India"},
80
+ {"id":360,"alpha2":"id","alpha3":"idn","name":"Indonesia"},
81
+ {"id":364,"alpha2":"ir","alpha3":"irn","name":"Iran"},
82
+ {"id":368,"alpha2":"iq","alpha3":"irq","name":"Iraq"},
83
+ {"id":372,"alpha2":"ie","alpha3":"irl","name":"Ireland"},
84
+ {"id":376,"alpha2":"il","alpha3":"isr","name":"Israel"},
85
+ {"id":380,"alpha2":"it","alpha3":"ita","name":"Italy"},
86
+ {"id":388,"alpha2":"jm","alpha3":"jam","name":"Jamaica"},
87
+ {"id":392,"alpha2":"jp","alpha3":"jpn","name":"Japan"},
88
+ {"id":400,"alpha2":"jo","alpha3":"jor","name":"Jordan"},
89
+ {"id":398,"alpha2":"kz","alpha3":"kaz","name":"Kazakhstan"},
90
+ {"id":404,"alpha2":"ke","alpha3":"ken","name":"Kenya"},
91
+ {"id":296,"alpha2":"ki","alpha3":"kir","name":"Kiribati"},
92
+ {"id":414,"alpha2":"kw","alpha3":"kwt","name":"Kuwait"},
93
+ {"id":417,"alpha2":"kg","alpha3":"kgz","name":"Kyrgyzstan"},
94
+ {"id":418,"alpha2":"la","alpha3":"lao","name":"Lao People's Democratic Republic"},
95
+ {"id":428,"alpha2":"lv","alpha3":"lva","name":"Latvia"},
96
+ {"id":422,"alpha2":"lb","alpha3":"lbn","name":"Lebanon"},
97
+ {"id":426,"alpha2":"ls","alpha3":"lso","name":"Lesotho"},
98
+ {"id":430,"alpha2":"lr","alpha3":"lbr","name":"Liberia"},
99
+ {"id":434,"alpha2":"ly","alpha3":"lby","name":"Libya"},
100
+ {"id":438,"alpha2":"li","alpha3":"lie","name":"Liechtenstein"},
101
+ {"id":440,"alpha2":"lt","alpha3":"ltu","name":"Lithuania"},
102
+ {"id":442,"alpha2":"lu","alpha3":"lux","name":"Luxembourg"},
103
+ {"id":450,"alpha2":"mg","alpha3":"mdg","name":"Madagascar"},
104
+ {"id":454,"alpha2":"mw","alpha3":"mwi","name":"Malawi"},
105
+ {"id":458,"alpha2":"my","alpha3":"mys","name":"Malaysia"},
106
+ {"id":462,"alpha2":"mv","alpha3":"mdv","name":"Maldives"},
107
+ {"id":466,"alpha2":"ml","alpha3":"mli","name":"Mali"},
108
+ {"id":470,"alpha2":"mt","alpha3":"mlt","name":"Malta"},
109
+ {"id":584,"alpha2":"mh","alpha3":"mhl","name":"Marshall Islands"},
110
+ {"id":478,"alpha2":"mr","alpha3":"mrt","name":"Mauritania"},
111
+ {"id":480,"alpha2":"mu","alpha3":"mus","name":"Mauritius"},
112
+ {"id":484,"alpha2":"mx","alpha3":"mex","name":"Mexico"},
113
+ {"id":583,"alpha2":"fm","alpha3":"fsm","name":"Micronesia"},
114
+ {"id":498,"alpha2":"md","alpha3":"mda","name":"Moldova"},
115
+ {"id":492,"alpha2":"mc","alpha3":"mco","name":"Monaco"},
116
+ {"id":496,"alpha2":"mn","alpha3":"mng","name":"Mongolia"},
117
+ {"id":499,"alpha2":"me","alpha3":"mne","name":"Montenegro"},
118
+ {"id":504,"alpha2":"ma","alpha3":"mar","name":"Morocco"},
119
+ {"id":508,"alpha2":"mz","alpha3":"moz","name":"Mozambique"},
120
+ {"id":104,"alpha2":"mm","alpha3":"mmr","name":"Myanmar"},
121
+ {"id":516,"alpha2":"na","alpha3":"nam","name":"Namibia"},
122
+ {"id":520,"alpha2":"nr","alpha3":"nru","name":"Nauru"},
123
+ {"id":524,"alpha2":"np","alpha3":"npl","name":"Nepal"},
124
+ {"id":528,"alpha2":"nl","alpha3":"nld","name":"Netherlands"},
125
+ {"id":554,"alpha2":"nz","alpha3":"nzl","name":"New Zealand"},
126
+ {"id":558,"alpha2":"ni","alpha3":"nic","name":"Nicaragua"},
127
+ {"id":562,"alpha2":"ne","alpha3":"ner","name":"Niger"},
128
+ {"id":566,"alpha2":"ng","alpha3":"nga","name":"Nigeria"},
129
+ {"id":408,"alpha2":"kp","alpha3":"prk","name":"North Korea"},
130
+ {"id":807,"alpha2":"mk","alpha3":"mkd","name":"North Macedonia"},
131
+ {"id":578,"alpha2":"no","alpha3":"nor","name":"Norway"},
132
+ {"id":512,"alpha2":"om","alpha3":"omn","name":"Oman"},
133
+ {"id":586,"alpha2":"pk","alpha3":"pak","name":"Pakistan"},
134
+ {"id":585,"alpha2":"pw","alpha3":"plw","name":"Palau"},
135
+ {"id":591,"alpha2":"pa","alpha3":"pan","name":"Panama"},
136
+ {"id":598,"alpha2":"pg","alpha3":"png","name":"Papua New Guinea"},
137
+ {"id":600,"alpha2":"py","alpha3":"pry","name":"Paraguay"},
138
+ {"id":604,"alpha2":"pe","alpha3":"per","name":"Peru"},
139
+ {"id":608,"alpha2":"ph","alpha3":"phl","name":"Philippines"},
140
+ {"id":616,"alpha2":"pl","alpha3":"pol","name":"Poland"},
141
+ {"id":620,"alpha2":"pt","alpha3":"prt","name":"Portugal"},
142
+ {"id":634,"alpha2":"qa","alpha3":"qat","name":"Qatar"},
143
+ {"id":642,"alpha2":"ro","alpha3":"rou","name":"Romania"},
144
+ {"id":643,"alpha2":"ru","alpha3":"rus","name":"Russian Federation"},
145
+ {"id":646,"alpha2":"rw","alpha3":"rwa","name":"Rwanda"},
146
+ {"id":659,"alpha2":"kn","alpha3":"kna","name":"Saint Kitts and Nevis"},
147
+ {"id":662,"alpha2":"lc","alpha3":"lca","name":"Saint Lucia"},
148
+ {"id":670,"alpha2":"vc","alpha3":"vct","name":"Saint Vincent and the Grenadines"},
149
+ {"id":882,"alpha2":"ws","alpha3":"wsm","name":"Samoa"},
150
+ {"id":674,"alpha2":"sm","alpha3":"smr","name":"San Marino"},
151
+ {"id":678,"alpha2":"st","alpha3":"stp","name":"Sao Tome and Principe"},
152
+ {"id":682,"alpha2":"sa","alpha3":"sau","name":"Saudi Arabia"},
153
+ {"id":686,"alpha2":"sn","alpha3":"sen","name":"Senegal"},
154
+ {"id":688,"alpha2":"rs","alpha3":"srb","name":"Serbia"},
155
+ {"id":690,"alpha2":"sc","alpha3":"syc","name":"Seychelles"},
156
+ {"id":694,"alpha2":"sl","alpha3":"sle","name":"Sierra Leone"},
157
+ {"id":702,"alpha2":"sg","alpha3":"sgp","name":"Singapore"},
158
+ {"id":703,"alpha2":"sk","alpha3":"svk","name":"Slovakia"},
159
+ {"id":705,"alpha2":"si","alpha3":"svn","name":"Slovenia"},
160
+ {"id":90,"alpha2":"sb","alpha3":"slb","name":"Solomon Islands"},
161
+ {"id":706,"alpha2":"so","alpha3":"som","name":"Somalia"},
162
+ {"id":710,"alpha2":"za","alpha3":"zaf","name":"South Africa"},
163
+ {"id":410,"alpha2":"kr","alpha3":"kor","name":"South Korea"},
164
+ {"id":728,"alpha2":"ss","alpha3":"ssd","name":"South Sudan"},
165
+ {"id":724,"alpha2":"es","alpha3":"esp","name":"Spain"},
166
+ {"id":144,"alpha2":"lk","alpha3":"lka","name":"Sri Lanka"},
167
+ {"id":729,"alpha2":"sd","alpha3":"sdn","name":"Sudan"},
168
+ {"id":740,"alpha2":"sr","alpha3":"sur","name":"Suriname"},
169
+ {"id":752,"alpha2":"se","alpha3":"swe","name":"Sweden"},
170
+ {"id":756,"alpha2":"ch","alpha3":"che","name":"Switzerland"},
171
+ {"id":760,"alpha2":"sy","alpha3":"syr","name":"Syria"},
172
+ {"id":762,"alpha2":"tj","alpha3":"tjk","name":"Tajikistan"},
173
+ {"id":834,"alpha2":"tz","alpha3":"tza","name":"Tanzania"},
174
+ {"id":764,"alpha2":"th","alpha3":"tha","name":"Thailand"},
175
+ {"id":626,"alpha2":"tl","alpha3":"tls","name":"Timor-Leste"},
176
+ {"id":768,"alpha2":"tg","alpha3":"tgo","name":"Togo"},
177
+ {"id":776,"alpha2":"to","alpha3":"ton","name":"Tonga"},
178
+ {"id":780,"alpha2":"tt","alpha3":"tto","name":"Trinidad and Tobago"},
179
+ {"id":788,"alpha2":"tn","alpha3":"tun","name":"Tunisia"},
180
+ {"id":792,"alpha2":"tr","alpha3":"tur","name":"Türkiye"},
181
+ {"id":795,"alpha2":"tm","alpha3":"tkm","name":"Turkmenistan"},
182
+ {"id":798,"alpha2":"tv","alpha3":"tuv","name":"Tuvalu"},
183
+ {"id":800,"alpha2":"ug","alpha3":"uga","name":"Uganda"},
184
+ {"id":804,"alpha2":"ua","alpha3":"ukr","name":"Ukraine"},
185
+ {"id":784,"alpha2":"ae","alpha3":"are","name":"United Arab Emirates"},
186
+ {"id":826,"alpha2":"gb","alpha3":"gbr","name":"United Kingdom"},
187
+ {"id":840,"alpha2":"us","alpha3":"usa","name":"United States of America"},
188
+ {"id":858,"alpha2":"uy","alpha3":"ury","name":"Uruguay"},
189
+ {"id":860,"alpha2":"uz","alpha3":"uzb","name":"Uzbekistan"},
190
+ {"id":548,"alpha2":"vu","alpha3":"vut","name":"Vanuatu"},
191
+ {"id":862,"alpha2":"ve","alpha3":"ven","name":"Venezuela"},
192
+ {"id":704,"alpha2":"vn","alpha3":"vnm","name":"Viet Nam"},
193
+ {"id":887,"alpha2":"ye","alpha3":"yem","name":"Yemen"},
194
+ {"id":894,"alpha2":"zm","alpha3":"zmb","name":"Zambia"},
195
+ {"id":716,"alpha2":"zw","alpha3":"zwe","name":"Zimbabwe"}
196
+ ]
@@ -0,0 +1,11 @@
1
+ import React from 'react'
2
+ import { type LucideProps } from 'lucide-react'
3
+
4
+ const Btc: React.FC<LucideProps> = (props: LucideProps) => (
5
+ <svg viewBox="0.004 0 64 64" {...props}>
6
+ <path d="M63.04 39.741c-4.274 17.143-21.638 27.575-38.783 23.301C7.12 58.768-3.313 41.404.962 24.262 5.234 7.117 22.597-3.317 39.737.957c17.144 4.274 27.576 21.64 23.302 38.784z" fill="#f7931a"/>
7
+ <path d="M46.11 27.441c.636-4.258-2.606-6.547-7.039-8.074l1.438-5.768-3.512-.875-1.4 5.616c-.922-.23-1.87-.447-2.812-.662l1.41-5.653-3.509-.875-1.439 5.766c-.764-.174-1.514-.346-2.242-.527l.004-.018-4.842-1.209-.934 3.75s2.605.597 2.55.634c1.422.355 1.68 1.296 1.636 2.042l-1.638 6.571c.098.025.225.061.365.117l-.37-.092-2.297 9.205c-.174.432-.615 1.08-1.609.834.035.051-2.552-.637-2.552-.637l-1.743 4.02 4.57 1.139c.85.213 1.683.436 2.502.646l-1.453 5.835 3.507.875 1.44-5.772c.957.26 1.887.5 2.797.726L27.504 50.8l3.511.875 1.453-5.823c5.987 1.133 10.49.676 12.383-4.738 1.527-4.36-.075-6.875-3.225-8.516 2.294-.531 4.022-2.04 4.483-5.157zM38.087 38.69c-1.086 4.36-8.426 2.004-10.807 1.412l1.928-7.729c2.38.594 10.011 1.77 8.88 6.317zm1.085-11.312c-.99 3.966-7.1 1.951-9.083 1.457l1.748-7.01c1.983.494 8.367 1.416 7.335 5.553z" fill="#ffffff"/>
8
+ </svg>
9
+ )
10
+
11
+ export default Btc
@@ -0,0 +1,20 @@
1
+ import React from 'react'
2
+ import { type LucideProps } from 'lucide-react'
3
+
4
+ const Eth: React.FC<LucideProps> = (props: LucideProps) => (
5
+ <svg viewBox="0 0 32 32" {...props}>
6
+ <g fill="none" fillRule="evenodd">
7
+ <circle cx="16" cy="16" r="16" fill="#627EEA"/>
8
+ <g fill="#FFF" fillRule="nonzero">
9
+ <path fillOpacity=".602" d="M16.498 4v8.87l7.497 3.35z"/>
10
+ <path d="M16.498 4L9 16.22l7.498-3.35z"/>
11
+ <path fillOpacity=".602" d="M16.498 21.968v6.027L24 17.616z"/>
12
+ <path d="M16.498 27.995v-6.028L9 17.616z"/>
13
+ <path fillOpacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/>
14
+ <path fillOpacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/>
15
+ </g>
16
+ </g>
17
+ </svg>
18
+ )
19
+
20
+ export default Eth
@@ -0,0 +1,13 @@
1
+ import React from 'react'
2
+ import { type LucideProps } from 'lucide-react'
3
+
4
+ const Usdt: React.FC<LucideProps> = (props: LucideProps) => (
5
+ <svg viewBox="0 0 32 32" {...props}>
6
+ <g fill="none" fill-rule="evenodd">
7
+ <circle cx="16" cy="16" r="16" fill="#26A17B"/>
8
+ <path fill="#FFF" d="M17.922 17.383v-.002c-.11.008-.677.042-1.942.042-1.01 0-1.721-.03-1.971-.042v.003c-3.888-.171-6.79-.848-6.79-1.658 0-.809 2.902-1.486 6.79-1.66v2.644c.254.018.982.061 1.988.061 1.207 0 1.812-.05 1.925-.06v-2.643c3.88.173 6.775.85 6.775 1.658 0 .81-2.895 1.485-6.775 1.657m0-3.59v-2.366h5.414V7.819H8.595v3.608h5.414v2.365c-4.4.202-7.709 1.074-7.709 2.118 0 1.044 3.309 1.915 7.709 2.118v7.582h3.913v-7.584c4.393-.202 7.694-1.073 7.694-2.116 0-1.043-3.301-1.914-7.694-2.117"/>
9
+ </g>
10
+ </svg>
11
+ )
12
+
13
+ export default Usdt
@@ -0,0 +1,134 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { ChevronLeft } from 'lucide-react'
5
+ import { observer } from 'mobx-react-lite'
6
+
7
+ import { zodResolver } from '@hookform/resolvers/zod'
8
+ import * as z from 'zod'
9
+ import { useForm } from 'react-hook-form'
10
+
11
+ import { ApplyTypography, Button, Main, Separator } from '@hanzo/ui/primitives'
12
+ import { cn } from '@hanzo/ui/util'
13
+ import { useAuth } from '@hanzo/auth/service'
14
+ import { LoginComponent } from '@hanzo/auth/components'
15
+
16
+ import ShippingInfo from './shipping-info'
17
+ import ThankYou from './thank-you'
18
+ import PayWithCrypto from './pay-with-crypto'
19
+ import PayByBankTransfer from './pay-by-bank-transfer'
20
+ import ContactInfo from './contact-info'
21
+ import { Cart } from '..'
22
+ import { useCommerce } from '../../service/context'
23
+
24
+ const contactFormSchema = z.object({
25
+ name: z.string().min(2, 'Name must be at least 2 characters.'),
26
+ email: z.string().email(),
27
+ })
28
+
29
+ const Checkout: React.FC<{toggleCheckout: () => void}> = observer(({toggleCheckout}) => {
30
+ const auth = useAuth()
31
+ const cmmc = useCommerce()
32
+
33
+ const [currentStep, setCurrentStep] = useState(1)
34
+ const [paymentMethod, setPaymentMethod] = useState<'crypto' | 'bank' | undefined>()
35
+ const [orderId, setOrderId] = useState<string>()
36
+
37
+ const contactForm = useForm<z.infer<typeof contactFormSchema>>({
38
+ resolver: zodResolver(contactFormSchema),
39
+ defaultValues: {
40
+ name: auth.user?.displayName ?? '',
41
+ email: auth.user?.email ?? '',
42
+ },
43
+ })
44
+
45
+ useEffect(() => {
46
+ if (auth.loggedIn) {
47
+ contactForm.setValue('name', auth.user?.displayName ?? '')
48
+ contactForm.setValue('email', auth.user?.email ?? '')
49
+ }
50
+ }, [auth.loggedIn])
51
+
52
+ const selectPaymentMethod = async (method: 'crypto' | 'bank') => {
53
+ contactForm.handleSubmit(async () => {
54
+ if (auth.user) {
55
+ if (!!orderId) {
56
+ await cmmc.updateOrder(orderId, auth.user.email, method)
57
+ } else {
58
+ const id = await cmmc.createOrder(auth.user.email, method)
59
+ setOrderId(id)
60
+ }
61
+ }
62
+ setPaymentMethod(method)
63
+ setCurrentStep(2)
64
+ })()
65
+ }
66
+
67
+
68
+ const step1 = auth.loggedIn ? (
69
+ <ContactInfo form={contactForm} selectPaymentMethod={selectPaymentMethod}/>
70
+ ) : (
71
+ <LoginComponent hideHeader className='max-w-[20rem] mx-auto'/>
72
+ )
73
+
74
+ const step2 = paymentMethod === 'crypto' ? (
75
+ <PayWithCrypto setCurrentStep={setCurrentStep}/>
76
+ ) : (
77
+ <PayByBankTransfer setCurrentStep={setCurrentStep}/>
78
+ )
79
+
80
+ const step3 = (
81
+ <ShippingInfo orderId={orderId} paymentMethod={paymentMethod} setCurrentStep={setCurrentStep}/>
82
+ )
83
+
84
+ const step4 = <ThankYou/>
85
+
86
+ const steps = [step1, step2, step3, step4]
87
+
88
+ return (
89
+ <div className="fixed top-0 left-0 !max-w-none w-full h-full min-h-screen bg-background z-[51]">
90
+ <Main className='grid grid-cols-5 justify-center gap-8 overflow-y-auto h-full'>
91
+ <Button
92
+ variant='ghost'
93
+ size='icon'
94
+ className='absolute sm:ml-2 sm:mt-2'
95
+ onClick={() => {
96
+ setCurrentStep(0)
97
+ toggleCheckout()
98
+ }}
99
+ >
100
+ <ChevronLeft/>
101
+ </Button>
102
+
103
+ <div className='col-span-2 hidden md:flex'>
104
+ <Cart hideCheckout className='fixed justify-center border-none mt-10 w-1/3 max-w-[40rem]'/>
105
+ </div>
106
+
107
+ <div className='flex flex-col gap-8 sm:gap-14 col-span-5 md:col-span-3 max-w-[40rem] mx-auto'>
108
+ <ApplyTypography className='w-full'>
109
+ <h3 className='!text-center w-1/2 mx-auto sm:w-full sm:mt-5'>FINALIZE PAYMENT</h3>
110
+ </ApplyTypography>
111
+ <div className='flex gap-2 mx-auto items-center text-xxs sm:text-base'>
112
+ <div className={cn('w-6 h-6 rounded-full border border-foreground flex flex-col justify-center items-center', currentStep === 1 ? 'bg-foreground text-muted-4' : '')}>
113
+ 1
114
+ <div className='relative text-foreground top-2 h-0 whitespace-nowrap'>Contact info</div>
115
+ </div>
116
+ <Separator className='w-[4rem] sm:w-[6rem]'/>
117
+ <div className={cn('w-6 h-6 rounded-full border border-foreground flex flex-col justify-center items-center', currentStep === 2 ? 'bg-foreground text-muted-4' : '')}>
118
+ 2
119
+ <div className='relative text-foreground top-2 h-0 whitespace-nowrap'>Payment</div>
120
+ </div>
121
+ <Separator className='w-[4rem] sm:w-[6rem]'/>
122
+ <div className={cn('w-6 h-6 rounded-full border border-foreground flex flex-col justify-center items-center', currentStep === 3 ? 'bg-foreground text-muted-4' : '')}>
123
+ 3
124
+ <div className='relative text-foreground top-2 h-0 whitespace-nowrap'>Shipping info</div>
125
+ </div>
126
+ </div>
127
+ {steps[currentStep - 1]}
128
+ </div>
129
+ </Main>
130
+ </div>
131
+ )
132
+ })
133
+
134
+ export default Checkout
@@ -0,0 +1,83 @@
1
+ 'use client'
2
+
3
+ import type { ReactNode } from 'react'
4
+ import { Copy } from 'lucide-react'
5
+
6
+ import { Button, Tabs, TabsContent, TabsList, TabsTrigger, toast } from '@hanzo/ui/primitives'
7
+ import { EnhHeadingBlockComponent, type EnhHeadingBlock, type Block } from '@hanzo/ui/blocks'
8
+
9
+ const InfoField: React.FC<{label: string, value: ReactNode, copyValue: string}> = ({label, value, copyValue}) => {
10
+ const copyToClipboard = (label: string, text: string) => {
11
+ navigator.clipboard.writeText(text)
12
+ toast({title: `${label} copied to clipboard`})
13
+ }
14
+
15
+ return (
16
+ <div className='flex flex-col gap-1'>
17
+ <p className='text-xs'>{label}</p>
18
+ <div className='flex items-center justify-between text-lg border rounded-lg py-2 px-4'>
19
+ {value}
20
+ <Button variant='ghost' size='icon' onClick={() => copyToClipboard(label, copyValue)}>
21
+ <Copy className='h-4 w-4'/>
22
+ </Button>
23
+ </div>
24
+ </div>
25
+ )
26
+ }
27
+
28
+ const PayByBankTransfer: React.FC<{setCurrentStep: (step: number) => void}> = ({setCurrentStep}) => {
29
+ return (<>
30
+ <EnhHeadingBlockComponent
31
+ block={{blockType: 'enh-heading',
32
+ specifiers: 'center byline-center',
33
+ heading: { text: `WIRE INFO`, level: 3 },
34
+ byline: { text: 'Click the copy icon to copy text to your clipboard.', level: 6}
35
+ } satisfies EnhHeadingBlock as Block}
36
+ />
37
+ <Tabs defaultValue="usd" className='w-full mx-auto max-w-[50rem]'>
38
+ <TabsList className="grid w-full grid-cols-2 max-w-[15rem] mx-auto bg-level-2">
39
+ <TabsTrigger value="usd">USD</TabsTrigger>
40
+ <TabsTrigger value="eur">EUR/GBP</TabsTrigger>
41
+ </TabsList>
42
+ <TabsContent value="usd">
43
+ <div className='flex flex-col gap-4 w-full'>
44
+ <InfoField
45
+ label='Beneficiary Bank'
46
+ value={<div>Bank of America<br/>NA 222 Broadway<br/>New York, New York. 10038</div>}
47
+ copyValue='Bank of America, NA 222 Broadway, New York, New York. 10038'
48
+ />
49
+ <InfoField
50
+ label='Beneficiary'
51
+ value={<div>Hanzo, Inc<br/>4811 Mastin Street<br/>Merriam, KS 66203</div>}
52
+ copyValue='Hanzo, Inc, 4811 Mastin Street, Merriam, KS 66203'
53
+ />
54
+ <InfoField label='Routing Number - ACH' value='113000023 / 111000025' copyValue='113000023 / 111000025'/>
55
+ <InfoField label='Routing Number - Wire' value='026009593' copyValue='026009593'/>
56
+ <InfoField label='Routing Number - SWIFT' value='BOFAUS3N' copyValue='BOFAUS3N'/>
57
+ <InfoField label='Reference' value='Lux' copyValue='Lux'/>
58
+ </div>
59
+ </TabsContent>
60
+ <TabsContent value="eur">
61
+ <div className='flex flex-col gap-4 w-full'>
62
+ <InfoField
63
+ label='Beneficiary Bank'
64
+ value={<div>Clear Junction Limited<br/>4th floor<br/>Imperial House</div>}
65
+ copyValue='Clear Junction Limited, 4th floor, Imperial House'
66
+ />
67
+ <InfoField label='Account Number (GBP)' value='33781813' copyValue='33781813'/>
68
+ <InfoField label='Account Name' value='AiDLT Global Ltd.' copyValue='AiDLT Global Ltd.'/>
69
+ <InfoField label='SWIFT/BIC' value='CLJUGB21' copyValue='CLJUGB21'/>
70
+ <InfoField label='Sort Code (GBP)' value='041307' copyValue='041307'/>
71
+ <InfoField label='IBAN' value='GB75 CLJU 0413 0733 78181 3' copyValue='GB75 CLJU 0413 0733 78181 3'/>
72
+ <InfoField label='Reference' value='Lux' copyValue='Lux'/>
73
+ </div>
74
+ </TabsContent>
75
+ </Tabs>
76
+ <div className='flex gap-4 items-center mt-6'>
77
+ <Button variant='outline' onClick={() => setCurrentStep(1)} className='mx-auto w-full'>Back</Button>
78
+ <Button onClick={() => setCurrentStep(3)} className='mx-auto w-full'>Continue</Button>
79
+ </div>
80
+ </>)
81
+ }
82
+
83
+ export default PayByBankTransfer
@@ -0,0 +1,178 @@
1
+ 'use client'
2
+
3
+ import React, { useEffect, useState } from 'react'
4
+
5
+ import { autorun } from 'mobx'
6
+ import { observer } from 'mobx-react-lite'
7
+
8
+ import { ethers } from 'ethers'
9
+
10
+ import {
11
+ Button,
12
+ Input,
13
+ Select,
14
+ SelectContent,
15
+ SelectGroup,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue,
19
+ toast
20
+ } from '@hanzo/ui/primitives'
21
+
22
+ import { useAuth } from '@hanzo/auth/service'
23
+ import { Ethereum as EthIconFromAuth } from '@hanzo/auth/icons'
24
+
25
+ import Eth from './icons/eth'
26
+ import Btc from './icons/btc'
27
+ import Usdt from './icons/usdt'
28
+ import { useCommerce } from '../..'
29
+ import { formatPrice } from '../../util'
30
+
31
+ declare global {
32
+ interface Window{
33
+ ethereum?: any
34
+ }
35
+ }
36
+
37
+ const PayWithCrypto: React.FC<{
38
+ setCurrentStep: (currentStep: number) => void
39
+ }> = observer(({
40
+ setCurrentStep
41
+ }) => {
42
+
43
+ const c = useCommerce()
44
+ const auth = useAuth()
45
+ const [loadingPrice, setLoadingPrice] = useState(false)
46
+ //const [selectedToken, setSelectedToken] = useState('eth')
47
+ const [amount, setAmount] = useState<number>()
48
+ const [availableAmount, setAvailableAmount] = useState<number>()
49
+ const [provider, setProvider] = useState<ethers.BrowserProvider>()
50
+
51
+ //const selectedToken = 'eth'
52
+
53
+ useEffect(() => {
54
+ // responding to changes in user.walletAddress
55
+ return autorun(() => {
56
+ const newProvider = new ethers.BrowserProvider(window.ethereum)
57
+ setProvider(newProvider)
58
+ if (auth.user?.walletAddress) {
59
+ newProvider.getBalance(auth.user?.walletAddress).then((balance: any) => {
60
+ setAvailableAmount(Number(balance)/(10**18))
61
+ })
62
+ }
63
+ })
64
+ }, [])
65
+
66
+ // Get latest USD -> ETH exchange rate
67
+ useEffect(() => {
68
+ const fetchPrice = () => {
69
+ setLoadingPrice(true)
70
+ fetch(process.env.NEXT_PUBLIC_ETH_EXCHANGE_RATE_API ?? '')
71
+ .then(res => res.json())
72
+ .then((exchangeRate) => {
73
+ const oneUsdInWei = (10**18) / exchangeRate.data.amount
74
+ const usdAmountInWei = oneUsdInWei * c.cartTotal
75
+ setAmount(usdAmountInWei)
76
+ setLoadingPrice(false)
77
+ })
78
+ }
79
+
80
+ // Call immediately on load
81
+ fetchPrice()
82
+
83
+ // Then set interval to call every 30 seconds
84
+ const interval = setInterval(fetchPrice, 30000)
85
+
86
+ return () => clearInterval(interval)
87
+ }, [c.cartTotal])
88
+
89
+ const sendPayment = async (ether: number) => {
90
+ // Check that we are on ethereum network
91
+ try {
92
+ await window.ethereum.request({
93
+ method: 'wallet_switchEthereumChain',
94
+ params: [{ chainId: "0x1"}],
95
+ })
96
+ const newProvider = new ethers.BrowserProvider(window.ethereum)
97
+ setProvider(newProvider)
98
+ if (auth.user?.walletAddress) {
99
+ newProvider.getBalance(auth.user?.walletAddress).then((balance: any) => {
100
+ setAvailableAmount(Number(balance)/(10**18))
101
+ })
102
+ }
103
+ } catch (err) {
104
+ toast({title: 'Please switch your wallet to the Ethereum network.'})
105
+ return
106
+ }
107
+
108
+ try {
109
+ if (!provider) {
110
+ // :aa TODO string table
111
+ throw new Error('No crypto wallet found. Please install it.')
112
+ }
113
+
114
+ await window.ethereum.send('eth_requestAccounts')
115
+
116
+ const signer = await provider.getSigner()
117
+ ethers.getAddress(process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS ?? '')
118
+ const tx = await signer.sendTransaction({
119
+ to: process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS,
120
+ value: ethers.parseEther(ether.toString())
121
+ })
122
+ console.log({ ether, addr: process.env.NEXT_PUBLIC_ETH_PAYMENT_ADDRESS })
123
+ console.log('tx', tx)
124
+ setCurrentStep(3)
125
+ } catch (err) {
126
+ console.log(err)
127
+ // :aa TODO string table
128
+ toast({title: 'Not enough funds in your wallet'})
129
+ }
130
+ }
131
+
132
+ const payWidget = !!!(auth.user?.walletAddress) ? (
133
+ <div className='w-full mx-auto max-w-[20rem]'>
134
+ <Button variant='outline' className='w-full flex items-center gap-2' onClick={auth.associateWallet.bind(auth)}>
135
+ <EthIconFromAuth height={20}/>Connect your wallet
136
+ </Button>
137
+ </div>
138
+ ) : (
139
+ <div className='flex flex-col gap-2 w-full mx-auto max-w-[20rem]'>
140
+ <div>Cart value: {formatPrice(c.cartTotal)}</div>
141
+ <Select onValueChange={(token) => {/*ONLY ETH setSelectedToken(token) */}} defaultValue='eth'>
142
+ <SelectTrigger>
143
+ <SelectValue defaultValue='eth' />
144
+ </SelectTrigger>
145
+ <SelectContent>
146
+ <SelectGroup>
147
+ <SelectItem value='eth'><div className='flex items-center gap-2'><Eth height={14}/>ETH</div></SelectItem>
148
+ {/* <SelectItem value='btc' ><div className='flex items-center gap-2'><Btc height={14}/>BTC</div></SelectItem>
149
+ <SelectItem value='usdt' ><div className='flex items-center gap-2'><Usdt height={14}/>USDT</div></SelectItem> */}
150
+ </SelectGroup>
151
+ </SelectContent>
152
+ </Select>
153
+ <div>Available funds in your wallet: {availableAmount} ETH</div>
154
+ <div>
155
+ <Input value={amount ? amount/(10**18) : amount} contentEditable={false}/>
156
+ <div className='relative flex items-center gap-2 -top-[32px] justify-end px-2 py-1 rounded-lg bg-muted-4 w-fit text-xs float-right mr-3'><Eth height={10}/>ETH</div>
157
+ </div>
158
+ <Button
159
+ onClick={() => sendPayment(amount ? amount/(10**18) : 0)}
160
+ disabled={!amount || loadingPrice}
161
+ >
162
+ Pay now
163
+ </Button>
164
+ </div>
165
+ )
166
+
167
+ return (
168
+ <div className='flex flex-col gap-6'>
169
+ {payWidget}
170
+ <div className='flex gap-4 items-center mt-6'>
171
+ <Button variant='outline' onClick={() => setCurrentStep(1)} className='mx-auto w-full'>Back</Button>
172
+ <Button onClick={() => setCurrentStep(3)} className='mx-auto w-full'>Continue (Pay later)</Button>
173
+ </div>
174
+ </div>
175
+ )
176
+ })
177
+
178
+ export default PayWithCrypto
@@ -0,0 +1,208 @@
1
+ 'use client'
2
+
3
+ import {
4
+ Form,
5
+ FormControl,
6
+ FormField,
7
+ FormItem,
8
+ FormLabel,
9
+ FormMessage,
10
+ } from '@hanzo/ui/primitives/form'
11
+ import { Input, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@hanzo/ui/primitives'
12
+ import { BookUser } from 'lucide-react'
13
+ import { EnhHeadingBlockComponent, type EnhHeadingBlock } from '@hanzo/ui/blocks'
14
+ import { useAuth } from '@hanzo/auth/service'
15
+ import * as z from 'zod'
16
+ import { useForm } from 'react-hook-form'
17
+ import { zodResolver } from '@hookform/resolvers/zod'
18
+ import { useCommerce } from '../..'
19
+ import { countries } from './countries'
20
+
21
+ const shippingFormSchema = z.object({
22
+ firstName: z.string().min(2, 'First name must be at least 2 characters.'),
23
+ lastName: z.string().min(2, 'Last name must be at least 2 characters.'),
24
+ addressLine1: z.string().min(2, 'Address must be at least 2 characters.'),
25
+ addressLine2: z.string().optional(),
26
+ zipCode: z.string().min(2, 'Zip code is invalid.'),
27
+ city: z.string().min(2, 'City is invalid.'),
28
+ state: z.string().optional(),
29
+ country: z.string().min(2, 'Country is invalid.'),
30
+ })
31
+
32
+ const ShippingInfo: React.FC<{
33
+ orderId?: string,
34
+ paymentMethod?: string,
35
+ setCurrentStep: (currentStep: number) => void
36
+ }> = ({
37
+ orderId,
38
+ paymentMethod,
39
+ setCurrentStep
40
+ }) => {
41
+ const auth = useAuth()
42
+ const cmmc = useCommerce()
43
+
44
+ const shippingForm = useForm<z.infer<typeof shippingFormSchema>>({
45
+ resolver: zodResolver(shippingFormSchema),
46
+ defaultValues: {
47
+ firstName: '',
48
+ lastName: '',
49
+ addressLine1: '',
50
+ addressLine2: '',
51
+ zipCode: '',
52
+ city: '',
53
+ state: '',
54
+ country: '',
55
+ },
56
+ })
57
+
58
+ const onSubmit = async (values: z.infer<typeof shippingFormSchema>) => {
59
+ if (auth.user && orderId && paymentMethod) {
60
+ await cmmc.updateOrder(orderId, auth.user.email, paymentMethod, values)
61
+ }
62
+ setCurrentStep(4)
63
+ }
64
+
65
+ return (
66
+ <Form {...shippingForm}>
67
+ <form onSubmit={shippingForm.handleSubmit(onSubmit)} className='text-left'>
68
+ <div className='flex flex-col gap-4'>
69
+ <div className='flex gap-4 items-center'>
70
+ <BookUser />
71
+ <EnhHeadingBlockComponent block={{blockType: 'enh-heading',
72
+ heading: { text: `Shipping address`, level: 4 },
73
+ } as EnhHeadingBlock}/>
74
+ </div>
75
+ <div className='flex gap-4 items-end'>
76
+ <FormField
77
+ control={shippingForm.control}
78
+ name='firstName'
79
+ render={({ field }) => (
80
+ <FormItem className='space-y-1 w-full'>
81
+ <FormLabel>First name</FormLabel>
82
+ <FormControl>
83
+ <Input {...field} />
84
+ </FormControl>
85
+ <FormMessage />
86
+ </FormItem>
87
+ )}
88
+ />
89
+ <FormField
90
+ control={shippingForm.control}
91
+ name='lastName'
92
+ render={({ field }) => (
93
+ <FormItem className='space-y-1 w-full'>
94
+ <FormLabel>Last name</FormLabel>
95
+ <FormControl>
96
+ <Input {...field} />
97
+ </FormControl>
98
+ <FormMessage />
99
+ </FormItem>
100
+ )}
101
+ />
102
+ </div>
103
+ <div className='flex gap-4 items-end'>
104
+ <FormField
105
+ control={shippingForm.control}
106
+ name='addressLine1'
107
+ render={({ field }) => (
108
+ <FormItem className='space-y-1 w-full'>
109
+ <FormLabel>Address line 1</FormLabel>
110
+ <FormControl>
111
+ <Input {...field} />
112
+ </FormControl>
113
+ <FormMessage />
114
+ </FormItem>
115
+ )}
116
+ />
117
+ <FormField
118
+ control={shippingForm.control}
119
+ name='addressLine2'
120
+ render={({ field }) => (
121
+ <FormItem className='space-y-1 w-full'>
122
+ <FormLabel>Address line 2 (optional)</FormLabel>
123
+ <FormControl>
124
+ <Input {...field} />
125
+ </FormControl>
126
+ <FormMessage />
127
+ </FormItem>
128
+ )}
129
+ />
130
+ </div>
131
+ <div className='flex gap-4 items-end'>
132
+ <FormField
133
+ control={shippingForm.control}
134
+ name='zipCode'
135
+ render={({ field }) => (
136
+ <FormItem className='space-y-1 w-full'>
137
+ <FormLabel>Zip code</FormLabel>
138
+ <FormControl>
139
+ <Input {...field} />
140
+ </FormControl>
141
+ <FormMessage />
142
+ </FormItem>
143
+ )}
144
+ />
145
+ <FormField
146
+ control={shippingForm.control}
147
+ name='city'
148
+ render={({ field }) => (
149
+ <FormItem className='space-y-1 w-full'>
150
+ <FormLabel>City</FormLabel>
151
+ <FormControl>
152
+ <Input {...field} />
153
+ </FormControl>
154
+ <FormMessage />
155
+ </FormItem>
156
+ )}
157
+ />
158
+ </div>
159
+ <div className='flex gap-4 items-start'>
160
+ <FormField
161
+ control={shippingForm.control}
162
+ name='state'
163
+ render={({ field }) => (
164
+ <FormItem className='space-y-1 w-full'>
165
+ <FormLabel>State (Optional)</FormLabel>
166
+ <FormControl>
167
+ <Input {...field} />
168
+ </FormControl>
169
+ <FormMessage />
170
+ </FormItem>
171
+ )}
172
+ />
173
+ <FormField
174
+ control={shippingForm.control}
175
+ name='country'
176
+ render={({ field }) => (
177
+ <FormItem className='space-y-1 w-full'>
178
+ <FormLabel>Country</FormLabel>
179
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
180
+ <FormControl>
181
+ <SelectTrigger>
182
+ <SelectValue placeholder='Select a country' />
183
+ </SelectTrigger>
184
+ </FormControl>
185
+ <SelectContent>
186
+ {countries.map((country) => (
187
+ <SelectItem key={country.id} value={country.name}>
188
+ {country.name}
189
+ </SelectItem>
190
+ ))}
191
+ </SelectContent>
192
+ </Select>
193
+ </FormItem>
194
+ )}
195
+ />
196
+ </div>
197
+ </div>
198
+
199
+ <div className='flex gap-4 items-center mt-6'>
200
+ <Button variant='outline' onClick={() => setCurrentStep(2)} className='mx-auto w-full'>Back</Button>
201
+ <Button type='submit' className='mx-auto w-full'>Confirm order</Button>
202
+ </div>
203
+ </form>
204
+ </Form>
205
+ )
206
+ }
207
+
208
+ export default ShippingInfo
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+ import Link from 'next/link'
3
+ import { ApplyTypography } from '@hanzo/ui/primitives'
4
+
5
+ const ThankYou: React.FC = () => (
6
+ <ApplyTypography className='flex flex-col gap-4 text-center mt-10'>
7
+ <h3>Thank you for your order!</h3>
8
+ <h6>Once your payment has been confirmed we will send additional information to your email.</h6>
9
+ <p>
10
+ While you wait, we cordially invite you to join the <Link href='https://warpcast.com/~/channel/lux'>Lux Channel</Link> on <Link href='https://warpcast.com/~/invite-page/227706?id=fbc9ca91'>Warpcast</Link>.
11
+ </p>
12
+ </ApplyTypography>
13
+ )
14
+
15
+ export default ThankYou
@@ -8,4 +8,5 @@ export { default as ProductSelectionMobilePicker } from './product-selection-mob
8
8
  export { default as ProductSelectionRadioGroup } from './product-selection-radio-group'
9
9
  export { default as SelectCategoryAndItemWidget } from './select-category-and-item-widget'
10
10
  export { default as SelectItemInCategoryView } from './select-item-in-category-view'
11
+ export { default as Checkout } from './checkout'
11
12
  export { Icons } from './Icons'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/commerce",
3
- "version": "1.1.10",
3
+ "version": "1.1.12",
4
4
  "description": "Library with shopping cart components.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -27,10 +27,13 @@
27
27
  "clean": "rm -rf dist && rm -rf node_modules"
28
28
  },
29
29
  "dependencies": {
30
- "@hookform/resolvers": "^3.3.2",
30
+ "@hookform/resolvers": "^3.3.4",
31
+ "ethers": "^6.11.1",
31
32
  "lucide-react": "^0.307.0",
32
33
  "next-usequerystate": "^1.17.0",
33
- "react-mobile-picker": "^1.0.0"
34
+ "react-hook-form": "7.50.1",
35
+ "react-mobile-picker": "^1.0.0",
36
+ "zod": "3.21.4"
34
37
  },
35
38
  "peerDependencies": {
36
39
  "@hanzo/auth": "^1.0.12",