@nockchain/rose 0.1.4-nightly.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.
Files changed (205) hide show
  1. package/.github/workflows/artifacts.yml +33 -0
  2. package/.github/workflows/ci.yml +68 -0
  3. package/.github/workflows/publish-sdk.yml +35 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +5 -0
  6. package/.prettierrc +8 -0
  7. package/LICENSE +22 -0
  8. package/README.md +117 -0
  9. package/extension/background/index.ts +1500 -0
  10. package/extension/content/index.ts +59 -0
  11. package/extension/icons/rose.svg +27 -0
  12. package/extension/icons/rose128.png +0 -0
  13. package/extension/icons/rose16.png +0 -0
  14. package/extension/icons/rose256.png +0 -0
  15. package/extension/icons/rose32.png +0 -0
  16. package/extension/icons/rose48.png +0 -0
  17. package/extension/icons/rose512.png +0 -0
  18. package/extension/inpage/index.ts +86 -0
  19. package/extension/manifest.json +48 -0
  20. package/extension/popup/Popup.tsx +94 -0
  21. package/extension/popup/Router.tsx +121 -0
  22. package/extension/popup/assets/arrow-down-icon.svg +3 -0
  23. package/extension/popup/assets/arrow-left-icon.svg +3 -0
  24. package/extension/popup/assets/arrow-right-icon.svg +3 -0
  25. package/extension/popup/assets/arrow-up-icon.svg +3 -0
  26. package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
  27. package/extension/popup/assets/checkmark-icon.svg +3 -0
  28. package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
  29. package/extension/popup/assets/checkmark-success-icon.svg +3 -0
  30. package/extension/popup/assets/clock-icon.svg +3 -0
  31. package/extension/popup/assets/close-x-icon.svg +3 -0
  32. package/extension/popup/assets/copy-icon.svg +6 -0
  33. package/extension/popup/assets/explorer-icon.svg +3 -0
  34. package/extension/popup/assets/eye-off-icon.svg +3 -0
  35. package/extension/popup/assets/eye-open-icon.svg +4 -0
  36. package/extension/popup/assets/feedback-icon.svg +3 -0
  37. package/extension/popup/assets/green-status-dot.svg +3 -0
  38. package/extension/popup/assets/info-icon.svg +3 -0
  39. package/extension/popup/assets/iris-logo-40.svg +27 -0
  40. package/extension/popup/assets/iris-logo-96.svg +27 -0
  41. package/extension/popup/assets/iris-logo-blue.svg +27 -0
  42. package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
  43. package/extension/popup/assets/iris-logo-orange.svg +27 -0
  44. package/extension/popup/assets/iris-logo.svg +27 -0
  45. package/extension/popup/assets/key-icon.svg +3 -0
  46. package/extension/popup/assets/lock-icon-yellow.svg +3 -0
  47. package/extension/popup/assets/lock-icon.svg +3 -0
  48. package/extension/popup/assets/pencil-edit-icon.svg +3 -0
  49. package/extension/popup/assets/permissions-icon.svg +3 -0
  50. package/extension/popup/assets/receipt-icon.svg +5 -0
  51. package/extension/popup/assets/refresh-icon.svg +3 -0
  52. package/extension/popup/assets/settings-gear-icon.svg +8 -0
  53. package/extension/popup/assets/settings-icon.svg +3 -0
  54. package/extension/popup/assets/theme-icon.svg +3 -0
  55. package/extension/popup/assets/trash-bin-icon.svg +3 -0
  56. package/extension/popup/assets/trend-down-arrow.svg +5 -0
  57. package/extension/popup/assets/trend-up-arrow.svg +5 -0
  58. package/extension/popup/assets/user-account-icon.svg +3 -0
  59. package/extension/popup/assets/vector-bottom-left.svg +9 -0
  60. package/extension/popup/assets/vector-left.svg +9 -0
  61. package/extension/popup/assets/vector-right.svg +9 -0
  62. package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
  63. package/extension/popup/assets/vector-top-right.svg +9 -0
  64. package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
  65. package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
  66. package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
  67. package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
  68. package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
  69. package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
  70. package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
  71. package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
  72. package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
  73. package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
  74. package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
  75. package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
  76. package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
  77. package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
  78. package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
  79. package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
  80. package/extension/popup/components/AccountIcon.tsx +78 -0
  81. package/extension/popup/components/AccountSelector.tsx +246 -0
  82. package/extension/popup/components/Alert.tsx +48 -0
  83. package/extension/popup/components/ConfirmModal.tsx +81 -0
  84. package/extension/popup/components/PasswordInput.tsx +49 -0
  85. package/extension/popup/components/ScreenContainer.tsx +17 -0
  86. package/extension/popup/components/SiteIcon.tsx +60 -0
  87. package/extension/popup/components/ThemeToggle.tsx +44 -0
  88. package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
  89. package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
  90. package/extension/popup/components/icons/CheckIcon.tsx +20 -0
  91. package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
  92. package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
  93. package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
  94. package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
  95. package/extension/popup/components/icons/CloseIcon.tsx +26 -0
  96. package/extension/popup/components/icons/CopyIcon.tsx +20 -0
  97. package/extension/popup/components/icons/EditIcon.tsx +20 -0
  98. package/extension/popup/components/icons/EyeIcon.tsx +13 -0
  99. package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
  100. package/extension/popup/components/icons/InfoIcon.tsx +20 -0
  101. package/extension/popup/components/icons/LockIcon.tsx +20 -0
  102. package/extension/popup/components/icons/PlusIcon.tsx +15 -0
  103. package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
  104. package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
  105. package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
  106. package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
  107. package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
  108. package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
  109. package/extension/popup/components/icons/UploadIcon.tsx +20 -0
  110. package/extension/popup/components/icons/WalletIcon.tsx +20 -0
  111. package/extension/popup/contexts/ThemeContext.tsx +105 -0
  112. package/extension/popup/hooks/useApprovalDetection.ts +128 -0
  113. package/extension/popup/hooks/useAutoFocus.ts +36 -0
  114. package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
  115. package/extension/popup/hooks/useClickOutside.ts +33 -0
  116. package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
  117. package/extension/popup/hooks/useFavicon.ts +64 -0
  118. package/extension/popup/hooks/useNumericInput.ts +93 -0
  119. package/extension/popup/index.html +13 -0
  120. package/extension/popup/index.tsx +24 -0
  121. package/extension/popup/screens/AboutScreen.tsx +118 -0
  122. package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
  123. package/extension/popup/screens/HomeScreen.tsx +902 -0
  124. package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
  125. package/extension/popup/screens/LockTimeScreen.tsx +155 -0
  126. package/extension/popup/screens/ReceiveScreen.tsx +149 -0
  127. package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
  128. package/extension/popup/screens/SendReviewScreen.tsx +308 -0
  129. package/extension/popup/screens/SendScreen.tsx +825 -0
  130. package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
  131. package/extension/popup/screens/SettingsScreen.tsx +116 -0
  132. package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
  133. package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
  134. package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
  135. package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
  136. package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
  137. package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
  138. package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
  139. package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
  140. package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
  141. package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
  142. package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
  143. package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
  144. package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
  145. package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
  146. package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
  147. package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
  148. package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
  149. package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
  150. package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
  151. package/extension/popup/screens/system/LockedScreen.tsx +288 -0
  152. package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
  153. package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
  154. package/extension/popup/store.ts +482 -0
  155. package/extension/popup/styles.css +246 -0
  156. package/extension/popup/utils/format.ts +58 -0
  157. package/extension/popup/utils/formatWalletError.ts +36 -0
  158. package/extension/popup/utils/memo.ts +299 -0
  159. package/extension/popup/utils/messaging.ts +16 -0
  160. package/extension/shared/address-encoding.ts +69 -0
  161. package/extension/shared/balance-query.ts +123 -0
  162. package/extension/shared/constants.ts +386 -0
  163. package/extension/shared/currency.ts +128 -0
  164. package/extension/shared/first-name-derivation.ts +128 -0
  165. package/extension/shared/keyfile.ts +58 -0
  166. package/extension/shared/onboarding.ts +78 -0
  167. package/extension/shared/price-api.ts +79 -0
  168. package/extension/shared/rpc-client-browser.ts +315 -0
  169. package/extension/shared/transaction-builder.ts +443 -0
  170. package/extension/shared/types.ts +450 -0
  171. package/extension/shared/utxo-diff.ts +212 -0
  172. package/extension/shared/utxo-store.ts +548 -0
  173. package/extension/shared/utxo-sync.ts +343 -0
  174. package/extension/shared/validators.ts +26 -0
  175. package/extension/shared/vault.ts +1580 -0
  176. package/extension/shared/wallet-crypto.ts +77 -0
  177. package/extension/shared/wasm-utils.ts +76 -0
  178. package/extension/shared/webcrypto.ts +67 -0
  179. package/extension/types/wasm.d.ts +13 -0
  180. package/package.json +39 -0
  181. package/postcss.config.js +6 -0
  182. package/rose-extension-dist.zip +0 -0
  183. package/sdk/README.md +88 -0
  184. package/sdk/examples/app.ts +166 -0
  185. package/sdk/examples/index.html +51 -0
  186. package/sdk/examples/tsconfig.json +15 -0
  187. package/sdk/examples/tx-builder.html +532 -0
  188. package/sdk/examples/tx-builder.ts +1766 -0
  189. package/sdk/package-lock.json +424 -0
  190. package/sdk/package.json +68 -0
  191. package/sdk/src/constants.ts +28 -0
  192. package/sdk/src/errors.ts +74 -0
  193. package/sdk/src/hooks/index.ts +1 -0
  194. package/sdk/src/hooks/use-rose.ts +94 -0
  195. package/sdk/src/index.ts +12 -0
  196. package/sdk/src/provider.ts +396 -0
  197. package/sdk/src/transaction.ts +163 -0
  198. package/sdk/src/types/rose-wasm.d.ts +14 -0
  199. package/sdk/src/types.ts +97 -0
  200. package/sdk/src/wasm.ts +13 -0
  201. package/sdk/tsconfig.json +20 -0
  202. package/sdk/vite.config.examples.ts +32 -0
  203. package/tailwind.config.ts +38 -0
  204. package/tsconfig.json +20 -0
  205. package/vite.config.ts +60 -0
@@ -0,0 +1,381 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useStore } from '../store';
3
+ import { send } from '../utils/messaging';
4
+ import { INTERNAL_METHODS } from '../../shared/constants';
5
+ import { formatWalletError } from '../utils/formatWalletError';
6
+ import { AccountIcon } from '../components/AccountIcon';
7
+ import { ConfirmModal } from '../components/ConfirmModal';
8
+ import { CloseIcon } from '../components/icons/CloseIcon';
9
+ import { ChevronRightIcon } from '../components/icons/ChevronRightIcon';
10
+ import UserAccountIcon from '../assets/user-account-icon.svg';
11
+ import ThemeIcon from '../assets/theme-icon.svg';
12
+ import CopyIcon from '../assets/copy-icon.svg';
13
+ import CheckmarkIcon from '../assets/checkmark-pencil-icon.svg';
14
+ import PencilEditIcon from '../assets/pencil-edit-icon.svg';
15
+ import SettingsGearIcon from '../assets/settings-gear-icon.svg';
16
+
17
+ export function WalletSettingsScreen() {
18
+ const { navigate, wallet, syncWallet } = useStore();
19
+
20
+ // Get current account from vault
21
+ const currentAccount = wallet.currentAccount || wallet.accounts[0];
22
+ const walletName = currentAccount?.name || 'Wallet';
23
+ const walletAddress = currentAccount?.address || '';
24
+
25
+ const [isEditingName, setIsEditingName] = useState(false);
26
+ const [editedName, setEditedName] = useState(walletName);
27
+ const [copySuccess, setCopySuccess] = useState(false);
28
+ const [showRemoveConfirm, setShowRemoveConfirm] = useState(false);
29
+ const [showLastAccountError, setShowLastAccountError] = useState(false);
30
+
31
+ // Format creation date
32
+ const walletCreatedDate = currentAccount?.createdAt
33
+ ? new Date(currentAccount.createdAt).toLocaleDateString('en-US', {
34
+ month: 'long',
35
+ day: 'numeric',
36
+ year: 'numeric',
37
+ hour: '2-digit',
38
+ minute: '2-digit',
39
+ })
40
+ : 'Unknown';
41
+
42
+ // Keep editedName in sync with current account name
43
+ useEffect(() => {
44
+ setEditedName(walletName);
45
+ }, [walletName]);
46
+
47
+ function handleClose() {
48
+ navigate('home');
49
+ }
50
+ function handleStyling() {
51
+ navigate('wallet-styling');
52
+ }
53
+ function handleEditName() {
54
+ setIsEditingName(true);
55
+ setEditedName(walletName);
56
+ }
57
+
58
+ async function handleSaveName() {
59
+ if (!editedName.trim() || !currentAccount) {
60
+ setIsEditingName(false);
61
+ return;
62
+ }
63
+
64
+ // Call the vault to rename the account
65
+ const result = await send<{ ok?: boolean; error?: string }>(INTERNAL_METHODS.RENAME_ACCOUNT, [
66
+ currentAccount.index,
67
+ editedName.trim(),
68
+ ]);
69
+
70
+ if (result?.ok) {
71
+ // Update wallet state with new name
72
+ const updatedAccounts = wallet.accounts.map(acc =>
73
+ acc.index === currentAccount.index ? { ...acc, name: editedName.trim() } : acc
74
+ );
75
+ const updatedCurrentAccount = { ...currentAccount, name: editedName.trim() };
76
+
77
+ syncWallet({
78
+ ...wallet,
79
+ accounts: updatedAccounts,
80
+ currentAccount: updatedCurrentAccount,
81
+ });
82
+
83
+ setIsEditingName(false);
84
+ } else if (result?.error) {
85
+ alert(`Failed to rename account: ${formatWalletError(result.error)}`);
86
+ setIsEditingName(false);
87
+ }
88
+ }
89
+ function handleNameInputChange(e: React.ChangeEvent<HTMLInputElement>) {
90
+ setEditedName(e.target.value);
91
+ }
92
+ function handleNameInputKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
93
+ if (e.key === 'Enter') handleSaveName();
94
+ if (e.key === 'Escape') {
95
+ setIsEditingName(false);
96
+ setEditedName(walletName);
97
+ }
98
+ }
99
+ async function handleCopyAddress() {
100
+ if (!walletAddress) return;
101
+
102
+ try {
103
+ await navigator.clipboard.writeText(walletAddress);
104
+ setCopySuccess(true);
105
+ setTimeout(() => setCopySuccess(false), 2000);
106
+ } catch (err) {
107
+ console.error('Failed to copy address:', err);
108
+ }
109
+ }
110
+ function handleRemoveWallet() {
111
+ setShowRemoveConfirm(true);
112
+ }
113
+
114
+ async function confirmRemoveWallet() {
115
+ if (!currentAccount) return;
116
+
117
+ const result = await send<{ ok?: boolean; switchedTo?: number; error?: string }>(
118
+ INTERNAL_METHODS.HIDE_ACCOUNT,
119
+ [currentAccount.index]
120
+ );
121
+
122
+ if (result?.ok) {
123
+ // Update wallet state - filter out hidden account
124
+ const updatedAccounts = wallet.accounts.map(acc =>
125
+ acc.index === currentAccount.index ? { ...acc, hidden: true } : acc
126
+ );
127
+
128
+ // If we switched accounts, update the current account
129
+ let updatedCurrentAccount = currentAccount;
130
+ if (result.switchedTo !== undefined) {
131
+ updatedCurrentAccount =
132
+ updatedAccounts.find(acc => acc.index === result.switchedTo) || currentAccount;
133
+ }
134
+
135
+ syncWallet({
136
+ ...wallet,
137
+ accounts: updatedAccounts,
138
+ currentAccount: updatedCurrentAccount,
139
+ });
140
+
141
+ // Close settings and go back to home
142
+ setShowRemoveConfirm(false);
143
+ navigate('home');
144
+ } else if (result?.error) {
145
+ setShowRemoveConfirm(false);
146
+ if (result.error === 'CANNOT_HIDE_LAST_ACCOUNT') {
147
+ setShowLastAccountError(true);
148
+ } else {
149
+ alert(`Failed to hide account: ${formatWalletError(result.error)}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ function cancelRemoveWallet() {
155
+ setShowRemoveConfirm(false);
156
+ }
157
+
158
+ const addressStart = walletAddress.slice(0, 6);
159
+ const addressMiddle = walletAddress.slice(6, -5);
160
+ const addressEnd = walletAddress.slice(-5);
161
+
162
+ return (
163
+ <div
164
+ className="w-[357px] h-[600px] flex flex-col"
165
+ style={{ backgroundColor: 'var(--color-bg)', color: 'var(--color-text-primary)' }}
166
+ >
167
+ {/* Header */}
168
+ <header
169
+ className="flex items-center justify-between px-4 py-3 min-h-[64px]"
170
+ style={{ backgroundColor: 'var(--color-bg)' }}
171
+ >
172
+ <div className="w-8 h-8 flex items-center justify-center">
173
+ <AccountIcon
174
+ styleId={currentAccount?.iconStyleId}
175
+ color={currentAccount?.iconColor}
176
+ className="w-6 h-6"
177
+ />
178
+ </div>
179
+
180
+ <h1 className="m-0 text-base font-medium leading-[22px] tracking-[0.16px]">{walletName}</h1>
181
+
182
+ <button
183
+ type="button"
184
+ onClick={handleClose}
185
+ className="w-8 h-8 flex items-center justify-center rounded-lg transition-colors focus:outline-none focus-visible:ring-2"
186
+ style={{ color: 'var(--color-text-primary)' }}
187
+ onMouseEnter={e => (e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')}
188
+ onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
189
+ aria-label="Close"
190
+ >
191
+ <CloseIcon />
192
+ </button>
193
+ </header>
194
+
195
+ {/* Content */}
196
+ <div
197
+ className="flex flex-col justify-between h-[536px]"
198
+ style={{ backgroundColor: 'var(--color-bg)' }}
199
+ >
200
+ <div className="flex flex-col gap-4 px-4 py-2">
201
+ {/* Settings Options */}
202
+ <div className="flex flex-col gap-2">
203
+ {/* Account Name */}
204
+ <div
205
+ className="flex items-center justify-between p-2 rounded-lg transition-colors"
206
+ onMouseEnter={e =>
207
+ (e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')
208
+ }
209
+ onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
210
+ >
211
+ <div className="flex items-center gap-2.5 flex-1">
212
+ <div
213
+ className="w-8 h-8 flex items-center justify-center rounded-lg"
214
+ style={{ backgroundColor: 'var(--color-surface-800)' }}
215
+ >
216
+ <img src={UserAccountIcon} alt="Account" className="w-5 h-5" />
217
+ </div>
218
+ <div className="text-sm font-medium leading-[18px] tracking-[0.14px]">
219
+ Wallet name
220
+ </div>
221
+ </div>
222
+
223
+ {isEditingName ? (
224
+ <div
225
+ className="flex items-center gap-2 rounded-lg px-2 py-1.5"
226
+ style={{ border: '1px solid var(--color-divider)' }}
227
+ >
228
+ <input
229
+ type="text"
230
+ value={editedName}
231
+ onChange={handleNameInputChange}
232
+ onKeyDown={handleNameInputKeyDown}
233
+ autoFocus
234
+ maxLength={30}
235
+ className="w-[100px] bg-transparent outline-none text-sm font-medium leading-[18px] tracking-[0.14px]"
236
+ style={{ color: 'var(--color-text-primary)' }}
237
+ placeholder="Wallet name"
238
+ />
239
+ <button
240
+ type="button"
241
+ onClick={handleSaveName}
242
+ className="p-1 rounded transition-opacity hover:opacity-80 focus:outline-none focus-visible:ring-2"
243
+ aria-label="Save name"
244
+ >
245
+ <img src={CheckmarkIcon} alt="" className="w-5 h-5" />
246
+ </button>
247
+ </div>
248
+ ) : (
249
+ <button
250
+ type="button"
251
+ onClick={handleEditName}
252
+ className="flex items-center gap-2 rounded-lg px-2 py-1.5 transition-colors focus:outline-none focus-visible:ring-2"
253
+ style={{ backgroundColor: 'var(--color-surface-800)' }}
254
+ onMouseEnter={e =>
255
+ (e.currentTarget.style.backgroundColor = 'var(--color-surface-700)')
256
+ }
257
+ onMouseLeave={e =>
258
+ (e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')
259
+ }
260
+ >
261
+ <span
262
+ className="text-sm font-medium leading-[18px] tracking-[0.14px] whitespace-nowrap"
263
+ style={{ color: 'var(--color-text-muted)' }}
264
+ >
265
+ {walletName}
266
+ </span>
267
+ <img src={PencilEditIcon} alt="" className="w-5 h-5" />
268
+ </button>
269
+ )}
270
+ </div>
271
+
272
+ {/* Styling */}
273
+ <button
274
+ type="button"
275
+ onClick={handleStyling}
276
+ className="flex items-center justify-between p-2 rounded-lg transition-colors focus:outline-none focus-visible:ring-2"
277
+ onMouseEnter={e =>
278
+ (e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')
279
+ }
280
+ onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
281
+ >
282
+ <div className="flex items-center gap-2.5 flex-1">
283
+ <div
284
+ className="w-8 h-8 flex items-center justify-center rounded-lg"
285
+ style={{ backgroundColor: 'var(--color-surface-800)' }}
286
+ >
287
+ <AccountIcon styleId={1} color="var(--color-text-muted)" className="w-5 h-5" />
288
+ </div>
289
+ <div className="text-sm font-medium leading-[18px] tracking-[0.14px]">Styling</div>
290
+ </div>
291
+ <div className="p-1">
292
+ <ChevronRightIcon className="w-4 h-4" />
293
+ </div>
294
+ </button>
295
+ </div>
296
+
297
+ {/* Address Box */}
298
+ <div
299
+ className="flex flex-col items-center gap-5 px-3 pt-5 pb-3 rounded-lg"
300
+ style={{ backgroundColor: 'var(--color-surface-800)' }}
301
+ >
302
+ <div
303
+ className="text-sm font-medium leading-[18px] tracking-[0.14px] text-center break-words w-full"
304
+ style={{ wordBreak: 'break-all' }}
305
+ >
306
+ <span style={{ color: 'var(--color-text-primary)' }}>{addressStart}</span>
307
+ <span style={{ color: 'var(--color-text-muted)' }}>{addressMiddle}</span>
308
+ <span style={{ color: 'var(--color-text-primary)' }}>{addressEnd}</span>
309
+ </div>
310
+ <button
311
+ type="button"
312
+ onClick={handleCopyAddress}
313
+ className="inline-flex items-center justify-center gap-[6px] py-[7px] pl-3 pr-4 bg-transparent rounded-full text-sm font-medium leading-[18px] tracking-[0.14px] transition-opacity hover:opacity-80 focus:outline-none focus-visible:ring-2"
314
+ style={{
315
+ border: '1px solid var(--color-text-primary)',
316
+ color: 'var(--color-text-primary)',
317
+ }}
318
+ >
319
+ <img src={CopyIcon} alt="" className="w-4 h-4 shrink-0" />
320
+ {copySuccess ? 'Copied!' : 'Copy address'}
321
+ </button>
322
+ </div>
323
+ </div>
324
+
325
+ {/* Footer */}
326
+ <div className="flex flex-col gap-2 px-4 py-3">
327
+ <div
328
+ className="flex items-center justify-between gap-2 px-2 rounded-lg"
329
+ style={{ backgroundColor: 'var(--color-bg)' }}
330
+ >
331
+ <div
332
+ className="text-[10px] leading-4 tracking-[0.24px] flex-1"
333
+ style={{ color: 'var(--color-text-muted)' }}
334
+ >
335
+ Wallet created on {walletCreatedDate}
336
+ </div>
337
+ <div className="flex items-center justify-center rounded-lg py-2 px-3">
338
+ <AccountIcon styleId={1} color="var(--color-text-muted)" className="w-4 h-4" />
339
+ </div>
340
+ </div>
341
+
342
+ <button
343
+ type="button"
344
+ onClick={handleRemoveWallet}
345
+ className="flex items-center justify-center px-5 py-[15px] rounded-lg transition-opacity hover:opacity-80 focus:outline-none focus-visible:ring-2"
346
+ style={{ backgroundColor: 'var(--color-red-light)' }}
347
+ >
348
+ <div
349
+ className="text-sm font-medium leading-[18px] tracking-[0.14px] text-center"
350
+ style={{ color: 'var(--color-red)' }}
351
+ >
352
+ Remove wallet
353
+ </div>
354
+ </button>
355
+ </div>
356
+ </div>
357
+
358
+ {/* Remove Confirmation Modal */}
359
+ <ConfirmModal
360
+ isOpen={showRemoveConfirm}
361
+ title="Are you sure?"
362
+ message={`This will remove "${walletName}" from your wallet. The account and its funds will remain on the blockchain, but no longer be visible in this wallet.`}
363
+ confirmText="Confirm"
364
+ cancelText="Cancel"
365
+ onConfirm={confirmRemoveWallet}
366
+ onCancel={cancelRemoveWallet}
367
+ variant="danger"
368
+ />
369
+
370
+ {/* Last Account Error Modal */}
371
+ <ConfirmModal
372
+ isOpen={showLastAccountError}
373
+ title="Cannot Remove Wallet"
374
+ message="You cannot remove your last visible account. You must have at least one account in your wallet."
375
+ confirmText="OK"
376
+ onConfirm={() => setShowLastAccountError(false)}
377
+ onCancel={() => setShowLastAccountError(false)}
378
+ />
379
+ </div>
380
+ );
381
+ }
@@ -0,0 +1,306 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { useStore } from '../store';
3
+ import { send } from '../utils/messaging';
4
+ import { INTERNAL_METHODS, ACCOUNT_COLORS } from '../../shared/constants';
5
+ import { ChevronLeftIcon } from '../components/icons/ChevronLeftIcon';
6
+ import { ChevronRightIcon } from '../components/icons/ChevronRightIcon';
7
+
8
+ // Icons
9
+ import WalletStyle1 from '../assets/wallet-icon-style-1.svg';
10
+ import WalletStyle2 from '../assets/wallet-icon-style-2.svg';
11
+ import WalletStyle3 from '../assets/wallet-icon-style-3.svg';
12
+ import WalletStyle4 from '../assets/wallet-icon-style-4.svg';
13
+ import WalletStyle5 from '../assets/wallet-icon-style-5.svg';
14
+ import WalletStyle6 from '../assets/wallet-icon-style-6.svg';
15
+ import WalletStyle7 from '../assets/wallet-icon-style-7.svg';
16
+ import WalletStyle8 from '../assets/wallet-icon-style-8.svg';
17
+ import WalletStyle9 from '../assets/wallet-icon-style-9.svg';
18
+ import WalletStyle10 from '../assets/wallet-icon-style-10.svg';
19
+ import WalletStyle11 from '../assets/wallet-icon-style-11.svg';
20
+ import WalletStyle12 from '../assets/wallet-icon-style-12.svg';
21
+ import WalletStyle13 from '../assets/wallet-icon-style-13.svg';
22
+ import WalletStyle14 from '../assets/wallet-icon-style-14.svg';
23
+ import WalletStyle15 from '../assets/wallet-icon-style-15.svg';
24
+
25
+ export function WalletStylingScreen() {
26
+ const { navigate, wallet, syncWallet } = useStore();
27
+
28
+ // Get current account
29
+ const currentAccount = wallet.currentAccount || wallet.accounts[0];
30
+
31
+ // Load initial values from current account or use defaults
32
+ const [selectedStyle, setSelectedStyle] = useState(currentAccount?.iconStyleId || 1);
33
+ const [selectedColor, setSelectedColor] = useState(currentAccount?.iconColor || '#5968fb');
34
+ const [svgContent, setSvgContent] = useState<string>('');
35
+
36
+ // Track if we're scrolled to the end (false = at start, true = at end)
37
+ const [isScrolledRight, setIsScrolledRight] = useState(false);
38
+ const colorScrollRef = useRef<HTMLDivElement>(null);
39
+
40
+ const iconStyles = [
41
+ { id: 1, icon: WalletStyle1 },
42
+ { id: 2, icon: WalletStyle2 },
43
+ { id: 3, icon: WalletStyle3 },
44
+ { id: 4, icon: WalletStyle4 },
45
+ { id: 5, icon: WalletStyle5 },
46
+ { id: 6, icon: WalletStyle6 },
47
+ { id: 7, icon: WalletStyle7 },
48
+ { id: 8, icon: WalletStyle8 },
49
+ { id: 9, icon: WalletStyle9 },
50
+ { id: 10, icon: WalletStyle10 },
51
+ { id: 11, icon: WalletStyle11 },
52
+ { id: 12, icon: WalletStyle12 },
53
+ { id: 13, icon: WalletStyle13 },
54
+ { id: 14, icon: WalletStyle14 },
55
+ { id: 15, icon: WalletStyle15 },
56
+ ];
57
+
58
+ // Use shared color constants
59
+ const colors = ACCOUNT_COLORS;
60
+
61
+ // Sync state when current account changes
62
+ useEffect(() => {
63
+ if (currentAccount) {
64
+ setSelectedStyle(currentAccount.iconStyleId || 1);
65
+ const color = currentAccount.iconColor || '#5968fb';
66
+ setSelectedColor(color);
67
+ }
68
+ }, [currentAccount?.index]);
69
+
70
+ // Load and modify SVG based on selected style and color
71
+ useEffect(() => {
72
+ const selectedIcon = iconStyles.find(s => s.id === selectedStyle);
73
+ if (!selectedIcon) return;
74
+
75
+ fetch(selectedIcon.icon)
76
+ .then(res => res.text())
77
+ .then(text => {
78
+ // Replace CSS var `--fill-0` with the chosen color
79
+ const modifiedSvg = text.replace(/var\(--fill-0,\s*#[A-Fa-f0-9]{6}\)/g, selectedColor);
80
+ setSvgContent(modifiedSvg);
81
+ })
82
+ .catch(err => console.error('Failed to load SVG:', err));
83
+ }, [selectedStyle, selectedColor]);
84
+
85
+ // Persist styling changes
86
+ async function handleStyleChange(styleId: number) {
87
+ if (!currentAccount) return;
88
+
89
+ setSelectedStyle(styleId);
90
+
91
+ const result = await send<{ ok?: boolean; error?: string }>(
92
+ INTERNAL_METHODS.UPDATE_ACCOUNT_STYLING,
93
+ [currentAccount.index, styleId, selectedColor]
94
+ );
95
+
96
+ if (result?.ok) {
97
+ // Update wallet state
98
+ const updatedAccounts = wallet.accounts.map(acc =>
99
+ acc.index === currentAccount.index
100
+ ? { ...acc, iconStyleId: styleId, iconColor: selectedColor }
101
+ : acc
102
+ );
103
+ const updatedCurrentAccount = {
104
+ ...currentAccount,
105
+ iconStyleId: styleId,
106
+ iconColor: selectedColor,
107
+ };
108
+
109
+ syncWallet({
110
+ ...wallet,
111
+ accounts: updatedAccounts,
112
+ currentAccount: updatedCurrentAccount,
113
+ });
114
+ } else if (result?.error) {
115
+ console.error('Failed to update styling:', result.error);
116
+ }
117
+ }
118
+
119
+ async function handleColorChange(color: string) {
120
+ if (!currentAccount) return;
121
+
122
+ setSelectedColor(color);
123
+
124
+ const result = await send<{ ok?: boolean; error?: string }>(
125
+ INTERNAL_METHODS.UPDATE_ACCOUNT_STYLING,
126
+ [currentAccount.index, selectedStyle, color]
127
+ );
128
+
129
+ if (result?.ok) {
130
+ // Update wallet state
131
+ const updatedAccounts = wallet.accounts.map(acc =>
132
+ acc.index === currentAccount.index
133
+ ? { ...acc, iconStyleId: selectedStyle, iconColor: color }
134
+ : acc
135
+ );
136
+ const updatedCurrentAccount = {
137
+ ...currentAccount,
138
+ iconStyleId: selectedStyle,
139
+ iconColor: color,
140
+ };
141
+
142
+ syncWallet({
143
+ ...wallet,
144
+ accounts: updatedAccounts,
145
+ currentAccount: updatedCurrentAccount,
146
+ });
147
+ } else if (result?.error) {
148
+ console.error('Failed to update styling:', result.error);
149
+ }
150
+ }
151
+
152
+ function handleBack() {
153
+ navigate('wallet-settings');
154
+ }
155
+
156
+ function handleColorScrollLeft() {
157
+ if (!colorScrollRef.current) return;
158
+ // Scroll to the start
159
+ colorScrollRef.current.scrollTo({
160
+ left: 0,
161
+ behavior: 'smooth',
162
+ });
163
+ setIsScrolledRight(false);
164
+ }
165
+
166
+ function handleColorScrollRight() {
167
+ if (!colorScrollRef.current) return;
168
+ // Scroll to the end
169
+ colorScrollRef.current.scrollTo({
170
+ left: colorScrollRef.current.scrollWidth,
171
+ behavior: 'smooth',
172
+ });
173
+ setIsScrolledRight(true);
174
+ }
175
+
176
+ return (
177
+ <div
178
+ className="w-[357px] h-[600px] flex flex-col"
179
+ style={{ backgroundColor: 'var(--color-bg)', color: 'var(--color-text-primary)' }}
180
+ >
181
+ {/* Header */}
182
+ <header
183
+ className="flex items-center justify-between px-4 py-3 min-h-[64px]"
184
+ style={{ backgroundColor: 'var(--color-bg)' }}
185
+ >
186
+ <button
187
+ type="button"
188
+ onClick={handleBack}
189
+ aria-label="Back"
190
+ className="w-8 h-8 p-2 flex items-center justify-center rounded-lg transition-colors focus:outline-none focus-visible:ring-2"
191
+ style={{ color: 'var(--color-text-primary)' }}
192
+ onMouseEnter={e => (e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')}
193
+ onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
194
+ >
195
+ <ChevronLeftIcon className="w-5 h-5" />
196
+ </button>
197
+ <h1 className="m-0 text-base font-medium leading-[22px] tracking-[0.16px]">Styling</h1>
198
+ <div className="w-8 h-8" />
199
+ </header>
200
+
201
+ {/* Content */}
202
+ <div className="flex flex-col gap-[20px] h-[536px] pt-[16px] px-0 pb-0">
203
+ {/* Preview */}
204
+ <div className="flex items-center justify-center shrink-0">
205
+ <div className="w-24 h-24 block" dangerouslySetInnerHTML={{ __html: svgContent }} />
206
+ </div>
207
+
208
+ {/* Inner wrapper for padding */}
209
+ <div className="flex flex-col gap-[32px] px-[16px] py-[12px] flex-1 min-h-0">
210
+ {/* Icon Styles Section */}
211
+ <div className="flex flex-col gap-[10px] flex-1 min-h-0">
212
+ <h2 className="text-sm font-medium leading-[18px] tracking-[0.14px] text-center m-0">
213
+ Icon style
214
+ </h2>
215
+ <div className="flex-1 overflow-y-auto min-h-0">
216
+ <div className="flex flex-wrap gap-[8px] justify-center">
217
+ {iconStyles.map(style => {
218
+ const selected = selectedStyle === style.id;
219
+ return (
220
+ <button
221
+ key={style.id}
222
+ type="button"
223
+ onClick={() => handleStyleChange(style.id)}
224
+ className={`flex items-center justify-center p-[10px] rounded-[12px] transition-colors focus:outline-none focus-visible:ring-2 shrink-0 ${!selected ? 'hover:bg-[var(--color-surface-900)]' : ''}`}
225
+ style={{
226
+ width: '58px',
227
+ height: '58px',
228
+ backgroundColor: 'var(--color-bg)',
229
+ border: `1px solid ${selected ? 'var(--color-text-primary)' : 'var(--color-surface-800)'}`,
230
+ }}
231
+ aria-pressed={selected}
232
+ >
233
+ <img src={style.icon} alt={`Style ${style.id}`} className="w-6 h-6" />
234
+ </button>
235
+ );
236
+ })}
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ {/* Icon Color Section */}
242
+ <div className="shrink-0 flex flex-col gap-[10px] pb-[24px]">
243
+ <div className="flex items-center gap-[9px]">
244
+ <button
245
+ type="button"
246
+ onClick={handleColorScrollLeft}
247
+ disabled={!isScrolledRight}
248
+ className="p-[8px] transition-opacity focus:outline-none focus-visible:ring-2 disabled:opacity-30"
249
+ aria-label="Previous color"
250
+ >
251
+ <ChevronLeftIcon className="w-5 h-5" />
252
+ </button>
253
+ <h2 className="flex-1 text-sm font-medium leading-[18px] tracking-[0.14px] text-center m-0">
254
+ Icon color
255
+ </h2>
256
+ <button
257
+ type="button"
258
+ onClick={handleColorScrollRight}
259
+ disabled={isScrolledRight}
260
+ className="p-[8px] transition-opacity focus:outline-none focus-visible:ring-2 disabled:opacity-30"
261
+ aria-label="Next color"
262
+ >
263
+ <ChevronRightIcon className="w-5 h-5" />
264
+ </button>
265
+ </div>
266
+ {/* The rail */}
267
+ <div className="overflow-hidden">
268
+ <div
269
+ ref={colorScrollRef}
270
+ className="flex gap-[8px] justify-start overflow-x-auto snap-x snap-mandatory"
271
+ style={{
272
+ scrollbarWidth: 'none',
273
+ msOverflowStyle: 'none',
274
+ scrollPaddingLeft: 0,
275
+ scrollPaddingRight: 0,
276
+ }}
277
+ >
278
+ {colors.map(color => {
279
+ const selected = selectedColor === color;
280
+ return (
281
+ <button
282
+ key={color}
283
+ type="button"
284
+ onClick={() => handleColorChange(color)}
285
+ className={`flex items-center justify-center p-0 rounded-[12px] transition-colors focus:outline-none focus-visible:ring-2 shrink-0 snap-start ${!selected ? 'hover:bg-[var(--color-surface-900)]' : ''}`}
286
+ style={{
287
+ width: '46px',
288
+ height: '46px',
289
+ backgroundColor: 'var(--color-bg)',
290
+ border: `1px solid ${selected ? 'var(--color-text-primary)' : 'var(--color-surface-800)'}`,
291
+ }}
292
+ aria-label={`Color ${color}`}
293
+ aria-pressed={selected}
294
+ >
295
+ <div className="w-5 h-5 rounded-full" style={{ backgroundColor: color }} />
296
+ </button>
297
+ );
298
+ })}
299
+ </div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ </div>
305
+ );
306
+ }