Fix wallet modal, add Lute wallet, dark mode, add comments
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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' })
|
||||
|
||||
Reference in New Issue
Block a user