@jaw.id/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.
Files changed (170) hide show
  1. package/.babelrc +12 -0
  2. package/README.md +55 -0
  3. package/components.json +22 -0
  4. package/dist/ccip-Cdkx99iZ.js +147 -0
  5. package/dist/components/ConnectDialog/index.d.ts +4 -0
  6. package/dist/components/ConnectDialog/index.d.ts.map +1 -0
  7. package/dist/components/ConnectDialog/types.d.ts +20 -0
  8. package/dist/components/ConnectDialog/types.d.ts.map +1 -0
  9. package/dist/components/DefaultDialog/index.d.ts +14 -0
  10. package/dist/components/DefaultDialog/index.d.ts.map +1 -0
  11. package/dist/components/Eip712Dialog/index.d.ts +4 -0
  12. package/dist/components/Eip712Dialog/index.d.ts.map +1 -0
  13. package/dist/components/Eip712Dialog/types.d.ts +19 -0
  14. package/dist/components/Eip712Dialog/types.d.ts.map +1 -0
  15. package/dist/components/FeeTokenSelector/index.d.ts +26 -0
  16. package/dist/components/FeeTokenSelector/index.d.ts.map +1 -0
  17. package/dist/components/OnboardingDialog/index.d.ts +4 -0
  18. package/dist/components/OnboardingDialog/index.d.ts.map +1 -0
  19. package/dist/components/OnboardingDialog/types.d.ts +36 -0
  20. package/dist/components/OnboardingDialog/types.d.ts.map +1 -0
  21. package/dist/components/OrSeparator/index.d.ts +2 -0
  22. package/dist/components/OrSeparator/index.d.ts.map +1 -0
  23. package/dist/components/PermissionDialog/index.d.ts +4 -0
  24. package/dist/components/PermissionDialog/index.d.ts.map +1 -0
  25. package/dist/components/PermissionDialog/types.d.ts +49 -0
  26. package/dist/components/PermissionDialog/types.d.ts.map +1 -0
  27. package/dist/components/SignatureDialog/index.d.ts +4 -0
  28. package/dist/components/SignatureDialog/index.d.ts.map +1 -0
  29. package/dist/components/SignatureDialog/types.d.ts +19 -0
  30. package/dist/components/SignatureDialog/types.d.ts.map +1 -0
  31. package/dist/components/SiweDialog/index.d.ts +4 -0
  32. package/dist/components/SiweDialog/index.d.ts.map +1 -0
  33. package/dist/components/SiweDialog/types.d.ts +22 -0
  34. package/dist/components/SiweDialog/types.d.ts.map +1 -0
  35. package/dist/components/TransactionDialog/DecodedCalldata.d.ts +9 -0
  36. package/dist/components/TransactionDialog/DecodedCalldata.d.ts.map +1 -0
  37. package/dist/components/TransactionDialog/index.d.ts +4 -0
  38. package/dist/components/TransactionDialog/index.d.ts.map +1 -0
  39. package/dist/components/TransactionDialog/types.d.ts +34 -0
  40. package/dist/components/TransactionDialog/types.d.ts.map +1 -0
  41. package/dist/components/ui/accordion.d.ts +8 -0
  42. package/dist/components/ui/accordion.d.ts.map +1 -0
  43. package/dist/components/ui/avatar.d.ts +7 -0
  44. package/dist/components/ui/avatar.d.ts.map +1 -0
  45. package/dist/components/ui/button.d.ts +11 -0
  46. package/dist/components/ui/button.d.ts.map +1 -0
  47. package/dist/components/ui/card.d.ts +10 -0
  48. package/dist/components/ui/card.d.ts.map +1 -0
  49. package/dist/components/ui/checkbox.d.ts +5 -0
  50. package/dist/components/ui/checkbox.d.ts.map +1 -0
  51. package/dist/components/ui/dialog.d.ts +18 -0
  52. package/dist/components/ui/dialog.d.ts.map +1 -0
  53. package/dist/components/ui/dropdown-menu.d.ts +26 -0
  54. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  55. package/dist/components/ui/form.d.ts +25 -0
  56. package/dist/components/ui/form.d.ts.map +1 -0
  57. package/dist/components/ui/input.d.ts +8 -0
  58. package/dist/components/ui/input.d.ts.map +1 -0
  59. package/dist/components/ui/label.d.ts +5 -0
  60. package/dist/components/ui/label.d.ts.map +1 -0
  61. package/dist/components/ui/popover.d.ts +8 -0
  62. package/dist/components/ui/popover.d.ts.map +1 -0
  63. package/dist/components/ui/scroll-area.d.ts +6 -0
  64. package/dist/components/ui/scroll-area.d.ts.map +1 -0
  65. package/dist/components/ui/select.d.ts +14 -0
  66. package/dist/components/ui/select.d.ts.map +1 -0
  67. package/dist/components/ui/separator.d.ts +5 -0
  68. package/dist/components/ui/separator.d.ts.map +1 -0
  69. package/dist/components/ui/sheet.d.ts +14 -0
  70. package/dist/components/ui/sheet.d.ts.map +1 -0
  71. package/dist/components/ui/spinner.d.ts +3 -0
  72. package/dist/components/ui/spinner.d.ts.map +1 -0
  73. package/dist/components/ui/tabs.d.ts +8 -0
  74. package/dist/components/ui/tabs.d.ts.map +1 -0
  75. package/dist/components/ui/tooltip.d.ts +8 -0
  76. package/dist/components/ui/tooltip.d.ts.map +1 -0
  77. package/dist/hooks/index.d.ts +6 -0
  78. package/dist/hooks/index.d.ts.map +1 -0
  79. package/dist/hooks/useChainIconURI.d.ts +12 -0
  80. package/dist/hooks/useChainIconURI.d.ts.map +1 -0
  81. package/dist/hooks/useDecodedCalldata.d.ts +15 -0
  82. package/dist/hooks/useDecodedCalldata.d.ts.map +1 -0
  83. package/dist/hooks/useFeeTokenPrice.d.ts +8 -0
  84. package/dist/hooks/useFeeTokenPrice.d.ts.map +1 -0
  85. package/dist/hooks/useGasEstimation.d.ts +77 -0
  86. package/dist/hooks/useGasEstimation.d.ts.map +1 -0
  87. package/dist/hooks/useIsMobile.d.ts +2 -0
  88. package/dist/hooks/useIsMobile.d.ts.map +1 -0
  89. package/dist/icons/index.d.ts +12 -0
  90. package/dist/icons/index.d.ts.map +1 -0
  91. package/dist/index-DlfjdYW4.js +83119 -0
  92. package/dist/index.css +1 -0
  93. package/dist/index.d.ts +16 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +38 -0
  96. package/dist/lib/utils.d.ts +3 -0
  97. package/dist/lib/utils.d.ts.map +1 -0
  98. package/dist/react/ReactUIHandler.d.ts +35 -0
  99. package/dist/react/ReactUIHandler.d.ts.map +1 -0
  100. package/dist/react/index.d.ts +2 -0
  101. package/dist/react/index.d.ts.map +1 -0
  102. package/dist/utils/formatAddress.d.ts +18 -0
  103. package/dist/utils/formatAddress.d.ts.map +1 -0
  104. package/dist/utils/index.d.ts +5 -0
  105. package/dist/utils/index.d.ts.map +1 -0
  106. package/dist/utils/justaNameInstance.d.ts +7 -0
  107. package/dist/utils/justaNameInstance.d.ts.map +1 -0
  108. package/dist/utils/tokenBalance.d.ts +10 -0
  109. package/dist/utils/tokenBalance.d.ts.map +1 -0
  110. package/dist/utils/tokenPrice.d.ts +12 -0
  111. package/dist/utils/tokenPrice.d.ts.map +1 -0
  112. package/package.json +67 -0
  113. package/postcss.config.cjs +6 -0
  114. package/src/components/ConnectDialog/index.tsx +270 -0
  115. package/src/components/ConnectDialog/types.ts +34 -0
  116. package/src/components/DefaultDialog/index.tsx +99 -0
  117. package/src/components/Eip712Dialog/index.tsx +525 -0
  118. package/src/components/Eip712Dialog/types.ts +27 -0
  119. package/src/components/FeeTokenSelector/index.tsx +308 -0
  120. package/src/components/OnboardingDialog/index.tsx +317 -0
  121. package/src/components/OnboardingDialog/types.ts +43 -0
  122. package/src/components/OrSeparator/index.tsx +13 -0
  123. package/src/components/PermissionDialog/index.tsx +598 -0
  124. package/src/components/PermissionDialog/types.ts +77 -0
  125. package/src/components/SignatureDialog/index.tsx +180 -0
  126. package/src/components/SignatureDialog/types.ts +27 -0
  127. package/src/components/SiweDialog/index.tsx +231 -0
  128. package/src/components/SiweDialog/types.ts +34 -0
  129. package/src/components/TransactionDialog/DecodedCalldata.tsx +79 -0
  130. package/src/components/TransactionDialog/index.tsx +663 -0
  131. package/src/components/TransactionDialog/types.ts +54 -0
  132. package/src/components/ui/accordion.tsx +66 -0
  133. package/src/components/ui/avatar.tsx +53 -0
  134. package/src/components/ui/button.tsx +59 -0
  135. package/src/components/ui/card.tsx +92 -0
  136. package/src/components/ui/checkbox.tsx +32 -0
  137. package/src/components/ui/dialog.tsx +183 -0
  138. package/src/components/ui/dropdown-menu.tsx +258 -0
  139. package/src/components/ui/form.tsx +167 -0
  140. package/src/components/ui/input.tsx +60 -0
  141. package/src/components/ui/label.tsx +24 -0
  142. package/src/components/ui/popover.tsx +48 -0
  143. package/src/components/ui/scroll-area.tsx +58 -0
  144. package/src/components/ui/select.tsx +160 -0
  145. package/src/components/ui/separator.tsx +28 -0
  146. package/src/components/ui/sheet.tsx +169 -0
  147. package/src/components/ui/spinner.tsx +18 -0
  148. package/src/components/ui/tabs.tsx +69 -0
  149. package/src/components/ui/tooltip.tsx +61 -0
  150. package/src/hooks/index.ts +5 -0
  151. package/src/hooks/useChainIconURI.tsx +117 -0
  152. package/src/hooks/useDecodedCalldata.ts +128 -0
  153. package/src/hooks/useFeeTokenPrice.tsx +36 -0
  154. package/src/hooks/useGasEstimation.ts +474 -0
  155. package/src/hooks/useIsMobile.ts +36 -0
  156. package/src/icons/index.tsx +114 -0
  157. package/src/index.ts +19 -0
  158. package/src/lib/utils.ts +6 -0
  159. package/src/react/ReactUIHandler.tsx +3004 -0
  160. package/src/react/index.ts +2 -0
  161. package/src/styles.css +210 -0
  162. package/src/utils/formatAddress.ts +24 -0
  163. package/src/utils/index.ts +4 -0
  164. package/src/utils/justaNameInstance.ts +25 -0
  165. package/src/utils/tokenBalance.ts +41 -0
  166. package/src/utils/tokenPrice.ts +58 -0
  167. package/tailwind.config.js +130 -0
  168. package/tsconfig.json +19 -0
  169. package/tsconfig.lib.json +43 -0
  170. package/vite.config.ts +45 -0
@@ -0,0 +1,308 @@
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
+ };
@@ -0,0 +1,317 @@
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';
@@ -0,0 +1,43 @@
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
+ }
@@ -0,0 +1,13 @@
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
+ }