@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.
- package/.github/workflows/artifacts.yml +33 -0
- package/.github/workflows/ci.yml +68 -0
- package/.github/workflows/publish-sdk.yml +35 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/.prettierrc +8 -0
- package/LICENSE +22 -0
- package/README.md +117 -0
- package/extension/background/index.ts +1500 -0
- package/extension/content/index.ts +59 -0
- package/extension/icons/rose.svg +27 -0
- package/extension/icons/rose128.png +0 -0
- package/extension/icons/rose16.png +0 -0
- package/extension/icons/rose256.png +0 -0
- package/extension/icons/rose32.png +0 -0
- package/extension/icons/rose48.png +0 -0
- package/extension/icons/rose512.png +0 -0
- package/extension/inpage/index.ts +86 -0
- package/extension/manifest.json +48 -0
- package/extension/popup/Popup.tsx +94 -0
- package/extension/popup/Router.tsx +121 -0
- package/extension/popup/assets/arrow-down-icon.svg +3 -0
- package/extension/popup/assets/arrow-left-icon.svg +3 -0
- package/extension/popup/assets/arrow-right-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
- package/extension/popup/assets/checkmark-icon.svg +3 -0
- package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
- package/extension/popup/assets/checkmark-success-icon.svg +3 -0
- package/extension/popup/assets/clock-icon.svg +3 -0
- package/extension/popup/assets/close-x-icon.svg +3 -0
- package/extension/popup/assets/copy-icon.svg +6 -0
- package/extension/popup/assets/explorer-icon.svg +3 -0
- package/extension/popup/assets/eye-off-icon.svg +3 -0
- package/extension/popup/assets/eye-open-icon.svg +4 -0
- package/extension/popup/assets/feedback-icon.svg +3 -0
- package/extension/popup/assets/green-status-dot.svg +3 -0
- package/extension/popup/assets/info-icon.svg +3 -0
- package/extension/popup/assets/iris-logo-40.svg +27 -0
- package/extension/popup/assets/iris-logo-96.svg +27 -0
- package/extension/popup/assets/iris-logo-blue.svg +27 -0
- package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
- package/extension/popup/assets/iris-logo-orange.svg +27 -0
- package/extension/popup/assets/iris-logo.svg +27 -0
- package/extension/popup/assets/key-icon.svg +3 -0
- package/extension/popup/assets/lock-icon-yellow.svg +3 -0
- package/extension/popup/assets/lock-icon.svg +3 -0
- package/extension/popup/assets/pencil-edit-icon.svg +3 -0
- package/extension/popup/assets/permissions-icon.svg +3 -0
- package/extension/popup/assets/receipt-icon.svg +5 -0
- package/extension/popup/assets/refresh-icon.svg +3 -0
- package/extension/popup/assets/settings-gear-icon.svg +8 -0
- package/extension/popup/assets/settings-icon.svg +3 -0
- package/extension/popup/assets/theme-icon.svg +3 -0
- package/extension/popup/assets/trash-bin-icon.svg +3 -0
- package/extension/popup/assets/trend-down-arrow.svg +5 -0
- package/extension/popup/assets/trend-up-arrow.svg +5 -0
- package/extension/popup/assets/user-account-icon.svg +3 -0
- package/extension/popup/assets/vector-bottom-left.svg +9 -0
- package/extension/popup/assets/vector-left.svg +9 -0
- package/extension/popup/assets/vector-right.svg +9 -0
- package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
- package/extension/popup/assets/vector-top-right.svg +9 -0
- package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
- package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
- package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
- package/extension/popup/components/AccountIcon.tsx +78 -0
- package/extension/popup/components/AccountSelector.tsx +246 -0
- package/extension/popup/components/Alert.tsx +48 -0
- package/extension/popup/components/ConfirmModal.tsx +81 -0
- package/extension/popup/components/PasswordInput.tsx +49 -0
- package/extension/popup/components/ScreenContainer.tsx +17 -0
- package/extension/popup/components/SiteIcon.tsx +60 -0
- package/extension/popup/components/ThemeToggle.tsx +44 -0
- package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
- package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
- package/extension/popup/components/icons/CheckIcon.tsx +20 -0
- package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
- package/extension/popup/components/icons/CloseIcon.tsx +26 -0
- package/extension/popup/components/icons/CopyIcon.tsx +20 -0
- package/extension/popup/components/icons/EditIcon.tsx +20 -0
- package/extension/popup/components/icons/EyeIcon.tsx +13 -0
- package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
- package/extension/popup/components/icons/InfoIcon.tsx +20 -0
- package/extension/popup/components/icons/LockIcon.tsx +20 -0
- package/extension/popup/components/icons/PlusIcon.tsx +15 -0
- package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
- package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
- package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
- package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
- package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
- package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
- package/extension/popup/components/icons/UploadIcon.tsx +20 -0
- package/extension/popup/components/icons/WalletIcon.tsx +20 -0
- package/extension/popup/contexts/ThemeContext.tsx +105 -0
- package/extension/popup/hooks/useApprovalDetection.ts +128 -0
- package/extension/popup/hooks/useAutoFocus.ts +36 -0
- package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
- package/extension/popup/hooks/useClickOutside.ts +33 -0
- package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
- package/extension/popup/hooks/useFavicon.ts +64 -0
- package/extension/popup/hooks/useNumericInput.ts +93 -0
- package/extension/popup/index.html +13 -0
- package/extension/popup/index.tsx +24 -0
- package/extension/popup/screens/AboutScreen.tsx +118 -0
- package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
- package/extension/popup/screens/HomeScreen.tsx +902 -0
- package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
- package/extension/popup/screens/LockTimeScreen.tsx +155 -0
- package/extension/popup/screens/ReceiveScreen.tsx +149 -0
- package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
- package/extension/popup/screens/SendReviewScreen.tsx +308 -0
- package/extension/popup/screens/SendScreen.tsx +825 -0
- package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
- package/extension/popup/screens/SettingsScreen.tsx +116 -0
- package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
- package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
- package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
- package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
- package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
- package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
- package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
- package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
- package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
- package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
- package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
- package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
- package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
- package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
- package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
- package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
- package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
- package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
- package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
- package/extension/popup/screens/system/LockedScreen.tsx +288 -0
- package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
- package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
- package/extension/popup/store.ts +482 -0
- package/extension/popup/styles.css +246 -0
- package/extension/popup/utils/format.ts +58 -0
- package/extension/popup/utils/formatWalletError.ts +36 -0
- package/extension/popup/utils/memo.ts +299 -0
- package/extension/popup/utils/messaging.ts +16 -0
- package/extension/shared/address-encoding.ts +69 -0
- package/extension/shared/balance-query.ts +123 -0
- package/extension/shared/constants.ts +386 -0
- package/extension/shared/currency.ts +128 -0
- package/extension/shared/first-name-derivation.ts +128 -0
- package/extension/shared/keyfile.ts +58 -0
- package/extension/shared/onboarding.ts +78 -0
- package/extension/shared/price-api.ts +79 -0
- package/extension/shared/rpc-client-browser.ts +315 -0
- package/extension/shared/transaction-builder.ts +443 -0
- package/extension/shared/types.ts +450 -0
- package/extension/shared/utxo-diff.ts +212 -0
- package/extension/shared/utxo-store.ts +548 -0
- package/extension/shared/utxo-sync.ts +343 -0
- package/extension/shared/validators.ts +26 -0
- package/extension/shared/vault.ts +1580 -0
- package/extension/shared/wallet-crypto.ts +77 -0
- package/extension/shared/wasm-utils.ts +76 -0
- package/extension/shared/webcrypto.ts +67 -0
- package/extension/types/wasm.d.ts +13 -0
- package/package.json +39 -0
- package/postcss.config.js +6 -0
- package/rose-extension-dist.zip +0 -0
- package/sdk/README.md +88 -0
- package/sdk/examples/app.ts +166 -0
- package/sdk/examples/index.html +51 -0
- package/sdk/examples/tsconfig.json +15 -0
- package/sdk/examples/tx-builder.html +532 -0
- package/sdk/examples/tx-builder.ts +1766 -0
- package/sdk/package-lock.json +424 -0
- package/sdk/package.json +68 -0
- package/sdk/src/constants.ts +28 -0
- package/sdk/src/errors.ts +74 -0
- package/sdk/src/hooks/index.ts +1 -0
- package/sdk/src/hooks/use-rose.ts +94 -0
- package/sdk/src/index.ts +12 -0
- package/sdk/src/provider.ts +396 -0
- package/sdk/src/transaction.ts +163 -0
- package/sdk/src/types/rose-wasm.d.ts +14 -0
- package/sdk/src/types.ts +97 -0
- package/sdk/src/wasm.ts +13 -0
- package/sdk/tsconfig.json +20 -0
- package/sdk/vite.config.examples.ts +32 -0
- package/tailwind.config.ts +38 -0
- package/tsconfig.json +20 -0
- 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
|
+
}
|