feat: add unified login UX/UI
This commit is contained in:
1
examples/web3auth-ui-demo/.env.example
Normal file
1
examples/web3auth-ui-demo/.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_WEB3AUTH_CLIENT_ID=your-web3auth-client-id
|
||||||
12
examples/web3auth-ui-demo/index.html
Normal file
12
examples/web3auth-ui-demo/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Web3Auth UI Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3528
examples/web3auth-ui-demo/package-lock.json
generated
Normal file
3528
examples/web3auth-ui-demo/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
examples/web3auth-ui-demo/package.json
Normal file
29
examples/web3auth-ui-demo/package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "web3auth-ui-demo",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tokenizerwa/web3auth-algorand": "file:../../packages/web3auth-algorand",
|
||||||
|
"@tokenizerwa/web3auth-algorand-ui": "file:../../packages/web3auth-algorand-ui",
|
||||||
|
"@web3auth/base": "^9.7.0",
|
||||||
|
"@web3auth/base-provider": "^9.7.0",
|
||||||
|
"@web3auth/modal": "^9.7.0",
|
||||||
|
"algosdk": "^3.0.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.11",
|
||||||
|
"@types/react-dom": "^18.2.4",
|
||||||
|
"typescript": "^5.1.6",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
examples/web3auth-ui-demo/src/main.tsx
Normal file
32
examples/web3auth-ui-demo/src/main.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { Web3AuthGoogleButton } from '@tokenizerwa/web3auth-algorand-ui'
|
||||||
|
import { AlgorandWeb3AuthProvider } from '@tokenizerwa/web3auth-algorand'
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
const WEB3AUTH_CLIENT_ID = import.meta.env.VITE_WEB3AUTH_CLIENT_ID || ''
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<header>
|
||||||
|
<h1>Algorand + Web3Auth Demo</h1>
|
||||||
|
<p>Sign in with Google and get an Algorand signer instantly.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<AlgorandWeb3AuthProvider config={{ clientId: WEB3AUTH_CLIENT_ID }}>
|
||||||
|
<section className="card">
|
||||||
|
<h2>Connect</h2>
|
||||||
|
<p>Use the ready-made Google button from the UI package.</p>
|
||||||
|
<Web3AuthGoogleButton />
|
||||||
|
</section>
|
||||||
|
</AlgorandWeb3AuthProvider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
63
examples/web3auth-ui-demo/src/styles.css
Normal file
63
examples/web3auth-ui-demo/src/styles.css
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
background: radial-gradient(circle at 20% 20%, #f2f7ff, #ffffff 45%), radial-gradient(circle at 80% 0%, #fef6e8, #ffffff 40%);
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 48px 16px 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 2.4rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
margin: 0;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 16px 60px -32px rgba(15, 23, 42, 0.35);
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
margin: 0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
15
examples/web3auth-ui-demo/tsconfig.json
Normal file
15
examples/web3auth-ui-demo/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
6
examples/web3auth-ui-demo/vite.config.ts
Normal file
6
examples/web3auth-ui-demo/vite.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
33
packages/web3auth-algorand-ui/package.json
Normal file
33
packages/web3auth-algorand-ui/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@tokenizerwa/web3auth-algorand-ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Prebuilt React components for the @tokenizerwa/web3auth-algorand hook.",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"module": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"@tokenizerwa/web3auth-algorand": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"clean": "rimraf dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-icons": "^5.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.1.6",
|
||||||
|
"rimraf": "^5.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
235
packages/web3auth-algorand-ui/src/Web3AuthGoogleButton.tsx
Normal file
235
packages/web3auth-algorand-ui/src/Web3AuthGoogleButton.tsx
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { AiOutlineLoading3Quarters } from 'react-icons/ai'
|
||||||
|
import { FaCheck, FaCopy, FaGoogle } from 'react-icons/fa'
|
||||||
|
import { useWeb3Auth } from '@tokenizerwa/web3auth-algorand'
|
||||||
|
|
||||||
|
export function Web3AuthGoogleButton() {
|
||||||
|
const { isConnected, isLoading, error, algorandAccount, userInfo, login, logout } = useWeb3Auth()
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
if (!target.closest('[data-dropdown]')) {
|
||||||
|
setIsDropdownOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('click', handleClickOutside)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getAddressString = (): string => {
|
||||||
|
if (!algorandAccount?.address) return ''
|
||||||
|
if (typeof algorandAccount.address === 'object' && algorandAccount.address !== null) {
|
||||||
|
if ('toString' in algorandAccount.address && typeof algorandAccount.address.toString === 'function') {
|
||||||
|
return algorandAccount.address.toString()
|
||||||
|
}
|
||||||
|
if ('addr' in algorandAccount.address) {
|
||||||
|
return String(algorandAccount.address.addr)
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(algorandAccount.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ellipseAddress = (address: string = '', startChars = 6, endChars = 4): string => {
|
||||||
|
if (!address || address.length <= startChars + endChars) {
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
return `${address.slice(0, startChars)}...${address.slice(-endChars)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
await login()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login error:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await logout()
|
||||||
|
setIsDropdownOpen(false)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Logout error:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopyAddress = () => {
|
||||||
|
const address = getAddressString()
|
||||||
|
if (!address) return
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(address)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error && !isConnected) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button onClick={handleLogin} disabled={isLoading} className="btn btn-sm btn-outline btn-error">
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<AiOutlineLoading3Quarters className="animate-spin" />
|
||||||
|
Connecting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Retry Login'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className="text-xs text-error max-w-xs truncate">{error}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleLogin}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="btn btn-sm bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 gap-2 font-medium shadow-sm transition-all"
|
||||||
|
title="Sign in with your Google account to create an Algorand wallet"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<AiOutlineLoading3Quarters className="animate-spin text-gray-600" />
|
||||||
|
<span>Connecting...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FaGoogle className="text-lg text-blue-500" />
|
||||||
|
<span>Sign in with Google</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (algorandAccount && isConnected) {
|
||||||
|
const address = getAddressString()
|
||||||
|
const firstLetter = address ? address[0].toUpperCase() : 'A'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dropdown dropdown-end" data-dropdown>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||||
|
className="btn btn-sm btn-ghost gap-2 hover:bg-base-200"
|
||||||
|
title={`Connected: ${address}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{userInfo?.profileImage ? (
|
||||||
|
<img
|
||||||
|
src={userInfo.profileImage}
|
||||||
|
alt="Profile"
|
||||||
|
className="w-6 h-6 rounded-full object-cover ring-2 ring-primary ring-offset-1"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-6 h-6 rounded-full bg-primary text-primary-content flex items-center justify-center text-xs font-bold">
|
||||||
|
{firstLetter}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="font-mono text-sm font-medium">{ellipseAddress(address)}</span>
|
||||||
|
<svg
|
||||||
|
className={`w-4 h-4 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isDropdownOpen && (
|
||||||
|
<ul className="dropdown-content menu p-2 shadow-lg bg-base-100 rounded-box w-72 border border-base-300 mt-2">
|
||||||
|
{userInfo && (userInfo.name || userInfo.email) && (
|
||||||
|
<>
|
||||||
|
<li className="menu-title px-3 py-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{userInfo.profileImage ? (
|
||||||
|
<img
|
||||||
|
src={userInfo.profileImage}
|
||||||
|
alt="Profile"
|
||||||
|
className="w-10 h-10 rounded-full object-cover ring-2 ring-primary"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-primary text-primary-content flex items-center justify-center text-lg font-bold">
|
||||||
|
{firstLetter}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{userInfo.name && <span className="font-semibold text-base-content">{userInfo.name}</span>}
|
||||||
|
{userInfo.email && <span className="text-xs text-base-content/70 break-all">{userInfo.email}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<div className="divider my-1"></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<li className="menu-title px-3">
|
||||||
|
<span className="text-xs uppercase">Algorand Address</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div className="bg-base-200 rounded-lg p-2 font-mono text-xs break-all cursor-default hover:bg-base-200">
|
||||||
|
{address}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button onClick={handleCopyAddress} className="text-sm gap-2">
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<FaCheck className="text-success" />
|
||||||
|
<span>Copied!</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FaCopy />
|
||||||
|
<span>Copy Address</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<div className="divider my-1"></div>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<button onClick={handleLogout} disabled={isLoading} className="text-sm text-error hover:bg-error/10 gap-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<AiOutlineLoading3Quarters className="animate-spin" />
|
||||||
|
<span>Disconnecting...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
|
</svg>
|
||||||
|
<span>Disconnect</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<button disabled className="btn btn-sm btn-ghost gap-2">
|
||||||
|
<AiOutlineLoading3Quarters className="animate-spin" />
|
||||||
|
<span>Initializing...</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Web3AuthGoogleButton
|
||||||
1
packages/web3auth-algorand-ui/src/index.ts
Normal file
1
packages/web3auth-algorand-ui/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Web3AuthGoogleButton'
|
||||||
21
packages/web3auth-algorand-ui/tsconfig.json
Normal file
21
packages/web3auth-algorand-ui/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["react"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
47
packages/web3auth-algorand/PUBLISHING.md
Normal file
47
packages/web3auth-algorand/PUBLISHING.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Publishing to npm
|
||||||
|
|
||||||
|
This repo already has the package source under `packages/web3auth-algorand` and the UI add-on under `packages/web3auth-algorand-ui`. Both use plain TypeScript builds (no bundler).
|
||||||
|
|
||||||
|
## Prereqs
|
||||||
|
- npm account with 2FA configured (recommended).
|
||||||
|
- Access to the `@tokenizerwa` npm scope (or change `name` in each `package.json` to your scope).
|
||||||
|
- Node 20+.
|
||||||
|
|
||||||
|
## Build locally
|
||||||
|
```bash
|
||||||
|
cd packages/web3auth-algorand
|
||||||
|
npm install
|
||||||
|
npm run build # emits dist/
|
||||||
|
|
||||||
|
cd ../web3auth-algorand-ui
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
To double-check what will be published:
|
||||||
|
```bash
|
||||||
|
npm pack --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publish steps
|
||||||
|
1) Set the version you want in each `package.json` (`version` field). Use semver.
|
||||||
|
2) Make sure `files` includes `dist` (already set) and that `dist` exists (run `npm run build`).
|
||||||
|
3) Log in once if needed: `npm login`.
|
||||||
|
4) From each package folder, publish:
|
||||||
|
```bash
|
||||||
|
cd packages/web3auth-algorand
|
||||||
|
npm publish --access public
|
||||||
|
|
||||||
|
cd ../web3auth-algorand-ui
|
||||||
|
npm publish --access public
|
||||||
|
```
|
||||||
|
|
||||||
|
## Releasing updates
|
||||||
|
- Bump the version in the package you changed.
|
||||||
|
- Rebuild that package.
|
||||||
|
- Publish only the changed package(s).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
- 403 errors: ensure your npm token owns the scope or rename the package.
|
||||||
|
- Missing files in npm: verify `npm pack --dry-run` includes `dist` and `package.json` points to `dist/index.js`.
|
||||||
|
- Type errors: run `npm run build` to catch missing types before publishing.
|
||||||
72
packages/web3auth-algorand/README.md
Normal file
72
packages/web3auth-algorand/README.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# @tokenizerwa/web3auth-algorand
|
||||||
|
|
||||||
|
Algorand + Web3Auth hook and provider that turns a Google login into an Algorand signer. Ships the Web3Auth wiring, Algorand account derivation, and signer utilities so you can drop it into any React app.
|
||||||
|
|
||||||
|
## What’s inside
|
||||||
|
- `AlgorandWeb3AuthProvider` – wraps your app and bootstraps Web3Auth + Algorand account derivation.
|
||||||
|
- `useWeb3Auth()` – access connection state, Google profile info, and the derived Algorand account.
|
||||||
|
- `createWeb3AuthSigner(...)` – get an AlgoKit-compatible `TransactionSigner`.
|
||||||
|
- Helper utilities for balance/amount formatting and account inspection.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
```bash
|
||||||
|
npm install @tokenizerwa/web3auth-algorand
|
||||||
|
# peer deps you likely already have
|
||||||
|
npm install react @web3auth/modal
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
```tsx
|
||||||
|
import { AlgorandWeb3AuthProvider, useWeb3Auth, createWeb3AuthSigner } from '@tokenizerwa/web3auth-algorand'
|
||||||
|
|
||||||
|
const WEB3AUTH_CLIENT_ID = import.meta.env.VITE_WEB3AUTH_CLIENT_ID
|
||||||
|
|
||||||
|
function ConnectButton() {
|
||||||
|
const { isConnected, algorandAccount, login, logout, isLoading } = useWeb3Auth()
|
||||||
|
|
||||||
|
if (isConnected && algorandAccount) {
|
||||||
|
const signer = createWeb3AuthSigner(algorandAccount)
|
||||||
|
// use signer with AlgoKit / algosdk
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={isConnected ? logout : login} disabled={isLoading}>
|
||||||
|
{isConnected ? 'Disconnect' : 'Sign in with Google'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<AlgorandWeb3AuthProvider
|
||||||
|
config={{
|
||||||
|
clientId: WEB3AUTH_CLIENT_ID,
|
||||||
|
// optional overrides:
|
||||||
|
// web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET,
|
||||||
|
// chainConfig: { rpcTarget: 'https://mainnet-api.algonode.cloud', displayName: 'Algorand MainNet' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ConnectButton />
|
||||||
|
</AlgorandWeb3AuthProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI add-on (optional)
|
||||||
|
If you want a plug-and-play Google button and dropdown, install the UI companion:
|
||||||
|
```bash
|
||||||
|
npm install @tokenizerwa/web3auth-algorand-ui
|
||||||
|
```
|
||||||
|
```tsx
|
||||||
|
import { AlgorandWeb3AuthProvider } from '@tokenizerwa/web3auth-algorand'
|
||||||
|
import { Web3AuthGoogleButton } from '@tokenizerwa/web3auth-algorand-ui'
|
||||||
|
|
||||||
|
<AlgorandWeb3AuthProvider config={{ clientId: WEB3AUTH_CLIENT_ID }}>
|
||||||
|
<Web3AuthGoogleButton />
|
||||||
|
</AlgorandWeb3AuthProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- You need a Web3Auth client id from the Web3Auth dashboard.
|
||||||
|
- Default network is Sapphire Devnet + Algorand TestNet RPC; override via `config.chainConfig` and `config.web3AuthNetwork`.
|
||||||
|
- The provider must wrap any component that calls `useWeb3Auth`.
|
||||||
35
packages/web3auth-algorand/package.json
Normal file
35
packages/web3auth-algorand/package.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@tokenizerwa/web3auth-algorand",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Algorand + Web3Auth React provider, hooks, and signer helpers for Google login flows.",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"module": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"clean": "rimraf dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@web3auth/base": "^9.7.0",
|
||||||
|
"@web3auth/base-provider": "^9.7.0",
|
||||||
|
"@web3auth/modal": "^9.7.0",
|
||||||
|
"algosdk": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.1.6",
|
||||||
|
"rimraf": "^5.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/web3auth-algorand/src/index.ts
Normal file
5
packages/web3auth-algorand/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './provider'
|
||||||
|
export * from './types'
|
||||||
|
export * from './utils/algorandAdapter'
|
||||||
|
export * from './utils/web3authIntegration'
|
||||||
|
export * from './web3authConfig'
|
||||||
187
packages/web3auth-algorand/src/provider.tsx
Normal file
187
packages/web3auth-algorand/src/provider.tsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import { Web3Auth } from '@web3auth/modal'
|
||||||
|
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
|
||||||
|
import { AlgorandAccountFromWeb3Auth, getAlgorandAccount } from './utils/algorandAdapter'
|
||||||
|
import { getWeb3AuthUserInfo, initWeb3Auth, logoutFromWeb3Auth, Web3AuthUserInfo } from './web3authConfig'
|
||||||
|
import { type AlgorandWeb3AuthConfig } from './types'
|
||||||
|
|
||||||
|
export interface Web3AuthContextType {
|
||||||
|
isConnected: boolean
|
||||||
|
isLoading: boolean
|
||||||
|
isInitialized: boolean
|
||||||
|
error: string | null
|
||||||
|
web3AuthInstance: Web3Auth | null
|
||||||
|
algorandAccount: AlgorandAccountFromWeb3Auth | null
|
||||||
|
userInfo: Web3AuthUserInfo | null
|
||||||
|
login: () => Promise<void>
|
||||||
|
logout: () => Promise<void>
|
||||||
|
refreshUserInfo: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Web3AuthContext = createContext<Web3AuthContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export function AlgorandWeb3AuthProvider({ config, children }: { config: AlgorandWeb3AuthConfig; children: ReactNode }) {
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const [web3AuthInstance, setWeb3AuthInstance] = useState<Web3Auth | null>(null)
|
||||||
|
const [algorandAccount, setAlgorandAccount] = useState<AlgorandAccountFromWeb3Auth | null>(null)
|
||||||
|
const [userInfo, setUserInfo] = useState<Web3AuthUserInfo | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeWeb3Auth = async () => {
|
||||||
|
try {
|
||||||
|
if (!config?.clientId) {
|
||||||
|
setError('Web3Auth clientId is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
const web3auth = await initWeb3Auth(config)
|
||||||
|
|
||||||
|
setWeb3AuthInstance(web3auth)
|
||||||
|
|
||||||
|
if (web3auth.status === 'connected' && web3auth.provider) {
|
||||||
|
setIsConnected(true)
|
||||||
|
try {
|
||||||
|
const account = await getAlgorandAccount(web3auth.provider)
|
||||||
|
setAlgorandAccount(account)
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to derive Algorand account. Please reconnect.')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userInformation = await getWeb3AuthUserInfo()
|
||||||
|
if (userInformation) {
|
||||||
|
setUserInfo(userInformation)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch user info:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsInitialized(true)
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize Web3Auth'
|
||||||
|
console.error('WEB3AUTH: Initialization error:', err)
|
||||||
|
setError(errorMessage)
|
||||||
|
setIsInitialized(true)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeWeb3Auth()
|
||||||
|
}, [config])
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
if (!web3AuthInstance) {
|
||||||
|
setError('Web3Auth not initialized')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInitialized) {
|
||||||
|
setError('Web3Auth is still initializing, please try again')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
const web3authProvider = await web3AuthInstance.connect()
|
||||||
|
|
||||||
|
if (!web3authProvider) {
|
||||||
|
throw new Error('Failed to connect Web3Auth provider')
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsConnected(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const account = await getAlgorandAccount(web3authProvider)
|
||||||
|
setAlgorandAccount(account)
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to derive Algorand account'
|
||||||
|
setError(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userInformation = await getWeb3AuthUserInfo()
|
||||||
|
if (userInformation) {
|
||||||
|
setUserInfo(userInformation)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('LOGIN: Failed to fetch user info:', err)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Login failed'
|
||||||
|
console.error('LOGIN: Error:', err)
|
||||||
|
setError(errorMessage)
|
||||||
|
setIsConnected(false)
|
||||||
|
setAlgorandAccount(null)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
await logoutFromWeb3Auth()
|
||||||
|
|
||||||
|
setIsConnected(false)
|
||||||
|
setAlgorandAccount(null)
|
||||||
|
setUserInfo(null)
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Logout failed'
|
||||||
|
console.error('LOGOUT: Error:', err)
|
||||||
|
setError(errorMessage)
|
||||||
|
setIsConnected(false)
|
||||||
|
setAlgorandAccount(null)
|
||||||
|
setUserInfo(null)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const userInformation = await getWeb3AuthUserInfo()
|
||||||
|
if (userInformation) {
|
||||||
|
setUserInfo(userInformation)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('REFRESH: Failed:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: Web3AuthContextType = {
|
||||||
|
isConnected,
|
||||||
|
isLoading,
|
||||||
|
isInitialized,
|
||||||
|
error,
|
||||||
|
web3AuthInstance,
|
||||||
|
algorandAccount,
|
||||||
|
userInfo,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
refreshUserInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Web3AuthContext.Provider value={value}>{children}</Web3AuthContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWeb3Auth(): Web3AuthContextType {
|
||||||
|
const context = useContext(Web3AuthContext)
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useWeb3Auth must be used within an AlgorandWeb3AuthProvider')
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
31
packages/web3auth-algorand/src/types.ts
Normal file
31
packages/web3auth-algorand/src/types.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { CHAIN_NAMESPACES, WEB3AUTH_NETWORK, type IWeb3AuthConfiguration } from '@web3auth/base'
|
||||||
|
import type { CommonPrivateKeyProviderConfig } from '@web3auth/base-provider'
|
||||||
|
|
||||||
|
export type { AlgorandAccountFromWeb3Auth } from './utils/algorandAdapter'
|
||||||
|
|
||||||
|
export interface AlgorandChainConfig extends CommonPrivateKeyProviderConfig['config']['chainConfig'] {}
|
||||||
|
|
||||||
|
export interface AlgorandWeb3AuthConfig {
|
||||||
|
clientId: string
|
||||||
|
web3AuthNetwork?: WEB3AUTH_NETWORK
|
||||||
|
chainConfig?: AlgorandChainConfig
|
||||||
|
uiConfig?: IWeb3AuthConfiguration['uiConfig']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_ALGORAND_CHAIN_CONFIG: AlgorandChainConfig = {
|
||||||
|
chainNamespace: CHAIN_NAMESPACES.OTHER,
|
||||||
|
chainId: '0x1',
|
||||||
|
rpcTarget: 'https://testnet-api.algonode.cloud',
|
||||||
|
displayName: 'Algorand TestNet',
|
||||||
|
blockExplorerUrl: 'https://testnet.algoexplorer.io',
|
||||||
|
ticker: 'ALGO',
|
||||||
|
tickerName: 'Algorand',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_UI_CONFIG: IWeb3AuthConfiguration['uiConfig'] = {
|
||||||
|
appName: 'Algorand Web3Auth',
|
||||||
|
mode: 'light',
|
||||||
|
loginMethodsOrder: ['google'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_WEB3AUTH_NETWORK = WEB3AUTH_NETWORK.SAPPHIRE_DEVNET
|
||||||
79
packages/web3auth-algorand/src/utils/algorandAdapter.ts
Normal file
79
packages/web3auth-algorand/src/utils/algorandAdapter.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { IProvider } from '@web3auth/base'
|
||||||
|
import algosdk from 'algosdk'
|
||||||
|
|
||||||
|
export interface AlgorandAccountFromWeb3Auth {
|
||||||
|
address: string
|
||||||
|
mnemonic: string
|
||||||
|
secretKey: Uint8Array
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAlgorandAccount(provider: IProvider): Promise<AlgorandAccountFromWeb3Auth> {
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error('Provider is required to derive Algorand account')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const privKey = await provider.request({
|
||||||
|
method: 'private_key',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!privKey || typeof privKey !== 'string') {
|
||||||
|
throw new Error('Failed to retrieve private key from Web3Auth provider')
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanHexKey = privKey.startsWith('0x') ? privKey.slice(2) : privKey
|
||||||
|
const privateKeyBytes = new Uint8Array(Buffer.from(cleanHexKey, 'hex'))
|
||||||
|
const ed25519SecretKey = privateKeyBytes.slice(0, 32)
|
||||||
|
const mnemonic = algosdk.secretKeyToMnemonic(ed25519SecretKey)
|
||||||
|
const accountFromMnemonic = algosdk.mnemonicToSecretKey(mnemonic)
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: accountFromMnemonic.addr,
|
||||||
|
mnemonic: mnemonic,
|
||||||
|
secretKey: accountFromMnemonic.sk,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(`Failed to derive Algorand account from Web3Auth: ${error.message}`)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAlgorandSigner(secretKey: Uint8Array) {
|
||||||
|
return async (transactions: Uint8Array[]): Promise<Uint8Array[]> => {
|
||||||
|
const signedTxns: Uint8Array[] = []
|
||||||
|
|
||||||
|
for (const txn of transactions) {
|
||||||
|
try {
|
||||||
|
const signedTxn = algosdk.signTransaction(txn, secretKey)
|
||||||
|
signedTxns.push(signedTxn.blob)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to sign transaction: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedTxns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidAlgorandAddress(address: string): boolean {
|
||||||
|
if (!address || typeof address !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
algosdk.decodeAddress(address)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPublicKeyFromSecretKey(secretKey: Uint8Array): Uint8Array {
|
||||||
|
if (secretKey.length !== 64) {
|
||||||
|
throw new Error(`Invalid secret key length: expected 64 bytes, got ${secretKey.length}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretKey.slice(32)
|
||||||
|
}
|
||||||
125
packages/web3auth-algorand/src/utils/web3authIntegration.ts
Normal file
125
packages/web3auth-algorand/src/utils/web3authIntegration.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import algosdk, { TransactionSigner } from 'algosdk'
|
||||||
|
import { type AlgorandAccountFromWeb3Auth } from './algorandAdapter'
|
||||||
|
|
||||||
|
export interface AlgorandTransactionSigner {
|
||||||
|
sign: (transactions: Uint8Array[]) => Promise<Uint8Array[]>
|
||||||
|
sender?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWeb3AuthSigner(account: AlgorandAccountFromWeb3Auth): TransactionSigner {
|
||||||
|
const sk = account.secretKey
|
||||||
|
const addr = account.address
|
||||||
|
|
||||||
|
const secretKey: Uint8Array =
|
||||||
|
sk instanceof Uint8Array
|
||||||
|
? sk
|
||||||
|
: Array.isArray(sk)
|
||||||
|
? Uint8Array.from(sk)
|
||||||
|
: (() => {
|
||||||
|
throw new Error('Web3Auth secretKey is not a Uint8Array (or number[]). Cannot sign transactions.')
|
||||||
|
})()
|
||||||
|
|
||||||
|
return algosdk.makeBasicAccountTransactionSigner({
|
||||||
|
addr,
|
||||||
|
sk: secretKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWeb3AuthSignerObject(account: AlgorandAccountFromWeb3Auth): AlgorandTransactionSigner {
|
||||||
|
const signerFn = createWeb3AuthSigner(account)
|
||||||
|
|
||||||
|
const sign = async (transactions: Uint8Array[]) => {
|
||||||
|
const txns = transactions.map((b) => algosdk.decodeUnsignedTransaction(b))
|
||||||
|
const signed = await signerFn(txns, txns.map((_, i) => i))
|
||||||
|
return signed
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sign,
|
||||||
|
sender: account.address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWeb3AuthMultiSigSigner(account: AlgorandAccountFromWeb3Auth) {
|
||||||
|
return {
|
||||||
|
signer: createWeb3AuthSigner(account),
|
||||||
|
sender: account.address,
|
||||||
|
account,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeb3AuthAccountInfo(account: AlgorandAccountFromWeb3Auth) {
|
||||||
|
const decodedAddress = algosdk.decodeAddress(account.address)
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: account.address,
|
||||||
|
publicKeyBytes: decodedAddress.publicKey,
|
||||||
|
publicKeyBase64: Buffer.from(decodedAddress.publicKey).toString('base64'),
|
||||||
|
secretKeyHex: Buffer.from(account.secretKey).toString('hex'),
|
||||||
|
mnemonicPhrase: account.mnemonic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyWeb3AuthSignature(signedTransaction: Uint8Array, account: AlgorandAccountFromWeb3Auth): boolean {
|
||||||
|
try {
|
||||||
|
const decodedTxn = algosdk.decodeSignedTransaction(signedTransaction)
|
||||||
|
const txnSigner = decodedTxn.sig?.signers?.[0] ?? decodedTxn.sig?.signer
|
||||||
|
|
||||||
|
if (!txnSigner) return false
|
||||||
|
|
||||||
|
const decodedAddress = algosdk.decodeAddress(account.address)
|
||||||
|
return Buffer.from(txnSigner).equals(decodedAddress.publicKey)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying signature:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analyzeTransactionGroup(transactions: Uint8Array[]) {
|
||||||
|
return {
|
||||||
|
count: transactions.length,
|
||||||
|
totalSize: transactions.reduce((sum, txn) => sum + txn.length, 0),
|
||||||
|
averageSize: transactions.reduce((sum, txn) => sum + txn.length, 0) / transactions.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAmount(amount: bigint | number, decimals: number = 6): string {
|
||||||
|
const amountStr = amount.toString()
|
||||||
|
const decimalPoints = decimals
|
||||||
|
|
||||||
|
if (amountStr.length <= decimalPoints) {
|
||||||
|
return `0.${amountStr.padStart(decimalPoints, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const integerPart = amountStr.slice(0, -decimalPoints)
|
||||||
|
const decimalPart = amountStr.slice(-decimalPoints)
|
||||||
|
|
||||||
|
return `${integerPart}.${decimalPart}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAmount(amount: string, decimals: number = 6): bigint {
|
||||||
|
const trimmed = amount.trim()
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new Error('Amount is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+(\.\d+)?$/.test(trimmed)) {
|
||||||
|
throw new Error('Invalid amount format')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [integerPart = '0', decimalPart = ''] = trimmed.split('.')
|
||||||
|
|
||||||
|
if (decimalPart.length > decimals) {
|
||||||
|
throw new Error(`Too many decimal places (maximum ${decimals})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const paddedDecimal = decimalPart.padEnd(decimals, '0')
|
||||||
|
const combined = integerPart + paddedDecimal
|
||||||
|
return BigInt(combined)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasSufficientBalance(balance: bigint, requiredAmount: bigint, minFee: bigint = BigInt(1000)): boolean {
|
||||||
|
const totalRequired = requiredAmount + minFee
|
||||||
|
return balance >= totalRequired
|
||||||
|
}
|
||||||
97
packages/web3auth-algorand/src/web3authConfig.ts
Normal file
97
packages/web3auth-algorand/src/web3authConfig.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { CHAIN_NAMESPACES, IProvider, WEB3AUTH_NETWORK, type IWeb3AuthConfiguration } from '@web3auth/base'
|
||||||
|
import { CommonPrivateKeyProvider, type CommonPrivateKeyProviderConfig } from '@web3auth/base-provider'
|
||||||
|
import { Web3Auth, type Web3AuthOptions } from '@web3auth/modal'
|
||||||
|
import {
|
||||||
|
DEFAULT_ALGORAND_CHAIN_CONFIG,
|
||||||
|
DEFAULT_UI_CONFIG,
|
||||||
|
DEFAULT_WEB3AUTH_NETWORK,
|
||||||
|
type AlgorandWeb3AuthConfig,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
let web3authInstance: Web3Auth | null = null
|
||||||
|
|
||||||
|
const sanitizeChainConfig = (chainConfig?: CommonPrivateKeyProviderConfig['config']['chainConfig']) => {
|
||||||
|
if (!chainConfig) return DEFAULT_ALGORAND_CHAIN_CONFIG
|
||||||
|
return {
|
||||||
|
...DEFAULT_ALGORAND_CHAIN_CONFIG,
|
||||||
|
...chainConfig,
|
||||||
|
chainNamespace: chainConfig.chainNamespace ?? CHAIN_NAMESPACES.OTHER,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizeUiConfig = (uiConfig?: IWeb3AuthConfiguration['uiConfig']) => ({
|
||||||
|
...DEFAULT_UI_CONFIG,
|
||||||
|
...uiConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function initWeb3Auth(config: AlgorandWeb3AuthConfig): Promise<Web3Auth> {
|
||||||
|
if (web3authInstance) {
|
||||||
|
return web3authInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
const chainConfig = sanitizeChainConfig(config.chainConfig)
|
||||||
|
const uiConfig = sanitizeUiConfig(config.uiConfig)
|
||||||
|
const web3AuthNetwork = config.web3AuthNetwork ?? DEFAULT_WEB3AUTH_NETWORK
|
||||||
|
|
||||||
|
const privateKeyProvider = new CommonPrivateKeyProvider({
|
||||||
|
config: {
|
||||||
|
chainConfig,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const web3AuthConfig: Web3AuthOptions = {
|
||||||
|
clientId: config.clientId,
|
||||||
|
web3AuthNetwork,
|
||||||
|
privateKeyProvider,
|
||||||
|
uiConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
web3authInstance = new Web3Auth(web3AuthConfig)
|
||||||
|
await web3authInstance.initModal()
|
||||||
|
|
||||||
|
return web3authInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeb3AuthInstance(): Web3Auth | null {
|
||||||
|
return web3authInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeb3AuthProvider(): IProvider | null {
|
||||||
|
return web3authInstance?.provider || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWeb3AuthConnected(): boolean {
|
||||||
|
return web3authInstance?.status === 'connected'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Web3AuthUserInfo {
|
||||||
|
email?: string
|
||||||
|
name?: string
|
||||||
|
profileImage?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWeb3AuthUserInfo(): Promise<Web3AuthUserInfo | null> {
|
||||||
|
if (!web3authInstance || !isWeb3AuthConnected()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userInfo = await web3authInstance.getUserInfo()
|
||||||
|
return userInfo as Web3AuthUserInfo
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logoutFromWeb3Auth(): Promise<void> {
|
||||||
|
if (!web3authInstance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await web3authInstance.logout()
|
||||||
|
} finally {
|
||||||
|
web3authInstance = null
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/web3auth-algorand/tsconfig.json
Normal file
21
packages/web3auth-algorand/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["react"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
@ -2,143 +2,83 @@ import { useState } from 'react'
|
|||||||
import { NavLink, Outlet } from 'react-router-dom'
|
import { NavLink, Outlet } from 'react-router-dom'
|
||||||
import ConnectWallet from './components/ConnectWallet'
|
import ConnectWallet from './components/ConnectWallet'
|
||||||
import ThemeToggle from './components/ThemeToggle'
|
import ThemeToggle from './components/ThemeToggle'
|
||||||
import Web3AuthButton from './components/Web3AuthButton'
|
|
||||||
import { useUnifiedWallet } from './hooks/useUnifiedWallet'
|
import { useUnifiedWallet } from './hooks/useUnifiedWallet'
|
||||||
|
|
||||||
/**
|
|
||||||
* Main Layout Component
|
|
||||||
* Wraps the entire app with navigation, footer, and wallet connection modal
|
|
||||||
* Now with unified wallet support - shows mutual exclusion between Web3Auth and traditional wallets
|
|
||||||
*/
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const [openWalletModal, setOpenWalletModal] = useState(false)
|
const [openWalletModal, setOpenWalletModal] = useState(false)
|
||||||
const { walletType } = useUnifiedWallet()
|
const { isConnected, activeAddress, userInfo } = useUnifiedWallet()
|
||||||
|
|
||||||
const toggleWalletModal = () => setOpenWalletModal(!openWalletModal)
|
const toggleWalletModal = () => setOpenWalletModal(!openWalletModal)
|
||||||
|
|
||||||
// Determine button states based on which wallet is active
|
// Helper to format address: "ZBC...WXYZ"
|
||||||
const isWeb3AuthActive = walletType === 'web3auth'
|
const displayAddress =
|
||||||
const isTraditionalActive = walletType === 'traditional'
|
isConnected && activeAddress ? `${activeAddress.toString().slice(0, 4)}...${activeAddress.toString().slice(-4)}` : 'Sign in'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-white dark:bg-slate-950 text-slate-900 dark:text-slate-100">
|
<div className="min-h-screen flex flex-col bg-white dark:bg-slate-950 text-slate-900 dark:text-slate-100">
|
||||||
{/* Navbar */}
|
{/* Navbar */}
|
||||||
<nav className="bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 sticky top-0 z-50 shadow-sm">
|
<nav className="bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 sticky top-0 z-50 shadow-sm">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
|
||||||
<NavLink
|
<NavLink to="/" className="text-2xl font-bold text-slate-900 dark:text-white hover:text-teal-600 transition">
|
||||||
to="/"
|
|
||||||
className="text-2xl font-bold text-slate-900 dark:text-white hover:text-teal-600 dark:hover:text-teal-400 transition"
|
|
||||||
>
|
|
||||||
TokenizeRWA
|
TokenizeRWA
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<div className="hidden sm:flex items-center gap-6">
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden sm:flex items-center gap-8">
|
||||||
|
{['Home', 'Tokenize'].map((item) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/"
|
key={item}
|
||||||
|
to={item === 'Home' ? '/' : `/${item.toLowerCase()}`}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 font-medium transition ${isActive ? 'text-slate-900 dark:text-slate-100 border-b-2 border-teal-600' : ''}`
|
`text-sm font-semibold transition ${isActive ? 'text-teal-600' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white'}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Home
|
{item}
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
to="/tokenize"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 font-medium transition ${isActive ? 'text-slate-900 dark:text-slate-100 border-b-2 border-teal-600' : ''}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Tokenize
|
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-4">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
|
||||||
{/* Web3Auth Button - disabled if traditional wallet is active */}
|
{/* ONE Button to Rule Them All */}
|
||||||
<div className={isTraditionalActive ? 'opacity-50 pointer-events-none' : ''}>
|
|
||||||
<Web3AuthButton />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Traditional Wallet Button - disabled if Web3Auth is active */}
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleWalletModal}
|
onClick={toggleWalletModal}
|
||||||
disabled={isWeb3AuthActive}
|
className={`flex items-center gap-2 px-5 py-2 rounded-xl font-bold text-sm transition shadow-sm border ${
|
||||||
className={`px-4 py-2 rounded-lg font-medium transition text-sm shadow-sm ${
|
isConnected
|
||||||
isWeb3AuthActive
|
? 'bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-200'
|
||||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed dark:bg-slate-700 dark:text-slate-500'
|
: 'bg-teal-600 border-teal-600 text-white hover:bg-teal-700'
|
||||||
: isTraditionalActive
|
|
||||||
? 'bg-teal-600 text-white hover:bg-teal-700'
|
|
||||||
: 'bg-teal-600 text-white hover:bg-teal-700'
|
|
||||||
}`}
|
}`}
|
||||||
title={isWeb3AuthActive ? 'Using Web3Auth - disconnect to use traditional wallet' : undefined}
|
|
||||||
>
|
>
|
||||||
{isWeb3AuthActive
|
{isConnected && <span className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse" />}
|
||||||
? 'Using Web3Auth'
|
{displayAddress}
|
||||||
: isTraditionalActive
|
|
||||||
? 'Wallet Connected'
|
|
||||||
: 'Connect Wallet'}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Main */}
|
|
||||||
<main className="flex-1">
|
<main className="flex-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer (Simplified) */}
|
||||||
<footer className="bg-slate-900 dark:bg-slate-950 text-slate-300 dark:text-slate-400 border-t border-slate-800 dark:border-slate-800">
|
<footer className="bg-slate-900 text-slate-400 py-12 px-6 border-t border-slate-800">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
<div className="max-w-7xl mx-auto grid gap-8 md:grid-cols-3">
|
||||||
<div className="grid gap-8 md:grid-cols-3">
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xl font-bold text-white">TokenizeRWA</div>
|
<div className="text-xl font-bold text-white mb-3">TokenizeRWA</div>
|
||||||
<p className="mt-3 text-sm leading-relaxed">
|
<p className="text-sm">POC template for tokenizing real-world assets on Algorand.</p>
|
||||||
A lightweight proof-of-concept template for tokenizing real-world assets on Algorand.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
<div>
|
<span className="text-white font-bold block mb-2">Connect</span>
|
||||||
<div className="font-semibold text-white mb-4">Resources</div>
|
<a href="https://lora.algokit.io" target="_blank" className="hover:text-teal-400 transition">
|
||||||
<ul className="space-y-2 text-sm">
|
|
||||||
<li>
|
|
||||||
<NavLink to="/tokenize" className="hover:text-white transition">
|
|
||||||
Tokenize an Asset
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
className="hover:text-white transition"
|
|
||||||
href="https://dev.algorand.co/concepts/assets/overview/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
ASA Documentation →
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a className="hover:text-white transition" href="https://lora.algokit.io/testnet" target="_blank" rel="noreferrer">
|
|
||||||
Lora Explorer →
|
Lora Explorer →
|
||||||
</a>
|
</a>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold text-white mb-4">About</div>
|
|
||||||
<p className="text-sm leading-relaxed">
|
|
||||||
A POC template for founders. For production, add compliance workflows, identity verification, and audit logs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-12 pt-8 border-t border-gray-800 text-xs text-gray-400 flex flex-col sm:flex-row gap-2 sm:justify-between">
|
|
||||||
<span>© {new Date().getFullYear()} TokenizeRWA. All rights reserved.</span>
|
|
||||||
<span>Built with AlgoKit + Algorand</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xs">© {new Date().getFullYear()} TokenizeRWA. All rights reserved.</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
{/* The Unified Modal */}
|
||||||
<ConnectWallet openModal={openWalletModal} closeModal={toggleWalletModal} />
|
<ConnectWallet openModal={openWalletModal} closeModal={toggleWalletModal} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useWallet, Wallet, WalletId } from '@txnlab/use-wallet-react'
|
import { WalletId } from '@txnlab/use-wallet-react'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { SocialLoginProvider, useUnifiedWallet } from '../hooks/useUnifiedWallet'
|
||||||
import Account from './Account'
|
import Account from './Account'
|
||||||
|
|
||||||
interface ConnectWalletInterface {
|
interface ConnectWalletInterface {
|
||||||
@ -7,154 +8,160 @@ interface ConnectWalletInterface {
|
|||||||
closeModal: () => void
|
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 ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => {
|
||||||
const { wallets, activeAddress } = useWallet()
|
// Destructure the new clean methods from your unified hook
|
||||||
|
const { isConnected, walletType, userInfo, traditionalWallets, connectGoogle, connectFacebook, connectGithub, disconnect } =
|
||||||
|
useUnifiedWallet()
|
||||||
|
|
||||||
|
const [connectingProvider, setConnectingProvider] = useState<SocialLoginProvider | null>(null)
|
||||||
const dialogRef = useRef<HTMLDialogElement>(null)
|
const dialogRef = useRef<HTMLDialogElement>(null)
|
||||||
|
|
||||||
// Manage native dialog element's open/close state
|
const handleSocialLogin = async (provider: SocialLoginProvider, connectFn: () => Promise<void>) => {
|
||||||
|
try {
|
||||||
|
setConnectingProvider(provider)
|
||||||
|
await connectFn() // Bypasses the Web3Auth modal
|
||||||
|
closeModal()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${provider} login failed`, error)
|
||||||
|
} finally {
|
||||||
|
setConnectingProvider(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const socialOptions: { id: SocialLoginProvider; label: string; icon: string; action: () => Promise<void> }[] = [
|
||||||
|
{
|
||||||
|
id: 'google',
|
||||||
|
label: 'Continue with Google',
|
||||||
|
icon: 'https://www.gstatic.com/images/branding/product/1x/gsa_512dp.png',
|
||||||
|
action: connectGoogle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'facebook',
|
||||||
|
label: 'Continue with Facebook',
|
||||||
|
icon: 'https://www.facebook.com/images/fb_icon_325x325.png',
|
||||||
|
action: connectFacebook,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'github',
|
||||||
|
label: 'Continue with GitHub',
|
||||||
|
icon: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png',
|
||||||
|
action: connectGithub,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const formatSocialProvider = (provider?: string) => {
|
||||||
|
const normalized = provider?.toLowerCase()
|
||||||
|
switch (normalized) {
|
||||||
|
case 'google':
|
||||||
|
return 'Google'
|
||||||
|
case 'facebook':
|
||||||
|
return 'Facebook'
|
||||||
|
case 'github':
|
||||||
|
return 'GitHub'
|
||||||
|
default:
|
||||||
|
return 'Social Login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const dialog = dialogRef.current
|
const dialog = dialogRef.current
|
||||||
if (!dialog) return
|
if (!dialog) return
|
||||||
|
openModal ? dialog.showModal() : dialog.close()
|
||||||
if (openModal) {
|
|
||||||
dialog.showModal()
|
|
||||||
} else {
|
|
||||||
dialog.close()
|
|
||||||
}
|
|
||||||
}, [openModal])
|
}, [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
|
|
||||||
|
|
||||||
const activeWallet = getActiveWallet()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog
|
<dialog
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
id="connect_wallet_modal"
|
className="fixed inset-0 w-full max-w-md mx-auto my-auto rounded-3xl bg-white dark:bg-slate-900 shadow-2xl border-none p-0 backdrop:bg-gray-900/50 backdrop:backdrop-blur-sm"
|
||||||
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) => e.target === dialogRef.current && closeModal()}
|
||||||
onClick={(e) => {
|
|
||||||
// Close when clicking the backdrop
|
|
||||||
if (e.target === dialogRef.current) {
|
|
||||||
closeModal()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="p-6 sm:p-7">
|
<div className="p-8">
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-slate-100">Select wallet provider</h3>
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">{isConnected ? 'Account' : 'Sign in'}</h3>
|
||||||
<button
|
<button onClick={closeModal} className="p-2 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-full transition">
|
||||||
className="text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300 transition text-sm"
|
<span className="text-xl text-gray-500">✕</span>
|
||||||
onClick={closeModal}
|
|
||||||
aria-label="Close wallet modal"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-gray-500 dark:text-slate-400 mb-4">
|
<div className="space-y-6">
|
||||||
Choose the wallet you want to connect. Supported: Pera, Defly, LocalNet (KMD), and others.
|
{isConnected ? (
|
||||||
</p>
|
/* --- CONNECTED STATE --- */
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{activeAddress && (
|
<div className="bg-slate-50 dark:bg-slate-800 rounded-2xl p-4 border border-slate-100 dark:border-slate-700">
|
||||||
<>
|
|
||||||
<div className="rounded-xl border border-gray-100 dark:border-slate-700 bg-gray-50 dark:bg-slate-700 p-4">
|
|
||||||
<Account />
|
<Account />
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Wallet Info */}
|
{walletType === 'web3auth' && userInfo && (
|
||||||
<div className="space-y-3 bg-white dark:bg-slate-700 border border-gray-200 dark:border-slate-600 rounded-xl p-4">
|
<div className="mt-3 pt-3 border-t border-slate-200 dark:border-slate-700 flex items-center gap-3">
|
||||||
{activeWallet && (
|
{userInfo.profileImage && (
|
||||||
<div>
|
<img src={userInfo.profileImage} alt="Profile" className="w-8 h-8 rounded-full border border-white" />
|
||||||
<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 className="overflow-hidden">
|
||||||
|
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-bold">
|
||||||
|
Connected via {formatSocialProvider(userInfo.typeOfLogin)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-medium dark:text-slate-200 truncate">{userInfo.email || userInfo.name}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-px bg-gray-200 dark:bg-slate-600" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!activeAddress &&
|
|
||||||
wallets?.map((wallet) => (
|
|
||||||
<button
|
<button
|
||||||
data-test-id={`${wallet.id}-connect`}
|
onClick={disconnect} // Use the unified disconnect method
|
||||||
className={`
|
className="w-full py-3 bg-red-50 text-red-600 hover:bg-red-100 dark:bg-red-900/20 dark:text-red-400 font-semibold rounded-xl transition"
|
||||||
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={() => {
|
|
||||||
// Close modal before initiating wallet connection
|
|
||||||
closeModal()
|
|
||||||
wallet.connect()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{!isKmd(wallet) && (
|
Disconnect
|
||||||
<img
|
</button>
|
||||||
alt={`wallet_icon_${wallet.id}`}
|
</div>
|
||||||
src={wallet.metadata.icon}
|
) : (
|
||||||
className="w-9 h-9 object-contain rounded-md border border-gray-100 dark:border-slate-600 bg-white dark:bg-slate-700"
|
/* --- DISCONNECTED STATE --- */
|
||||||
/>
|
<>
|
||||||
)}
|
<div className="space-y-3">
|
||||||
<span className="font-medium text-sm text-left flex-1 text-gray-900 dark:text-slate-100">
|
<p className="text-xs font-bold uppercase tracking-wider text-gray-400 px-1">Social Login</p>
|
||||||
{isKmd(wallet) ? 'LocalNet Wallet' : wallet.metadata.name}
|
{socialOptions.map((option) => (
|
||||||
</span>
|
<button
|
||||||
{wallet.isActive && <span className="text-sm text-emerald-500">✓</span>}
|
key={option.id}
|
||||||
|
onClick={() => handleSocialLogin(option.id, option.action)}
|
||||||
|
disabled={!!connectingProvider}
|
||||||
|
className="w-full flex items-center justify-center gap-3 px-4 py-3.5 rounded-xl border border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-800 transition shadow-sm font-medium text-gray-700 dark:text-slate-200 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<img src={option.icon} className="w-5 h-5" alt={option.label} />
|
||||||
|
{connectingProvider === option.id ? 'Connecting...' : option.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex gap-3">
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-100 dark:border-slate-800"></div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-white dark:bg-slate-900 px-2 text-gray-400">Or use a wallet</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
{traditionalWallets?.map((wallet) => (
|
||||||
<button
|
<button
|
||||||
data-test-id="close-wallet-modal"
|
key={wallet.id}
|
||||||
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"
|
className="flex items-center gap-4 p-4 rounded-xl border border-gray-100 dark:border-slate-800 hover:border-indigo-500 dark:hover:border-indigo-500 hover:bg-indigo-50/30 transition group"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
closeModal()
|
closeModal()
|
||||||
|
wallet.connect()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Close
|
<img src={wallet.metadata.icon} alt={wallet.id} className="w-10 h-10 rounded-lg group-hover:scale-110 transition" />
|
||||||
</button>
|
<span className="font-semibold text-gray-800 dark:text-slate-200">
|
||||||
|
{wallet.id === WalletId.KMD ? 'LocalNet' : wallet.metadata.name}
|
||||||
{activeAddress && (
|
</span>
|
||||||
<button
|
|
||||||
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 wallet = wallets.find((w) => w.isActive)
|
|
||||||
if (wallet) {
|
|
||||||
await wallet.disconnect()
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('@txnlab/use-wallet:v3')
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConnectWallet
|
export default ConnectWallet
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { AiOutlineLoading3Quarters } from 'react-icons/ai'
|
import { AiOutlineLoading3Quarters } from 'react-icons/ai'
|
||||||
import { FaGoogle, FaCopy, FaCheck } from 'react-icons/fa'
|
import { FaCheck, FaCopy, FaGoogle } from 'react-icons/fa'
|
||||||
import { useWeb3Auth } from './Web3AuthProvider'
|
import { useWeb3Auth } from './Web3AuthProvider'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,12 +46,12 @@ export function Web3AuthButton() {
|
|||||||
// Handle if address is an object (like from algosdk with publicKey property)
|
// Handle if address is an object (like from algosdk with publicKey property)
|
||||||
if (typeof algorandAccount.address === 'object' && algorandAccount.address !== null) {
|
if (typeof algorandAccount.address === 'object' && algorandAccount.address !== null) {
|
||||||
// If it has a toString method, use it
|
// If it has a toString method, use it
|
||||||
if ('toString' in algorandAccount.address && typeof algorandAccount.address.toString === 'function') {
|
if ('toString' in algorandAccount.address && typeof algorandAccount.address === 'function') {
|
||||||
return algorandAccount.address.toString()
|
return algorandAccount.address
|
||||||
}
|
}
|
||||||
// If it has an addr property (algosdk Account object)
|
// If it has an addr property (algosdk Account object)
|
||||||
if ('addr' in algorandAccount.address) {
|
if ('addr' in algorandAccount.address) {
|
||||||
return String(algorandAccount.address.addr)
|
return String(algorandAccount.address)
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@ -59,14 +59,6 @@ export function Web3AuthButton() {
|
|||||||
return String(algorandAccount.address)
|
return String(algorandAccount.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ellipsize long addresses for better UI
|
|
||||||
const ellipseAddress = (address: string = '', startChars = 6, endChars = 4): string => {
|
|
||||||
if (!address || address.length <= startChars + endChars) {
|
|
||||||
return address
|
|
||||||
}
|
|
||||||
return `${address.slice(0, startChars)}...${address.slice(-endChars)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login with error feedback
|
// Handle login with error feedback
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
try {
|
try {
|
||||||
@ -149,7 +141,7 @@ export function Web3AuthButton() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||||
className="btn btn-sm btn-ghost gap-2 hover:bg-base-200"
|
className="btn btn-sm btn-ghost gap-2 hover:bg-base-200"
|
||||||
title={`Connected: ${address}`}
|
title={`Connected: ${address} ${userInfo?.email}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Profile picture - always show first letter of address */}
|
{/* Profile picture - always show first letter of address */}
|
||||||
@ -164,7 +156,7 @@ export function Web3AuthButton() {
|
|||||||
{firstLetter}
|
{firstLetter}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="font-mono text-sm font-medium">{ellipseAddress(address)}</span>
|
<span className="font-mono text-sm font-medium">{address}</span>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`}
|
className={`w-4 h-4 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -184,23 +176,15 @@ export function Web3AuthButton() {
|
|||||||
<li className="menu-title px-3 py-2">
|
<li className="menu-title px-3 py-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{userInfo.profileImage ? (
|
{userInfo.profileImage ? (
|
||||||
<img
|
<img src={userInfo.profileImage} alt="Profile" className="w-10 h-10 rounded-full object-cover ring-2 ring-primary" />
|
||||||
src={userInfo.profileImage}
|
|
||||||
alt="Profile"
|
|
||||||
className="w-10 h-10 rounded-full object-cover ring-2 ring-primary"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="w-10 h-10 rounded-full bg-primary text-primary-content flex items-center justify-center text-lg font-bold">
|
<div className="w-10 h-10 rounded-full bg-primary text-primary-content flex items-center justify-center text-lg font-bold">
|
||||||
{firstLetter}
|
{firstLetter}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{userInfo.name && (
|
{userInfo.name && <span className="font-semibold text-base-content">{userInfo.name}</span>}
|
||||||
<span className="font-semibold text-base-content">{userInfo.name}</span>
|
{userInfo.email && <span className="text-xs text-base-content/70 break-all">{userInfo.email}</span>}
|
||||||
)}
|
|
||||||
{userInfo.email && (
|
|
||||||
<span className="text-xs text-base-content/70 break-all">{userInfo.email}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -213,17 +197,12 @@ export function Web3AuthButton() {
|
|||||||
<span className="text-xs uppercase">Algorand Address</span>
|
<span className="text-xs uppercase">Algorand Address</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div className="bg-base-200 rounded-lg p-2 font-mono text-xs break-all cursor-default hover:bg-base-200">
|
<div className="bg-base-200 rounded-lg p-2 font-mono text-xs break-all cursor-default hover:bg-base-200">{address}</div>
|
||||||
{address}
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{/* Copy Address Button */}
|
{/* Copy Address Button */}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button onClick={handleCopyAddress} className="text-sm gap-2">
|
||||||
onClick={handleCopyAddress}
|
|
||||||
className="text-sm gap-2"
|
|
||||||
>
|
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<>
|
<>
|
||||||
<FaCheck className="text-success" />
|
<FaCheck className="text-success" />
|
||||||
@ -242,11 +221,7 @@ export function Web3AuthButton() {
|
|||||||
|
|
||||||
{/* Disconnect Button */}
|
{/* Disconnect Button */}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button onClick={handleLogout} disabled={isLoading} className="text-sm text-error hover:bg-error/10 gap-2">
|
||||||
onClick={handleLogout}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="text-sm text-error hover:bg-error/10 gap-2"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<AiOutlineLoading3Quarters className="animate-spin" />
|
<AiOutlineLoading3Quarters className="animate-spin" />
|
||||||
@ -255,7 +230,12 @@ export function Web3AuthButton() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Disconnect</span>
|
<span>Disconnect</span>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -13,7 +13,11 @@ interface Web3AuthContextType {
|
|||||||
web3AuthInstance: Web3Auth | null
|
web3AuthInstance: Web3Auth | null
|
||||||
algorandAccount: AlgorandAccountFromWeb3Auth | null
|
algorandAccount: AlgorandAccountFromWeb3Auth | null
|
||||||
userInfo: Web3AuthUserInfo | null
|
userInfo: Web3AuthUserInfo | null
|
||||||
login: () => Promise<void>
|
/**
|
||||||
|
* login handles both modal and direct social login.
|
||||||
|
* Passing arguments bypasses the Web3Auth modal.
|
||||||
|
*/
|
||||||
|
login: (adapter?: string, provider?: string) => Promise<void>
|
||||||
logout: () => Promise<void>
|
logout: () => Promise<void>
|
||||||
refreshUserInfo: () => Promise<void>
|
refreshUserInfo: () => Promise<void>
|
||||||
}
|
}
|
||||||
@ -31,6 +35,7 @@ export function Web3AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const [algorandAccount, setAlgorandAccount] = useState<AlgorandAccountFromWeb3Auth | null>(null)
|
const [algorandAccount, setAlgorandAccount] = useState<AlgorandAccountFromWeb3Auth | null>(null)
|
||||||
const [userInfo, setUserInfo] = useState<Web3AuthUserInfo | null>(null)
|
const [userInfo, setUserInfo] = useState<Web3AuthUserInfo | null>(null)
|
||||||
|
|
||||||
|
// Initialization logic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeWeb3Auth = async () => {
|
const initializeWeb3Auth = async () => {
|
||||||
try {
|
try {
|
||||||
@ -38,7 +43,6 @@ export function Web3AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
const web3auth = await initWeb3Auth()
|
const web3auth = await initWeb3Auth()
|
||||||
|
|
||||||
setWeb3AuthInstance(web3auth)
|
setWeb3AuthInstance(web3auth)
|
||||||
|
|
||||||
if (web3auth.status === 'connected' && web3auth.provider) {
|
if (web3auth.status === 'connected' && web3auth.provider) {
|
||||||
@ -49,19 +53,17 @@ export function Web3AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const account = await getAlgorandAccount(web3auth.provider)
|
const account = await getAlgorandAccount(web3auth.provider)
|
||||||
setAlgorandAccount(account)
|
setAlgorandAccount(account)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('🎯 Account derivation error:', err)
|
||||||
setError('Failed to derive Algorand account. Please reconnect.')
|
setError('Failed to derive Algorand account. Please reconnect.')
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userInformation = await getWeb3AuthUserInfo()
|
const userInformation = await getWeb3AuthUserInfo()
|
||||||
if (userInformation) {
|
if (userInformation) setUserInfo(userInformation)
|
||||||
setUserInfo(userInformation)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('🎯 Failed to fetch user info:', err)
|
console.error('🎯 Failed to fetch user info:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsInitialized(true)
|
setIsInitialized(true)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize Web3Auth'
|
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize Web3Auth'
|
||||||
@ -76,25 +78,32 @@ export function Web3AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
initializeWeb3Auth()
|
initializeWeb3Auth()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const login = async () => {
|
/**
|
||||||
|
* Unified Login Function
|
||||||
|
* @param adapter - (Optional) e.g., WALLET_ADAPTERS.AUTH
|
||||||
|
* @param loginProvider - (Optional) e.g., 'google'
|
||||||
|
*/
|
||||||
|
const login = async (adapter?: string, loginProvider?: string) => {
|
||||||
if (!web3AuthInstance) {
|
if (!web3AuthInstance) {
|
||||||
console.error('🎯 LOGIN: Web3Auth not initialized')
|
|
||||||
setError('Web3Auth not initialized')
|
setError('Web3Auth not initialized')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isInitialized) {
|
|
||||||
console.error('🎯 LOGIN: Web3Auth still initializing')
|
|
||||||
setError('Web3Auth is still initializing, please try again')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
const web3authProvider = await web3AuthInstance.connect()
|
let web3authProvider: IProvider | null
|
||||||
|
|
||||||
|
// Check if we are triggering a specific social login (bypasses modal)
|
||||||
|
if (adapter && loginProvider) {
|
||||||
|
web3authProvider = await web3AuthInstance.connectTo(adapter, {
|
||||||
|
loginProvider: loginProvider,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Fallback to showing the default Web3Auth Modal
|
||||||
|
web3authProvider = await web3AuthInstance.connect()
|
||||||
|
}
|
||||||
|
|
||||||
if (!web3authProvider) {
|
if (!web3authProvider) {
|
||||||
throw new Error('Failed to connect Web3Auth provider')
|
throw new Error('Failed to connect Web3Auth provider')
|
||||||
@ -103,23 +112,12 @@ export function Web3AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setProvider(web3authProvider)
|
setProvider(web3authProvider)
|
||||||
setIsConnected(true)
|
setIsConnected(true)
|
||||||
|
|
||||||
try {
|
// Post-connection: Derive Algorand Address and Fetch Profile
|
||||||
const account = await getAlgorandAccount(web3authProvider)
|
const account = await getAlgorandAccount(web3authProvider)
|
||||||
setAlgorandAccount(account)
|
setAlgorandAccount(account)
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to derive Algorand account'
|
|
||||||
setError(errorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const userInformation = await getWeb3AuthUserInfo()
|
const userInformation = await getWeb3AuthUserInfo()
|
||||||
if (userInformation) {
|
if (userInformation) setUserInfo(userInformation)
|
||||||
setUserInfo(userInformation)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('🎯 LOGIN: Failed to fetch user info:', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Login failed'
|
const errorMessage = err instanceof Error ? err.message : 'Login failed'
|
||||||
console.error('🎯 LOGIN: Error:', err)
|
console.error('🎯 LOGIN: Error:', err)
|
||||||
@ -143,16 +141,9 @@ export function Web3AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setIsConnected(false)
|
setIsConnected(false)
|
||||||
setAlgorandAccount(null)
|
setAlgorandAccount(null)
|
||||||
setUserInfo(null)
|
setUserInfo(null)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Logout failed'
|
|
||||||
console.error('🎯 LOGOUT: Error:', err)
|
console.error('🎯 LOGOUT: Error:', err)
|
||||||
setError(errorMessage)
|
setError(err instanceof Error ? err.message : 'Logout failed')
|
||||||
|
|
||||||
setProvider(null)
|
|
||||||
setIsConnected(false)
|
|
||||||
setAlgorandAccount(null)
|
|
||||||
setUserInfo(null)
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@ -161,9 +152,7 @@ export function Web3AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const refreshUserInfo = async () => {
|
const refreshUserInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const userInformation = await getWeb3AuthUserInfo()
|
const userInformation = await getWeb3AuthUserInfo()
|
||||||
if (userInformation) {
|
if (userInformation) setUserInfo(userInformation)
|
||||||
setUserInfo(userInformation)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('🎯 REFRESH: Failed:', err)
|
console.error('🎯 REFRESH: Failed:', err)
|
||||||
}
|
}
|
||||||
@ -188,10 +177,8 @@ export function Web3AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
export function useWeb3Auth(): Web3AuthContextType {
|
export function useWeb3Auth(): Web3AuthContextType {
|
||||||
const context = useContext(Web3AuthContext)
|
const context = useContext(Web3AuthContext)
|
||||||
|
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error('useWeb3Auth must be used within a Web3AuthProvider')
|
throw new Error('useWeb3Auth must be used within a Web3AuthProvider')
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,219 +1,47 @@
|
|||||||
/**
|
|
||||||
* Unified Wallet Hook
|
|
||||||
*
|
|
||||||
* Combines Web3Auth (Google OAuth) and traditional wallet (Pera/Defly/etc) into ONE interface.
|
|
||||||
* Provides a single source of truth for activeAddress and signer across the entire app.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Returns ONE activeAddress (from either Web3Auth OR traditional wallet)
|
|
||||||
* - Returns ONE signer (compatible with AlgorandClient)
|
|
||||||
* - Indicates which wallet type is active
|
|
||||||
* - Handles mutual exclusion (only one can be active at a time)
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```typescript
|
|
||||||
* const { activeAddress, signer, walletType, isConnected } = useUnifiedWallet()
|
|
||||||
*
|
|
||||||
* // walletType will be: 'web3auth' | 'traditional' | null
|
|
||||||
* // signer works with: algorand.send.assetCreate({ sender: activeAddress, signer, ... })
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useWallet } from '@txnlab/use-wallet-react'
|
import { useWallet } from '@txnlab/use-wallet-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useWeb3Auth } from '../components/Web3AuthProvider'
|
import { useWeb3Auth } from '../components/Web3AuthProvider'
|
||||||
import { createWeb3AuthSigner } from '../utils/web3auth/web3authIntegration'
|
import { createWeb3AuthSigner } from '../utils/web3auth/web3authIntegration'
|
||||||
|
import { WALLET_ADAPTERS } from '@web3auth/base'
|
||||||
|
|
||||||
export type WalletType = 'web3auth' | 'traditional' | null
|
export type SocialLoginProvider = 'google' | 'facebook' | 'github'
|
||||||
|
|
||||||
export interface UnifiedWalletState {
|
export function useUnifiedWallet() {
|
||||||
/** The active Algorand address (from either Web3Auth or traditional wallet) */
|
const { isConnected, algorandAccount, userInfo, login, logout, isLoading } = useWeb3Auth()
|
||||||
activeAddress: string | null
|
|
||||||
|
|
||||||
/** Transaction signer compatible with AlgorandClient */
|
|
||||||
signer: any | null
|
|
||||||
|
|
||||||
/** Which wallet system is currently active */
|
|
||||||
walletType: WalletType
|
|
||||||
|
|
||||||
/** Whether any wallet is connected */
|
|
||||||
isConnected: boolean
|
|
||||||
|
|
||||||
/** Loading state (either wallet system initializing/connecting) */
|
|
||||||
isLoading: boolean
|
|
||||||
|
|
||||||
/** Error from either wallet system */
|
|
||||||
error: string | null
|
|
||||||
|
|
||||||
/** Original Web3Auth data (for accessing userInfo, etc) */
|
|
||||||
web3auth: {
|
|
||||||
algorandAccount: ReturnType<typeof useWeb3Auth>['algorandAccount']
|
|
||||||
userInfo: ReturnType<typeof useWeb3Auth>['userInfo']
|
|
||||||
login: ReturnType<typeof useWeb3Auth>['login']
|
|
||||||
logout: ReturnType<typeof useWeb3Auth>['logout']
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Original traditional wallet data (for accessing wallet-specific features) */
|
|
||||||
traditional: {
|
|
||||||
wallets: ReturnType<typeof useWallet>['wallets']
|
|
||||||
activeWallet: ReturnType<typeof useWallet>['activeWallet']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useUnifiedWallet Hook
|
|
||||||
*
|
|
||||||
* Combines Web3Auth and traditional wallet into a single interface.
|
|
||||||
* Priority: Web3Auth takes precedence if both are somehow connected.
|
|
||||||
*
|
|
||||||
* @returns UnifiedWalletState with activeAddress, signer, and wallet metadata
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // In TokenizeAsset.tsx:
|
|
||||||
* const { activeAddress, signer, walletType } = useUnifiedWallet()
|
|
||||||
*
|
|
||||||
* if (!activeAddress) {
|
|
||||||
* return <p>Please connect a wallet</p>
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // Use signer with AlgorandClient - works with BOTH wallet types!
|
|
||||||
* const result = await algorand.send.assetCreate({
|
|
||||||
* sender: activeAddress,
|
|
||||||
* signer: signer,
|
|
||||||
* total: BigInt(1000000),
|
|
||||||
* decimals: 6,
|
|
||||||
* assetName: 'My Token',
|
|
||||||
* unitName: 'MYT',
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function useUnifiedWallet(): UnifiedWalletState {
|
|
||||||
// Get both wallet systems
|
|
||||||
const web3auth = useWeb3Auth()
|
|
||||||
const traditional = useWallet()
|
const traditional = useWallet()
|
||||||
|
|
||||||
// Compute unified state
|
return useMemo(() => {
|
||||||
const state = useMemo<UnifiedWalletState>(() => {
|
// Determine which source is actually providing an account
|
||||||
// Priority 1: Web3Auth (if connected)
|
const isWeb3AuthActive = isConnected && !!algorandAccount
|
||||||
if (web3auth.isConnected && web3auth.algorandAccount) {
|
const isTraditionalActive = !!traditional.activeAddress
|
||||||
|
|
||||||
|
const activeAddress = isWeb3AuthActive ? algorandAccount!.address : traditional.activeAddress || null
|
||||||
|
|
||||||
|
const connectWithSocial = async (provider: SocialLoginProvider) => {
|
||||||
|
await login(WALLET_ADAPTERS.AUTH, provider)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeAddress: web3auth.algorandAccount.address,
|
activeAddress,
|
||||||
signer: createWeb3AuthSigner(web3auth.algorandAccount),
|
isConnected: !!activeAddress,
|
||||||
walletType: 'web3auth',
|
walletType: isWeb3AuthActive ? 'web3auth' : isTraditionalActive ? 'traditional' : null,
|
||||||
isConnected: true,
|
isLoading,
|
||||||
isLoading: web3auth.isLoading,
|
|
||||||
error: web3auth.error,
|
|
||||||
web3auth: {
|
|
||||||
algorandAccount: web3auth.algorandAccount,
|
|
||||||
userInfo: web3auth.userInfo,
|
|
||||||
login: web3auth.login,
|
|
||||||
logout: web3auth.logout,
|
|
||||||
},
|
|
||||||
traditional: {
|
|
||||||
wallets: traditional.wallets,
|
|
||||||
activeWallet: traditional.activeWallet,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: Traditional wallet (Pera/Defly/etc)
|
// Connection Methods
|
||||||
if (traditional.activeAddress) {
|
connectSocial: connectWithSocial,
|
||||||
return {
|
connectGoogle: async () => connectWithSocial('google'),
|
||||||
activeAddress: traditional.activeAddress,
|
connectFacebook: async () => connectWithSocial('facebook'),
|
||||||
signer: traditional.transactionSigner,
|
connectGithub: async () => connectWithSocial('github'),
|
||||||
walletType: 'traditional',
|
|
||||||
isConnected: true,
|
disconnect: async () => {
|
||||||
isLoading: false,
|
if (isWeb3AuthActive) await logout()
|
||||||
error: null,
|
if (isTraditionalActive) await traditional.activeWallet?.disconnect()
|
||||||
web3auth: {
|
|
||||||
algorandAccount: null,
|
|
||||||
userInfo: null,
|
|
||||||
login: web3auth.login,
|
|
||||||
logout: web3auth.logout,
|
|
||||||
},
|
},
|
||||||
traditional: {
|
|
||||||
wallets: traditional.wallets,
|
|
||||||
activeWallet: traditional.activeWallet,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No wallet connected
|
// Metadata
|
||||||
return {
|
userInfo,
|
||||||
activeAddress: null,
|
traditionalWallets: traditional.wallets,
|
||||||
signer: null,
|
signer: isWeb3AuthActive ? createWeb3AuthSigner(algorandAccount) : traditional.transactionSigner,
|
||||||
walletType: null,
|
|
||||||
isConnected: false,
|
|
||||||
isLoading: web3auth.isLoading,
|
|
||||||
error: web3auth.error,
|
|
||||||
web3auth: {
|
|
||||||
algorandAccount: null,
|
|
||||||
userInfo: null,
|
|
||||||
login: web3auth.login,
|
|
||||||
logout: web3auth.logout,
|
|
||||||
},
|
|
||||||
traditional: {
|
|
||||||
wallets: traditional.wallets,
|
|
||||||
activeWallet: traditional.activeWallet,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [isConnected, algorandAccount, userInfo, traditional, isLoading])
|
||||||
web3auth.isConnected,
|
|
||||||
web3auth.algorandAccount,
|
|
||||||
web3auth.isLoading,
|
|
||||||
web3auth.error,
|
|
||||||
web3auth.userInfo,
|
|
||||||
web3auth.login,
|
|
||||||
web3auth.logout,
|
|
||||||
traditional.activeAddress,
|
|
||||||
traditional.transactionSigner,
|
|
||||||
traditional.wallets,
|
|
||||||
traditional.activeWallet,
|
|
||||||
])
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper hook: Get just the address (most common use case)
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const address = useActiveAddress()
|
|
||||||
* if (!address) return <ConnectButton />
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function useActiveAddress(): string | null {
|
|
||||||
const { activeAddress } = useUnifiedWallet()
|
|
||||||
return activeAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper hook: Check if any wallet is connected
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const isConnected = useIsWalletConnected()
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function useIsWalletConnected(): boolean {
|
|
||||||
const { isConnected } = useUnifiedWallet()
|
|
||||||
return isConnected
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper hook: Get wallet type
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const walletType = useWalletType()
|
|
||||||
* if (walletType === 'web3auth') {
|
|
||||||
* // Show Google profile info
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function useWalletType(): WalletType {
|
|
||||||
const { walletType } = useUnifiedWallet()
|
|
||||||
return walletType
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export async function initWeb3Auth(): Promise<Web3Auth> {
|
|||||||
primary: '#000000',
|
primary: '#000000',
|
||||||
},
|
},
|
||||||
mode: 'light' as const,
|
mode: 'light' as const,
|
||||||
loginMethodsOrder: ['google', 'github', 'twitter'],
|
loginMethodsOrder: ['google', 'facebook', 'github', 'twitter'],
|
||||||
defaultLanguage: 'en',
|
defaultLanguage: 'en',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -78,6 +78,7 @@ export interface Web3AuthUserInfo {
|
|||||||
email?: string
|
email?: string
|
||||||
name?: string
|
name?: string
|
||||||
profileImage?: string
|
profileImage?: string
|
||||||
|
typeOfLogin?: string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user