Skip to content

Commit 8bbd01a

Browse files
committed
feat: Next.js use-wallet demo app
1 parent 0a62ed9 commit 8bbd01a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2812
-529
lines changed

.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
NEXT_PUBLIC_NODE_NETWORK=mainnet
2+
NEXT_PUBLIC_NODE_URL=https://mainnet-api.algonode.cloud

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"extends": "next/core-web-vitals"
2+
"extends": ["next/core-web-vitals", "prettier"]
33
}

.prettierignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# dependencies
2+
/node_modules
3+
/.pnp
4+
.pnp.js
5+
6+
# testing
7+
/coverage
8+
9+
# next.js
10+
/.next/
11+
/out/
12+
13+
# production
14+
/build
15+
16+
# misc
17+
.DS_Store
18+
*.pem
19+
20+
# debug
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*
24+
.pnpm-debug.log*
25+
26+
# local env files
27+
.env*.local
28+
29+
# vercel
30+
.vercel
31+
32+
# typescript
33+
*.tsbuildinfo
34+
next-env.d.ts

.prettierrc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"semi": false,
3+
"singleQuote": true,
4+
"printWidth": 100,
5+
"trailingComma": "none"
6+
}

components/Account.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { ArrowTopRightOnSquareIcon, ClipboardIcon } from '@heroicons/react/20/solid'
2+
import { useWallet } from '@txnlab/use-wallet'
3+
import Image from 'next/image'
4+
import Tooltip from 'components/Tooltip'
5+
import tooltipStyles from 'styles/Tooltip.module.css'
6+
import useWalletBalance from 'hooks/useWalletBalance'
7+
import { copyToClipboard } from 'utils/clipboard'
8+
9+
export default function Account() {
10+
const { activeAccount, providers } = useWallet()
11+
12+
const { walletBalance, walletAvailableBalance } = useWalletBalance()
13+
14+
const activeProvider = providers?.find(
15+
(provider) => provider.metadata.id === activeAccount?.providerId
16+
)
17+
18+
if (!activeAccount) {
19+
return null
20+
}
21+
22+
return (
23+
<div className="overflow-hidden bg-white shadow rounded-lg">
24+
<div className="p-5 sm:px-6">
25+
<h3 className="text-lg font-medium leading-6 text-gray-900">Active Account</h3>
26+
</div>
27+
<div className="border-t border-gray-200 px-5 py-2 sm:p-0">
28+
<dl className="sm:divide-y sm:divide-gray-200">
29+
<div className="py-3 sm:grid sm:grid-cols-5 sm:gap-4 sm:py-5 sm:px-6">
30+
<dt className="inline-flex items-center text-sm font-medium text-gray-500">Name</dt>
31+
<dd className="text-gray-900 sm:col-span-4 truncate">{activeAccount.name}</dd>
32+
</div>
33+
<div className="py-3 sm:grid sm:grid-cols-5 sm:gap-4 sm:py-5 sm:px-6">
34+
<dt className="inline-flex items-center text-sm font-medium text-gray-500">Address</dt>
35+
<dd className="text-gray-900 sm:col-span-4 flex items-center min-w-0">
36+
<span className="truncate">{activeAccount.address}</span>
37+
<div className="flex items-center flex-nowrap -my-1">
38+
<div className="inline-flex -space-x-px rounded-md shadow-sm ml-3 sm:ml-4">
39+
<a
40+
href={`https://algoexplorer.io/address/${activeAccount.address}`}
41+
className="relative inline-flex items-center first:rounded-l-md last:rounded-r-md border border-gray-300 bg-gray-50 px-3.5 py-2.5 sm:px-2.5 sm:py-2 text-sm font-medium text-gray-500 hover:bg-gray-100 focus:z-20 outline-brand-500"
42+
target="_blank"
43+
rel="noreferrer"
44+
id="view-on-algoexplorer"
45+
data-tooltip-content="View on AlgoExplorer"
46+
>
47+
<span className="sr-only">View on AlgoExplorer</span>
48+
<ArrowTopRightOnSquareIcon className="h-5 w-5" aria-hidden="true" />
49+
</a>
50+
<button
51+
type="button"
52+
className="relative inline-flex items-center first:rounded-l-md last:rounded-r-md border border-gray-300 bg-gray-50 px-3.5 py-2.5 sm:px-2.5 sm:py-2 text-sm font-medium text-gray-500 hover:bg-gray-100 focus:z-20 outline-brand-500"
53+
data-clipboard-text={activeAccount.address}
54+
data-clipboard-message="Address copied to clipboard"
55+
onClick={copyToClipboard}
56+
id="copy-address"
57+
data-tooltip-content="Copy address"
58+
>
59+
<span className="sr-only">Copy address</span>
60+
<ClipboardIcon className="h-5 w-5" aria-hidden="true" />
61+
</button>
62+
</div>
63+
</div>
64+
<Tooltip anchorId="view-on-algoexplorer" className={tooltipStyles.custom} />
65+
<Tooltip anchorId="copy-address" className={tooltipStyles.custom} />
66+
</dd>
67+
</div>
68+
<div className="py-3 sm:grid sm:grid-cols-5 sm:gap-4 sm:py-5 sm:px-6">
69+
<dt className="inline-flex items-center text-sm font-medium text-gray-500">Balance</dt>
70+
<dd className="inline-flex items-center text-gray-900 sm:col-span-4 truncate">
71+
<strong className="block font-semibold">{walletBalance}</strong>
72+
{walletAvailableBalance !== walletBalance && (
73+
<>
74+
<span className="mx-3 text-gray-500">/</span>
75+
<span
76+
id="available-balance"
77+
data-tooltip-content="Available balance"
78+
className="block text-gray-500"
79+
>
80+
{walletAvailableBalance}
81+
</span>
82+
<Tooltip anchorId="available-balance" className={tooltipStyles.custom} />
83+
</>
84+
)}
85+
</dd>
86+
</div>
87+
<div className="py-3 sm:grid sm:grid-cols-5 sm:gap-4 sm:py-5 sm:px-6">
88+
<dt className="sm:inline-flex sm:items-center text-sm font-medium text-gray-500">
89+
Provider
90+
</dt>
91+
<dd className="inline-flex items-center text-gray-900 sm:col-span-4">
92+
{activeProvider && (
93+
<>
94+
<Image
95+
width={40}
96+
height={40}
97+
alt={activeProvider.metadata.name}
98+
src={activeProvider.metadata.icon}
99+
className="h-8 w-8 mr-3 my-1 sm:-my-2 flex-shrink-0 rounded-full bg-white"
100+
/>
101+
{activeProvider.metadata.name}
102+
</>
103+
)}
104+
</dd>
105+
</div>
106+
</dl>
107+
</div>
108+
</div>
109+
)
110+
}

components/Connect.tsx

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { Listbox } from '@headlessui/react'
2+
import { BoltIcon, CheckIcon, SignalIcon, SignalSlashIcon } from '@heroicons/react/20/solid'
3+
import { Provider, PROVIDER_ID, useWallet } from '@txnlab/use-wallet'
4+
import Image from 'next/image'
5+
import { useMemo } from 'react'
6+
import SelectMenu from 'components/SelectMenu'
7+
import { classNames } from 'utils'
8+
9+
export default function Connect() {
10+
const { providers, activeAccount } = useWallet()
11+
12+
const renderActiveAccount = (provider: Provider) => {
13+
if (
14+
!provider.isActive ||
15+
!activeAccount ||
16+
!provider.accounts.find((acct) => acct.address === activeAccount.address)
17+
) {
18+
return null
19+
}
20+
21+
if (provider.metadata.id === PROVIDER_ID.MYALGO) {
22+
const options = provider.accounts.map((account) => ({
23+
value: account.address,
24+
label: (
25+
<>
26+
<span className="inline-flex items-center rounded bg-gray-100 px-2.5 py-0.5 text-sm font-medium text-gray-800 mr-3">
27+
{account.name}
28+
</span>
29+
<span className="text-sm">{account.address}</span>
30+
</>
31+
),
32+
account
33+
}))
34+
35+
const selected =
36+
options.find((option) => option.value === activeAccount.address) || options[0]
37+
38+
return (
39+
<SelectMenu
40+
selected={selected}
41+
setSelected={(selected) => provider.setActiveAccount(selected.value)}
42+
>
43+
{options.map((option) => (
44+
<Listbox.Option
45+
key={option.value}
46+
className={({ active }) =>
47+
classNames(
48+
active ? 'text-white bg-sky-600' : 'text-gray-900',
49+
'relative cursor-default select-none py-2 pl-3 pr-10'
50+
)
51+
}
52+
value={option}
53+
>
54+
{({ selected, active }) => (
55+
<>
56+
<span
57+
className={classNames(
58+
selected ? 'font-semibold' : 'font-normal',
59+
'block truncate'
60+
)}
61+
>
62+
<span
63+
className={classNames(
64+
selected && active
65+
? 'bg-sky-700 text-white'
66+
: selected
67+
? 'bg-gray-100 text-gray-800'
68+
: active
69+
? 'bg-sky-600 text-white'
70+
: 'bg-white text-gray-800',
71+
'inline-flex items-center rounded px-2.5 py-0.5 text-sm font-medium mr-3'
72+
)}
73+
>
74+
{option.account.name}
75+
</span>
76+
<span className="text-sm">{option.account.address}</span>
77+
</span>
78+
79+
{selected ? (
80+
<span
81+
className={classNames(
82+
active ? 'text-white' : 'text-sky-600',
83+
'absolute inset-y-0 right-0 flex items-center pr-3'
84+
)}
85+
>
86+
<CheckIcon className="h-5 w-5" aria-hidden="true" />
87+
</span>
88+
) : null}
89+
</>
90+
)}
91+
</Listbox.Option>
92+
))}
93+
</SelectMenu>
94+
)
95+
}
96+
97+
const account = provider.accounts[0]
98+
99+
if (!account) {
100+
return null
101+
}
102+
103+
return (
104+
<div
105+
className={classNames(
106+
!provider.isActive ? 'opacity-50 pointer-events-none' : '',
107+
'mt-1 flex items-center w-full rounded-md border-2 border-transparent py-2 sm:text-sm'
108+
)}
109+
>
110+
<span className="truncate">
111+
<span className="inline-flex items-center rounded bg-gray-100 px-2.5 py-0.5 text-sm font-medium text-gray-800 mr-3">
112+
{account.name}
113+
</span>
114+
<span className="text-sm">{account.address}</span>
115+
</span>
116+
</div>
117+
)
118+
}
119+
120+
const showSetActiveButtons = useMemo(() => {
121+
if (!providers) return false
122+
123+
const areMultipleConnected = providers?.filter((provider) => provider.isConnected).length > 1
124+
const areNoneActive = !Object.values(providers).some((provider) => provider.isActive)
125+
126+
return areMultipleConnected || areNoneActive
127+
}, [providers])
128+
129+
const renderActions = (provider: Provider) => {
130+
const showSetActiveButton = showSetActiveButtons && provider.isConnected
131+
132+
return (
133+
<div className="-mt-px flex divide-x divide-gray-200">
134+
<div className="flex w-0 flex-1">
135+
{provider.isConnected ? (
136+
<button
137+
type="button"
138+
onClick={provider.disconnect}
139+
className="relative -mr-px inline-flex w-0 flex-1 items-center justify-center rounded-bl-lg border border-transparent py-4 text-sm font-medium text-gray-700 hover:text-gray-500"
140+
>
141+
<SignalSlashIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
142+
<span className="ml-3">Disconnect</span>
143+
</button>
144+
) : (
145+
<button
146+
type="button"
147+
onClick={provider.connect}
148+
className="relative -mr-px inline-flex w-0 flex-1 items-center justify-center rounded-bl-lg border border-transparent py-4 text-sm font-medium text-gray-700 hover:text-gray-500"
149+
>
150+
<SignalIcon className="h-5 w-5 text-green-400" aria-hidden="true" />
151+
<span className="ml-3">Connect</span>
152+
</button>
153+
)}
154+
</div>
155+
156+
{showSetActiveButton && (
157+
<div className="-ml-px flex w-0 flex-1">
158+
<button
159+
type="button"
160+
onClick={provider.setActiveProvider}
161+
className="relative inline-flex w-0 flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium text-gray-700 hover:text-gray-500 disabled:opacity-50 disabled:text-gray-700 group"
162+
disabled={provider.isActive}
163+
>
164+
<BoltIcon
165+
className="h-5 w-5 text-yellow-400 group-disabled:text-gray-300"
166+
aria-hidden="true"
167+
/>
168+
<span className="ml-3">Set Active</span>
169+
</button>
170+
</div>
171+
)}
172+
</div>
173+
)
174+
}
175+
176+
const renderCard = (provider: Provider) => {
177+
return (
178+
<li
179+
key={provider.metadata.id}
180+
className="col-span-1 divide-y divide-gray-200 rounded-lg bg-white shadow"
181+
>
182+
<div className="px-5 py-3 sm:px-6 sm:py-4">
183+
{/* Provider name and icon */}
184+
<div className="flex w-full items-start justify-between">
185+
<div className="flex-1 truncate">
186+
<div className="flex items-center space-x-3">
187+
<h3 className="flex items-center truncate sm:text-lg font-medium text-gray-900 h-8 sm:h-10">
188+
{provider.metadata.name}
189+
</h3>
190+
{provider.isActive && (
191+
<span className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
192+
Active
193+
</span>
194+
)}
195+
</div>
196+
</div>
197+
<Image
198+
width={40}
199+
height={40}
200+
alt={provider.metadata.name}
201+
src={provider.metadata.icon}
202+
className="h-8 w-8 sm:h-10 sm:w-10 sm:-mr-2 flex-shrink-0 rounded-full bg-white"
203+
/>
204+
</div>
205+
206+
{/* Active account(s) */}
207+
<div
208+
className={classNames(
209+
!provider.isConnected ? 'hidden sm:block sm:invisible' : '',
210+
'mt-5 sm:mt-6 sm:h-10'
211+
)}
212+
>
213+
<label className="sr-only">Active account for {provider.metadata.name}</label>
214+
{renderActiveAccount(provider)}
215+
</div>
216+
</div>
217+
218+
{/* Action(s) */}
219+
<div>{renderActions(provider)}</div>
220+
</li>
221+
)
222+
}
223+
224+
return (
225+
<ul role="list" className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
226+
{providers?.map(renderCard)}
227+
</ul>
228+
)
229+
}

0 commit comments

Comments
 (0)