Fix wallet modal, add Lute wallet, dark mode, add comments

This commit is contained in:
SaraJane
2026-01-02 20:41:49 +00:00
parent ceaf5b33f4
commit f9e4e3dd3d
11 changed files with 168 additions and 27 deletions

View File

@ -1,9 +1,9 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AlgoKit React Template</title>
<title>Tokenization Template</title>
</head>
<body>
<div id="root"></div>

View File

@ -15,6 +15,7 @@
"@txnlab/use-wallet-react": "^4.0.0",
"algosdk": "^3.0.0",
"daisyui": "^4.0.0",
"lute-connect": "^1.6.3",
"notistack": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -8785,6 +8786,12 @@
"yallist": "^3.0.2"
}
},
"node_modules/lute-connect": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/lute-connect/-/lute-connect-1.6.3.tgz",
"integrity": "sha512-QSBHj1fG9QhkgzezcRrG1piYCxTt0Tlf13ZOTLuXsJfagEC+IERKBKORo0CgzBMOlo47nr4w8n19vcFpfCvJNQ==",
"license": "ISC"
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",

View File

@ -42,6 +42,7 @@
"@txnlab/use-wallet-react": "^4.0.0",
"algosdk": "^3.0.0",
"daisyui": "^4.0.0",
"lute-connect": "^1.6.3",
"notistack": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@ -1,13 +1,15 @@
import { SupportedWallet, WalletId, WalletManager, WalletProvider } from '@txnlab/use-wallet-react'
import { SnackbarProvider } from 'notistack'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Home from './Home'
import Layout from './Layout'
import TokenizePage from './TokenizePage'
import { getAlgodConfigFromViteEnvironment, getKmdConfigFromViteEnvironment } from './utils/network/getAlgoClientConfigs'
// Configure supported wallets based on network environment
let supportedWallets: SupportedWallet[]
if (import.meta.env.VITE_ALGOD_NETWORK === 'localnet') {
// LocalNet: KMD wallet for local development
const kmdConfig = getKmdConfigFromViteEnvironment()
supportedWallets = [
{
@ -18,11 +20,17 @@ if (import.meta.env.VITE_ALGOD_NETWORK === 'localnet') {
port: String(kmdConfig.port),
},
},
{ id: WalletId.LUTE },
]
} else {
supportedWallets = [{ id: WalletId.DEFLY }, { id: WalletId.PERA }, { id: WalletId.EXODUS }]
// TestNet/MainNet: Browser extension wallets (Pera, Defly, Exodus, Lute)
supportedWallets = [{ id: WalletId.DEFLY }, { id: WalletId.PERA }, { id: WalletId.EXODUS }, { id: WalletId.LUTE }]
}
/**
* Main App Component
* Sets up wallet provider and routing for the Tokenization dApp
*/
export default function App() {
const algodConfig = getAlgodConfigFromViteEnvironment()

View File

@ -1,6 +1,11 @@
import { useWallet } from '@txnlab/use-wallet-react'
import { Link } from 'react-router-dom'
/**
* Home Page
* Landing page showcasing the RWA tokenization platform
* Displays features, how it works, and CTAs to connect wallet and create assets
*/
export default function Home() {
const { activeAddress } = useWallet()

View File

@ -4,6 +4,10 @@ import { NavLink, Outlet } from 'react-router-dom'
import ConnectWallet from './components/ConnectWallet'
import ThemeToggle from './components/ThemeToggle'
/**
* Main Layout Component
* Wraps the entire app with navigation, footer, and wallet connection modal
*/
export default function Layout() {
const [openWalletModal, setOpenWalletModal] = useState(false)
const { activeAddress } = useWallet()

View File

@ -1,5 +1,9 @@
import TokenizeAsset from './components/TokenizeAsset'
/**
* Tokenize Page
* Main page for creating new Algorand Standard Assets (ASAs)
*/
export default function TokenizePage() {
return (
<div className="bg-white dark:bg-slate-950 min-h-screen py-12">

View File

@ -3,6 +3,11 @@ import { useMemo } from 'react'
import { ellipseAddress } from '../utils/ellipseAddress'
import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
/**
* Account Component
* Displays the connected wallet address (shortened) and current network
* Address links to Lora explorer for easy account tracking
*/
const Account = () => {
const { activeAddress } = useWallet()
const algoConfig = getAlgodConfigFromViteEnvironment()
@ -13,10 +18,14 @@ const Account = () => {
return (
<div>
<a className="text-xl" target="_blank" href={`https://lora.algokit.io/${networkName}/account/${activeAddress}/`}>
<a
className="text-xl text-gray-900 dark:text-slate-100 hover:text-teal-600 dark:hover:text-teal-400 transition"
target="_blank"
href={`https://lora.algokit.io/${networkName}/account/${activeAddress}/`}
>
Address: {ellipseAddress(activeAddress)}
</a>
<div className="text-xl">Network: {networkName}</div>
<div className="text-xl text-gray-900 dark:text-slate-100 mt-2">Network: {networkName}</div>
</div>
)
}

View File

@ -1,4 +1,5 @@
import { useWallet, Wallet, WalletId } from '@txnlab/use-wallet-react'
import { useEffect, useRef } from 'react'
import Account from './Account'
interface ConnectWalletInterface {
@ -6,21 +7,87 @@ interface ConnectWalletInterface {
closeModal: () => void
}
/**
* ConnectWallet Modal Component
* Displays wallet connection options (Pera, Defly, Lute, KMD for LocalNet)
* Also shows connected wallet details and network information when logged in
*/
const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => {
const { wallets, activeAddress } = useWallet()
const dialogRef = useRef<HTMLDialogElement>(null)
// Manage native dialog element's open/close state
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
if (openModal) {
dialog.showModal()
} else {
dialog.close()
}
}, [openModal])
const getActiveWallet = () => {
if (!wallets) return null
return wallets.find((w) => w.isActive)
}
const getWalletDisplayName = (wallet: Wallet) => {
if (wallet.id === WalletId.KMD) return 'LocalNet Wallet'
return wallet.metadata.name
}
const isKmd = (wallet: Wallet) => wallet.id === WalletId.KMD
return (
<dialog id="connect_wallet_modal" className={`modal ${openModal ? 'modal-open' : ''}`}>
<form method="dialog" className="modal-box">
<h3 className="font-bold text-2xl">Select wallet provider</h3>
const activeWallet = getActiveWallet()
<div className="grid m-2 pt-5">
return (
<dialog
ref={dialogRef}
id="connect_wallet_modal"
className="fixed inset-0 w-full max-w-lg mx-auto my-auto rounded-2xl bg-white dark:bg-slate-800 shadow-2xl border border-gray-200 dark:border-slate-700 overflow-hidden"
onClick={(e) => {
// Close when clicking the backdrop
if (e.target === dialogRef.current) {
closeModal()
}
}}
>
<div className="p-6 sm:p-7">
<div className="flex items-center justify-between mb-5">
<h3 className="text-xl font-semibold text-gray-900 dark:text-slate-100">Select wallet provider</h3>
<button
className="text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300 transition text-sm"
onClick={closeModal}
aria-label="Close wallet modal"
>
</button>
</div>
<p className="text-sm text-gray-500 dark:text-slate-400 mb-4">
Choose the wallet you want to connect. Supported: Pera, Defly, LocalNet (KMD), and others.
</p>
<div className="space-y-4">
{activeAddress && (
<>
<Account />
<div className="divider" />
<div className="rounded-xl border border-gray-100 dark:border-slate-700 bg-gray-50 dark:bg-slate-700 p-4">
<Account />
</div>
{/* Wallet Info */}
<div className="space-y-3 bg-white dark:bg-slate-700 border border-gray-200 dark:border-slate-600 rounded-xl p-4">
{activeWallet && (
<div>
<p className="text-xs text-gray-500 dark:text-slate-400 font-medium mb-1">Connected Wallet</p>
<p className="text-sm font-semibold text-gray-900 dark:text-slate-100">{getWalletDisplayName(activeWallet)}</p>
</div>
)}
</div>
<div className="h-px bg-gray-200 dark:bg-slate-600" />
</>
)}
@ -28,47 +95,54 @@ const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => {
wallets?.map((wallet) => (
<button
data-test-id={`${wallet.id}-connect`}
className="btn border-teal-800 border-1 m-2"
className={`
w-full flex items-center gap-4 px-4 py-3 rounded-xl bg-white dark:bg-slate-700 border border-gray-200 dark:border-slate-600
hover:border-indigo-200 dark:hover:border-indigo-500 hover:bg-indigo-50/50 dark:hover:bg-slate-600 transition
focus:outline-none focus:ring-2 focus:ring-indigo-200 dark:focus:ring-indigo-500
`}
key={`provider-${wallet.id}`}
onClick={() => {
return wallet.connect()
// Close modal before initiating wallet connection
closeModal()
wallet.connect()
}}
>
{!isKmd(wallet) && (
<img
alt={`wallet_icon_${wallet.id}`}
src={wallet.metadata.icon}
style={{ objectFit: 'contain', width: '30px', height: 'auto' }}
className="w-9 h-9 object-contain rounded-md border border-gray-100 dark:border-slate-600 bg-white dark:bg-slate-700"
/>
)}
<span>{isKmd(wallet) ? 'LocalNet Wallet' : wallet.metadata.name}</span>
<span className="font-medium text-sm text-left flex-1 text-gray-900 dark:text-slate-100">
{isKmd(wallet) ? 'LocalNet Wallet' : wallet.metadata.name}
</span>
{wallet.isActive && <span className="text-sm text-emerald-500"></span>}
</button>
))}
</div>
<div className="modal-action ">
<div className="mt-6 flex gap-3">
<button
data-test-id="close-wallet-modal"
className="btn"
className="w-full sm:w-auto px-4 py-2.5 rounded-lg border border-gray-200 dark:border-slate-600 bg-gray-50 dark:bg-slate-700 text-gray-700 dark:text-slate-300 text-sm hover:bg-gray-100 dark:hover:bg-slate-600 transition"
onClick={() => {
closeModal()
}}
>
Close
</button>
{activeAddress && (
<button
className="btn btn-warning"
className="w-full sm:w-auto px-4 py-2.5 rounded-lg bg-red-500 hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700 text-white text-sm transition"
data-test-id="logout"
onClick={async () => {
if (wallets) {
const activeWallet = wallets.find((w) => w.isActive)
if (activeWallet) {
await activeWallet.disconnect()
const wallet = wallets.find((w) => w.isActive)
if (wallet) {
await wallet.disconnect()
} else {
// Required for logout/cleanup of inactive providers
// For instance, when you login to localnet wallet and switch network
// to testnet/mainnet or vice verse.
localStorage.removeItem('@txnlab/use-wallet:v3')
window.location.reload()
}
@ -79,7 +153,7 @@ const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => {
</button>
)}
</div>
</form>
</div>
</dialog>
)
}

View File

@ -3,6 +3,9 @@ import { useEffect, useState } from 'react'
const THEME_KEY = 'tokenize_theme'
type Theme = 'light' | 'dark'
/**
* Get initial theme preference from localStorage or system preference
*/
function getInitialTheme(): Theme {
const saved = localStorage.getItem(THEME_KEY)
if (saved === 'light' || saved === 'dark') return saved
@ -10,6 +13,11 @@ function getInitialTheme(): Theme {
return prefersDark ? 'dark' : 'light'
}
/**
* ThemeToggle Component
* Allows users to toggle between light and dark modes
* Persists theme preference to localStorage and applies Tailwind's dark class
*/
export default function ThemeToggle() {
const [theme, setTheme] = useState<Theme>(() => getInitialTheme())
const [mounted, setMounted] = useState(false)

View File

@ -6,6 +6,10 @@ import { AiOutlineInfoCircle, AiOutlineLoading3Quarters } from 'react-icons/ai'
import { BsCoin } from 'react-icons/bs'
import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
/**
* Type for created assets stored in browser localStorage
* Captures all ASA configuration including compliance fields
*/
type CreatedAsset = {
assetId: number
assetName: string
@ -23,6 +27,9 @@ type CreatedAsset = {
const STORAGE_KEY = 'tokenize_assets'
const LORA_BASE = 'https://lora.algokit.io/testnet'
/**
* Load created assets from browser localStorage
*/
function loadAssets(): CreatedAsset[] {
try {
const raw = localStorage.getItem(STORAGE_KEY)
@ -32,6 +39,10 @@ function loadAssets(): CreatedAsset[] {
}
}
/**
* Save a newly created asset to localStorage
* Returns updated asset list with new asset at the top
*/
function persistAsset(asset: CreatedAsset): CreatedAsset[] {
const existing = loadAssets()
const next = [asset, ...existing]
@ -39,6 +50,12 @@ function persistAsset(asset: CreatedAsset): CreatedAsset[] {
return next
}
/**
* TokenizeAsset Component
* Main form for creating Algorand Standard Assets (ASAs)
* Collects basic and advanced compliance metadata
* Persists created assets to localStorage for tracking
*/
export default function TokenizeAsset() {
const [assetName, setAssetName] = useState<string>('Tokenized Coffee Membership')
const [unitName, setUnitName] = useState<string>('COFFEE')
@ -84,6 +101,10 @@ export default function TokenizeAsset() {
const isWholeNumber = (v: string) => /^\d+$/.test(v)
/**
* Handle ASA creation with validation and on-chain transaction
* Adjusts total supply by decimals and saves asset to localStorage
*/
const handleTokenize = async () => {
if (!transactionSigner || !activeAddress) {
enqueueSnackbar('Please connect your wallet first.', { variant: 'warning' })