@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,678 @@
1
+ /**
2
+ * Onboarding Import Screen - Import V0 wallet from mnemonic
3
+ */
4
+
5
+ import { useState, useRef, useEffect } from 'react';
6
+ import { useStore } from '../../store';
7
+ import { Alert } from '../../components/Alert';
8
+ import { useAutoFocus } from '../../hooks/useAutoFocus';
9
+ import { markOnboardingComplete } from '../../../shared/onboarding';
10
+ import { INTERNAL_METHODS, UI_CONSTANTS, ERROR_CODES } from '../../../shared/constants';
11
+ import { send } from '../../utils/messaging';
12
+ import { formatWalletError } from '../../utils/formatWalletError';
13
+ import lockIcon from '../../assets/lock-icon.svg';
14
+ import { EyeIcon } from '../../components/icons/EyeIcon';
15
+ import { EyeOffIcon } from '../../components/icons/EyeOffIcon';
16
+ import { InfoIcon } from '../../components/icons/InfoIcon';
17
+
18
+ export function ImportScreenV0() {
19
+ const { navigate, wallet, syncWallet, onboardingMnemonicV0, setOnboardingMnemonicV0 } =
20
+ useStore();
21
+ const isOnboarding = !Boolean(wallet.currentAccount);
22
+ const [isImported, setIsImported] = useState<boolean | null>(null);
23
+ const [isClearingImport, setIsClearingImport] = useState(false);
24
+
25
+ // Clear any stale mnemonic state on mount to ensure fresh start
26
+ useEffect(() => {
27
+ if (onboardingMnemonicV0) {
28
+ setOnboardingMnemonicV0(null);
29
+ }
30
+ }, []);
31
+ const [wordsV0, setWordsV0] = useState<string[]>(
32
+ Array(UI_CONSTANTS.MNEMONIC_WORD_COUNT).fill('')
33
+ );
34
+ const [password, setPassword] = useState('');
35
+ const [confirmPassword, setConfirmPassword] = useState('');
36
+ const [showPassword, setShowPassword] = useState(false);
37
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
38
+ const [error, setError] = useState('');
39
+ const [step, setStep] = useState<'mnemonic' | 'password'>('mnemonic');
40
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
41
+ const firstInputRef = useAutoFocus<HTMLInputElement>();
42
+
43
+ // Check existing v0 seed stored in vault when in v0 flow
44
+ useEffect(() => {
45
+ (async () => {
46
+ const res = await send<{ ok?: boolean; has?: boolean; error?: unknown }>(
47
+ INTERNAL_METHODS.HAS_V0_MNEMONIC,
48
+ []
49
+ );
50
+ if (res?.ok) {
51
+ setIsImported(Boolean(res.has));
52
+ }
53
+ })();
54
+ }, []);
55
+
56
+ function handleWordChange(index: number, value: string) {
57
+ const trimmedValue = value.trim().toLowerCase();
58
+ wordsV0[index] = trimmedValue;
59
+ setWordsV0(wordsV0);
60
+
61
+ setError('');
62
+
63
+ // Auto-advance to next field on space
64
+ if (value.endsWith(' ')) {
65
+ const nextIndex = index + 1;
66
+ if (nextIndex < UI_CONSTANTS.MNEMONIC_WORD_COUNT) {
67
+ inputRefs.current[nextIndex]?.focus();
68
+ }
69
+ }
70
+ }
71
+
72
+ // Handle paste in first field to auto-fill all words
73
+ function handlePaste(index: number, e: React.ClipboardEvent<HTMLInputElement>) {
74
+ if (index === 0) {
75
+ const pasteData = e.clipboardData.getData('text');
76
+ const pastedWords = pasteData.trim().toLowerCase().split(/\s+/);
77
+
78
+ if (pastedWords.length === UI_CONSTANTS.MNEMONIC_WORD_COUNT) {
79
+ e.preventDefault();
80
+ setWordsV0(pastedWords);
81
+ setError('');
82
+ }
83
+ }
84
+ }
85
+
86
+ async function handleKeyDown(index: number, e: React.KeyboardEvent<HTMLInputElement>) {
87
+ // Backspace on empty field goes to previous
88
+ if (e.key === 'Backspace' && !wordsV0[index] && index > 0) {
89
+ inputRefs.current[index - 1]?.focus();
90
+ }
91
+ // Enter advances to next field
92
+ if (e.key === 'Enter') {
93
+ e.preventDefault();
94
+ const nextIndex = index + 1;
95
+ if (nextIndex < UI_CONSTANTS.MNEMONIC_WORD_COUNT) {
96
+ inputRefs.current[nextIndex]?.focus();
97
+ } else {
98
+ await handleContinue();
99
+ }
100
+ }
101
+ }
102
+
103
+ async function handleContinue() {
104
+ const mnemonicV0 = wordsV0.join(' ').trim();
105
+ if (isOnboarding) {
106
+ if (wordsV0.some(w => !w)) {
107
+ setError('Please enter all 24 words');
108
+ return;
109
+ }
110
+
111
+ setOnboardingMnemonicV0(mnemonicV0);
112
+ setStep('password');
113
+ } else {
114
+ handleImport();
115
+ }
116
+ }
117
+ async function handleImport() {
118
+ // Use stored mnemonic (set either by manual entry or keyfile import)
119
+ const mnemonicV0 = onboardingMnemonicV0 || wordsV0.join(' ').trim();
120
+
121
+ let result:
122
+ | {
123
+ ok?: boolean;
124
+ address?: string;
125
+ mnemonic?: string;
126
+ error?: string;
127
+ }
128
+ | undefined;
129
+
130
+ // Onboarding flow - setup new wallet
131
+ if (isOnboarding) {
132
+ // Validate password
133
+ if (!password) {
134
+ setError('Please enter a password');
135
+ return;
136
+ }
137
+
138
+ if (password.length < UI_CONSTANTS.MIN_PASSWORD_LENGTH) {
139
+ setError(`Password must be at least ${UI_CONSTANTS.MIN_PASSWORD_LENGTH} characters`);
140
+ return;
141
+ }
142
+
143
+ if (password !== confirmPassword) {
144
+ setError('Passwords do not match');
145
+ return;
146
+ }
147
+
148
+ result = await send<{
149
+ ok?: boolean;
150
+ address?: string;
151
+ mnemonic?: string;
152
+ error?: string;
153
+ }>(INTERNAL_METHODS.SETUP, [password, '', mnemonicV0]);
154
+
155
+ if (result?.error) {
156
+ if (result.error === ERROR_CODES.INVALID_V0_MNEMONIC) {
157
+ setError('Invalid secret phrase. Please check your words and try again.');
158
+ } else {
159
+ setError(formatWalletError(result.error));
160
+ }
161
+ } else {
162
+ // Successfully imported - mark onboarding complete (user already has their seed)
163
+ await markOnboardingComplete();
164
+
165
+ const firstAccount = {
166
+ name: 'Wallet 1',
167
+ address: result.address || '',
168
+ index: 0,
169
+ };
170
+ syncWallet({
171
+ locked: false,
172
+ address: result.address || null,
173
+ accounts: [firstAccount],
174
+ currentAccount: firstAccount,
175
+ balance: 0,
176
+ availableBalance: 0,
177
+ spendableBalance: 0,
178
+ accountBalances: {},
179
+ accountSpendableBalances: {},
180
+ accountBalanceDetails: {},
181
+ });
182
+ setOnboardingMnemonicV0(null);
183
+ navigate('onboarding-import-success');
184
+ }
185
+ } else {
186
+ // Import flow - import existing wallet
187
+ result = await send<{
188
+ ok?: boolean;
189
+ error?: string;
190
+ }>(INTERNAL_METHODS.SET_V0_MNEMONIC, [mnemonicV0]);
191
+
192
+ if (result?.error) {
193
+ setError(formatWalletError(result.error));
194
+ } else {
195
+ navigate('onboarding-import-success');
196
+ }
197
+ }
198
+ }
199
+
200
+ function handleBack() {
201
+ if (step === 'password') {
202
+ setStep('mnemonic');
203
+ } else {
204
+ if (isOnboarding) {
205
+ navigate('onboarding-start');
206
+ } else {
207
+ navigate('home');
208
+ }
209
+ }
210
+ }
211
+
212
+ function handleClearV0Mnemonic() {
213
+ setOnboardingMnemonicV0(null);
214
+ setWordsV0(Array(UI_CONSTANTS.MNEMONIC_WORD_COUNT).fill(''));
215
+ setError('');
216
+ }
217
+
218
+ async function handleClearStoredV0() {
219
+ setIsClearingImport(true);
220
+ setError('');
221
+ try {
222
+ const res = await send<{ ok?: boolean; error?: unknown }>(
223
+ INTERNAL_METHODS.CLEAR_V0_MNEMONIC,
224
+ []
225
+ );
226
+ if ((res as any)?.error) {
227
+ setError(formatWalletError((res as any).error));
228
+ } else {
229
+ setIsImported(false);
230
+ handleClearV0Mnemonic();
231
+ }
232
+ } catch {
233
+ setError('Failed to remove stored v0 seedphrase');
234
+ } finally {
235
+ setIsClearingImport(false);
236
+ }
237
+ }
238
+
239
+ // Password setup step
240
+ if (step === 'password') {
241
+ return (
242
+ <div className="relative w-[357px] h-[600px] bg-[var(--color-bg)]">
243
+ {/* Header with back button */}
244
+ <div className="flex items-center justify-between h-16 px-4 py-3 border-b border-[var(--color-divider)]">
245
+ <button
246
+ onClick={handleBack}
247
+ className="p-2 -ml-2 hover:opacity-70 transition-opacity"
248
+ aria-label="Go back"
249
+ >
250
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
251
+ <path
252
+ d="M10 12L6 8L10 4"
253
+ stroke="var(--color-text-primary)"
254
+ strokeWidth="2"
255
+ strokeLinecap="round"
256
+ strokeLinejoin="round"
257
+ />
258
+ </svg>
259
+ </button>
260
+ <h2
261
+ className="font-sans font-medium text-[var(--color-text-primary)]"
262
+ style={{
263
+ fontSize: 'var(--font-size-lg)',
264
+ lineHeight: 'var(--line-height-normal)',
265
+ letterSpacing: '0.01em',
266
+ }}
267
+ >
268
+ Encrypt your wallet
269
+ </h2>
270
+ <div className="w-8" />
271
+ </div>
272
+
273
+ {/* Main content */}
274
+ <div className="flex flex-col justify-between h-[536px]">
275
+ <div className="px-4 py-2 flex flex-col gap-6">
276
+ {/* Icon and heading */}
277
+ <div className="flex flex-col items-center gap-3">
278
+ <div className="w-10 h-10">
279
+ <img src={lockIcon} alt="" className="w-full h-full" />
280
+ </div>
281
+ <div className="flex flex-col gap-2 items-center text-center w-full">
282
+ <h1
283
+ className="font-serif font-medium text-[var(--color-text-primary)]"
284
+ style={{
285
+ fontSize: 'var(--font-size-xl)',
286
+ lineHeight: 'var(--line-height-relaxed)',
287
+ letterSpacing: '-0.02em',
288
+ }}
289
+ >
290
+ Choose a strong password
291
+ </h1>
292
+ <p
293
+ className="font-sans text-[var(--color-text-muted)]"
294
+ style={{
295
+ fontSize: 'var(--font-size-sm)',
296
+ lineHeight: 'var(--line-height-snug)',
297
+ letterSpacing: '0.02em',
298
+ }}
299
+ >
300
+ This password encrypts your wallet
301
+ </p>
302
+ </div>
303
+ </div>
304
+
305
+ {/* Password fields */}
306
+ <div className="flex flex-col gap-6 w-full">
307
+ {/* Password input */}
308
+ <div className="flex flex-col gap-1.5">
309
+ <label
310
+ htmlFor="password"
311
+ className="font-sans font-medium text-[var(--color-text-primary)]"
312
+ style={{
313
+ fontSize: 'var(--font-size-sm)',
314
+ lineHeight: 'var(--line-height-snug)',
315
+ letterSpacing: '0.02em',
316
+ }}
317
+ >
318
+ Create password
319
+ </label>
320
+ <div className="relative">
321
+ <input
322
+ id="password"
323
+ type={showPassword ? 'text' : 'password'}
324
+ value={password}
325
+ onChange={e => {
326
+ setPassword(e.target.value);
327
+ setError('');
328
+ }}
329
+ className="w-full h-[52px] px-3 py-4 bg-transparent border border-[var(--color-surface-700)] rounded-lg font-sans font-medium text-[var(--color-text-primary)] placeholder:text-[var(--color-text-secondary)] focus:outline-none focus:border-[var(--color-primary)]"
330
+ style={{
331
+ fontSize: 'var(--font-size-base)',
332
+ lineHeight: 'var(--line-height-snug)',
333
+ letterSpacing: '0.01em',
334
+ }}
335
+ autoFocus
336
+ />
337
+ <button
338
+ type="button"
339
+ onClick={() => setShowPassword(!showPassword)}
340
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] transition-colors"
341
+ tabIndex={-1}
342
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
343
+ >
344
+ {showPassword ? (
345
+ <EyeIcon className="w-4 h-4" />
346
+ ) : (
347
+ <EyeOffIcon className="w-4 h-4" />
348
+ )}
349
+ </button>
350
+ </div>
351
+ </div>
352
+
353
+ {/* Confirm password input */}
354
+ <div className="flex flex-col gap-1.5">
355
+ <label
356
+ htmlFor="confirmPassword"
357
+ className="font-sans font-medium text-[var(--color-text-primary)]"
358
+ style={{
359
+ fontSize: 'var(--font-size-sm)',
360
+ lineHeight: 'var(--line-height-snug)',
361
+ letterSpacing: '0.02em',
362
+ }}
363
+ >
364
+ Confirm password
365
+ </label>
366
+ <div className="relative">
367
+ <input
368
+ id="confirmPassword"
369
+ type={showConfirmPassword ? 'text' : 'password'}
370
+ value={confirmPassword}
371
+ onChange={e => {
372
+ setConfirmPassword(e.target.value);
373
+ setError('');
374
+ }}
375
+ onKeyDown={e => e.key === 'Enter' && handleImport()}
376
+ className="w-full h-[52px] px-3 py-4 bg-transparent border border-[var(--color-surface-700)] rounded-lg font-sans font-medium text-[var(--color-text-primary)] placeholder:text-[var(--color-text-secondary)] focus:outline-none focus:border-[var(--color-primary)]"
377
+ style={{
378
+ fontSize: 'var(--font-size-base)',
379
+ lineHeight: 'var(--line-height-snug)',
380
+ letterSpacing: '0.01em',
381
+ }}
382
+ />
383
+ <button
384
+ type="button"
385
+ onClick={() => setShowConfirmPassword(!showConfirmPassword)}
386
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] transition-colors"
387
+ tabIndex={-1}
388
+ aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
389
+ >
390
+ {showConfirmPassword ? (
391
+ <EyeIcon className="w-4 h-4" />
392
+ ) : (
393
+ <EyeOffIcon className="w-4 h-4" />
394
+ )}
395
+ </button>
396
+ </div>
397
+ </div>
398
+ </div>
399
+
400
+ {/* Info box */}
401
+ <div className="bg-[var(--color-surface-900)] rounded-lg p-3">
402
+ <p
403
+ className="font-sans font-medium text-center text-[var(--color-text-muted)]"
404
+ style={{
405
+ fontSize: 'var(--font-size-xs)',
406
+ lineHeight: 'var(--line-height-tight)',
407
+ letterSpacing: '0.02em',
408
+ }}
409
+ >
410
+ This password encrypts your wallet on this device. Choose something strong but
411
+ memorable. Your private keys never leave your browser.
412
+ </p>
413
+ </div>
414
+
415
+ {/* Error message */}
416
+ {error && <Alert type="error">{error}</Alert>}
417
+ </div>
418
+
419
+ {/* Bottom buttons */}
420
+ <div className="border-t border-[var(--color-surface-800)] px-4 py-3">
421
+ <div className="flex gap-3">
422
+ <button
423
+ onClick={handleBack}
424
+ className="flex-1 h-12 px-5 py-[15px] bg-[var(--color-surface-800)] text-[var(--color-text-primary)] rounded-lg flex items-center justify-center transition-opacity hover:opacity-90"
425
+ style={{
426
+ fontFamily: 'var(--font-sans)',
427
+ fontSize: 'var(--font-size-base)',
428
+ fontWeight: 500,
429
+ lineHeight: 'var(--line-height-snug)',
430
+ letterSpacing: '0.01em',
431
+ }}
432
+ >
433
+ Back
434
+ </button>
435
+ <button
436
+ onClick={handleImport}
437
+ className="flex-1 h-12 px-5 py-[15px] btn-primary text-[#000000] rounded-lg flex items-center justify-center transition-opacity hover:opacity-90"
438
+ style={{
439
+ fontFamily: 'var(--font-sans)',
440
+ fontSize: 'var(--font-size-base)',
441
+ fontWeight: 500,
442
+ lineHeight: 'var(--line-height-snug)',
443
+ letterSpacing: '0.01em',
444
+ }}
445
+ >
446
+ Import v0 wallet
447
+ </button>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ </div>
452
+ );
453
+ }
454
+
455
+ // Mnemonic entry step
456
+ return (
457
+ <div className="relative w-[357px] h-[600px] bg-[var(--color-bg)]">
458
+ {/* Header with back button */}
459
+ <div className="flex items-center justify-between h-16 px-4 py-3 border-b border-[var(--color-divider)]">
460
+ <button
461
+ onClick={handleBack}
462
+ className="p-2 -ml-2 hover:opacity-70 transition-opacity"
463
+ aria-label="Go back"
464
+ >
465
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
466
+ <path
467
+ d="M10 12L6 8L10 4"
468
+ stroke="var(--color-text-primary)"
469
+ strokeWidth="2"
470
+ strokeLinecap="round"
471
+ strokeLinejoin="round"
472
+ />
473
+ </svg>
474
+ </button>
475
+ <h2
476
+ className="font-sans font-medium text-[var(--color-text-primary)]"
477
+ style={{
478
+ fontSize: 'var(--font-size-lg)',
479
+ lineHeight: 'var(--line-height-normal)',
480
+ letterSpacing: '0.01em',
481
+ }}
482
+ >
483
+ {isImported === true ? 'Upgrade v0 Wallet' : 'Import v0 Wallet'}
484
+ </h2>
485
+ <div className="w-8" />
486
+ </div>
487
+
488
+ {/* Main content - scrollable */}
489
+ <div className="h-[536px] flex flex-col">
490
+ <div className="flex-1 overflow-y-auto no-scrollbar">
491
+ <div className="px-4 py-2 flex flex-col gap-6">
492
+ {isImported === true && (
493
+ <>
494
+ {/* migration hint box */}
495
+ <div
496
+ className="flex items-start gap-2 p-3 rounded-lg"
497
+ style={{
498
+ backgroundColor: 'var(--color-surface-800)',
499
+ border: '1px solid var(--color-surface-700)',
500
+ }}
501
+ >
502
+ <InfoIcon className="w-5 h-5 flex-shrink-0 mt-0.5 m-auto" />
503
+ <div className="flex-1">
504
+ <p
505
+ className="font-sans text-center text-[var(--color-text-muted)]"
506
+ style={{
507
+ fontSize: 'var(--font-size-sm)',
508
+ lineHeight: 'var(--line-height-snug)',
509
+ letterSpacing: '0.01em',
510
+ }}
511
+ >
512
+ Hint: Upgrade your v0 notes with the upgrade tool.
513
+ <br />
514
+ <a
515
+ href="https://nocknames.com/upgrade"
516
+ target="_blank"
517
+ rel="noopener noreferrer"
518
+ className="underline hover:opacity-70 text-[#37f] font-bold"
519
+ >
520
+ nocknames.com/upgrade
521
+ </a>
522
+ </p>
523
+ </div>
524
+ </div>
525
+ <div
526
+ className="flex flex-col gap-2 p-3 rounded-lg border"
527
+ style={{
528
+ borderColor: 'var(--color-surface-700)',
529
+ backgroundColor: 'var(--color-surface-900)',
530
+ }}
531
+ >
532
+ <p
533
+ className="font-sans text-[var(--color-text-muted)] text-sm leading-[18px] m-0 text-center"
534
+ style={{ letterSpacing: '0.01em' }}
535
+ >
536
+ A v0 seedphrase is already stored in your vault. Delete it to enter a new one.
537
+ </p>
538
+ <button
539
+ onClick={handleClearStoredV0}
540
+ className="btn-secondary h-10 rounded-lg font-sans font-medium text-sm transition-opacity hover:opacity-90"
541
+ disabled={isClearingImport}
542
+ >
543
+ {isClearingImport ? 'Deleting…' : 'Delete v0 seed and enter a new one'}
544
+ </button>
545
+ </div>
546
+ </>
547
+ )}
548
+ {isImported !== true && (
549
+ <>
550
+ {/* Icon and instructions */}
551
+ <div className="flex flex-col items-center gap-3">
552
+ <div className="w-10 h-10">
553
+ <img src={lockIcon} alt="" className="w-full h-full" />
554
+ </div>
555
+ <p
556
+ className="font-sans font-medium text-center text-[var(--color-text-primary)]"
557
+ style={{
558
+ fontSize: 'var(--font-size-base)',
559
+ lineHeight: 'var(--line-height-snug)',
560
+ letterSpacing: '0.01em',
561
+ }}
562
+ >
563
+ Enter your 24-word secret phrase.
564
+ <br />
565
+ Paste into first field to auto-fill all words.
566
+ </p>
567
+ </div>
568
+ {/* migration hint box */}
569
+ <div
570
+ className="flex items-start gap-2 p-3 rounded-lg"
571
+ style={{
572
+ backgroundColor: 'var(--color-surface-800)',
573
+ border: '1px solid var(--color-surface-700)',
574
+ }}
575
+ >
576
+ <InfoIcon className="w-5 h-5 flex-shrink-0 mt-0.5 m-auto" />
577
+ <div className="flex-1">
578
+ <p
579
+ className="font-sans text-center text-[var(--color-text-muted)]"
580
+ style={{
581
+ fontSize: 'var(--font-size-sm)',
582
+ lineHeight: 'var(--line-height-snug)',
583
+ letterSpacing: '0.01em',
584
+ }}
585
+ >
586
+ Hint: Use the upgrade tool to move your notes after import.
587
+ <br />
588
+ <a
589
+ href="https://nocknames.com/upgrade"
590
+ target="_blank"
591
+ rel="noopener noreferrer"
592
+ className="underline hover:opacity-70 text-[#37f] font-bold"
593
+ >
594
+ nocknames.com/upgrade
595
+ </a>
596
+ </p>
597
+ </div>
598
+ </div>
599
+
600
+ {/* 24-word input grid */}
601
+ <div className="flex flex-col gap-2 w-full pb-4">
602
+ {Array.from({ length: 12 }).map((_, rowIndex) => (
603
+ <div key={rowIndex} className="flex gap-2 w-full">
604
+ {[0, 1].map(col => {
605
+ const index = rowIndex * 2 + col;
606
+ return (
607
+ <div
608
+ key={col}
609
+ className="flex-1 min-w-0 bg-[var(--color-bg)] border border-[var(--color-surface-700)] rounded-lg p-2 flex items-center gap-2.5 h-11"
610
+ >
611
+ <span
612
+ className="bg-[var(--color-surface-700)] rounded w-7 h-7 flex items-center justify-center font-sans font-medium text-[var(--color-text-primary)] flex-shrink-0"
613
+ style={{
614
+ fontSize: 'var(--font-size-base)',
615
+ lineHeight: 'var(--line-height-snug)',
616
+ letterSpacing: '0.01em',
617
+ }}
618
+ >
619
+ {index + 1}
620
+ </span>
621
+ <input
622
+ ref={el => {
623
+ inputRefs.current[index] = el;
624
+ if (index === 0) {
625
+ // @ts-ignore - Assign to auto-focus ref
626
+ firstInputRef.current = el;
627
+ }
628
+ }}
629
+ type="text"
630
+ value={wordsV0[index]}
631
+ onChange={e => handleWordChange(index, e.target.value)}
632
+ onKeyDown={e => handleKeyDown(index, e)}
633
+ onPaste={e => handlePaste(index, e)}
634
+ placeholder="word"
635
+ autoComplete="off"
636
+ spellCheck="false"
637
+ className="flex-1 min-w-0 bg-transparent font-sans font-medium text-[var(--color-text-primary)] placeholder:text-[var(--color-text-secondary)] outline-none"
638
+ style={{
639
+ fontSize: 'var(--font-size-base)',
640
+ lineHeight: 'var(--line-height-snug)',
641
+ letterSpacing: '0.01em',
642
+ }}
643
+ />
644
+ </div>
645
+ );
646
+ })}
647
+ </div>
648
+ ))}
649
+ </div>
650
+ </>
651
+ )}
652
+
653
+ {/* Error message */}
654
+ {error && <Alert type="error">{error}</Alert>}
655
+ </div>
656
+ </div>
657
+
658
+ {/* Bottom button */}
659
+ <div className="border-t border-[var(--color-surface-800)] px-4 py-3">
660
+ <button
661
+ onClick={handleContinue}
662
+ disabled={wordsV0.some(w => !w)}
663
+ className="w-full h-12 px-5 py-[15px] btn-primary text-[#000000] rounded-lg flex items-center justify-center transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
664
+ style={{
665
+ fontFamily: 'var(--font-sans)',
666
+ fontSize: 'var(--font-size-base)',
667
+ fontWeight: 500,
668
+ lineHeight: 'var(--line-height-snug)',
669
+ letterSpacing: '0.01em',
670
+ }}
671
+ >
672
+ Import v0 wallet
673
+ </button>
674
+ </div>
675
+ </div>
676
+ </div>
677
+ );
678
+ }