@jaw.id/ui 0.0.3 → 0.0.5
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/cjs-error.cjs +3 -0
- package/dist/{ccip-BYa9WTP9.js → ccip-nTJDT8tV.js} +1 -1
- package/dist/{index-DBYNWeek.js → index-C2yN5Jh0.js} +20536 -20246
- package/dist/index.js +1 -1
- package/package.json +8 -2
- package/.babelrc +0 -12
- package/CHANGELOG.md +0 -23
- package/components.json +0 -22
- package/postcss.config.cjs +0 -6
- package/src/components/ConnectDialog/index.tsx +0 -270
- package/src/components/ConnectDialog/types.ts +0 -34
- package/src/components/DefaultDialog/index.tsx +0 -99
- package/src/components/Eip712Dialog/index.tsx +0 -525
- package/src/components/Eip712Dialog/types.ts +0 -27
- package/src/components/FeeTokenSelector/index.tsx +0 -308
- package/src/components/OnboardingDialog/index.tsx +0 -317
- package/src/components/OnboardingDialog/types.ts +0 -43
- package/src/components/OrSeparator/index.tsx +0 -13
- package/src/components/PermissionDialog/index.tsx +0 -598
- package/src/components/PermissionDialog/types.ts +0 -77
- package/src/components/SignatureDialog/index.tsx +0 -180
- package/src/components/SignatureDialog/types.ts +0 -27
- package/src/components/SiweDialog/index.tsx +0 -231
- package/src/components/SiweDialog/types.ts +0 -34
- package/src/components/TransactionDialog/DecodedCalldata.tsx +0 -79
- package/src/components/TransactionDialog/index.tsx +0 -663
- package/src/components/TransactionDialog/types.ts +0 -54
- package/src/components/ui/accordion.tsx +0 -66
- package/src/components/ui/avatar.tsx +0 -53
- package/src/components/ui/button.tsx +0 -59
- package/src/components/ui/card.tsx +0 -92
- package/src/components/ui/checkbox.tsx +0 -32
- package/src/components/ui/dialog.tsx +0 -183
- package/src/components/ui/dropdown-menu.tsx +0 -258
- package/src/components/ui/form.tsx +0 -167
- package/src/components/ui/input.tsx +0 -60
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/popover.tsx +0 -48
- package/src/components/ui/scroll-area.tsx +0 -58
- package/src/components/ui/select.tsx +0 -160
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/sheet.tsx +0 -169
- package/src/components/ui/spinner.tsx +0 -18
- package/src/components/ui/tabs.tsx +0 -69
- package/src/components/ui/tooltip.tsx +0 -61
- package/src/hooks/index.ts +0 -5
- package/src/hooks/useChainIconURI.tsx +0 -117
- package/src/hooks/useDecodedCalldata.ts +0 -128
- package/src/hooks/useFeeTokenPrice.tsx +0 -36
- package/src/hooks/useGasEstimation.ts +0 -474
- package/src/hooks/useIsMobile.ts +0 -36
- package/src/icons/index.tsx +0 -114
- package/src/index.ts +0 -19
- package/src/lib/utils.ts +0 -6
- package/src/react/ReactUIHandler.tsx +0 -3004
- package/src/react/index.ts +0 -2
- package/src/styles.css +0 -210
- package/src/utils/formatAddress.ts +0 -24
- package/src/utils/index.ts +0 -4
- package/src/utils/justaNameInstance.ts +0 -25
- package/src/utils/tokenBalance.ts +0 -41
- package/src/utils/tokenPrice.ts +0 -58
- package/tailwind.config.js +0 -130
- package/tsconfig.json +0 -19
- package/tsconfig.lib.json +0 -43
- package/vite.config.ts +0 -45
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
|
5
|
-
import { Button } from '../ui/button';
|
|
6
|
-
import { Spinner } from '../ui/spinner';
|
|
7
|
-
import { EthIcon, UsdcIcon, UsdtIcon, GenericTokenIcon } from '../../icons';
|
|
8
|
-
import { cn } from '../../lib/utils';
|
|
9
|
-
import { ChevronDown, X } from 'lucide-react';
|
|
10
|
-
|
|
11
|
-
export interface FeeTokenOption {
|
|
12
|
-
uid: string;
|
|
13
|
-
symbol: string;
|
|
14
|
-
address: string;
|
|
15
|
-
decimals: number;
|
|
16
|
-
balance: bigint;
|
|
17
|
-
balanceFormatted: string;
|
|
18
|
-
isNative: boolean;
|
|
19
|
-
isSelectable: boolean;
|
|
20
|
-
// Optional: USD values for display
|
|
21
|
-
balanceUsd?: string;
|
|
22
|
-
gasCostUsd?: string;
|
|
23
|
-
gasCostFormatted?: string;
|
|
24
|
-
// Optional: Token logo URI from API
|
|
25
|
-
logoURI?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface FeeTokenSelectorProps {
|
|
29
|
-
tokens: FeeTokenOption[];
|
|
30
|
-
selectedToken: FeeTokenOption | null;
|
|
31
|
-
onSelect: (token: FeeTokenOption) => void;
|
|
32
|
-
isLoading: boolean;
|
|
33
|
-
disabled?: boolean;
|
|
34
|
-
nativeTokenPrice?: number;
|
|
35
|
-
estimatedGasEth?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Get token icon - use logoURI if available, otherwise fall back to symbol-based icons
|
|
39
|
-
const getTokenIcon = (symbol: string, className?: string, logoURI?: string) => {
|
|
40
|
-
const iconClass = cn('size-8 shrink-0', className);
|
|
41
|
-
|
|
42
|
-
// Use logoURI if available
|
|
43
|
-
if (logoURI) {
|
|
44
|
-
return (
|
|
45
|
-
<img
|
|
46
|
-
src={logoURI}
|
|
47
|
-
alt={symbol}
|
|
48
|
-
className={cn(iconClass, 'rounded-full object-cover')}
|
|
49
|
-
onError={(e) => {
|
|
50
|
-
// Fallback to generic icon if image fails to load
|
|
51
|
-
e.currentTarget.style.display = 'none';
|
|
52
|
-
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
|
53
|
-
}}
|
|
54
|
-
/>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Fallback to symbol-based icons
|
|
59
|
-
switch (symbol.toUpperCase()) {
|
|
60
|
-
case 'ETH':
|
|
61
|
-
return <EthIcon className={iconClass} />;
|
|
62
|
-
case 'USDC':
|
|
63
|
-
return <UsdcIcon className={iconClass} />;
|
|
64
|
-
case 'USDT':
|
|
65
|
-
return <UsdtIcon className={iconClass} />;
|
|
66
|
-
default:
|
|
67
|
-
return <GenericTokenIcon className={iconClass} />;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Format balance for display (max 6 decimal places, min 4 for small values)
|
|
72
|
-
const formatBalance = (balance: string, symbol: string) => {
|
|
73
|
-
const num = parseFloat(balance);
|
|
74
|
-
if (num === 0) return '0';
|
|
75
|
-
if (num < 0.0001) return '<0.0001';
|
|
76
|
-
// For ETH, show more decimals; for stablecoins, show 2
|
|
77
|
-
const decimals = symbol.toUpperCase() === 'ETH' ? 6 : 2;
|
|
78
|
-
return num.toLocaleString('en-US', {
|
|
79
|
-
minimumFractionDigits: 0,
|
|
80
|
-
maximumFractionDigits: decimals,
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// Format USD value (show at least 4 decimals for small gas amounts)
|
|
85
|
-
const formatUsd = (value: number) => {
|
|
86
|
-
return `$${value.toLocaleString('en-US', {
|
|
87
|
-
minimumFractionDigits: 4,
|
|
88
|
-
maximumFractionDigits: 4,
|
|
89
|
-
})}`;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export const FeeTokenSelector = ({
|
|
93
|
-
tokens,
|
|
94
|
-
selectedToken,
|
|
95
|
-
onSelect,
|
|
96
|
-
isLoading,
|
|
97
|
-
disabled,
|
|
98
|
-
nativeTokenPrice = 0,
|
|
99
|
-
estimatedGasEth,
|
|
100
|
-
}: FeeTokenSelectorProps) => {
|
|
101
|
-
const [open, setOpen] = useState(false);
|
|
102
|
-
|
|
103
|
-
if (isLoading) {
|
|
104
|
-
return (
|
|
105
|
-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
106
|
-
<Spinner className="size-3" />
|
|
107
|
-
<span>Loading payment options...</span>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Don't render if no ERC-20 tokens available (only native)
|
|
113
|
-
const hasErc20Options = tokens.some(t => !t.isNative);
|
|
114
|
-
if (!hasErc20Options) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const nativeToken = tokens.find(t => t.isNative);
|
|
119
|
-
const erc20Tokens = tokens.filter(t => !t.isNative);
|
|
120
|
-
|
|
121
|
-
const handleSelect = (token: FeeTokenOption) => {
|
|
122
|
-
if (token.isSelectable) {
|
|
123
|
-
onSelect(token);
|
|
124
|
-
setOpen(false);
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
// Calculate USD values for tokens
|
|
129
|
-
const getBalanceUsd = (token: FeeTokenOption): string => {
|
|
130
|
-
if (token.isNative && nativeTokenPrice > 0) {
|
|
131
|
-
const usd = parseFloat(token.balanceFormatted) * nativeTokenPrice;
|
|
132
|
-
return formatUsd(usd);
|
|
133
|
-
}
|
|
134
|
-
// For non-native ERC-20 tokens, show balance with token decimals
|
|
135
|
-
if (!token.isNative) {
|
|
136
|
-
const balance = parseFloat(token.balanceFormatted);
|
|
137
|
-
// Show appropriate decimal places based on token decimals
|
|
138
|
-
const displayDecimals = token.decimals >= 6 ? 2 : token.decimals;
|
|
139
|
-
return `${balance.toLocaleString('en-US', {
|
|
140
|
-
minimumFractionDigits: 0,
|
|
141
|
-
maximumFractionDigits: displayDecimals,
|
|
142
|
-
})} ${token.symbol}`;
|
|
143
|
-
}
|
|
144
|
-
return '';
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// Get gas cost for a token
|
|
148
|
-
// For ERC-20 tokens, use pre-computed gasCostFormatted if available (from paymaster quote)
|
|
149
|
-
const getGasCost = (token: FeeTokenOption): { formatted: string; usd: string } | null => {
|
|
150
|
-
// Tokens with 0 balance - no gas cost to show
|
|
151
|
-
if (token.balance === 0n) {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// If token has pre-computed gas cost (from paymaster quote), use that
|
|
156
|
-
if (token.gasCostFormatted) {
|
|
157
|
-
// Check if it's a valid numeric value (not "Insufficient" or "Estimation failed")
|
|
158
|
-
const numericPart = token.gasCostFormatted.replace(/[^0-9.]/g, '');
|
|
159
|
-
const tokenCost = parseFloat(numericPart);
|
|
160
|
-
|
|
161
|
-
// If not a valid number, don't show gas cost
|
|
162
|
-
if (isNaN(tokenCost) || numericPart === '') {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
formatted: token.gasCostFormatted,
|
|
168
|
-
usd: ['USDC', 'USDT', 'DAI'].includes(token.symbol.toUpperCase()) && !isNaN(tokenCost)
|
|
169
|
-
? formatUsd(tokenCost)
|
|
170
|
-
: '',
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Fallback: calculate from ETH gas estimate
|
|
175
|
-
if (!estimatedGasEth || !nativeTokenPrice) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const gasEth = parseFloat(estimatedGasEth);
|
|
180
|
-
if (isNaN(gasEth)) {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const gasUsd = gasEth * nativeTokenPrice;
|
|
185
|
-
|
|
186
|
-
if (token.isNative) {
|
|
187
|
-
return {
|
|
188
|
-
formatted: `${formatBalance(estimatedGasEth, token.symbol)} ${token.symbol}`,
|
|
189
|
-
usd: formatUsd(gasUsd),
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// For non-native ERC-20 tokens, gas cost in token
|
|
194
|
-
// Show appropriate decimal places based on token decimals
|
|
195
|
-
const displayDecimals = token.decimals >= 6 ? 3 : token.decimals;
|
|
196
|
-
return {
|
|
197
|
-
formatted: `${gasUsd.toFixed(displayDecimals)} ${token.symbol}`,
|
|
198
|
-
usd: formatUsd(gasUsd),
|
|
199
|
-
};
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// Token row component
|
|
203
|
-
const TokenRow = ({ token, showGasCost = true }: { token: FeeTokenOption; showGasCost?: boolean }) => {
|
|
204
|
-
const balanceUsd = getBalanceUsd(token);
|
|
205
|
-
const gasCost = showGasCost ? getGasCost(token) : null;
|
|
206
|
-
const isSelected = selectedToken?.address === token.address;
|
|
207
|
-
|
|
208
|
-
return (
|
|
209
|
-
<button
|
|
210
|
-
onClick={() => handleSelect(token)}
|
|
211
|
-
disabled={!token.isSelectable || disabled}
|
|
212
|
-
className={cn(
|
|
213
|
-
'w-full flex items-center gap-2 px-2 py-2 rounded-md transition-colors',
|
|
214
|
-
'hover:bg-muted/60',
|
|
215
|
-
isSelected && 'bg-zinc-200',
|
|
216
|
-
!token.isSelectable && 'opacity-50 cursor-not-allowed',
|
|
217
|
-
token.isSelectable && 'cursor-pointer'
|
|
218
|
-
)}
|
|
219
|
-
>
|
|
220
|
-
{/* Token Icon */}
|
|
221
|
-
{getTokenIcon(token.symbol, 'size-6', token.logoURI)}
|
|
222
|
-
|
|
223
|
-
{/* Token Info */}
|
|
224
|
-
<div className="flex-1 text-left min-w-0">
|
|
225
|
-
<div className="flex items-center gap-1">
|
|
226
|
-
<span className={cn("font-semibold text-xs", isSelected && "text-foreground")}>{token.symbol}</span>
|
|
227
|
-
</div>
|
|
228
|
-
<div className="text-[10px] text-muted-foreground truncate">
|
|
229
|
-
Bal: {balanceUsd || `${formatBalance(token.balanceFormatted, token.symbol)} ${token.symbol}`}
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
{/* Gas Cost / Status */}
|
|
234
|
-
<div className="text-right shrink-0">
|
|
235
|
-
{gasCost?.usd ? (
|
|
236
|
-
<>
|
|
237
|
-
<div className="font-semibold text-xs">{gasCost.usd}</div>
|
|
238
|
-
<div className="text-[10px] text-muted-foreground">Up to {gasCost.formatted}</div>
|
|
239
|
-
</>
|
|
240
|
-
) : !token.isSelectable ? (
|
|
241
|
-
<span className="text-[10px] text-destructive">
|
|
242
|
-
{token.balance === 0n ? '0' : 'Insufficient'}
|
|
243
|
-
</span>
|
|
244
|
-
) : null}
|
|
245
|
-
</div>
|
|
246
|
-
</button>
|
|
247
|
-
);
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
return (
|
|
251
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
252
|
-
<PopoverTrigger asChild>
|
|
253
|
-
<Button
|
|
254
|
-
variant="outline"
|
|
255
|
-
role="combobox"
|
|
256
|
-
aria-expanded={open}
|
|
257
|
-
disabled={disabled}
|
|
258
|
-
className="h-7 px-2 gap-1 text-xs font-medium rounded-md border-muted-foreground/30"
|
|
259
|
-
>
|
|
260
|
-
{selectedToken && getTokenIcon(selectedToken.symbol, 'size-3.5', selectedToken.logoURI)}
|
|
261
|
-
<span>{selectedToken?.symbol || 'Select'}</span>
|
|
262
|
-
<ChevronDown className="size-3 opacity-50" />
|
|
263
|
-
</Button>
|
|
264
|
-
</PopoverTrigger>
|
|
265
|
-
<PopoverContent
|
|
266
|
-
className="w-64 p-0"
|
|
267
|
-
align="end"
|
|
268
|
-
sideOffset={4}
|
|
269
|
-
>
|
|
270
|
-
{/* Header */}
|
|
271
|
-
<div className="flex items-center justify-between px-3 py-2.5 border-b">
|
|
272
|
-
<h3 className="font-semibold text-sm">Select a token</h3>
|
|
273
|
-
<button
|
|
274
|
-
onClick={() => setOpen(false)}
|
|
275
|
-
className="rounded-full p-0.5 hover:bg-muted transition-colors"
|
|
276
|
-
>
|
|
277
|
-
<X className="size-4" />
|
|
278
|
-
</button>
|
|
279
|
-
</div>
|
|
280
|
-
|
|
281
|
-
{/* Token List */}
|
|
282
|
-
<div className="p-1.5 max-h-64 overflow-y-auto">
|
|
283
|
-
{/* Native Token Section */}
|
|
284
|
-
{nativeToken && (
|
|
285
|
-
<div className="mb-1">
|
|
286
|
-
<p className="text-[10px] font-medium text-muted-foreground px-2 py-1">
|
|
287
|
-
Pay with {nativeToken.symbol}
|
|
288
|
-
</p>
|
|
289
|
-
<TokenRow token={nativeToken} />
|
|
290
|
-
</div>
|
|
291
|
-
)}
|
|
292
|
-
|
|
293
|
-
{/* ERC-20 Tokens Section */}
|
|
294
|
-
{erc20Tokens.length > 0 && (
|
|
295
|
-
<div>
|
|
296
|
-
<p className="text-[10px] font-medium text-muted-foreground px-2 py-1">
|
|
297
|
-
Pay with other tokens
|
|
298
|
-
</p>
|
|
299
|
-
{erc20Tokens.map((token) => (
|
|
300
|
-
<TokenRow key={token.address} token={token} showGasCost={true} />
|
|
301
|
-
))}
|
|
302
|
-
</div>
|
|
303
|
-
)}
|
|
304
|
-
</div>
|
|
305
|
-
</PopoverContent>
|
|
306
|
-
</Popover>
|
|
307
|
-
);
|
|
308
|
-
};
|
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Button } from '../ui/button';
|
|
4
|
-
import { Input } from '../ui/input';
|
|
5
|
-
import { Spinner } from '../ui/spinner';
|
|
6
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
|
7
|
-
import { WalletIcon } from '../../icons';
|
|
8
|
-
import { ChevronRight } from 'lucide-react';
|
|
9
|
-
import { OrSeparator } from '../OrSeparator';
|
|
10
|
-
import { OnboardingDialogProps } from './types';
|
|
11
|
-
import { useState, useEffect, useRef } from 'react';
|
|
12
|
-
import { getJustaNameInstance } from '../../utils/justaNameInstance';
|
|
13
|
-
import {toCoinType} from "viem";
|
|
14
|
-
|
|
15
|
-
export function OnboardingDialog({
|
|
16
|
-
accounts,
|
|
17
|
-
onAccountSelect,
|
|
18
|
-
loggingInAccount,
|
|
19
|
-
onImportAccount,
|
|
20
|
-
isImporting,
|
|
21
|
-
onCreateAccount,
|
|
22
|
-
onAccountCreationComplete,
|
|
23
|
-
isCreating,
|
|
24
|
-
ensDomain,
|
|
25
|
-
chainId,
|
|
26
|
-
mainnetRpcUrl,
|
|
27
|
-
apiKey,
|
|
28
|
-
supportedChains,
|
|
29
|
-
subnameTextRecords,
|
|
30
|
-
}: OnboardingDialogProps) {
|
|
31
|
-
// Ref for scrollable container
|
|
32
|
-
const scrollableRef = useRef<HTMLDivElement>(null);
|
|
33
|
-
|
|
34
|
-
// Validation state
|
|
35
|
-
const [isValid, setIsValid] = useState(false);
|
|
36
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
37
|
-
const [message, setMessage] = useState('');
|
|
38
|
-
const [username, setUsername] = useState('')
|
|
39
|
-
const [debouncedUsername, setDebouncedUsername] = useState(username);
|
|
40
|
-
|
|
41
|
-
// Error state
|
|
42
|
-
const [error, setError] = useState<string | null>(null);
|
|
43
|
-
|
|
44
|
-
// Debounce username input
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
const handler = setTimeout(() => {
|
|
47
|
-
setDebouncedUsername(username);
|
|
48
|
-
}, 500);
|
|
49
|
-
|
|
50
|
-
return () => {
|
|
51
|
-
clearTimeout(handler);
|
|
52
|
-
};
|
|
53
|
-
}, [username]);
|
|
54
|
-
|
|
55
|
-
// Validate username and check availability
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
const validateUsername = async () => {
|
|
58
|
-
// Reset state
|
|
59
|
-
setIsLoading(false);
|
|
60
|
-
setIsValid(false);
|
|
61
|
-
setMessage('');
|
|
62
|
-
|
|
63
|
-
// Check if username includes dots
|
|
64
|
-
if (username.includes('.')) {
|
|
65
|
-
setMessage('Invalid format');
|
|
66
|
-
setIsValid(false);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Check minimum length
|
|
71
|
-
if (username.length > 0 && username.length <= 2) {
|
|
72
|
-
setMessage('Minimum 3 characters');
|
|
73
|
-
setIsValid(false);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// If username is empty, don't show anything
|
|
78
|
-
if (username.length === 0) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// If no ensDomain, just validate format and length
|
|
83
|
-
if (!ensDomain) {
|
|
84
|
-
setMessage('Available');
|
|
85
|
-
setIsValid(true);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Check availability with SDK
|
|
90
|
-
if (debouncedUsername.length > 2 && chainId) {
|
|
91
|
-
setIsLoading(true);
|
|
92
|
-
setMessage('Checking availability...');
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const justaName = getJustaNameInstance(mainnetRpcUrl);
|
|
96
|
-
const result = await justaName.subnames.isSubnameAvailable({
|
|
97
|
-
subname: debouncedUsername + '.' + ensDomain,
|
|
98
|
-
chainId: 1, // ENS offchain subnames must always be issued on Ethereum mainnet (chainId 1)
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
if (result?.isAvailable) {
|
|
102
|
-
setMessage('Available');
|
|
103
|
-
setIsValid(true);
|
|
104
|
-
} else {
|
|
105
|
-
setMessage('Unavailable');
|
|
106
|
-
setIsValid(false);
|
|
107
|
-
}
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.error('Error checking subname availability:', error);
|
|
110
|
-
setMessage('Error checking availability');
|
|
111
|
-
setIsValid(false);
|
|
112
|
-
} finally {
|
|
113
|
-
setIsLoading(false);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
validateUsername();
|
|
119
|
-
}, [debouncedUsername, username, ensDomain, chainId]);
|
|
120
|
-
|
|
121
|
-
// Handle wheel events for smooth scrolling over buttons
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
const scrollable = scrollableRef.current;
|
|
124
|
-
if (!scrollable) return;
|
|
125
|
-
|
|
126
|
-
const handleWheel = (e: WheelEvent) => {
|
|
127
|
-
// Prevent default to handle scroll manually
|
|
128
|
-
e.preventDefault();
|
|
129
|
-
// Smooth scroll
|
|
130
|
-
scrollable.scrollTop += e.deltaY;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// Add event listener with passive: false to allow preventDefault
|
|
134
|
-
scrollable.addEventListener('wheel', handleWheel, { passive: false });
|
|
135
|
-
|
|
136
|
-
return () => {
|
|
137
|
-
scrollable.removeEventListener('wheel', handleWheel);
|
|
138
|
-
};
|
|
139
|
-
}, []);
|
|
140
|
-
|
|
141
|
-
const handleCreateAccountClick = async () => {
|
|
142
|
-
// Clear any previous errors
|
|
143
|
-
setError(null);
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
// onCreateAccount now returns full account data (not just address)
|
|
147
|
-
const accountData = await onCreateAccount(username);
|
|
148
|
-
|
|
149
|
-
if (ensDomain && chainId && apiKey && supportedChains && accountData.address) {
|
|
150
|
-
try {
|
|
151
|
-
const justaName = getJustaNameInstance(mainnetRpcUrl);
|
|
152
|
-
|
|
153
|
-
const addresses = supportedChains.map(chain => ({
|
|
154
|
-
address: accountData.address,
|
|
155
|
-
coinType: toCoinType(chain.id).toString(),
|
|
156
|
-
}));
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
console.log('subnameTextRecords', subnameTextRecords);
|
|
160
|
-
|
|
161
|
-
// Use subnameTextRecords from capabilities if provided (only used during new account creation)
|
|
162
|
-
// If not provided or empty, use empty array (no text records will be set)
|
|
163
|
-
await justaName.subnames.addSubname(
|
|
164
|
-
{
|
|
165
|
-
username: username,
|
|
166
|
-
ensDomain: ensDomain,
|
|
167
|
-
chainId: 1, // ENS offchain subnames must always be issued on Ethereum mainnet (chainId 1)
|
|
168
|
-
addresses: addresses,
|
|
169
|
-
overrideSignatureCheck: true,
|
|
170
|
-
text: subnameTextRecords && subnameTextRecords.length > 0 ? subnameTextRecords : [],
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
xApiKey: apiKey,
|
|
174
|
-
xAddress: accountData.address,
|
|
175
|
-
xMessage: "",
|
|
176
|
-
}
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
} catch (subnameError) {
|
|
180
|
-
const errorMessage = `Failed to register subname: ${subnameError instanceof Error ? subnameError.message : 'Unknown error'}`;
|
|
181
|
-
console.error('❌ SUBNAME ERROR:', errorMessage, subnameError);
|
|
182
|
-
setError(errorMessage);
|
|
183
|
-
return; // Don't complete if subname registration fails
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Pass account data through to completion handler
|
|
188
|
-
await onAccountCreationComplete(accountData);
|
|
189
|
-
} catch (error) {
|
|
190
|
-
const errorMessage = `Account creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
191
|
-
console.error('❌ ACCOUNT CREATION ERROR:', errorMessage, error);
|
|
192
|
-
setError(errorMessage);
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
return (
|
|
197
|
-
<Card className="w-full max-w-md shadow-xl">
|
|
198
|
-
<CardHeader className="flex flex-col gap-1">
|
|
199
|
-
<CardTitle className="text-xl font-normal">
|
|
200
|
-
Sign In
|
|
201
|
-
</CardTitle>
|
|
202
|
-
|
|
203
|
-
<CardDescription className='text-xs font-medium'>
|
|
204
|
-
{`Choose one of your existing accounts below to sign in instantly.
|
|
205
|
-
If you're on a new device or don't see your account listed, you can
|
|
206
|
-
import it from your cloud backup.`}
|
|
207
|
-
</CardDescription>
|
|
208
|
-
</CardHeader>
|
|
209
|
-
|
|
210
|
-
<CardContent className="flex flex-col gap-5">
|
|
211
|
-
{/* Existing Accounts */}
|
|
212
|
-
<div ref={scrollableRef} className="flex flex-col gap-1 max-h-[40vh] overflow-y-auto overscroll-contain">
|
|
213
|
-
{accounts.map((account) => (
|
|
214
|
-
<Button
|
|
215
|
-
key={account.credentialId || account.username || Math.random().toString()}
|
|
216
|
-
onClick={() => onAccountSelect(account)}
|
|
217
|
-
variant="ghost"
|
|
218
|
-
className="w-full h-auto !py-2 !px-3 flex items-center justify-between hover:bg-muted/50"
|
|
219
|
-
disabled={loggingInAccount !== null}
|
|
220
|
-
>
|
|
221
|
-
<div className="flex items-center flex-row gap-2">
|
|
222
|
-
<WalletIcon className='!w-6 !h-6' />
|
|
223
|
-
<div className="text-left">
|
|
224
|
-
<p className="text-sm font-normal text-foreground">
|
|
225
|
-
{account.username || 'Unnamed Account'}
|
|
226
|
-
</p>
|
|
227
|
-
<p className="text-xs font-semibold text-muted-foreground">
|
|
228
|
-
{new Date(account.creationDate).toLocaleDateString('en-US', {
|
|
229
|
-
year: 'numeric',
|
|
230
|
-
month: 'short',
|
|
231
|
-
day: 'numeric'
|
|
232
|
-
})}
|
|
233
|
-
</p>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
{loggingInAccount === account.username ? (
|
|
237
|
-
<Spinner className="!h-5 !w-5" />
|
|
238
|
-
) : (
|
|
239
|
-
<ChevronRight className="!h-5 !w-5 text-black" />
|
|
240
|
-
)}
|
|
241
|
-
</Button>
|
|
242
|
-
))}
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
{/* Import Account Button */}
|
|
246
|
-
<Button
|
|
247
|
-
onClick={onImportAccount}
|
|
248
|
-
variant="outline"
|
|
249
|
-
className="w-full h-10 flex items-center flex-row gap-2"
|
|
250
|
-
disabled={isImporting}
|
|
251
|
-
>
|
|
252
|
-
<WalletIcon className='!w-6 !h-6' stroke='black' />
|
|
253
|
-
<span>{isImporting ? 'Opening Passkey...' : 'Import an existing account'}</span>
|
|
254
|
-
</Button>
|
|
255
|
-
|
|
256
|
-
<OrSeparator />
|
|
257
|
-
|
|
258
|
-
{/* Create New Account */}
|
|
259
|
-
<div className='flex flex-col gap-2'>
|
|
260
|
-
<div className="flex flex-row items-center gap-2">
|
|
261
|
-
<Input
|
|
262
|
-
placeholder="Username"
|
|
263
|
-
value={username}
|
|
264
|
-
onChange={(e) => setUsername(e.target.value)}
|
|
265
|
-
className="flex-1"
|
|
266
|
-
right={ensDomain ? <span className="text-sm font-bold text-foreground">{`.${ensDomain}`}</span> : undefined}
|
|
267
|
-
/>
|
|
268
|
-
{isCreating ? (
|
|
269
|
-
<Spinner className="w-10 h-10" />
|
|
270
|
-
) : (
|
|
271
|
-
<Button
|
|
272
|
-
onClick={async () => {
|
|
273
|
-
try {
|
|
274
|
-
await handleCreateAccountClick();
|
|
275
|
-
} catch (err) {
|
|
276
|
-
console.error('❌ Button onClick caught error:', err);
|
|
277
|
-
}
|
|
278
|
-
}}
|
|
279
|
-
disabled={!isValid || isLoading}
|
|
280
|
-
>
|
|
281
|
-
Create Account
|
|
282
|
-
</Button>
|
|
283
|
-
)}
|
|
284
|
-
</div>
|
|
285
|
-
{username.length > 0 && message && !error && (
|
|
286
|
-
<div className="flex items-center justify-between px-1">
|
|
287
|
-
<span className={`text-xs font-medium ${isLoading
|
|
288
|
-
? 'text-muted-foreground'
|
|
289
|
-
: isValid
|
|
290
|
-
? 'text-green-600'
|
|
291
|
-
: 'text-red-600'
|
|
292
|
-
}`}>
|
|
293
|
-
{message}
|
|
294
|
-
</span>
|
|
295
|
-
</div>
|
|
296
|
-
)}
|
|
297
|
-
{error && (
|
|
298
|
-
<div className="flex flex-col gap-2 px-1 py-2 bg-red-50 border border-red-200 rounded-md overflow-hidden">
|
|
299
|
-
<span className="text-xs font-medium text-red-600 break-all">
|
|
300
|
-
{error}
|
|
301
|
-
</span>
|
|
302
|
-
<Button
|
|
303
|
-
onClick={() => setError(null)}
|
|
304
|
-
variant="ghost"
|
|
305
|
-
className="h-6 text-xs text-red-600 hover:text-red-700 hover:bg-red-100"
|
|
306
|
-
>
|
|
307
|
-
Dismiss
|
|
308
|
-
</Button>
|
|
309
|
-
</div>
|
|
310
|
-
)}
|
|
311
|
-
</div>
|
|
312
|
-
</CardContent>
|
|
313
|
-
</Card>
|
|
314
|
-
)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export * from './types';
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { SubnameTextRecordCapabilityRequest } from "@jaw.id/core";
|
|
2
|
-
|
|
3
|
-
export interface LocalStorageAccount {
|
|
4
|
-
username: string;
|
|
5
|
-
creationDate: Date;
|
|
6
|
-
credentialId?: string;
|
|
7
|
-
isImported?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Data returned from account creation, passed through to completion handler.
|
|
12
|
-
* This allows data to flow naturally through callbacks without intermediate state.
|
|
13
|
-
*/
|
|
14
|
-
export interface CreatedAccountData {
|
|
15
|
-
address: string;
|
|
16
|
-
credentialId: string;
|
|
17
|
-
username: string;
|
|
18
|
-
publicKey: `0x${string}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface OnboardingDialogProps {
|
|
22
|
-
// Account list section
|
|
23
|
-
accounts: LocalStorageAccount[];
|
|
24
|
-
onAccountSelect: (account: LocalStorageAccount) => Promise<void>;
|
|
25
|
-
loggingInAccount: string | null;
|
|
26
|
-
|
|
27
|
-
// Import existing account section
|
|
28
|
-
onImportAccount: () => void;
|
|
29
|
-
isImporting: boolean;
|
|
30
|
-
|
|
31
|
-
// Create new account section
|
|
32
|
-
onCreateAccount: (username: string) => Promise<CreatedAccountData>;
|
|
33
|
-
onAccountCreationComplete: (account: CreatedAccountData) => Promise<void>;
|
|
34
|
-
isCreating: boolean;
|
|
35
|
-
|
|
36
|
-
// Configuration
|
|
37
|
-
ensDomain?: string;
|
|
38
|
-
chainId?: number;
|
|
39
|
-
mainnetRpcUrl: string;
|
|
40
|
-
apiKey?: string; // API key for JustaName API authentication (xApiKey header)
|
|
41
|
-
supportedChains?: Array<{ id: number }>;
|
|
42
|
-
subnameTextRecords?: SubnameTextRecordCapabilityRequest;
|
|
43
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Separator } from "../ui/separator";
|
|
2
|
-
|
|
3
|
-
export const OrSeparator = () => {
|
|
4
|
-
return (
|
|
5
|
-
<div className="flex flex-row gap-2 items-center max-w-full">
|
|
6
|
-
<Separator className='w-full shrink' />
|
|
7
|
-
<span className="bg-background text-sm text-muted-foreground">
|
|
8
|
-
Or
|
|
9
|
-
</span>
|
|
10
|
-
<Separator className='w-full shrink' />
|
|
11
|
-
</div>
|
|
12
|
-
)
|
|
13
|
-
}
|