create-openfort 0.1.10 → 1.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 (65) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +24 -23
  3. package/package.json +1 -1
  4. package/template/openfort-templates/firebase/AGENTS.md +1 -1
  5. package/template/openfort-templates/firebase/package.json +3 -3
  6. package/template/openfort-templates/firebase/src/App.tsx +2 -1
  7. package/template/openfort-templates/firebase/src/integrations/openfort/providers.tsx +36 -35
  8. package/template/openfort-templates/firebase/src/lib/contracts.ts +34 -0
  9. package/template/openfort-templates/firebase/src/ui/openfort/blockchain/ActionsCard.tsx +25 -13
  10. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletCreation.tsx +108 -63
  11. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletListCard.tsx +211 -41
  12. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletPasswordSheets.tsx +45 -21
  13. package/template/openfort-templates/headless/AGENTS.md +1 -1
  14. package/template/openfort-templates/headless/package.json +2 -2
  15. package/template/openfort-templates/headless/src/components/cards/actions.tsx +30 -21
  16. package/template/openfort-templates/headless/src/components/cards/profile.tsx +0 -2
  17. package/template/openfort-templates/headless/src/components/cards/wallets.tsx +230 -67
  18. package/template/openfort-templates/headless/src/components/createWallet.tsx +115 -73
  19. package/template/openfort-templates/headless/src/components/passwordRecovery.tsx +48 -23
  20. package/template/openfort-templates/headless/src/components/providers.tsx +30 -25
  21. package/template/openfort-templates/headless/src/lib/contracts.ts +43 -0
  22. package/template/openfort-templates/openfort-ui/AGENTS.md +1 -1
  23. package/template/openfort-templates/openfort-ui/package.json +3 -3
  24. package/template/openfort-templates/openfort-ui/src/components/cards/actions.tsx +30 -15
  25. package/template/openfort-templates/openfort-ui/src/components/cards/auth.tsx +1 -1
  26. package/template/openfort-templates/openfort-ui/src/components/cards/wallets.tsx +232 -67
  27. package/template/openfort-templates/openfort-ui/src/components/createWallet.tsx +115 -73
  28. package/template/openfort-templates/openfort-ui/src/components/passwordRecovery.tsx +48 -23
  29. package/template/openfort-templates/openfort-ui/src/components/providers.tsx +34 -30
  30. package/template/openfort-templates/openfort-ui/src/lib/contracts.ts +35 -0
  31. package/template/openfort-templates/solana-headless/biome.json +70 -0
  32. package/template/openfort-templates/solana-headless/index.html +16 -0
  33. package/template/openfort-templates/solana-headless/package.json +34 -0
  34. package/template/openfort-templates/solana-headless/public/githubLogo.svg +5 -0
  35. package/template/openfort-templates/solana-headless/public/openfort.svg +13 -0
  36. package/template/openfort-templates/solana-headless/public/solanaLogo.svg +11 -0
  37. package/template/openfort-templates/solana-headless/src/App.tsx +7 -0
  38. package/template/openfort-templates/solana-headless/src/components/cards/auth.tsx +167 -0
  39. package/template/openfort-templates/solana-headless/src/components/cards/head.tsx +359 -0
  40. package/template/openfort-templates/solana-headless/src/components/cards/history.tsx +134 -0
  41. package/template/openfort-templates/solana-headless/src/components/cards/main.tsx +140 -0
  42. package/template/openfort-templates/solana-headless/src/components/cards/profile.tsx +80 -0
  43. package/template/openfort-templates/solana-headless/src/components/cards/send.tsx +242 -0
  44. package/template/openfort-templates/solana-headless/src/components/cards/sign.tsx +48 -0
  45. package/template/openfort-templates/solana-headless/src/components/cards/wallets.tsx +199 -0
  46. package/template/openfort-templates/solana-headless/src/components/createWallet.tsx +117 -0
  47. package/template/openfort-templates/solana-headless/src/components/passwordRecovery.tsx +167 -0
  48. package/template/openfort-templates/solana-headless/src/components/providers.tsx +23 -0
  49. package/template/openfort-templates/solana-headless/src/components/ui/Sheet.tsx +47 -0
  50. package/template/openfort-templates/solana-headless/src/components/ui/Tabs.tsx +111 -0
  51. package/template/openfort-templates/solana-headless/src/components/ui/TruncateData.tsx +31 -0
  52. package/template/openfort-templates/solana-headless/src/hooks/useSolanaMessageSigner.ts +37 -0
  53. package/template/openfort-templates/solana-headless/src/index.css +180 -0
  54. package/template/openfort-templates/solana-headless/src/lib/errors.ts +4 -0
  55. package/template/openfort-templates/solana-headless/src/lib/solana/balance.ts +17 -0
  56. package/template/openfort-templates/solana-headless/src/lib/solana/index.ts +4 -0
  57. package/template/openfort-templates/solana-headless/src/lib/solana/kora.ts +137 -0
  58. package/template/openfort-templates/solana-headless/src/lib/solana/transaction.ts +146 -0
  59. package/template/openfort-templates/solana-headless/src/lib/solana/transactionHistory.ts +39 -0
  60. package/template/openfort-templates/solana-headless/src/main.tsx +13 -0
  61. package/template/openfort-templates/solana-headless/src/vite-env.d.ts +1 -0
  62. package/template/openfort-templates/solana-headless/tsconfig.app.json +24 -0
  63. package/template/openfort-templates/solana-headless/tsconfig.json +7 -0
  64. package/template/openfort-templates/solana-headless/tsconfig.node.json +22 -0
  65. package/template/openfort-templates/solana-headless/vite.config.ts +8 -0
@@ -1,5 +1,12 @@
1
1
  import { CheckCircleIcon } from '@heroicons/react/24/outline'
2
- import { RecoveryMethod, type UserWallet, useWallets } from '@openfort/react'
2
+ import {
3
+ AccountTypeEnum,
4
+ RecoveryMethod,
5
+ type ConnectedEmbeddedEthereumWallet,
6
+ } from '@openfort/react'
7
+ import type { EmbeddedAccount } from '@openfort/react'
8
+ import { useEthereumEmbeddedWallet } from '@openfort/react/ethereum'
9
+ import { useState } from 'react'
3
10
 
4
11
  import { Sheet } from '../../../components/ui/Sheet'
5
12
 
@@ -7,21 +14,32 @@ type CreateWalletPasswordSheetProps = {
7
14
  open: boolean
8
15
  onClose: () => void
9
16
  onCreateWallet?: () => void
17
+ create: (options: {
18
+ recoveryMethod: RecoveryMethod
19
+ accountType?: AccountTypeEnum
20
+ password?: string
21
+ }) => Promise<EmbeddedAccount>
22
+ status: string
23
+ accountType: AccountTypeEnum
10
24
  }
11
25
 
12
26
  export function CreateWalletPasswordSheet({
13
27
  open,
14
28
  onClose,
15
29
  onCreateWallet,
30
+ create,
31
+ status,
32
+ accountType,
16
33
  }: CreateWalletPasswordSheetProps) {
17
- const { createWallet, error, isCreating, reset } = useWallets()
34
+ const [error, setError] = useState<string | null>(null)
35
+ const isCreating = status === 'creating'
18
36
 
19
37
  return (
20
38
  <Sheet
21
39
  open={open}
22
40
  onClose={() => {
23
41
  onClose()
24
- reset()
42
+ setError(null)
25
43
  }}
26
44
  title="Enter Password"
27
45
  description="Please enter the password of your wallet."
@@ -33,16 +51,16 @@ export function CreateWalletPasswordSheet({
33
51
  const formData = new FormData(event.target as HTMLFormElement)
34
52
  const password = formData.get('password') as string
35
53
 
36
- const { error: walletError } = await createWallet({
37
- recovery: {
54
+ try {
55
+ await create({
38
56
  recoveryMethod: RecoveryMethod.PASSWORD,
57
+ accountType,
39
58
  password,
40
- },
41
- })
42
-
43
- if (!walletError) {
59
+ })
44
60
  onCreateWallet?.()
45
61
  onClose()
62
+ } catch (err) {
63
+ setError(err instanceof Error ? err.message : 'Failed to create wallet')
46
64
  }
47
65
  }}
48
66
  >
@@ -62,11 +80,12 @@ export function CreateWalletPasswordSheet({
62
80
  <input
63
81
  type="password"
64
82
  name="password"
83
+ autoComplete="new-password"
65
84
  placeholder="Enter your wallet's password"
66
85
  className="w-full mt-2 p-2 border border-gray-300 rounded"
67
86
  />
68
87
  {error && (
69
- <span className="text-red-500 text-sm mt-2">{error?.message}</span>
88
+ <span className="text-red-500 text-sm mt-2">{error}</span>
70
89
  )}
71
90
  <button
72
91
  className="mt-4 w-full bg-zinc-700 text-white p-2 rounded cursor-pointer"
@@ -83,7 +102,7 @@ export function CreateWalletPasswordSheet({
83
102
  type WalletRecoverPasswordSheetProps = {
84
103
  open: boolean
85
104
  onClose: () => void
86
- wallet: UserWallet | null
105
+ wallet: ConnectedEmbeddedEthereumWallet | null
87
106
  }
88
107
 
89
108
  export function WalletRecoverPasswordSheet({
@@ -91,34 +110,38 @@ export function WalletRecoverPasswordSheet({
91
110
  onClose,
92
111
  wallet,
93
112
  }: WalletRecoverPasswordSheetProps) {
94
- const { setActiveWallet, error, isConnecting, reset } = useWallets()
113
+ const { setActive, status } = useEthereumEmbeddedWallet()
114
+ const [error, setError] = useState<string | null>(null)
115
+ const isConnecting = status === 'connecting'
95
116
 
96
117
  return (
97
118
  <Sheet
98
119
  open={open}
99
120
  onClose={() => {
100
121
  onClose()
101
- reset()
122
+ setError(null)
102
123
  }}
103
124
  title="Enter Password"
104
125
  description="Please enter the password of your wallet."
105
126
  >
106
127
  <form
107
128
  className="w-full flex-1 flex flex-col justify-center"
108
- onSubmit={(event) => {
129
+ onSubmit={async (event) => {
109
130
  event.preventDefault()
110
131
  const formData = new FormData(event.target as HTMLFormElement)
111
132
  const password = formData.get('password') as string
112
133
  if (!wallet) throw new Error('No wallet to recover')
113
134
 
114
- setActiveWallet({
115
- walletId: 'xyz.openfort',
116
- recovery: {
135
+ try {
136
+ await setActive({
137
+ address: wallet.address,
117
138
  recoveryMethod: RecoveryMethod.PASSWORD,
118
139
  password,
119
- },
120
- address: wallet.address,
121
- })
140
+ })
141
+ onClose()
142
+ } catch (err) {
143
+ setError(err instanceof Error ? err.message : 'Failed to recover wallet')
144
+ }
122
145
  }}
123
146
  >
124
147
  {wallet && (
@@ -130,11 +153,12 @@ export function WalletRecoverPasswordSheet({
130
153
  <input
131
154
  type="password"
132
155
  name="password"
156
+ autoComplete="current-password"
133
157
  placeholder="Enter your wallet's password"
134
158
  className="w-full mt-2 p-2 border border-gray-300 rounded"
135
159
  />
136
160
  {error && (
137
- <span className="text-red-500 text-sm mt-2">{error?.message}</span>
161
+ <span className="text-red-500 text-sm mt-2">{error}</span>
138
162
  )}
139
163
  <button
140
164
  className="mt-4 w-full bg-zinc-700 text-white p-2 rounded cursor-pointer"
@@ -84,7 +84,7 @@ Required environment variables for headless operation:
84
84
  ```env
85
85
  VITE_OPENFORT_PUBLISHABLE_KEY=pk_test_...
86
86
  VITE_SHIELD_PUBLISHABLE_KEY=sk_test_...
87
- VITE_POLICY_ID=pol_...
87
+ VITE_FEE_SPONSORSHIP_ID=pol_...
88
88
  VITE_CREATE_ENCRYPTED_SESSION_ENDPOINT=/api/shield-session
89
89
  VITE_WALLET_CONNECT_PROJECT_ID=your-wallet-connect-project-id
90
90
  ```
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@heroicons/react": "^2.2.0",
15
- "@openfort/react": "latest",
15
+ "@openfort/react": "workspace:^",
16
16
  "@tanstack/react-query": ">=5.45.1",
17
17
  "react": "^18.2.0",
18
18
  "react-dom": "^18.2.0",
@@ -22,10 +22,10 @@
22
22
  "devDependencies": {
23
23
  "@biomejs/biome": "^2.3.8",
24
24
  "@tailwindcss/vite": "^4.1.15",
25
- "tailwindcss": "^4.1.15",
26
25
  "@types/react": "^19.1.10",
27
26
  "@types/react-dom": "^19.1.7",
28
27
  "@vitejs/plugin-react": "^4.3.4",
28
+ "tailwindcss": "^4.1.15",
29
29
  "typescript": "~5.8.3",
30
30
  "vite": "^6.4.1"
31
31
  }
@@ -1,22 +1,20 @@
1
1
  import { useMemo } from 'react'
2
2
  import { getAddress, parseAbi } from 'viem'
3
- import {
4
- useAccount,
5
- useChains,
6
- useReadContract,
7
- useWriteContract
8
- } from 'wagmi'
3
+ import { useAccount, useChainId, useChains, useReadContract, useWriteContract } from 'wagmi'
4
+ import { getMintContractConfig } from '../../lib/contracts'
9
5
  import { TruncateData } from '../ui/TruncateData'
10
6
 
11
7
  const MintContract = () => {
12
8
  const { address } = useAccount()
9
+ const chainId = useChainId()
10
+ const config = getMintContractConfig(chainId)
13
11
 
14
12
  const {
15
13
  data: balance,
16
14
  refetch,
17
15
  error: balanceError,
18
16
  } = useReadContract({
19
- address: '0xef147ed8bb07a2a0e7df4c1ac09e96dec459ffac',
17
+ address: (config?.address ?? undefined) as `0x${string}` | undefined,
20
18
  abi: [
21
19
  {
22
20
  type: 'function',
@@ -27,11 +25,11 @@ const MintContract = () => {
27
25
  },
28
26
  ],
29
27
  functionName: 'balanceOf',
30
- args: [address!],
28
+ args: config && address ? [address] : undefined,
31
29
  })
32
30
 
33
31
  const { data: tokenSymbol } = useReadContract({
34
- address: '0xef147ed8bb07a2a0e7df4c1ac09e96dec459ffac',
32
+ address: (config?.address ?? undefined) as `0x${string}` | undefined,
35
33
  abi: [
36
34
  {
37
35
  type: 'function',
@@ -62,12 +60,23 @@ const MintContract = () => {
62
60
  })
63
61
 
64
62
  async function submit({ amount }: { amount: string }) {
65
- writeContract({
66
- address: getAddress('0xef147ed8bb07a2a0e7df4c1ac09e96dec459ffac'),
67
- abi: parseAbi(['function mint(address to, uint256 amount)']),
68
- functionName: 'mint',
69
- args: [address!, BigInt(amount)],
70
- })
63
+ if (!config?.address) return
64
+ const amountWei = BigInt(amount) * BigInt(10 ** 18)
65
+ if (config.type === 'claim') {
66
+ writeContract({
67
+ address: getAddress(config.address),
68
+ abi: parseAbi(['function claim(uint256 amount)']),
69
+ functionName: 'claim',
70
+ args: [amountWei],
71
+ })
72
+ } else {
73
+ writeContract({
74
+ address: getAddress(config.address),
75
+ abi: parseAbi(['function mint(address to, uint256 amount)']),
76
+ functionName: 'mint',
77
+ args: [address!, amountWei],
78
+ })
79
+ }
71
80
  }
72
81
 
73
82
  return (
@@ -90,7 +99,7 @@ const MintContract = () => {
90
99
  className="grow peer"
91
100
  name="amount"
92
101
  />
93
- <button className="btn" disabled={isPending || !address}>
102
+ <button className="btn" disabled={isPending || !address || !config}>
94
103
  {isPending ? 'Minting...' : 'Mint Tokens'}
95
104
  </button>
96
105
  </form>
@@ -102,7 +111,7 @@ const MintContract = () => {
102
111
  }
103
112
 
104
113
  export const Actions = () => {
105
- const hasSponsorPolicy = useMemo(() => !!import.meta.env.VITE_POLICY_ID, [])
114
+ const hasFeeSponsorship = useMemo(() => !!import.meta.env.VITE_FEE_SPONSORSHIP_ID, [])
106
115
  const chains = useChains()
107
116
  return (
108
117
  <div className="flex flex-col w-full">
@@ -110,7 +119,7 @@ export const Actions = () => {
110
119
  <span className="mb-4 text-zinc-400 text-sm">
111
120
  Interact with smart contracts on the blockchain.
112
121
  </span>
113
- {!hasSponsorPolicy && (
122
+ {!hasFeeSponsorship && (
114
123
  <div className="mb-3 p-3 bg-red-800 text-white rounded text-sm">
115
124
  <strong>Warning: Transactions are not sponsored.</strong> Minting may
116
125
  fail because transactions are not being sponsored. To sponsor
@@ -123,9 +132,9 @@ export const Actions = () => {
123
132
  >
124
133
  Openfort Dashboard
125
134
  </a>{' '}
126
- and <b>create a policy</b> sponsoring transactions in{' '}
127
- <b>{chains[0].name}</b>. Set the <code>VITE_POLICY_ID</code>{' '}
128
- environment variable with the policy ID.
135
+ and <b>create a fee sponsorship</b> for transactions in{' '}
136
+ <b>{chains[0].name}</b>. Set the <code>VITE_FEE_SPONSORSHIP_ID</code>{' '}
137
+ environment variable with the fee sponsorship ID.
129
138
  </div>
130
139
  )}
131
140
  <MintContract />
@@ -28,7 +28,6 @@ export const Profile = ({
28
28
  <br />
29
29
  You can sign messages and interact with smart contracts.
30
30
  </p>
31
- <p>
32
31
  <div className="border border-zinc-700 rounded p-4">
33
32
  <h2 className="mb-2">Get started</h2>
34
33
  <p className="mb-2 text-zinc-400 text-sm">
@@ -69,7 +68,6 @@ export const Profile = ({
69
68
  </a>
70
69
  </div>
71
70
  </div>
72
- </p>
73
71
 
74
72
  <button
75
73
  onClick={() => {
@@ -1,42 +1,175 @@
1
1
  import {
2
+ CheckIcon,
3
+ ChevronDownIcon,
4
+ ChevronUpIcon,
5
+ ClipboardDocumentIcon,
2
6
  FingerPrintIcon,
3
7
  KeyIcon,
4
8
  LockClosedIcon,
5
9
  } from '@heroicons/react/24/outline'
6
10
  import {
11
+ AccountTypeEnum,
7
12
  RecoveryMethod,
8
- type UserWallet,
13
+ type ConnectedEmbeddedEthereumWallet,
9
14
  useSignOut,
10
15
  useUser,
11
- useWallets,
12
16
  } from '@openfort/react'
13
- import { useState } from 'react'
17
+ import { useEthereumEmbeddedWallet } from '@openfort/react/ethereum'
18
+ import { useMemo, useState } from 'react'
14
19
  import { useAccount } from 'wagmi'
15
20
  import { CreateWallet, CreateWalletSheet } from '../createWallet'
16
21
  import { WalletRecoverPasswordSheet } from '../passwordRecovery'
17
22
 
23
+ const ACCOUNT_TYPE_LABELS: Record<AccountTypeEnum, string> = {
24
+ [AccountTypeEnum.EOA]: 'EOA',
25
+ [AccountTypeEnum.SMART_ACCOUNT]: 'SM',
26
+ [AccountTypeEnum.DELEGATED_ACCOUNT]: 'DE',
27
+ }
28
+
29
+ const VISIBLE_WALLET_COUNT = 4
30
+
31
+ const WalletRecoveryBadge = ({ wallet }: { wallet: ConnectedEmbeddedEthereumWallet }) => {
32
+ let Icon = LockClosedIcon
33
+ let text = 'Unknown'
34
+
35
+ switch (wallet.recoveryMethod) {
36
+ case RecoveryMethod.PASSWORD:
37
+ Icon = KeyIcon
38
+ text = 'Password'
39
+ break
40
+ case RecoveryMethod.AUTOMATIC:
41
+ Icon = LockClosedIcon
42
+ text = 'Automatic'
43
+ break
44
+ case RecoveryMethod.PASSKEY:
45
+ Icon = FingerPrintIcon
46
+ text = 'Passkey'
47
+ break
48
+ }
49
+
50
+ return (
51
+ <div className="flex items-center text-xs">
52
+ <span>{text}</span>
53
+ <Icon className="h-5 w-5 ml-2" />
54
+ </div>
55
+ )
56
+ }
57
+
58
+ const WalletItem = ({
59
+ wallet,
60
+ isActive,
61
+ isConnecting,
62
+ nested,
63
+ onClick,
64
+ }: {
65
+ wallet: ConnectedEmbeddedEthereumWallet
66
+ isActive: boolean
67
+ isConnecting: boolean
68
+ nested?: boolean
69
+ onClick: () => void
70
+ }) => (
71
+ <div className={nested ? 'ml-5 flex items-center gap-1' : undefined}>
72
+ {nested && <span className="text-zinc-500 text-sm">↳</span>}
73
+ <button
74
+ key={wallet.id + wallet.address}
75
+ className="px-4 py-3 border data-[active=true]:border-zinc-300 border-zinc-700 rounded data-[active=false]:cursor-pointer data-[active=false]:hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1 text-sm w-full"
76
+ onClick={onClick}
77
+ data-active={isActive}
78
+ disabled={isActive || isConnecting}
79
+ type="button"
80
+ >
81
+ {isConnecting && isActive ? (
82
+ <p>Connecting...</p>
83
+ ) : (
84
+ <div className="flex justify-between items-center">
85
+ <p className="font-medium mr-2">
86
+ {`${wallet.address.substring(0, 6)}...${wallet.address.substring(wallet.address.length - 4)}`}
87
+ </p>
88
+ <div className="flex items-center gap-2">
89
+ {wallet.accountType && (
90
+ <span className="text-[10px] font-semibold leading-none border border-zinc-600 rounded px-1 py-0.5 opacity-70">
91
+ {ACCOUNT_TYPE_LABELS[wallet.accountType]}
92
+ </span>
93
+ )}
94
+ <WalletRecoveryBadge wallet={wallet} />
95
+ </div>
96
+ </div>
97
+ )}
98
+ </button>
99
+ </div>
100
+ )
101
+
18
102
  export const Wallets = () => {
19
103
  const {
20
104
  wallets,
21
- isLoadingWallets,
105
+ status,
22
106
  activeWallet,
23
- availableWallets,
24
- setActiveWallet,
25
- isConnecting,
26
- } = useWallets()
107
+ setActive,
108
+ exportPrivateKey,
109
+ } = useEthereumEmbeddedWallet()
110
+ const isLoadingWallets = status === 'fetching-wallets'
111
+ const isConnecting = status === 'connecting'
27
112
  const { user, isAuthenticated } = useUser()
28
113
  const { isConnected } = useAccount()
29
114
  const [createWalletSheetOpen, setCreateWalletSheetOpen] = useState(false)
30
- const [walletToRecover, setWalletToRecover] = useState<UserWallet | null>(
31
- null,
32
- )
115
+ const [walletToRecover, setWalletToRecover] =
116
+ useState<ConnectedEmbeddedEthereumWallet | null>(null)
117
+ const [showAllWallets, setShowAllWallets] = useState(false)
118
+ const [exportedKey, setExportedKey] = useState<string | null>(null)
119
+ const [isExporting, setIsExporting] = useState(false)
120
+ const [exportError, setExportError] = useState<string | null>(null)
121
+ const [copied, setCopied] = useState(false)
33
122
  const { signOut } = useSignOut()
34
123
 
124
+ // Group wallets: EOAs at top level, Smart/Delegated accounts nested under their owner
125
+ const { topLevel, childrenByOwner } = useMemo(() => {
126
+ const ownerAddresses = new Set(wallets.map((w) => w.address.toLowerCase()))
127
+ const childrenByOwner = new Map<string, ConnectedEmbeddedEthereumWallet[]>()
128
+ const topLevel: ConnectedEmbeddedEthereumWallet[] = []
129
+
130
+ for (const wallet of wallets) {
131
+ const owner = wallet.ownerAddress?.toLowerCase()
132
+ if (owner && ownerAddresses.has(owner) && owner !== wallet.address.toLowerCase()) {
133
+ const existing = childrenByOwner.get(owner) ?? []
134
+ existing.push(wallet)
135
+ childrenByOwner.set(owner, existing)
136
+ } else {
137
+ topLevel.push(wallet)
138
+ }
139
+ }
140
+
141
+ return { topLevel, childrenByOwner }
142
+ }, [wallets])
143
+
144
+ const handleExportKey = async () => {
145
+ setIsExporting(true)
146
+ setExportError(null)
147
+ setExportedKey(null)
148
+ try {
149
+ const key = await exportPrivateKey()
150
+ setExportedKey(key)
151
+ } catch {
152
+ setExportError('Cannot export private key for this wallet.')
153
+ } finally {
154
+ setIsExporting(false)
155
+ }
156
+ }
157
+
158
+ const handleWalletClick = (wallet: ConnectedEmbeddedEthereumWallet) => {
159
+ const isActive = activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
160
+ if (isActive || isConnecting) return
161
+ if (wallet.recoveryMethod === RecoveryMethod.PASSWORD) {
162
+ setWalletToRecover(wallet)
163
+ } else {
164
+ setActive({ address: wallet.address })
165
+ }
166
+ }
167
+
35
168
  if (!activeWallet && isConnecting) return <div>recovering ...</div>
36
169
  if (isLoadingWallets || (!user && isAuthenticated)) {
37
170
  return <div>Loading wallets...</div>
38
171
  }
39
- if (availableWallets.length === 0) {
172
+ if (wallets.length === 0) {
40
173
  return (
41
174
  <div className="flex gap-2 flex-col w-full">
42
175
  <h1>Create a wallet</h1>
@@ -46,46 +179,9 @@ export const Wallets = () => {
46
179
  )
47
180
  }
48
181
 
49
- const renderWalletRecovery = (wallet: UserWallet) => {
50
- let Icon = LockClosedIcon
51
- let text = 'Unknown'
52
- const method = wallet.recoveryMethod
53
-
54
- switch (method) {
55
- case RecoveryMethod.PASSWORD:
56
- Icon = KeyIcon
57
- text = 'Password'
58
- break
59
- case RecoveryMethod.AUTOMATIC:
60
- Icon = LockClosedIcon
61
- text = 'Automatic'
62
- break
63
- case RecoveryMethod.PASSKEY:
64
- Icon = FingerPrintIcon
65
- text = 'Passkey'
66
- break
67
- }
68
-
69
- return (
70
- <div className="flex items-center text-xs">
71
- <span>{text}</span>
72
- <Icon className="h-5 w-5 ml-2" />
73
- </div>
74
- )
75
- }
76
-
77
- const handleWalletClick = (wallet: UserWallet) => {
78
- if (wallet.isActive || isConnecting) return
79
- const method = wallet.recoveryMethod
80
- if (method === RecoveryMethod.PASSWORD) {
81
- setWalletToRecover(wallet)
82
- } else {
83
- setActiveWallet({
84
- walletId: 'xyz.openfort',
85
- address: wallet.address,
86
- })
87
- }
88
- }
182
+ const visibleTopLevel = showAllWallets
183
+ ? topLevel
184
+ : topLevel.slice(0, VISIBLE_WALLET_COUNT)
89
185
 
90
186
  return (
91
187
  <div className="flex flex-col w-full">
@@ -96,34 +192,101 @@ export const Wallets = () => {
96
192
  <div className="space-y-4 pb-4">
97
193
  <h2>Your Wallets</h2>
98
194
  <div className="flex flex-col space-y-2">
99
- {wallets.map((wallet) => (
195
+ {visibleTopLevel.map((wallet) => {
196
+ const isActive =
197
+ activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
198
+ const children = childrenByOwner.get(wallet.address.toLowerCase())
199
+
200
+ return (
201
+ <div key={wallet.id} className="space-y-1">
202
+ <WalletItem
203
+ wallet={wallet}
204
+ isActive={isActive}
205
+ isConnecting={isConnecting}
206
+ onClick={() => handleWalletClick(wallet)}
207
+ />
208
+ {children?.map((child) => {
209
+ const isChildActive =
210
+ activeWallet?.address.toLowerCase() === child.address.toLowerCase()
211
+ return (
212
+ <WalletItem
213
+ key={child.id}
214
+ wallet={child}
215
+ isActive={isChildActive}
216
+ isConnecting={isConnecting}
217
+ nested
218
+ onClick={() => handleWalletClick(child)}
219
+ />
220
+ )
221
+ })}
222
+ </div>
223
+ )
224
+ })}
225
+
226
+ {topLevel.length > VISIBLE_WALLET_COUNT && (
100
227
  <button
101
- key={wallet.id + wallet.address}
102
- className="px-4 py-3 border data-[active=true]:border-zinc-300 border-zinc-700 rounded data-[active=false]:cursor-pointer data-[active=false]:hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1 text-sm"
103
- onClick={() => handleWalletClick(wallet)}
104
- data-active={wallet.isActive}
105
- disabled={wallet.isActive || isConnecting}
228
+ type="button"
229
+ className="flex items-center justify-center gap-1 text-xs text-zinc-400 hover:text-zinc-200 cursor-pointer transition-colors py-1"
230
+ onClick={() => setShowAllWallets((prev) => !prev)}
106
231
  >
107
- {wallet.isConnecting ? (
108
- <p>Connecting...</p>
232
+ {showAllWallets ? (
233
+ <>
234
+ Show less <ChevronUpIcon className="h-4 w-4" />
235
+ </>
109
236
  ) : (
110
- <div className="flex justify-between items-center">
111
- <p className="font-medium mr-2">
112
- {`${wallet.address.substring(0, 6)}...${wallet.address.substring(wallet.address.length - 4)}`}
113
- </p>
114
- {renderWalletRecovery(wallet)}
115
- </div>
237
+ <>
238
+ Show {topLevel.length - VISIBLE_WALLET_COUNT} more{' '}
239
+ <ChevronDownIcon className="h-4 w-4" />
240
+ </>
116
241
  )}
117
242
  </button>
118
- ))}
243
+ )}
119
244
 
120
245
  <button
121
246
  className="p-3 border border-zinc-700 rounded cursor-pointer hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1"
122
247
  onClick={() => setCreateWalletSheetOpen(true)}
248
+ type="button"
123
249
  >
124
250
  + Create Wallet
125
251
  </button>
126
252
  </div>
253
+
254
+ {activeWallet && (
255
+ <div className="mt-4 space-y-2">
256
+ <button
257
+ type="button"
258
+ className="w-full p-3 border border-zinc-700 rounded cursor-pointer hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors text-sm flex items-center justify-center gap-2"
259
+ onClick={handleExportKey}
260
+ disabled={isExporting}
261
+ >
262
+ <KeyIcon className="h-4 w-4" />
263
+ {isExporting ? 'Exporting...' : 'Export Private Key'}
264
+ </button>
265
+ {exportError && (
266
+ <p className="text-red-400 text-xs">{exportError}</p>
267
+ )}
268
+ {exportedKey && (
269
+ <div className="flex items-center gap-2 p-3 border border-zinc-700 rounded bg-zinc-800 text-xs break-all font-mono">
270
+ <span className="flex-1">{exportedKey}</span>
271
+ <button
272
+ type="button"
273
+ className="shrink-0 p-1 rounded hover:bg-zinc-700 transition-colors cursor-pointer"
274
+ onClick={() => {
275
+ navigator.clipboard.writeText(exportedKey)
276
+ setCopied(true)
277
+ setTimeout(() => setCopied(false), 2000)
278
+ }}
279
+ >
280
+ {copied ? (
281
+ <CheckIcon className="h-4 w-4 text-green-400" />
282
+ ) : (
283
+ <ClipboardDocumentIcon className="h-4 w-4 text-zinc-400" />
284
+ )}
285
+ </button>
286
+ </div>
287
+ )}
288
+ </div>
289
+ )}
127
290
  </div>
128
291
  <WalletRecoverPasswordSheet
129
292
  wallet={walletToRecover}