@d13co/liquid-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TxnLab, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@d13co/liquid-ui",
3
+ "version": "0.0.1",
4
+ "description": "Shared UI components for use-wallet-ui and Liquid Wallet Companion extensions",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "peerDependencies": {
10
+ "@tanstack/react-query": ">=5",
11
+ "react": "^18 || ^19"
12
+ },
13
+ "devDependencies": {
14
+ "@tanstack/react-query": "5.90.21",
15
+ "@types/react": "19.2.14",
16
+ "react": "19.2.4",
17
+ "typescript": "5.9.3"
18
+ },
19
+ "scripts": {
20
+ "typecheck": "tsc --noEmit"
21
+ }
22
+ }
@@ -0,0 +1,26 @@
1
+ interface AlgoSymbolProps {
2
+ className?: string
3
+ /**
4
+ * Size relative to current font size (1em = 100%)
5
+ * @default 0.85
6
+ */
7
+ scale?: number
8
+ }
9
+
10
+ export function AlgoSymbol({ className, scale = 0.85 }: AlgoSymbolProps) {
11
+ return (
12
+ <svg
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ viewBox="0 0 24 24"
15
+ className={`inline-block align-baseline ${className ?? ''}`}
16
+ style={{
17
+ width: `${scale}em`,
18
+ height: `${scale}em`,
19
+ }}
20
+ aria-label="Algorand"
21
+ fill="currentColor"
22
+ >
23
+ <path d="M23.98 23.99h-3.75l-2.44-9.07-5.25 9.07H8.34l8.1-14.04-1.3-4.88L4.22 24H.02L13.88 0h3.67l1.61 5.96h3.79l-2.59 4.5 3.62 13.53z" />
24
+ </svg>
25
+ )
26
+ }
@@ -0,0 +1,203 @@
1
+ import { useState } from 'react'
2
+ import { AlgoSymbol } from './AlgoSymbol'
3
+ import { OptInPanel, type OptInPanelProps } from './OptInPanel'
4
+ import { SendPanel, type SendPanelProps } from './SendPanel'
5
+
6
+ export interface ManagePanelProps {
7
+ displayBalance: number | null
8
+ showAvailableBalance: boolean
9
+ onToggleBalance: () => void
10
+ onBack: () => void
11
+ send?: Omit<SendPanelProps, 'onBack'>
12
+ optIn?: Omit<OptInPanelProps, 'onBack'>
13
+ onBridge?: () => void
14
+ onExplore?: () => void
15
+ }
16
+
17
+ const balanceFormatter = new Intl.NumberFormat(undefined, {
18
+ minimumFractionDigits: 4,
19
+ maximumFractionDigits: 4,
20
+ })
21
+
22
+ export function ManagePanel({
23
+ displayBalance,
24
+ showAvailableBalance,
25
+ onToggleBalance,
26
+ onBack,
27
+ send,
28
+ optIn,
29
+ onBridge,
30
+ onExplore,
31
+ }: ManagePanelProps) {
32
+ const [mode, setMode] = useState<'main' | 'send' | 'opt-in'>('main')
33
+
34
+ const backToMain = (resetFn?: () => void) => {
35
+ setMode('main')
36
+ resetFn?.()
37
+ }
38
+
39
+ if (mode === 'send' && send) {
40
+ return <SendPanel {...send} onBack={() => backToMain(send.reset)} />
41
+ }
42
+
43
+ if (mode === 'opt-in' && optIn) {
44
+ return <OptInPanel {...optIn} onBack={() => backToMain(optIn.reset)} />
45
+ }
46
+
47
+ return (
48
+ <>
49
+ {/* Header with back arrow */}
50
+ <div className="flex items-center gap-2 mb-4">
51
+ <button
52
+ onClick={onBack}
53
+ className="-ml-1 p-1 rounded-lg hover:bg-[var(--wui-color-bg-secondary)] transition-colors text-[var(--wui-color-text-secondary)] flex items-center justify-center"
54
+ title="Back"
55
+ >
56
+ <svg
57
+ xmlns="http://www.w3.org/2000/svg"
58
+ width="20"
59
+ height="20"
60
+ viewBox="0 0 24 24"
61
+ fill="none"
62
+ stroke="currentColor"
63
+ strokeWidth="2"
64
+ strokeLinecap="round"
65
+ strokeLinejoin="round"
66
+ >
67
+ <path d="m15 18-6-6 6-6" />
68
+ </svg>
69
+ </button>
70
+ <h3 className="text-lg font-bold leading-none text-[var(--wui-color-text)] wallet-custom-font">
71
+ Manage Liquid Account
72
+ </h3>
73
+ </div>
74
+
75
+ {/* Balance display */}
76
+ <div className="mb-4 bg-[var(--wui-color-bg-secondary)] rounded-lg p-3">
77
+ <div className="flex justify-between items-center">
78
+ {displayBalance !== null && (
79
+ <span className="text-base font-medium text-[var(--wui-color-text)] flex items-center gap-1">
80
+ {balanceFormatter.format(displayBalance)}
81
+ <AlgoSymbol />
82
+ </span>
83
+ )}
84
+ <button
85
+ onClick={onToggleBalance}
86
+ className="flex items-center gap-1 text-sm text-[var(--wui-color-text-secondary)] bg-[var(--wui-color-bg-tertiary)] py-1 pl-2.5 pr-2 rounded-md hover:brightness-90 transition-all focus:outline-none"
87
+ title={showAvailableBalance ? 'Show total balance' : 'Show available balance'}
88
+ >
89
+ {showAvailableBalance ? 'Available' : 'Total'}
90
+ <svg
91
+ xmlns="http://www.w3.org/2000/svg"
92
+ width="10"
93
+ height="10"
94
+ viewBox="0 0 24 24"
95
+ fill="none"
96
+ stroke="currentColor"
97
+ strokeWidth="2"
98
+ strokeLinecap="round"
99
+ strokeLinejoin="round"
100
+ className="ml-0.5 opacity-80"
101
+ >
102
+ <path d="m17 10-5-5-5 5" />
103
+ <path d="m17 14-5 5-5-5" />
104
+ </svg>
105
+ </button>
106
+ </div>
107
+ </div>
108
+
109
+ {/* Divider */}
110
+ <div className="border-t border-[var(--wui-color-border)] mb-3" />
111
+
112
+ {/* Action buttons grid */}
113
+ <div className="grid grid-cols-2 gap-2">
114
+ <button
115
+ onClick={() => setMode('send')}
116
+ disabled={!send}
117
+ className="py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm flex items-center justify-center disabled:opacity-40 disabled:pointer-events-none"
118
+ >
119
+ <svg
120
+ xmlns="http://www.w3.org/2000/svg"
121
+ className="h-4 w-4 mr-1.5"
122
+ viewBox="0 0 24 24"
123
+ fill="none"
124
+ stroke="currentColor"
125
+ strokeWidth="2"
126
+ strokeLinecap="round"
127
+ strokeLinejoin="round"
128
+ >
129
+ <path d="M5 12h14" />
130
+ <path d="m12 5 7 7-7 7" />
131
+ </svg>
132
+ Send
133
+ </button>
134
+ <button
135
+ onClick={() => setMode('opt-in')}
136
+ disabled={!optIn}
137
+ className="py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm flex items-center justify-center disabled:opacity-40 disabled:pointer-events-none"
138
+ >
139
+ <svg
140
+ xmlns="http://www.w3.org/2000/svg"
141
+ className="h-4 w-4 mr-1.5"
142
+ viewBox="0 0 24 24"
143
+ fill="none"
144
+ stroke="currentColor"
145
+ strokeWidth="2"
146
+ strokeLinecap="round"
147
+ strokeLinejoin="round"
148
+ >
149
+ <path d="M12 5v14" />
150
+ <path d="M5 12h14" />
151
+ </svg>
152
+ Opt In
153
+ </button>
154
+ <button
155
+ onClick={onBridge}
156
+ disabled={!onBridge}
157
+ className="py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm flex items-center justify-center disabled:opacity-40 disabled:pointer-events-none"
158
+ >
159
+ <svg
160
+ xmlns="http://www.w3.org/2000/svg"
161
+ className="h-4 w-4 mr-1.5"
162
+ viewBox="0 0 24 24"
163
+ fill="none"
164
+ stroke="currentColor"
165
+ strokeWidth="2"
166
+ strokeLinecap="round"
167
+ strokeLinejoin="round"
168
+ >
169
+ <path d="M8 3l4 4-4 4" />
170
+ <path d="M16 3l-4 4 4 4" />
171
+ <path d="M12 7H4" />
172
+ <path d="M12 7h8" />
173
+ <path d="M8 21l4-4-4-4" />
174
+ <path d="M16 21l-4-4 4-4" />
175
+ <path d="M12 17H4" />
176
+ <path d="M12 17h8" />
177
+ </svg>
178
+ Bridge
179
+ </button>
180
+ <button
181
+ onClick={onExplore}
182
+ disabled={!onExplore}
183
+ className="py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm flex items-center justify-center disabled:opacity-40 disabled:pointer-events-none"
184
+ >
185
+ <svg
186
+ xmlns="http://www.w3.org/2000/svg"
187
+ className="h-4 w-4 mr-1.5"
188
+ viewBox="0 0 24 24"
189
+ fill="none"
190
+ stroke="currentColor"
191
+ strokeWidth="2"
192
+ strokeLinecap="round"
193
+ strokeLinejoin="round"
194
+ >
195
+ <circle cx="11" cy="11" r="8" />
196
+ <path d="m21 21-4.3-4.3" />
197
+ </svg>
198
+ Explore
199
+ </button>
200
+ </div>
201
+ </>
202
+ )
203
+ }
@@ -0,0 +1,112 @@
1
+ import { Spinner } from './Spinner'
2
+ import { TransactionStatus, type TransactionStatusValue } from './TransactionStatus'
3
+
4
+ export interface OptInPanelProps {
5
+ assetIdInput: string
6
+ setAssetIdInput: (v: string) => void
7
+ assetInfo: { name: string; unitName: string; index: number } | null
8
+ assetLookupLoading: boolean
9
+ assetLookupError: string | null
10
+ status: TransactionStatusValue
11
+ error: string | null
12
+ handleOptIn: () => void
13
+ reset: () => void
14
+ retry: () => void
15
+ onBack: () => void
16
+ }
17
+
18
+ export function OptInPanel({
19
+ assetIdInput,
20
+ setAssetIdInput,
21
+ assetInfo,
22
+ assetLookupLoading,
23
+ assetLookupError,
24
+ status,
25
+ error,
26
+ handleOptIn,
27
+ reset: _reset,
28
+ retry,
29
+ onBack,
30
+ }: OptInPanelProps) {
31
+ return (
32
+ <>
33
+ {/* Header */}
34
+ <div className="flex items-center gap-2 mb-4">
35
+ <button
36
+ onClick={onBack}
37
+ className="-ml-1 p-1 rounded-lg hover:bg-[var(--wui-color-bg-secondary)] transition-colors text-[var(--wui-color-text-secondary)] flex items-center justify-center"
38
+ title="Back"
39
+ >
40
+ <svg
41
+ xmlns="http://www.w3.org/2000/svg"
42
+ width="20"
43
+ height="20"
44
+ viewBox="0 0 24 24"
45
+ fill="none"
46
+ stroke="currentColor"
47
+ strokeWidth="2"
48
+ strokeLinecap="round"
49
+ strokeLinejoin="round"
50
+ >
51
+ <path d="m15 18-6-6 6-6" />
52
+ </svg>
53
+ </button>
54
+ <h3 className="text-lg font-bold leading-none text-[var(--wui-color-text)] wallet-custom-font">Opt In Asset</h3>
55
+ </div>
56
+
57
+ {/* Asset ID input */}
58
+ <div className="mb-4">
59
+ <input
60
+ type="text"
61
+ inputMode="numeric"
62
+ pattern="[0-9]*"
63
+ placeholder="Enter Asset ID"
64
+ value={assetIdInput}
65
+ onChange={(e) => setAssetIdInput(e.target.value.replace(/[^0-9]/g, ''))}
66
+ className="w-full rounded-lg border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] py-2.5 px-3 text-sm text-[var(--wui-color-text)] placeholder:text-[var(--wui-color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--wui-color-primary)] focus:border-transparent"
67
+ />
68
+ </div>
69
+
70
+ {/* Loading */}
71
+ {assetLookupLoading && (
72
+ <div className="flex items-center justify-center py-4 text-sm text-[var(--wui-color-text-secondary)]">
73
+ <Spinner className="h-4 w-4 mr-2" />
74
+ Looking up asset...
75
+ </div>
76
+ )}
77
+
78
+ {/* Lookup error */}
79
+ {assetLookupError && (
80
+ <div className="py-3 text-center text-sm text-[var(--wui-color-danger-text)]">{assetLookupError}</div>
81
+ )}
82
+
83
+ {/* Asset result */}
84
+ {assetInfo && status === 'idle' && (
85
+ <div className="bg-[var(--wui-color-bg-secondary)] rounded-lg p-3">
86
+ <div className="flex justify-between items-start mb-3">
87
+ <div>
88
+ <p className="text-sm font-medium text-[var(--wui-color-text)]">{assetInfo.name}</p>
89
+ {assetInfo.unitName && (
90
+ <p className="text-xs text-[var(--wui-color-text-secondary)]">{assetInfo.unitName}</p>
91
+ )}
92
+ </div>
93
+ <span className="text-xs text-[var(--wui-color-text-tertiary)]">ID: {assetInfo.index}</span>
94
+ </div>
95
+ <button
96
+ onClick={handleOptIn}
97
+ className="w-full py-2 px-4 bg-[var(--wui-color-primary)] text-white font-medium rounded-xl hover:brightness-90 transition-all text-sm"
98
+ >
99
+ Opt In
100
+ </button>
101
+ </div>
102
+ )}
103
+
104
+ <TransactionStatus
105
+ status={status}
106
+ error={error}
107
+ successMessage="Opted in successfully!"
108
+ onRetry={retry}
109
+ />
110
+ </>
111
+ )
112
+ }
@@ -0,0 +1,171 @@
1
+ import { Spinner } from './Spinner'
2
+ import { TransactionStatus, type TransactionStatusValue } from './TransactionStatus'
3
+
4
+ export interface SendPanelProps {
5
+ sendType: 'algo' | 'asa'
6
+ setSendType: (type: 'algo' | 'asa') => void
7
+ receiver: string
8
+ setReceiver: (v: string) => void
9
+ amount: string
10
+ setAmount: (v: string) => void
11
+ assetIdInput: string
12
+ setAssetIdInput: (v: string) => void
13
+ assetInfo: { name: string; unitName: string; index: number } | null
14
+ assetLookupLoading: boolean
15
+ assetLookupError: string | null
16
+ status: TransactionStatusValue
17
+ error: string | null
18
+ handleSend: () => void
19
+ reset: () => void
20
+ retry: () => void
21
+ onBack: () => void
22
+ }
23
+
24
+ export function SendPanel({
25
+ sendType,
26
+ setSendType,
27
+ receiver,
28
+ setReceiver,
29
+ amount,
30
+ setAmount,
31
+ assetIdInput,
32
+ setAssetIdInput,
33
+ assetInfo,
34
+ assetLookupLoading,
35
+ assetLookupError,
36
+ status,
37
+ error,
38
+ handleSend,
39
+ reset: _reset,
40
+ retry,
41
+ onBack,
42
+ }: SendPanelProps) {
43
+ return (
44
+ <>
45
+ {/* Header */}
46
+ <div className="flex items-center gap-2 mb-4">
47
+ <button
48
+ onClick={onBack}
49
+ className="-ml-1 p-1 rounded-lg hover:bg-[var(--wui-color-bg-secondary)] transition-colors text-[var(--wui-color-text-secondary)] flex items-center justify-center"
50
+ title="Back"
51
+ >
52
+ <svg
53
+ xmlns="http://www.w3.org/2000/svg"
54
+ width="20"
55
+ height="20"
56
+ viewBox="0 0 24 24"
57
+ fill="none"
58
+ stroke="currentColor"
59
+ strokeWidth="2"
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ >
63
+ <path d="m15 18-6-6 6-6" />
64
+ </svg>
65
+ </button>
66
+ <h3 className="text-lg font-bold leading-none text-[var(--wui-color-text)] wallet-custom-font">Send</h3>
67
+ </div>
68
+
69
+ {/* ALGO / Asset toggle */}
70
+ <div className="flex mb-4 bg-[var(--wui-color-bg-secondary)] rounded-lg p-1">
71
+ <button
72
+ onClick={() => setSendType('algo')}
73
+ className={`flex-1 py-1.5 text-sm font-medium rounded-md transition-all ${
74
+ sendType === 'algo'
75
+ ? 'bg-[var(--wui-color-bg)] text-[var(--wui-color-text)] shadow-sm'
76
+ : 'text-[var(--wui-color-text-secondary)] hover:text-[var(--wui-color-text)]'
77
+ }`}
78
+ >
79
+ ALGO
80
+ </button>
81
+ <button
82
+ onClick={() => setSendType('asa')}
83
+ className={`flex-1 py-1.5 text-sm font-medium rounded-md transition-all ${
84
+ sendType === 'asa'
85
+ ? 'bg-[var(--wui-color-bg)] text-[var(--wui-color-text)] shadow-sm'
86
+ : 'text-[var(--wui-color-text-secondary)] hover:text-[var(--wui-color-text)]'
87
+ }`}
88
+ >
89
+ Asset
90
+ </button>
91
+ </div>
92
+
93
+ {/* Asset ID input (ASA mode only) */}
94
+ {sendType === 'asa' && (
95
+ <div className="mb-3">
96
+ <input
97
+ type="text"
98
+ inputMode="numeric"
99
+ pattern="[0-9]*"
100
+ placeholder="Asset ID"
101
+ value={assetIdInput}
102
+ onChange={(e) => setAssetIdInput(e.target.value.replace(/[^0-9]/g, ''))}
103
+ className="w-full rounded-lg border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] py-2.5 px-3 text-sm text-[var(--wui-color-text)] placeholder:text-[var(--wui-color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--wui-color-primary)] focus:border-transparent"
104
+ />
105
+ {assetLookupLoading && (
106
+ <div className="flex items-center mt-2 text-xs text-[var(--wui-color-text-secondary)]">
107
+ <Spinner className="h-3 w-3 mr-1.5" />
108
+ Looking up asset...
109
+ </div>
110
+ )}
111
+ {assetLookupError && (
112
+ <p className="mt-2 text-xs text-[var(--wui-color-danger-text)]">{assetLookupError}</p>
113
+ )}
114
+ {assetInfo && (
115
+ <div className="mt-2 flex items-center justify-between text-xs text-[var(--wui-color-text-secondary)] bg-[var(--wui-color-bg-secondary)] rounded-md px-2 py-1.5">
116
+ <span className="font-medium text-[var(--wui-color-text)]">{assetInfo.name}</span>
117
+ {assetInfo.unitName && <span>{assetInfo.unitName}</span>}
118
+ </div>
119
+ )}
120
+ </div>
121
+ )}
122
+
123
+ {/* Receiver address */}
124
+ <div className="mb-3">
125
+ <input
126
+ type="text"
127
+ placeholder="Receiver address"
128
+ value={receiver}
129
+ onChange={(e) => setReceiver(e.target.value)}
130
+ className="w-full rounded-lg border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] py-2.5 px-3 text-sm text-[var(--wui-color-text)] placeholder:text-[var(--wui-color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--wui-color-primary)] focus:border-transparent"
131
+ />
132
+ </div>
133
+
134
+ {/* Amount */}
135
+ <div className="mb-4">
136
+ <input
137
+ type="text"
138
+ inputMode="decimal"
139
+ placeholder={
140
+ sendType === 'algo'
141
+ ? 'Amount (ALGO)'
142
+ : assetInfo
143
+ ? `Amount (${assetInfo.unitName || assetInfo.name})`
144
+ : 'Amount'
145
+ }
146
+ value={amount}
147
+ onChange={(e) => setAmount(e.target.value.replace(/[^0-9.]/g, ''))}
148
+ className="w-full rounded-lg border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] py-2.5 px-3 text-sm text-[var(--wui-color-text)] placeholder:text-[var(--wui-color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--wui-color-primary)] focus:border-transparent"
149
+ />
150
+ </div>
151
+
152
+ {/* Send button */}
153
+ {status === 'idle' && (
154
+ <button
155
+ onClick={handleSend}
156
+ disabled={!receiver || !amount || (sendType === 'asa' && !assetInfo)}
157
+ className="w-full py-2.5 px-4 bg-[var(--wui-color-primary)] text-white font-medium rounded-xl hover:brightness-90 transition-all text-sm disabled:opacity-50 disabled:cursor-not-allowed"
158
+ >
159
+ Send {sendType === 'algo' ? 'ALGO' : assetInfo?.unitName || 'Asset'}
160
+ </button>
161
+ )}
162
+
163
+ <TransactionStatus
164
+ status={status}
165
+ error={error}
166
+ successMessage="Sent successfully!"
167
+ onRetry={retry}
168
+ />
169
+ </>
170
+ )
171
+ }
@@ -0,0 +1,21 @@
1
+ interface SpinnerProps {
2
+ className?: string
3
+ }
4
+
5
+ export function Spinner({ className }: SpinnerProps) {
6
+ return (
7
+ <svg
8
+ className={`animate-spin h-4 w-4 ${className ?? ''}`}
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ fill="none"
11
+ viewBox="0 0 24 24"
12
+ >
13
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
14
+ <path
15
+ className="opacity-75"
16
+ fill="currentColor"
17
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
18
+ />
19
+ </svg>
20
+ )
21
+ }
@@ -0,0 +1,124 @@
1
+ import type { TransactionData, AssetInfo } from '../types'
2
+ import { formatAssetAmount, assetLabel } from '../formatters'
3
+
4
+ export interface TransactionFlowProps {
5
+ txn: TransactionData
6
+ assetInfo?: AssetInfo
7
+ appEscrows?: Record<string, string>
8
+ }
9
+
10
+ export function TransactionFlow({ txn, assetInfo, appEscrows = {} }: TransactionFlowProps) {
11
+ const resolveAddr = (full: string | undefined, short: string): string =>
12
+ (full && appEscrows[full]) || short
13
+
14
+ const renderFlowLine = (
15
+ from: string,
16
+ label: string,
17
+ to: string,
18
+ isDanger: boolean = false,
19
+ isSecondary: boolean = false,
20
+ ) => (
21
+ <div className={`grid grid-cols-[1fr_auto_1fr] w-full items-center font-mono text-xs ${isSecondary ? 'mt-1' : ''}`}>
22
+ <span className="text-[var(--wui-color-text-secondary)] text-left">{from}</span>
23
+ <span className="flex items-center justify-center gap-1 px-1">
24
+ <span className="text-[var(--wui-color-text-tertiary)]">--[</span>
25
+ <span className={isDanger ? 'text-[var(--wui-color-danger-text)] font-bold' : 'text-[var(--wui-color-primary)] font-medium'}>
26
+ {label}
27
+ </span>
28
+ <span className="text-[var(--wui-color-text-tertiary)]">]--&gt;</span>
29
+ </span>
30
+ <span className={`text-right ${isDanger ? 'text-[var(--wui-color-danger-text)] font-bold' : 'text-[var(--wui-color-text-secondary)]'}`}>{to}</span>
31
+ </div>
32
+ )
33
+
34
+ const renderRemainderLine = (to: string) => (
35
+ <div className="grid grid-cols-[1fr_auto_1fr] w-full items-center font-mono text-xs mt-1">
36
+ <span />
37
+ <span className="flex items-center justify-center gap-1 px-1">
38
+ <span className="text-[var(--wui-color-text-tertiary)]">--[</span>
39
+ <span className="text-[var(--wui-color-danger-text)] font-bold">remainder</span>
40
+ <span className="text-[var(--wui-color-text-tertiary)]">]--&gt;</span>
41
+ </span>
42
+ <span className="text-[var(--wui-color-danger-text)] font-bold text-right">{to}</span>
43
+ </div>
44
+ )
45
+
46
+ const renderRekeyLine = (from: string, to: string) => (
47
+ <div className="grid grid-cols-[1fr_auto_1fr] w-full items-center font-mono text-xs mt-1">
48
+ <span className="text-[var(--wui-color-text-secondary)] text-left">{from}</span>
49
+ <span className="flex items-center justify-center gap-1 px-1">
50
+ <span className="text-[var(--wui-color-text-tertiary)]">--[</span>
51
+ <span className="text-[var(--wui-color-danger-text)] font-bold">REKEY</span>
52
+ <span className="text-[var(--wui-color-text-tertiary)]">]--&gt;</span>
53
+ </span>
54
+ <span className="text-[var(--wui-color-danger-text)] font-bold text-right">{to}</span>
55
+ </div>
56
+ )
57
+
58
+ return (
59
+ <div className="space-y-0">
60
+ {/* Payment transaction */}
61
+ {txn.type === 'pay' && txn.receiverShort && (
62
+ <>
63
+ {renderFlowLine(txn.senderShort, txn.amount || '0 ALGO', resolveAddr(txn.receiver, txn.receiverShort))}
64
+ {txn.closeRemainderToShort && renderRemainderLine(txn.closeRemainderToShort)}
65
+ </>
66
+ )}
67
+
68
+ {/* Asset transfer */}
69
+ {txn.type === 'axfer' && txn.receiverShort && (
70
+ <>
71
+ {renderFlowLine(
72
+ txn.senderShort,
73
+ assetInfo ? formatAssetAmount(txn.rawAmount, assetInfo) : `${txn.amount || '0'} ${assetLabel(txn)}`,
74
+ resolveAddr(txn.receiver, txn.receiverShort),
75
+ )}
76
+ {txn.closeRemainderToShort && renderRemainderLine(txn.closeRemainderToShort)}
77
+ </>
78
+ )}
79
+
80
+ {/* Asset freeze */}
81
+ {txn.type === 'afrz' && txn.freezeTargetShort && (
82
+ <>
83
+ {renderFlowLine(
84
+ txn.senderShort,
85
+ `${txn.isFreezing ? 'Freeze' : 'Unfreeze'} ${assetLabel(txn, assetInfo)}`,
86
+ txn.freezeTargetShort,
87
+ )}
88
+ </>
89
+ )}
90
+
91
+ {/* Asset config */}
92
+ {txn.type === 'acfg' && (
93
+ <>
94
+ {renderFlowLine(
95
+ txn.senderShort,
96
+ `Configure ${txn.assetIndex ? assetLabel(txn, assetInfo) : 'NEW'}`,
97
+ txn.senderShort,
98
+ )}
99
+ </>
100
+ )}
101
+
102
+ {/* Application call */}
103
+ {txn.type === 'appl' && (
104
+ <>
105
+ {renderFlowLine(
106
+ txn.senderShort,
107
+ 'APP CALL',
108
+ `App ${txn.appIndex || 'NEW'}`,
109
+ )}
110
+ </>
111
+ )}
112
+
113
+ {/* Key registration */}
114
+ {txn.type === 'keyreg' && (
115
+ <>
116
+ {renderFlowLine(txn.senderShort, 'KEY REG', txn.senderShort)}
117
+ </>
118
+ )}
119
+
120
+ {/* Rekey (applies to any transaction type) */}
121
+ {txn.rekeyToShort && renderRekeyLine(txn.senderShort, txn.rekeyToShort)}
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,159 @@
1
+ import type { ReactNode } from 'react'
2
+ import { TransactionFlow } from './TransactionFlow'
3
+ import { useTransactionData } from '../hooks/useTransactionData'
4
+ import type { TransactionData, TransactionDanger, AssetLookupClient } from '../types'
5
+
6
+ export interface TransactionReviewProps {
7
+ transactions: TransactionData[]
8
+ message: string
9
+ dangerous: TransactionDanger
10
+ algodClient?: AssetLookupClient
11
+ getApplicationAddress?: (appId: number) => { toString(): string }
12
+ onApprove: () => void
13
+ onReject: () => void
14
+ signing?: boolean
15
+ walletName?: string
16
+ origin?: string
17
+ headerAction?: ReactNode
18
+ payloadVerified?: boolean | null
19
+ }
20
+
21
+ export function TransactionReview({
22
+ transactions,
23
+ message,
24
+ dangerous,
25
+ algodClient,
26
+ getApplicationAddress,
27
+ onApprove,
28
+ onReject,
29
+ signing,
30
+ walletName,
31
+ origin,
32
+ headerAction,
33
+ payloadVerified,
34
+ }: TransactionReviewProps) {
35
+ const { loading, assets, appEscrows } = useTransactionData(transactions, {
36
+ algodClient,
37
+ getApplicationAddress,
38
+ })
39
+
40
+ return (
41
+ <div className="flex flex-col">
42
+ {/* Header */}
43
+ <div className="flex items-center justify-between px-6 pt-5 pb-1">
44
+ <h2 className={`text-lg font-bold ${dangerous ? 'text-[var(--wui-color-danger-text)]' : 'text-[var(--wui-color-text)]'}`}>
45
+ {dangerous ? 'Review Dangerous ' : 'Review '}
46
+ Transaction{transactions.length > 1 ? 's' : ''}
47
+ </h2>
48
+ {headerAction}
49
+ </div>
50
+
51
+ {/* Origin (extension shows request origin) */}
52
+ {origin && (
53
+ <div className="px-6 text-xs text-[var(--wui-color-text-tertiary)] truncate">
54
+ {origin}
55
+ </div>
56
+ )}
57
+
58
+ {/* Danger description */}
59
+ {dangerous ? (
60
+ <div className="px-6 pb-3 text-sm font-bold text-[var(--wui-color-danger-text)]">
61
+ {dangerous === 'rekey'
62
+ ? 'This transaction will rekey your account, transferring signing authority to a different address. You will no longer be able to sign transactions with your current key.'
63
+ : 'This transaction will close your account and transfer all remaining funds to another address. This action is irreversible.'}
64
+ </div>
65
+ ) : (
66
+ <div className="px-6 pb-3 text-sm text-[var(--wui-color-text-secondary)]">
67
+ {transactions.length === 1
68
+ ? 'You are about to sign the following transaction:'
69
+ : `You are about to sign ${transactions.length} transactions:`}
70
+ </div>
71
+ )}
72
+
73
+ {/* Transaction list */}
74
+ <div className="px-4 pb-4 max-h-80 overflow-y-auto">
75
+ {loading ? (
76
+ <div className="flex items-center justify-center py-6 text-sm text-[var(--wui-color-text-secondary)]">
77
+ <svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
78
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
79
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
80
+ </svg>
81
+ Loading asset info...
82
+ </div>
83
+ ) : (
84
+ <div className="space-y-2">
85
+ {transactions.map((txn) => (
86
+ <div
87
+ key={txn.index}
88
+ className="rounded-sm border p-3 border-[var(--wui-color-primary)] bg-[var(--wui-color-bg-secondary)]"
89
+ >
90
+ <TransactionFlow txn={txn} assetInfo={txn.assetIndex ? assets[txn.assetIndex.toString()] : undefined} appEscrows={appEscrows} />
91
+ </div>
92
+ ))}
93
+ </div>
94
+ )}
95
+ </div>
96
+
97
+ {/* Payload verification warning */}
98
+ {payloadVerified === false && (
99
+ <div className="px-4 pb-4">
100
+ <div className="text-sm font-bold text-[var(--wui-color-danger-text)] border border-[var(--wui-color-danger-text)] rounded-xl p-3 bg-[var(--wui-color-danger-bg)]">
101
+ Payload verification failed. The provided payload was invalid and has been recalculated from the raw transactions.
102
+ </div>
103
+ </div>
104
+ )}
105
+
106
+ {/* Sign payload */}
107
+ <div className="px-4 pb-4">
108
+ <div className="text-sm flex flex-col gap-2 border border-[var(--wui-color-border)] rounded-xl p-3">
109
+ <div className="flex items-center gap-2">
110
+ <span>Transaction ID to sign:</span>
111
+ {payloadVerified === true && (
112
+ <span className="text-xs text-green-600 font-medium">Verified</span>
113
+ )}
114
+ </div>
115
+ <div className="font-mono break-all text-[var(--wui-color-danger-text)]">{message}</div>
116
+ <div>Ensure the transaction ID is correct before approving.</div>
117
+ </div>
118
+ </div>
119
+
120
+ {/* Footer */}
121
+ {signing ? (
122
+ <div className="px-6 py-4 border-t border-[var(--wui-color-border)]">
123
+ <div className="flex items-center gap-2 text-sm text-[var(--wui-color-text-secondary)]">
124
+ <svg className="animate-spin h-4 w-4 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
125
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
126
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
127
+ </svg>
128
+ Signing...
129
+ </div>
130
+ </div>
131
+ ) : dangerous ? (
132
+ <div className="px-6 py-4 border-t border-[var(--wui-color-border)] flex gap-3">
133
+ <button
134
+ onClick={onReject}
135
+ className="flex-1 py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text-secondary)] font-medium rounded-xl hover:brightness-90 transition-all text-sm"
136
+ >
137
+ Reject
138
+ </button>
139
+ <button
140
+ onClick={onApprove}
141
+ className="flex-1 py-2.5 px-4 bg-[var(--wui-color-danger-text)] text-white font-medium rounded-xl hover:brightness-90 transition-all text-sm"
142
+ >
143
+ Sign (Dangerous)
144
+ </button>
145
+ </div>
146
+ ) : (
147
+ <div className="px-6 py-4 border-t border-[var(--wui-color-border)]">
148
+ <div className="flex items-center gap-2 text-sm text-[var(--wui-color-text-secondary)]">
149
+ <svg className="animate-spin h-4 w-4 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
150
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
151
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
152
+ </svg>
153
+ Review in {walletName || 'wallet'}...
154
+ </div>
155
+ </div>
156
+ )}
157
+ </div>
158
+ )
159
+ }
@@ -0,0 +1,61 @@
1
+ import { Spinner } from './Spinner'
2
+
3
+ export type TransactionStatusValue = 'idle' | 'signing' | 'sending' | 'success' | 'error'
4
+
5
+ interface TransactionStatusProps {
6
+ status: TransactionStatusValue
7
+ error: string | null
8
+ successMessage: string
9
+ onRetry: () => void
10
+ }
11
+
12
+ export function TransactionStatus({ status, error, successMessage, onRetry }: TransactionStatusProps) {
13
+ if (status === 'idle') return null
14
+
15
+ if (status === 'signing') {
16
+ return (
17
+ <div className="flex items-center justify-center py-4 text-sm text-[var(--wui-color-text-secondary)]">
18
+ <Spinner className="h-4 w-4 mr-2" />
19
+ Waiting for signature...
20
+ </div>
21
+ )
22
+ }
23
+
24
+ if (status === 'sending') {
25
+ return (
26
+ <div className="flex items-center justify-center py-4 text-sm text-[var(--wui-color-text-secondary)]">
27
+ <Spinner className="h-4 w-4 mr-2" />
28
+ Sending transaction...
29
+ </div>
30
+ )
31
+ }
32
+
33
+ if (status === 'success') {
34
+ return (
35
+ <div className="text-center py-4">
36
+ <svg
37
+ xmlns="http://www.w3.org/2000/svg"
38
+ className="h-8 w-8 mx-auto mb-2 text-green-500"
39
+ viewBox="0 0 20 20"
40
+ fill="currentColor"
41
+ >
42
+ <path
43
+ fillRule="evenodd"
44
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
45
+ clipRule="evenodd"
46
+ />
47
+ </svg>
48
+ <p className="text-sm font-medium text-[var(--wui-color-text)]">{successMessage}</p>
49
+ </div>
50
+ )
51
+ }
52
+
53
+ return (
54
+ <div className="text-center py-3">
55
+ <p className="text-sm text-[var(--wui-color-danger-text)] mb-2">{error}</p>
56
+ <button onClick={onRetry} className="text-sm text-[var(--wui-color-primary)] hover:underline">
57
+ Try again
58
+ </button>
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,171 @@
1
+ import { useCallback, useState } from 'react'
2
+
3
+ export interface WelcomeContentProps {
4
+ algorandAddress: string
5
+ evmAddress: string
6
+ onClose: () => void
7
+ labelId?: string
8
+ descriptionId?: string
9
+ }
10
+
11
+ function shortAddress(address: string) {
12
+ return `${address.slice(0, 6)}...${address.slice(-6)}`
13
+ }
14
+
15
+ export function WelcomeContent({
16
+ algorandAddress,
17
+ evmAddress,
18
+ onClose,
19
+ labelId,
20
+ descriptionId,
21
+ }: WelcomeContentProps) {
22
+ const [copiedField, setCopiedField] = useState<'evm' | 'algorand' | null>(null)
23
+
24
+ const handleCopy = useCallback((address: string, field: 'evm' | 'algorand') => {
25
+ navigator.clipboard.writeText(address)
26
+ setCopiedField(field)
27
+ setTimeout(() => setCopiedField(null), 1500)
28
+ }, [])
29
+
30
+ const shortAlgorandAddress = shortAddress(algorandAddress)
31
+ const shortEvmAddress = shortAddress(evmAddress)
32
+
33
+ return (
34
+ <>
35
+ {/* Header */}
36
+ <div className="relative flex items-center px-6 pt-5 pb-4">
37
+ <h2 id={labelId} className="text-xl font-bold text-[var(--wui-color-text)] wallet-custom-font">
38
+ Welcome to Algorand
39
+ </h2>
40
+ <button
41
+ onClick={onClose}
42
+ className="absolute right-4 top-5 w-9 h-9 flex items-center justify-center rounded-full bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text-secondary)] hover:brightness-90 transition-all"
43
+ aria-label="Close dialog"
44
+ >
45
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
46
+ <path
47
+ fillRule="evenodd"
48
+ d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
49
+ clipRule="evenodd"
50
+ />
51
+ </svg>
52
+ </button>
53
+ </div>
54
+
55
+ {/* Content */}
56
+ <div id={descriptionId} className="px-6 pb-4 space-y-4">
57
+ <p className="text-sm text-[var(--wui-color-text-secondary)]">
58
+ You can use Algorand with your very own Liquid EVM account, which wraps your EVM private key, preserving full
59
+ self-custodial control.{' '}
60
+ <a
61
+ className="text-[var(--wui-color-link)] hover:text-[var(--wui-color-link-hover)]"
62
+ rel="noopener noreferrer"
63
+ href="#"
64
+ onClick={() => {
65
+ alert('Soon')
66
+ return false
67
+ }}
68
+ >
69
+ Learn more.
70
+ </a>
71
+ </p>
72
+
73
+ <div className="!my-4 rounded-xl border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] p-3 space-y-2">
74
+ <div className="flex justify-between items-center text-xs">
75
+ <span className="text-[var(--wui-color-text-tertiary)]">Your EVM Address</span>
76
+ <span className="flex items-center gap-1.5">
77
+ <span className="text-[var(--wui-color-text-secondary)] font-mono">{shortEvmAddress}</span>
78
+ <button
79
+ onClick={() => handleCopy(evmAddress, 'evm')}
80
+ className="text-[var(--wui-color-text-tertiary)] hover:text-[var(--wui-color-text-secondary)] transition-colors"
81
+ aria-label="Copy EVM address"
82
+ >
83
+ {copiedField === 'evm' ? (
84
+ <svg
85
+ xmlns="http://www.w3.org/2000/svg"
86
+ className="h-3.5 w-3.5 text-green-500"
87
+ viewBox="0 0 20 20"
88
+ fill="currentColor"
89
+ >
90
+ <path
91
+ fillRule="evenodd"
92
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
93
+ clipRule="evenodd"
94
+ />
95
+ </svg>
96
+ ) : (
97
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
98
+ <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
99
+ <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
100
+ </svg>
101
+ )}
102
+ </button>
103
+ </span>
104
+ </div>
105
+ <div className="flex justify-between items-center text-xs">
106
+ <span className="text-[var(--wui-color-text-tertiary)]">Your Algorand Address</span>
107
+ <span className="flex items-center gap-1.5">
108
+ <span className="text-[var(--wui-color-text-secondary)] font-mono">{shortAlgorandAddress}</span>
109
+ <button
110
+ onClick={() => handleCopy(algorandAddress, 'algorand')}
111
+ className="text-[var(--wui-color-text-tertiary)] hover:text-[var(--wui-color-text-secondary)] transition-colors"
112
+ aria-label="Copy Algorand address"
113
+ >
114
+ {copiedField === 'algorand' ? (
115
+ <svg
116
+ xmlns="http://www.w3.org/2000/svg"
117
+ className="h-3.5 w-3.5 text-green-500"
118
+ viewBox="0 0 20 20"
119
+ fill="currentColor"
120
+ >
121
+ <path
122
+ fillRule="evenodd"
123
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
124
+ clipRule="evenodd"
125
+ />
126
+ </svg>
127
+ ) : (
128
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
129
+ <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
130
+ <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
131
+ </svg>
132
+ )}
133
+ </button>
134
+ </span>
135
+ </div>
136
+ </div>
137
+
138
+ <p className="text-sm text-[var(--wui-color-text-secondary)]">
139
+ To get started,{' '}
140
+ <a className="text-[var(--wui-color-link)] hover:text-[var(--wui-color-link-hover)]" rel="noopener noreferrer" target="_blank" href="https://algorand.co/ecosystem/directory?tags=CEX">
141
+ fund
142
+ </a>{' '}
143
+ your new Algorand account via CEX, Card onramp, or{' '}
144
+ <a className="text-[var(--wui-color-link)] hover:text-[var(--wui-color-link-hover)]" rel="noopener noreferrer" target="_blank" href="https://core.allbridge.io/?ft=USDC&tt=USDC&f=BAS&t=ALG">
145
+ bridge USDC
146
+ </a>{' '}
147
+ in 2 minutes.
148
+ </p>
149
+ </div>
150
+
151
+ {/* Action buttons */}
152
+ <div className="px-6 py-4 border-t border-[var(--wui-color-border)] flex flex gap-2">
153
+ <button
154
+ onClick={() => {
155
+ onClose()
156
+ window.open('https://algorand.co/algorand-start-here#hs_cos_wrapper_widget_1769533007886')
157
+ }}
158
+ className="w-full py-2.5 px-4 bg-[var(--wui-color-primary)] text-[var(--wui-color-primary-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm"
159
+ >
160
+ Get Started
161
+ </button>
162
+ <button
163
+ onClick={onClose}
164
+ className="w-full py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text-secondary)] font-medium rounded-xl hover:brightness-90 transition-all text-sm"
165
+ >
166
+ Close
167
+ </button>
168
+ </div>
169
+ </>
170
+ )
171
+ }
@@ -0,0 +1,19 @@
1
+ import type { TransactionData, AssetInfo } from './types'
2
+
3
+ export function formatAssetAmount(rawAmount: bigint | string | undefined, info: AssetInfo): string {
4
+ if (rawAmount === undefined) return `0 ${info.unitName}`
5
+ const amount = typeof rawAmount === 'bigint' ? rawAmount : BigInt(rawAmount)
6
+ if (info.decimals === 0) return `${amount} ${info.unitName}`
7
+ const divisor = 10n ** BigInt(info.decimals)
8
+ const whole = amount / divisor
9
+ const remainder = amount % divisor
10
+ if (remainder === 0n) return `${whole} ${info.unitName}`
11
+ const frac = remainder.toString().padStart(info.decimals, '0').replace(/0+$/, '')
12
+ return `${whole}.${frac} ${info.unitName}`
13
+ }
14
+
15
+ export function assetLabel(txn: TransactionData, info?: AssetInfo): string {
16
+ if (info?.unitName) return info.unitName
17
+ if (!txn.assetIndex) return 'ASA'
18
+ return `ASA#${txn.assetIndex}`
19
+ }
@@ -0,0 +1,43 @@
1
+ import { useQueries } from '@tanstack/react-query'
2
+ import { useMemo } from 'react'
3
+ import type { AssetInfo, AssetLookupClient } from '../types'
4
+
5
+ export function useAssets(
6
+ assetIds: string[],
7
+ algodClient: AssetLookupClient | undefined,
8
+ ): {
9
+ loading: boolean
10
+ assets: Record<string, AssetInfo>
11
+ } {
12
+ const results = useQueries({
13
+ queries: assetIds.map((id) => ({
14
+ queryKey: ['asset', id],
15
+ queryFn: async (): Promise<AssetInfo> => {
16
+ const result = await algodClient!.getAssetByID(Number(id)).do()
17
+ return {
18
+ decimals: result.params.decimals,
19
+ unitName: result.params.unitName || '',
20
+ }
21
+ },
22
+ retries: 0,
23
+ enabled: !!algodClient,
24
+ staleTime: Infinity,
25
+ gcTime: Infinity,
26
+ })),
27
+ })
28
+
29
+ const loading = results.some((r) => r.isLoading)
30
+
31
+ const assets = useMemo(() => {
32
+ const map: Record<string, AssetInfo> = {}
33
+ for (let i = 0; i < assetIds.length; i++) {
34
+ const data = results[i]?.data
35
+ if (data) {
36
+ map[assetIds[i]] = data
37
+ }
38
+ }
39
+ return map
40
+ }, [assetIds, results])
41
+
42
+ return { loading, assets }
43
+ }
@@ -0,0 +1,40 @@
1
+ import { useMemo } from 'react'
2
+ import { useAssets } from './useAssets'
3
+ import type { TransactionData, AssetInfo, AssetLookupClient } from '../types'
4
+
5
+ export function useTransactionData(
6
+ transactions: TransactionData[],
7
+ options?: {
8
+ algodClient?: AssetLookupClient
9
+ getApplicationAddress?: (appId: number) => { toString(): string }
10
+ },
11
+ ): { loading: boolean; assets: Record<string, AssetInfo>; appEscrows: Record<string, string> } {
12
+ const algodClient = options?.algodClient
13
+ const getApplicationAddress = options?.getApplicationAddress
14
+
15
+ const assetIds = useMemo(() => {
16
+ const ids = new Set<string>()
17
+ for (const txn of transactions) {
18
+ if (txn.assetIndex) {
19
+ ids.add(txn.assetIndex.toString())
20
+ }
21
+ }
22
+ return Array.from(ids)
23
+ }, [transactions])
24
+
25
+ const { loading, assets } = useAssets(assetIds, algodClient)
26
+
27
+ const appEscrows = useMemo(() => {
28
+ if (!getApplicationAddress) return {}
29
+ const escrows: Record<string, string> = {}
30
+ for (const txn of transactions) {
31
+ if (txn.type === 'appl' && txn.appIndex) {
32
+ const escrowAddr = getApplicationAddress(txn.appIndex)
33
+ escrows[escrowAddr.toString()] = `App ${txn.appIndex}`
34
+ }
35
+ }
36
+ return escrows
37
+ }, [transactions, getApplicationAddress])
38
+
39
+ return { loading, assets, appEscrows }
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ // Types
2
+ export type { TransactionData, TransactionDanger, AssetInfo, AssetLookupClient } from './types'
3
+
4
+ // Formatters
5
+ export { formatAssetAmount, assetLabel } from './formatters'
6
+
7
+ // Components
8
+ export { TransactionFlow } from './components/TransactionFlow'
9
+ export type { TransactionFlowProps } from './components/TransactionFlow'
10
+ export { TransactionReview } from './components/TransactionReview'
11
+ export type { TransactionReviewProps } from './components/TransactionReview'
12
+
13
+ // Shared UI
14
+ export { AlgoSymbol } from './components/AlgoSymbol'
15
+ export { Spinner } from './components/Spinner'
16
+ export { TransactionStatus } from './components/TransactionStatus'
17
+ export type { TransactionStatusValue } from './components/TransactionStatus'
18
+ export { OptInPanel } from './components/OptInPanel'
19
+ export type { OptInPanelProps } from './components/OptInPanel'
20
+ export { SendPanel } from './components/SendPanel'
21
+ export type { SendPanelProps } from './components/SendPanel'
22
+ export { ManagePanel } from './components/ManagePanel'
23
+ export type { ManagePanelProps } from './components/ManagePanel'
24
+ export { WelcomeContent } from './components/WelcomeContent'
25
+ export type { WelcomeContentProps } from './components/WelcomeContent'
26
+
27
+ // Hooks
28
+ export { useAssets } from './hooks/useAssets'
29
+ export { useTransactionData } from './hooks/useTransactionData'
package/src/types.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Unified transaction data type used by shared UI components.
3
+ *
4
+ * Both `DecodedTransaction` (bigint rawAmount) from the React package
5
+ * and `SerializableDecodedTransaction` (string rawAmount) from the extension
6
+ * are structurally compatible with this type.
7
+ */
8
+ export interface TransactionData {
9
+ index: number
10
+ type: string
11
+ typeLabel: string
12
+ sender: string
13
+ senderShort: string
14
+ receiver?: string
15
+ receiverShort?: string
16
+ amount?: string
17
+ rawAmount?: bigint | string
18
+ assetIndex?: number
19
+ appIndex?: number
20
+ rekeyTo?: string
21
+ rekeyToShort?: string
22
+ closeRemainderTo?: string
23
+ closeRemainderToShort?: string
24
+ freezeTarget?: string
25
+ freezeTargetShort?: string
26
+ isFreezing?: boolean
27
+ }
28
+
29
+ export type TransactionDanger = 'rekey' | 'closeTo' | false
30
+
31
+ export interface AssetInfo {
32
+ decimals: number
33
+ unitName: string
34
+ }
35
+
36
+ /**
37
+ * Minimal interface for algod client asset lookup.
38
+ * Avoids requiring algosdk as a dependency.
39
+ */
40
+ export interface AssetLookupClient {
41
+ getAssetByID(id: number): { do(): Promise<{ params: { decimals: number; unitName?: string } }> }
42
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+ "moduleResolution": "bundler",
8
+ "allowImportingTsExtensions": true,
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "noEmit": true,
12
+ "jsx": "react-jsx",
13
+ "strict": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noFallthroughCasesInSwitch": true
17
+ },
18
+ "include": ["src"]
19
+ }