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