refactor: 3311025/recoding_create_wallet_flow
This commit is contained in:
parent
a39f306f8d
commit
08ed2814c9
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,6 +13,8 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
.vite/build/*
|
||||||
|
wallets/*
|
||||||
|
|
||||||
/cypress/videos/
|
/cypress/videos/
|
||||||
/cypress/screenshots/
|
/cypress/screenshots/
|
||||||
|
|||||||
24
electron/ipcHandlers.ts
Normal file
24
electron/ipcHandlers.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import type { HDNodeWallet } from 'ethers';
|
||||||
|
import { Wallet } from 'ethers';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
|
||||||
|
const wallet = Wallet.fromPhrase(seed);
|
||||||
|
const keystore = await wallet.encrypt(password);
|
||||||
|
|
||||||
|
const savePath = path.join(process.cwd(), 'wallets');
|
||||||
|
fs.mkdirSync(savePath, { recursive: true });
|
||||||
|
|
||||||
|
const filePath = path.join(savePath, `${wallet.address}.json`);
|
||||||
|
fs.writeFileSync(filePath, keystore);
|
||||||
|
|
||||||
|
return { address: wallet.address, filePath };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('wallet:decryptKeystore', async (_event, filePath, password) => {
|
||||||
|
const json = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const wallet = await Wallet.fromEncryptedJson(json, password) as HDNodeWallet;
|
||||||
|
return { address: wallet.address, phrase: wallet.mnemonic };
|
||||||
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { app, BrowserWindow } from 'electron'
|
import { app, BrowserWindow } from 'electron'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import started from 'electron-squirrel-startup'
|
import started from 'electron-squirrel-startup'
|
||||||
|
import './ipcHandlers'
|
||||||
|
|
||||||
if (started) {
|
if (started) {
|
||||||
app.quit()
|
app.quit()
|
||||||
@ -8,10 +9,12 @@ if (started) {
|
|||||||
|
|
||||||
const createWindow = () => {
|
const createWindow = () => {
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 800,
|
width: 1000,
|
||||||
height: 600,
|
height: 800,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||||
|
|||||||
@ -1,2 +1,10 @@
|
|||||||
// See the Electron documentation for details on how to use preload scripts:
|
// See the Electron documentation for details on how to use preload scripts:
|
||||||
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('walletApi', {
|
||||||
|
createKeystore: (seed: string, password: string) =>
|
||||||
|
ipcRenderer.invoke('wallet:createKeystore', seed, password),
|
||||||
|
decryptKeystore: (filePath: string, password: string) =>
|
||||||
|
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password),
|
||||||
|
})
|
||||||
|
|||||||
107
package-lock.json
generated
107
package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
"ethers": "^6.15.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.3.0",
|
||||||
@ -52,6 +53,12 @@
|
|||||||
"vue-tsc": "^2.0.11"
|
"vue-tsc": "^2.0.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@adraffy/ens-normalize": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
|
||||||
|
"integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@ant-design/colors": {
|
"node_modules/@ant-design/colors": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz",
|
||||||
@ -2526,6 +2533,30 @@
|
|||||||
"resolved": "packages/neptune-wasm",
|
"resolved": "packages/neptune-wasm",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/curves": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -4307,6 +4338,12 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aes-js": {
|
||||||
|
"version": "4.0.0-beta.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
|
||||||
|
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@ -6697,6 +6734,49 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ethers": {
|
||||||
|
"version": "6.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz",
|
||||||
|
"integrity": "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/ethers-io/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.buymeacoffee.com/ricmoo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@adraffy/ens-normalize": "1.10.1",
|
||||||
|
"@noble/curves": "1.2.0",
|
||||||
|
"@noble/hashes": "1.3.2",
|
||||||
|
"@types/node": "22.7.5",
|
||||||
|
"aes-js": "4.0.0-beta.5",
|
||||||
|
"tslib": "2.7.0",
|
||||||
|
"ws": "8.17.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ethers/node_modules/@types/node": {
|
||||||
|
"version": "22.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
|
||||||
|
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ethers/node_modules/undici-types": {
|
||||||
|
"version": "6.19.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
@ -11271,6 +11351,12 @@
|
|||||||
"typescript": ">=4.2.0"
|
"typescript": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@ -12095,6 +12181,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wsl-utils": {
|
"node_modules/wsl-utils": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
"ethers": "^6.15.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.3.0",
|
||||||
|
|||||||
@ -8,22 +8,8 @@ const instance = axios.create({
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
method: 'POST',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
instance.interceptors.response.use(
|
|
||||||
function (response) {
|
|
||||||
// if (response?.status !== STATUS_CODE_SUCCESS) return Promise.reject(response?.data)
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
if (error?.response?.data) {
|
|
||||||
return Promise.reject(error?.response?.data)
|
|
||||||
}
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const setLocaleApi = (locale: string) => {
|
export const setLocaleApi = (locale: string) => {
|
||||||
instance.defaults.headers.common['lang'] = locale
|
instance.defaults.headers.common['lang'] = locale
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,19 +43,3 @@ export const sendTransaction = async (
|
|||||||
export const broadcastSignedTransaction = async (signedTxData: any): Promise<any> => {
|
export const broadcastSignedTransaction = async (signedTxData: any): Promise<any> => {
|
||||||
return await callJsonRpc('wallet_broadcastSignedTransaction', signedTxData)
|
return await callJsonRpc('wallet_broadcastSignedTransaction', signedTxData)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTransaction = async (txHash: string): Promise<any> => {
|
|
||||||
return await callJsonRpc('chain_transaction', [txHash])
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMempoolInfo = async (): Promise<any> => {
|
|
||||||
return await callJsonRpc('chain_mempool', [])
|
|
||||||
}
|
|
||||||
|
|
||||||
export const importKeystore = async (keystore: string, password: string): Promise<any> => {
|
|
||||||
const params = {
|
|
||||||
keystore,
|
|
||||||
password,
|
|
||||||
}
|
|
||||||
return await callJsonRpc('wallet_importKeystore', params)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -144,63 +144,6 @@ export function useNeptuneWallet() {
|
|||||||
return JSON.parse(resultJson)
|
return JSON.parse(resultJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
const importFromViewKey = async (viewKeyHex: string): Promise<{ receiver_identifier: string }> => {
|
|
||||||
try {
|
|
||||||
store.setLoading(true)
|
|
||||||
store.setError(null)
|
|
||||||
|
|
||||||
const result = await decodeViewKey(viewKeyHex)
|
|
||||||
|
|
||||||
store.setViewKey(viewKeyHex)
|
|
||||||
store.setReceiverId(result.receiver_identifier)
|
|
||||||
// Note: When importing from viewkey, we don't have the seed phrase
|
|
||||||
// and address needs to be derived from viewkey
|
|
||||||
|
|
||||||
return result
|
|
||||||
} catch (err) {
|
|
||||||
const errorMsg = err instanceof Error ? err.message : 'Failed to import from view key'
|
|
||||||
store.setError(errorMsg)
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
store.setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const importFromKeystore = async (keystore: string, password: string): Promise<any> => {
|
|
||||||
try {
|
|
||||||
store.setLoading(true)
|
|
||||||
store.setError(null)
|
|
||||||
|
|
||||||
const response = await API.importKeystore(keystore, password)
|
|
||||||
const result = response.data?.result || response.data
|
|
||||||
|
|
||||||
// Set wallet data from keystore import result
|
|
||||||
if (result.seed_phrase) {
|
|
||||||
store.setSeedPhrase(result.seed_phrase)
|
|
||||||
}
|
|
||||||
if (result.view_key) {
|
|
||||||
store.setViewKey(result.view_key)
|
|
||||||
}
|
|
||||||
if (result.address) {
|
|
||||||
store.setAddress(result.address)
|
|
||||||
}
|
|
||||||
if (result.receiver_identifier) {
|
|
||||||
store.setReceiverId(result.receiver_identifier)
|
|
||||||
}
|
|
||||||
if (result.network) {
|
|
||||||
store.setNetwork(result.network)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
} catch (err) {
|
|
||||||
const errorMsg = err instanceof Error ? err.message : 'Failed to import from keystore'
|
|
||||||
store.setError(errorMsg)
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
store.setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== API METHODS =====
|
// ===== API METHODS =====
|
||||||
|
|
||||||
const getUtxos = async (
|
const getUtxos = async (
|
||||||
@ -333,8 +276,6 @@ export function useNeptuneWallet() {
|
|||||||
initWasm: ensureWasmInitialized,
|
initWasm: ensureWasmInitialized,
|
||||||
generateWallet,
|
generateWallet,
|
||||||
importWallet,
|
importWallet,
|
||||||
importFromViewKey,
|
|
||||||
importFromKeystore,
|
|
||||||
getViewKeyFromSeed,
|
getViewKeyFromSeed,
|
||||||
getAddressFromSeed,
|
getAddressFromSeed,
|
||||||
validateSeedPhrase,
|
validateSeedPhrase,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export interface WalletState {
|
export interface WalletState {
|
||||||
seedPhrase: string[] | null
|
seedPhrase: string[] | null
|
||||||
|
password: string | null
|
||||||
receiverId: string | null
|
receiverId: string | null
|
||||||
viewKey: string | null
|
viewKey: string | null
|
||||||
address: string | null
|
address: string | null
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
import * as Page from '@/views'
|
import * as Page from '@/views'
|
||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
|
|
||||||
export const ifAuthenticated = (to: any, from: any, next: any) => {
|
export const ifAuthenticated = (to: any, from: any, next: any) => {
|
||||||
const neptuneStore = useNeptuneStore()
|
const neptuneStore = useNeptuneStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
if (neptuneStore.getReceiverId) {
|
if (neptuneStore.getReceiverId) {
|
||||||
next()
|
next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next('/login')
|
authStore.setState('login')
|
||||||
|
next('/auth')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routes: any = [
|
export const routes: any = [
|
||||||
@ -18,15 +22,10 @@ export const routes: any = [
|
|||||||
beforeEnter: ifAuthenticated,
|
beforeEnter: ifAuthenticated,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/auth',
|
||||||
name: 'login',
|
name: 'auth',
|
||||||
component: Page.Auth,
|
component: Page.Auth,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/password',
|
|
||||||
name: 'password',
|
|
||||||
component: Page.PasswordKeystore,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
component: Page.NotFound,
|
component: Page.NotFound,
|
||||||
|
|||||||
@ -13,52 +13,6 @@ export const useAuthStore = () => {
|
|||||||
currentState.value = state
|
currentState.value = state
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = () => {
|
|
||||||
switch (currentState.value) {
|
|
||||||
case 'onboarding':
|
|
||||||
setState('login')
|
|
||||||
break
|
|
||||||
case 'login':
|
|
||||||
// Stay in login, user chooses create or open
|
|
||||||
break
|
|
||||||
case 'create':
|
|
||||||
setState('recovery')
|
|
||||||
break
|
|
||||||
case 'recovery':
|
|
||||||
setState('confirm')
|
|
||||||
break
|
|
||||||
case 'confirm':
|
|
||||||
setState('complete')
|
|
||||||
break
|
|
||||||
case 'complete':
|
|
||||||
// Flow complete
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousStep = () => {
|
|
||||||
switch (currentState.value) {
|
|
||||||
case 'onboarding':
|
|
||||||
// Can't go back from onboarding
|
|
||||||
break
|
|
||||||
case 'login':
|
|
||||||
setState('onboarding')
|
|
||||||
break
|
|
||||||
case 'create':
|
|
||||||
setState('login')
|
|
||||||
break
|
|
||||||
case 'recovery':
|
|
||||||
setState('create')
|
|
||||||
break
|
|
||||||
case 'confirm':
|
|
||||||
setState('recovery')
|
|
||||||
break
|
|
||||||
case 'complete':
|
|
||||||
setState('confirm')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToCreate = () => {
|
const goToCreate = () => {
|
||||||
setState('create')
|
setState('create')
|
||||||
}
|
}
|
||||||
@ -67,19 +21,21 @@ export const useAuthStore = () => {
|
|||||||
setState('login')
|
setState('login')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToRecover = () => {
|
||||||
|
setState('recovery')
|
||||||
|
}
|
||||||
|
|
||||||
const resetFlow = () => {
|
const resetFlow = () => {
|
||||||
setState('onboarding')
|
setState('onboarding')
|
||||||
localStorage.removeItem('onboarding-completed')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentState: currentState.value,
|
currentState: currentState.value,
|
||||||
getCurrentState,
|
getCurrentState,
|
||||||
setState,
|
setState,
|
||||||
nextStep,
|
|
||||||
previousStep,
|
|
||||||
goToCreate,
|
goToCreate,
|
||||||
goToLogin,
|
goToLogin,
|
||||||
|
goToRecover,
|
||||||
resetFlow,
|
resetFlow,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
// ===== STATE =====
|
// ===== STATE =====
|
||||||
const wallet = ref<WalletState>({
|
const wallet = ref<WalletState>({
|
||||||
seedPhrase: null,
|
seedPhrase: null,
|
||||||
|
password: null,
|
||||||
receiverId: null,
|
receiverId: null,
|
||||||
viewKey: null,
|
viewKey: null,
|
||||||
address: null,
|
address: null,
|
||||||
@ -23,6 +24,10 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
wallet.value.seedPhrase = seedPhrase
|
wallet.value.seedPhrase = seedPhrase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setPassword = (password: string | null) => {
|
||||||
|
wallet.value.password = password
|
||||||
|
}
|
||||||
|
|
||||||
const setReceiverId = (receiverId: string | null) => {
|
const setReceiverId = (receiverId: string | null) => {
|
||||||
wallet.value.receiverId = receiverId
|
wallet.value.receiverId = receiverId
|
||||||
}
|
}
|
||||||
@ -62,6 +67,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
const clearWallet = () => {
|
const clearWallet = () => {
|
||||||
wallet.value = {
|
wallet.value = {
|
||||||
seedPhrase: null,
|
seedPhrase: null,
|
||||||
|
password: null,
|
||||||
receiverId: null,
|
receiverId: null,
|
||||||
viewKey: null,
|
viewKey: null,
|
||||||
address: null,
|
address: null,
|
||||||
@ -75,6 +81,8 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
// ===== GETTERS =====
|
// ===== GETTERS =====
|
||||||
const getWallet = computed(() => wallet.value)
|
const getWallet = computed(() => wallet.value)
|
||||||
const getSeedPhrase = computed(() => wallet.value.seedPhrase)
|
const getSeedPhrase = computed(() => wallet.value.seedPhrase)
|
||||||
|
const getSeedPhraseString = computed(() => wallet.value.seedPhrase?.join(' ') || '')
|
||||||
|
const getPassword = computed(() => wallet.value.password)
|
||||||
const getReceiverId = computed(() => wallet.value.receiverId)
|
const getReceiverId = computed(() => wallet.value.receiverId)
|
||||||
const getViewKey = computed(() => wallet.value.viewKey)
|
const getViewKey = computed(() => wallet.value.viewKey)
|
||||||
const getAddress = computed(() => wallet.value.address)
|
const getAddress = computed(() => wallet.value.address)
|
||||||
@ -88,6 +96,8 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
return {
|
return {
|
||||||
getWallet,
|
getWallet,
|
||||||
getSeedPhrase,
|
getSeedPhrase,
|
||||||
|
getSeedPhraseString,
|
||||||
|
getPassword,
|
||||||
getReceiverId,
|
getReceiverId,
|
||||||
getViewKey,
|
getViewKey,
|
||||||
getAddress,
|
getAddress,
|
||||||
@ -99,6 +109,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
getError,
|
getError,
|
||||||
|
|
||||||
setSeedPhrase,
|
setSeedPhrase,
|
||||||
|
setPassword,
|
||||||
setReceiverId,
|
setReceiverId,
|
||||||
setViewKey,
|
setViewKey,
|
||||||
setAddress,
|
setAddress,
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export const validateSeedPhrase18 = (words: string[]): boolean => words.length !== 18
|
export const validateSeedPhrase18 = (words: string[]): boolean => words.length === 18
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { OnboardingComponent } from './components'
|
import { OnboardingTab } from './components'
|
||||||
import { useAuthStore } from '@/stores'
|
import { useAuthStore } from '@/stores'
|
||||||
import { LoginTab, CreateTab, RecoveryTab, ConfirmTab } from './components'
|
import { LoginTab, CreateTab, RecoverSeedTab } from './components'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
@ -12,47 +12,21 @@ const handleGoToCreate = () => {
|
|||||||
authStore.goToCreate()
|
authStore.goToCreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGoToLogin = () => {
|
const handleGoToRecover = () => {
|
||||||
authStore.goToLogin()
|
authStore.goToRecover()
|
||||||
}
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
authStore.nextStep()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
authStore.previousStep()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
<OnboardingComponent
|
<OnboardingTab
|
||||||
v-if="currentState === 'onboarding'"
|
v-if="currentState === 'onboarding'"
|
||||||
@go-to-create="handleGoToCreate"
|
@go-to-create="handleGoToCreate"
|
||||||
@go-to-login="handleGoToLogin"
|
@go-to-recover="handleGoToRecover"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoginTab v-else-if="currentState === 'login'" @go-to-create="handleGoToCreate" />
|
<LoginTab v-else-if="currentState === 'login'" @go-to-create="handleGoToCreate" />
|
||||||
|
<CreateTab v-else-if="currentState === 'create'" @go-to-recover="handleGoToRecover" />
|
||||||
<CreateTab
|
<RecoverSeedTab v-else-if="currentState === 'recovery'" />
|
||||||
v-else-if="currentState === 'create'"
|
|
||||||
@go-to-login="handleGoToLogin"
|
|
||||||
@next="handleNext"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RecoveryTab
|
|
||||||
v-else-if="currentState === 'recovery'"
|
|
||||||
@back="handleBack"
|
|
||||||
@next="handleNext"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmTab v-else-if="currentState === 'confirm'" @back="handleBack" @next="handleNext" />
|
|
||||||
|
|
||||||
<div v-else-if="currentState === 'complete'" class="complete-state">
|
|
||||||
<h2>Wallet Setup Complete!</h2>
|
|
||||||
<p>Your wallet has been successfully created.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -2,24 +2,18 @@
|
|||||||
import { CreateWalletComponent } from '.'
|
import { CreateWalletComponent } from '.'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
goToLogin: []
|
goToRecover: []
|
||||||
next: []
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const handleNavigateToOpenWallet = () => {
|
const handleNavigateToRecoverWallet = () => {
|
||||||
emit('goToLogin')
|
emit('goToRecover')
|
||||||
}
|
|
||||||
|
|
||||||
const handleNavigateToRecoverySeed = () => {
|
|
||||||
emit('next')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="create-tab">
|
<div class="create-tab">
|
||||||
<CreateWalletComponent
|
<CreateWalletComponent
|
||||||
@navigateToOpenWallet="handleNavigateToOpenWallet"
|
@navigate-to-recover-wallet="handleNavigateToRecoverWallet"
|
||||||
@navigateToRecoverySeed="handleNavigateToRecoverySeed"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,176 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, defineEmits, onMounted } from 'vue'
|
|
||||||
import { KeystoreDownloadComponent, RecoverySeedComponent, ConfirmSeedComponent } from '.'
|
|
||||||
import { ChooseBackupMethodStep, CreatePasswordStep, WalletCreatedStep } from './steps'
|
|
||||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
|
||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
navigateToOpenWallet: [event: Event]
|
|
||||||
navigateToRecoverySeed: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { initWasm, generateWallet } = useNeptuneWallet()
|
|
||||||
const neptuneStore = useNeptuneStore()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const step = ref(1)
|
|
||||||
const backupMethod = ref<'none' | 'seed' | 'keystore'>('none')
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
await initWasm()
|
|
||||||
} catch (err) {
|
|
||||||
message.error('Failed to initialize wallet. Please refresh the page.')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleIHaveWallet = (e: Event) => {
|
|
||||||
e.preventDefault()
|
|
||||||
emit('navigateToOpenWallet', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChooseMethod = async (method: 'seed' | 'keystore'): Promise<void> => {
|
|
||||||
backupMethod.value = method
|
|
||||||
|
|
||||||
if (method === 'seed') {
|
|
||||||
try {
|
|
||||||
await generateWallet()
|
|
||||||
message.success('Wallet generated successfully!')
|
|
||||||
} catch (err) {
|
|
||||||
console.error('❌ Failed to generate wallet:', err)
|
|
||||||
message.error('Failed to generate wallet. Please try again.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
step.value = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNextFromRecoverySeed = () => {
|
|
||||||
step.value = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNextFromConfirmSeed = () => {
|
|
||||||
step.value = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNextFromPassword = () => {
|
|
||||||
step.value = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadKeystoreFile() {
|
|
||||||
const data = {
|
|
||||||
account: 'neptune-wallet',
|
|
||||||
version: 1,
|
|
||||||
enc: 'mock-data',
|
|
||||||
created: new Date().toISOString(),
|
|
||||||
note: 'Exported from web-wallet',
|
|
||||||
hint: 'Replace bằng file thực tế trong tích hợp thật.',
|
|
||||||
}
|
|
||||||
const file = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = URL.createObjectURL(file)
|
|
||||||
link.download = 'neptune-wallet-keystore.json'
|
|
||||||
link.click()
|
|
||||||
|
|
||||||
step.value = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBackToChoose = () => {
|
|
||||||
backupMethod.value = 'none'
|
|
||||||
neptuneStore.clearWallet()
|
|
||||||
step.value = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBackToRecoverySeed = () => {
|
|
||||||
step.value = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBackToPassword = () => {
|
|
||||||
step.value = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAccessWallet = () => {
|
|
||||||
router.push({ name: 'home' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetAll() {
|
|
||||||
step.value = 1
|
|
||||||
backupMethod.value = 'none'
|
|
||||||
neptuneStore.clearWallet()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div class="auth-container">
|
|
||||||
<div class="auth-card">
|
|
||||||
<!-- Step 1: Choose Backup Method -->
|
|
||||||
<ChooseBackupMethodStep v-if="step === 1" @choose-method="handleChooseMethod" />
|
|
||||||
|
|
||||||
<!-- Step 2: -->
|
|
||||||
<template v-else-if="step === 2">
|
|
||||||
<!-- Seed flow: Show recovery seed -->
|
|
||||||
<RecoverySeedComponent
|
|
||||||
v-if="backupMethod === 'seed'"
|
|
||||||
@next="handleNextFromRecoverySeed"
|
|
||||||
@back="handleBackToChoose"
|
|
||||||
/>
|
|
||||||
<!-- Keystore flow: Create password -->
|
|
||||||
<CreatePasswordStep
|
|
||||||
v-else-if="backupMethod === 'keystore'"
|
|
||||||
@next="handleNextFromPassword"
|
|
||||||
@navigate-to-open-wallet="handleIHaveWallet"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Step 3: Based on chosen method -->
|
|
||||||
<template v-else-if="step === 3">
|
|
||||||
<ConfirmSeedComponent
|
|
||||||
v-if="backupMethod === 'seed'"
|
|
||||||
@next="handleNextFromConfirmSeed"
|
|
||||||
@back="handleBackToRecoverySeed"
|
|
||||||
/>
|
|
||||||
<KeystoreDownloadComponent
|
|
||||||
v-else-if="backupMethod === 'keystore'"
|
|
||||||
@download="downloadKeystoreFile"
|
|
||||||
@back="handleBackToPassword"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Step 4: Success -->
|
|
||||||
<WalletCreatedStep
|
|
||||||
v-else-if="step === 4"
|
|
||||||
@access-wallet="handleAccessWallet"
|
|
||||||
@create-another="resetAll"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Fallback slot -->
|
|
||||||
<template v-else>
|
|
||||||
<slot></slot>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.auth-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
background: var(--bg-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-card {
|
|
||||||
@include card-base;
|
|
||||||
max-width: 720px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
@include screen(mobile) {
|
|
||||||
max-width: 100%;
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ButtonCommon } from '@/components'
|
|
||||||
import SeedPhraseTab from './SeedPhraseTab.vue'
|
|
||||||
import KeystoreTab from './KeystoreTab.vue'
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'import-success', data: { type: 'seed' | 'keystore'; value: string | string[] }): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const tab = ref<'seedphrase' | 'keystore'>('seedphrase')
|
|
||||||
const seedPhraseTabRef = ref<InstanceType<typeof SeedPhraseTab>>()
|
|
||||||
const keystoreTabRef = ref<InstanceType<typeof KeystoreTab>>()
|
|
||||||
const isSeedPhraseValid = ref(false)
|
|
||||||
const isKeystoreValid = ref(false)
|
|
||||||
|
|
||||||
const handleSeedPhraseSubmit = (words: string[]) => {
|
|
||||||
emit('import-success', {
|
|
||||||
type: 'seed',
|
|
||||||
value: words,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeystoreSubmit = (keystore: string) => {
|
|
||||||
emit('import-success', { type: 'keystore', value: keystore })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleContinue = () => {
|
|
||||||
if (tab.value === 'seedphrase') {
|
|
||||||
seedPhraseTabRef.value?.handleSubmit()
|
|
||||||
} else {
|
|
||||||
keystoreTabRef.value?.handleSubmit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div class="import-wallet dark-card">
|
|
||||||
<h2 class="title">Import Wallet</h2>
|
|
||||||
<div class="desc">Pick your import method</div>
|
|
||||||
<div class="tabs">
|
|
||||||
<button
|
|
||||||
:class="['tab-btn', tab === 'seedphrase' && 'active']"
|
|
||||||
@click="tab = 'seedphrase'"
|
|
||||||
>
|
|
||||||
Import by seed phrase
|
|
||||||
</button>
|
|
||||||
<button :class="['tab-btn', tab === 'keystore' && 'active']" @click="tab = 'keystore'">
|
|
||||||
Import by keystore
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="tab === 'seedphrase'" class="tab-pane">
|
|
||||||
<SeedPhraseTab
|
|
||||||
ref="seedPhraseTabRef"
|
|
||||||
@update:valid="isSeedPhraseValid = $event"
|
|
||||||
@submit="handleSeedPhraseSubmit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else class="tab-pane">
|
|
||||||
<KeystoreTab
|
|
||||||
ref="keystoreTabRef"
|
|
||||||
@update:valid="isKeystoreValid = $event"
|
|
||||||
@submit="handleKeystoreSubmit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ButtonCommon
|
|
||||||
class="mt-lg"
|
|
||||||
type="primary"
|
|
||||||
block
|
|
||||||
size="large"
|
|
||||||
:disabled="tab === 'seedphrase' ? !isSeedPhraseValid : !isKeystoreValid"
|
|
||||||
@click="handleContinue"
|
|
||||||
>Continue</ButtonCommon
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.import-wallet {
|
|
||||||
max-width: 420px;
|
|
||||||
width: 100%;
|
|
||||||
margin: 24px auto;
|
|
||||||
background: var(--bg-light);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
box-shadow: var(--shadow-primary);
|
|
||||||
padding: 32px 28px 24px 28px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.45rem;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.desc {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
background: var(--text-primary);
|
|
||||||
border-radius: 13px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
.tab-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 13px;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.18s;
|
|
||||||
&.active {
|
|
||||||
background: var(--primary-color);
|
|
||||||
color: var(--text-light);
|
|
||||||
}
|
|
||||||
&:hover:not(.active) {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tab-pane {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
.mt-lg {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@include screen(mobile) {
|
|
||||||
.import-wallet {
|
|
||||||
padding: 16px 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,241 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'download'): void
|
|
||||||
(e: 'back'): void
|
|
||||||
}>()
|
|
||||||
function handleDownload() {
|
|
||||||
emit('download')
|
|
||||||
}
|
|
||||||
function handleBack() {
|
|
||||||
emit('back')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div class="keystore-step">
|
|
||||||
<div class="step-content">
|
|
||||||
<h2 class="title">Download keystore file</h2>
|
|
||||||
<div class="desc">Important things to know before downloading your keystore file.</div>
|
|
||||||
<div class="box-list">
|
|
||||||
<div class="box">
|
|
||||||
<div class="icn">
|
|
||||||
<svg width="44" height="44" viewBox="0 0 36 36">
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
d="M15,29 L29,29 C30.1045695,29 31,28.1045695 31,27 L31,9 C31,7.8954305 30.1045695,7 29,7 L7,7 C5.8954305,7 5,7.8954305 5,9 L5,27 C5,28.1045695 5.8954305,29 7,29 L11,29"
|
|
||||||
fill="#d8f7fa"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.5,20.5 L17.5,25.5 L27.5,15.5"
|
|
||||||
stroke="#51c7ce"
|
|
||||||
stroke-width="2.2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="box-title">Don't lose it</div>
|
|
||||||
<div class="box-desc">Be careful, it can not be recovered if you lose it.</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="icn">
|
|
||||||
<svg width="46" height="46" viewBox="0 0 28 28">
|
|
||||||
<g>
|
|
||||||
<circle cx="16" cy="16" r="12" fill="#e3fae5" />
|
|
||||||
<text
|
|
||||||
x="11"
|
|
||||||
y="21"
|
|
||||||
font-size="15"
|
|
||||||
font-family="Arial"
|
|
||||||
fill="#48b783"
|
|
||||||
font-weight="bold"
|
|
||||||
>
|
|
||||||
$
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="box-title">Don't share it</div>
|
|
||||||
<div class="box-desc">
|
|
||||||
Your funds will be stolen if you use this file on a malicious phishing site.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="icn">
|
|
||||||
<svg width="46" height="46" viewBox="0 0 28 28">
|
|
||||||
<g>
|
|
||||||
<rect x="5" y="7" width="18" height="16" rx="3" fill="#c6f1fc" />
|
|
||||||
<rect x="7" y="10" width="14" height="10" rx="2" fill="#96e2fc" />
|
|
||||||
<text
|
|
||||||
x="10"
|
|
||||||
y="19"
|
|
||||||
font-size="9"
|
|
||||||
font-family="monospace"
|
|
||||||
fill="#418aaf"
|
|
||||||
>
|
|
||||||
{ }
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="box-title">Make a backup</div>
|
|
||||||
<div class="box-desc">
|
|
||||||
Secure it like the millions of dollars it may one day be worth.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="btn-row">
|
|
||||||
<button class="back-btn" @click="handleBack">Back</button>
|
|
||||||
<button class="main-btn" @click="handleDownload">Acknowledge & Download</button>
|
|
||||||
</div>
|
|
||||||
<div class="not-recommended">
|
|
||||||
<span class="warn-icn">⚠</span>
|
|
||||||
<div>
|
|
||||||
<span class="strong">NOT RECOMMENDED</span><br />
|
|
||||||
This information is sensitive, and these options should only be used in offline
|
|
||||||
or secure environments.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.keystore-step {
|
|
||||||
max-width: 650px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 14px;
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
padding: 34px 16px 28px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-content {
|
|
||||||
padding: 6px 5px 0 5px;
|
|
||||||
}
|
|
||||||
.step-title {
|
|
||||||
color: var(--primary-color);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.07em;
|
|
||||||
font-size: 1.07rem;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 1.36rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.desc {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1.09rem;
|
|
||||||
margin-bottom: 23px;
|
|
||||||
}
|
|
||||||
.box-list {
|
|
||||||
display: flex;
|
|
||||||
gap: 21px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.box {
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--bg-light);
|
|
||||||
padding: 21px 19px 17px 19px;
|
|
||||||
flex: 1 1 140px;
|
|
||||||
min-width: 196px;
|
|
||||||
max-width: 250px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
.icn {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.box-title {
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: 1.07em;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
.box-desc {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.99em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.btn-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 14px;
|
|
||||||
margin: 23px 0 0 0;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.back-btn {
|
|
||||||
padding: 10px 32px;
|
|
||||||
background: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1em;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.13s;
|
|
||||||
&:hover {
|
|
||||||
background: var(--bg-light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.main-btn {
|
|
||||||
padding: 10px 32px;
|
|
||||||
background: var(--primary-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-light);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1em;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.12s;
|
|
||||||
box-shadow: 0 4px 18px var(--shadow-primary);
|
|
||||||
&:hover {
|
|
||||||
background: var(--primary-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.not-recommended {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 10px;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
padding: 13px 17px;
|
|
||||||
margin-top: 28px;
|
|
||||||
font-size: 1.09em;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 11px;
|
|
||||||
.warn-icn {
|
|
||||||
font-size: 1.43em;
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
.strong {
|
|
||||||
font-weight: 700;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@include screen(tablet) {
|
|
||||||
.steps-bar .step {
|
|
||||||
min-width: 90px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include screen(mobile) {
|
|
||||||
.steps-bar .step {
|
|
||||||
min-width: 90px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keystore-step {
|
|
||||||
padding: 15px 3px 13px 3px;
|
|
||||||
}
|
|
||||||
.box {
|
|
||||||
padding: 12px 5px 10px 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { FormCommon } from '@/components'
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:valid', valid: boolean): void
|
|
||||||
(e: 'submit', keystore: string): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const keystore = ref('')
|
|
||||||
const keystoreError = ref('')
|
|
||||||
|
|
||||||
// Watch keystore and emit validity
|
|
||||||
watch(
|
|
||||||
[keystore, keystoreError],
|
|
||||||
() => {
|
|
||||||
const isValid = !!keystore.value.trim() && !keystoreError.value
|
|
||||||
emit('update:valid', isValid)
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const validateKeystore = () => {
|
|
||||||
if (!keystore.value.trim()) {
|
|
||||||
keystoreError.value = 'Please enter your keystore.'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
keystoreError.value = ''
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (validateKeystore()) {
|
|
||||||
emit('submit', keystore.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
handleSubmit,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="keystore-tab">
|
|
||||||
<div class="form-row mb-md">
|
|
||||||
<FormCommon
|
|
||||||
v-model="keystore"
|
|
||||||
type="text"
|
|
||||||
label="Keystore"
|
|
||||||
placeholder="Enter keystore"
|
|
||||||
:error="keystoreError"
|
|
||||||
@focus="keystoreError = ''"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.keystore-tab {
|
|
||||||
.form-row {
|
|
||||||
margin-top: 19px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.mb-md {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@ -1,19 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ImportWalletComponent } from '.'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const handleImported = (payload: { type: 'seed' | 'keystore'; value: string | string[] }) => {
|
|
||||||
if (payload.type === 'keystore') {
|
|
||||||
localStorage.setItem('temp_keystore', JSON.stringify(payload.value))
|
|
||||||
return router.push({ name: 'password' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="login-tab">
|
<div class="login-tab">
|
||||||
<ImportWalletComponent @import-success="handleImported" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -3,15 +3,15 @@ import { ButtonCommon } from '@/components'
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
goToCreate: []
|
goToCreate: []
|
||||||
goToLogin: []
|
goToRecover: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const handleGoToCreate = () => {
|
const handleGoToCreate = () => {
|
||||||
emit('goToCreate')
|
emit('goToCreate')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGoToLogin = () => {
|
const handleRecover = () => {
|
||||||
emit('goToLogin')
|
emit('goToRecover')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ const handleGoToLogin = () => {
|
|||||||
<div class="welcome-card flex-center">
|
<div class="welcome-card flex-center">
|
||||||
<div class="welcome-box">
|
<div class="welcome-box">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
<h2>Welcome to the New Wallet Experience</h2>
|
<h2>Welcome to the Neptune Wallet</h2>
|
||||||
<p>Choose the next action:</p>
|
<p>Choose the next action:</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -30,8 +30,8 @@ const handleGoToLogin = () => {
|
|||||||
<ButtonCommon type="primary" size="large" @click="handleGoToCreate">
|
<ButtonCommon type="primary" size="large" @click="handleGoToCreate">
|
||||||
Create new wallet
|
Create new wallet
|
||||||
</ButtonCommon>
|
</ButtonCommon>
|
||||||
<ButtonCommon type="default" size="large" @click="handleGoToLogin">
|
<ButtonCommon type="default" size="large" @click="handleRecover">
|
||||||
Open existing wallet
|
Recover wallet
|
||||||
</ButtonCommon>
|
</ButtonCommon>
|
||||||
</div>
|
</div>
|
||||||
<div class="note">Thank you for being a part of the Neptune community!</div>
|
<div class="note">Thank you for being a part of the Neptune community!</div>
|
||||||
@ -1,328 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { ButtonCommon, FormCommon } from '@/components'
|
|
||||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
success: []
|
|
||||||
error: [message: string]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { importFromKeystore } = useNeptuneWallet()
|
|
||||||
|
|
||||||
const password = ref('')
|
|
||||||
const passwordError = ref('')
|
|
||||||
const isLoading = ref(false)
|
|
||||||
|
|
||||||
const passwordValidation = computed(() => {
|
|
||||||
if (!password.value) return { isValid: false, message: '' }
|
|
||||||
|
|
||||||
const checks = {
|
|
||||||
length: password.value.length >= 8,
|
|
||||||
uppercase: /[A-Z]/.test(password.value),
|
|
||||||
lowercase: /[a-z]/.test(password.value),
|
|
||||||
number: /[0-9]/.test(password.value),
|
|
||||||
special: /[!@#$%^&*(),.?":{}|<>]/.test(password.value),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checks.length) return { isValid: false, message: 'Password must be at least 8 characters' }
|
|
||||||
if (!checks.uppercase)
|
|
||||||
return { isValid: false, message: 'Password must contain uppercase letter' }
|
|
||||||
if (!checks.lowercase)
|
|
||||||
return { isValid: false, message: 'Password must contain lowercase letter' }
|
|
||||||
if (!checks.number) return { isValid: false, message: 'Password must contain number' }
|
|
||||||
if (!checks.special)
|
|
||||||
return { isValid: false, message: 'Password must contain special character' }
|
|
||||||
|
|
||||||
return { isValid: true, message: 'Password format is valid' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const canProceed = computed(() => {
|
|
||||||
return password.value.length > 0 && passwordValidation.value.isValid
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!canProceed.value) {
|
|
||||||
if (password.value.length > 0 && !passwordValidation.value.isValid) {
|
|
||||||
passwordError.value = passwordValidation.value.message
|
|
||||||
} else {
|
|
||||||
passwordError.value = 'Please enter your password'
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
isLoading.value = true
|
|
||||||
passwordError.value = ''
|
|
||||||
|
|
||||||
const keystore = localStorage.getItem('temp_keystore')
|
|
||||||
if (!keystore) {
|
|
||||||
throw new Error('No keystore found. Please try importing again.')
|
|
||||||
}
|
|
||||||
|
|
||||||
await importFromKeystore(keystore, password.value)
|
|
||||||
|
|
||||||
localStorage.removeItem('temp_keystore')
|
|
||||||
|
|
||||||
router.push({ name: 'home' })
|
|
||||||
emit('success')
|
|
||||||
} catch (err) {
|
|
||||||
const errorMsg = err instanceof Error ? err.message : 'Failed to access wallet'
|
|
||||||
passwordError.value = errorMsg
|
|
||||||
emit('error', errorMsg)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="password-keystore-step">
|
|
||||||
<div class="auth-card-header">
|
|
||||||
<div class="logo-container">
|
|
||||||
<div class="logo-circle">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
class="neptune-logo"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
id="neptuneGradient"
|
|
||||||
x1="0%"
|
|
||||||
y1="0%"
|
|
||||||
x2="100%"
|
|
||||||
y2="100%"
|
|
||||||
>
|
|
||||||
<stop offset="0%" style="stop-color: #007fcf; stop-opacity: 1" />
|
|
||||||
<stop offset="100%" style="stop-color: #0066a6; stop-opacity: 1" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="ringGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" style="stop-color: #007fcf; stop-opacity: 0.3" />
|
|
||||||
<stop offset="50%" style="stop-color: #007fcf; stop-opacity: 0.6" />
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
style="stop-color: #007fcf; stop-opacity: 0.3"
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<circle cx="50" cy="50" r="28" fill="url(#neptuneGradient)" />
|
|
||||||
|
|
||||||
<ellipse cx="50" cy="45" rx="22" ry="6" fill="rgba(255, 255, 255, 0.1)" />
|
|
||||||
<ellipse cx="50" cy="55" rx="20" ry="5" fill="rgba(0, 0, 0, 0.1)" />
|
|
||||||
|
|
||||||
<ellipse
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
rx="42"
|
|
||||||
ry="12"
|
|
||||||
fill="none"
|
|
||||||
stroke="url(#ringGradient)"
|
|
||||||
stroke-width="4"
|
|
||||||
opacity="0.8"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<circle cx="42" cy="42" r="6" fill="rgba(255, 255, 255, 0.4)" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="logo-text">
|
|
||||||
<span class="coin-name">Neptune</span>
|
|
||||||
<span class="coin-symbol">NPTUN</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h1 class="auth-title">Access Wallet</h1>
|
|
||||||
<p class="auth-subtitle">Enter your keystore password to unlock your wallet</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-card-content">
|
|
||||||
<div class="form-group">
|
|
||||||
<FormCommon
|
|
||||||
v-model="password"
|
|
||||||
type="password"
|
|
||||||
label="Keystore Password"
|
|
||||||
placeholder="Enter your keystore password"
|
|
||||||
show-password-toggle
|
|
||||||
required
|
|
||||||
:error="passwordError"
|
|
||||||
@input="passwordError = ''"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="helper-text">
|
|
||||||
Password must be at least 8 characters with uppercase, lowercase, numbers, and
|
|
||||||
special characters.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="auth-button-group">
|
|
||||||
<ButtonCommon
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
class="auth-button"
|
|
||||||
block
|
|
||||||
:disabled="!canProceed || isLoading"
|
|
||||||
:loading="isLoading"
|
|
||||||
@click="handleSubmit"
|
|
||||||
>
|
|
||||||
Access Wallet
|
|
||||||
</ButtonCommon>
|
|
||||||
<div class="secondary-actions">
|
|
||||||
<button class="link-button" @click="handleBack">Back to Import</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.password-keystore-step {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-card-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: var(--spacing-2xl);
|
|
||||||
padding-bottom: var(--spacing-xl);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
|
|
||||||
.logo-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
|
|
||||||
.logo-circle {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(135deg, var(--primary-light), var(--bg-white));
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 127, 207, 0.15);
|
|
||||||
|
|
||||||
.neptune-logo {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
.coin-name {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: var(--font-bold);
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coin-symbol {
|
|
||||||
font-size: var(--font-xs);
|
|
||||||
font-weight: var(--font-medium);
|
|
||||||
color: var(--primary-color);
|
|
||||||
background: var(--primary-light);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title {
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
font-weight: var(--font-bold);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-subtitle {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-card-content {
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.helper-text {
|
|
||||||
font-size: var(--font-xs);
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin: 0 0 var(--spacing-xl);
|
|
||||||
line-height: var(--leading-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-button {
|
|
||||||
width: fit-content;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-button-group {
|
|
||||||
margin-top: var(--spacing-2xl);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.secondary-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
margin-top: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--primary-color);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--primary-hover);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include screen(mobile) {
|
|
||||||
.auth-card-header {
|
|
||||||
.logo-container {
|
|
||||||
.logo-circle {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-text {
|
|
||||||
.coin-name {
|
|
||||||
font-size: var(--font-md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
25
src/views/Auth/components/RecoverSeedTab.vue
Normal file
25
src/views/Auth/components/RecoverSeedTab.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RecoverWalletComponent } from '.'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleImported = (payload: { type: 'seed' | 'keystore'; value: string | string[] }) => {
|
||||||
|
if (payload.type === 'keystore') {
|
||||||
|
localStorage.setItem('temp_keystore', JSON.stringify(payload.value))
|
||||||
|
return router.push({ name: 'password' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="recover-seed-tab">
|
||||||
|
<RecoverWalletComponent @import-success="handleImported" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.recover-seed-tab {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,28 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { RecoverySeedComponent } from '.'
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
next: []
|
|
||||||
back: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
emit('next')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
emit('back')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="recovery-tab">
|
|
||||||
<RecoverySeedComponent @next="handleNext" @back="handleBack" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.recovery-tab {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -82,28 +82,29 @@ const handleAnswerSelect = (answer: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (isCorrect.value) {
|
emit('next')
|
||||||
correctCount.value++
|
// if (isCorrect.value) {
|
||||||
askedPositions.value.add(quizData.value!.position)
|
// correctCount.value++
|
||||||
|
// askedPositions.value.add(quizData.value!.position)
|
||||||
|
|
||||||
if (correctCount.value >= totalQuestions) {
|
// if (correctCount.value >= totalQuestions) {
|
||||||
emit('next')
|
// emit('next')
|
||||||
} else {
|
// } else {
|
||||||
showResult.value = false
|
// showResult.value = false
|
||||||
selectedAnswer.value = ''
|
// selectedAnswer.value = ''
|
||||||
const newQuiz = generateQuiz()
|
// const newQuiz = generateQuiz()
|
||||||
if (newQuiz) {
|
// if (newQuiz) {
|
||||||
quizData.value = newQuiz
|
// quizData.value = newQuiz
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
showResult.value = false
|
// showResult.value = false
|
||||||
selectedAnswer.value = ''
|
// selectedAnswer.value = ''
|
||||||
const newQuiz = generateQuiz()
|
// const newQuiz = generateQuiz()
|
||||||
if (newQuiz) {
|
// if (newQuiz) {
|
||||||
quizData.value = newQuiz
|
// quizData.value = newQuiz
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
@ -207,13 +208,20 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
NEXT QUESTION
|
NEXT QUESTION
|
||||||
</ButtonCommon>
|
</ButtonCommon>
|
||||||
<ButtonCommon
|
<!-- <ButtonCommon
|
||||||
v-if="showResult && isCorrect && correctCount + 1 >= totalQuestions"
|
v-if="showResult && isCorrect && correctCount + 1 >= totalQuestions"
|
||||||
type="primary"
|
type="primary"
|
||||||
size="large"
|
size="large"
|
||||||
@click="handleNext"
|
@click="handleNext"
|
||||||
>
|
>
|
||||||
CONTINUE
|
CONTINUE
|
||||||
|
</ButtonCommon> -->
|
||||||
|
<ButtonCommon
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
@click="handleNext"
|
||||||
|
>
|
||||||
|
CONTINUE
|
||||||
</ButtonCommon>
|
</ButtonCommon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1,12 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { ButtonCommon, FormCommon } from '@/components'
|
import { ButtonCommon, FormCommon } from '@/components'
|
||||||
|
import { useNeptuneStore } from '@/stores';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
next: []
|
next: []
|
||||||
navigateToOpenWallet: [event: Event]
|
navigateToOpenWallet: [event: Event]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const neptuneStore = useNeptuneStore()
|
||||||
|
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const confirmPassword = ref('')
|
const confirmPassword = ref('')
|
||||||
const passwordError = ref('')
|
const passwordError = ref('')
|
||||||
@ -54,6 +57,8 @@ const handleNext = () => {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
neptuneStore.setPassword(password.value)
|
||||||
|
// console.log('neptuneStore.getPassword :>> ', neptuneStore.getPassword)
|
||||||
emit('next')
|
emit('next')
|
||||||
}
|
}
|
||||||
|
|
||||||
103
src/views/Auth/components/create/CreateWalletComponent.vue
Normal file
103
src/views/Auth/components/create/CreateWalletComponent.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineEmits, onMounted } from 'vue'
|
||||||
|
import { SeedPhraseDisplayComponent, ConfirmSeedComponent } from '..'
|
||||||
|
import { CreatePasswordStep, WalletCreatedStep } from '.'
|
||||||
|
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
navigateToRecoverWallet: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { initWasm, generateWallet, clearWallet } = useNeptuneWallet()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const step = ref(1)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await initWasm()
|
||||||
|
} catch (err) {
|
||||||
|
message.error('Failed to initialize wallet. Please refresh the page.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleNavigateToRecoverWallet = () => {
|
||||||
|
emit('navigateToRecoverWallet')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNextToConfirmSeed = () => {
|
||||||
|
step.value = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNextToWalletCreated = () => {
|
||||||
|
step.value = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNextFromPassword = async () => {
|
||||||
|
const result = await generateWallet()
|
||||||
|
if (result) step.value = 2
|
||||||
|
else message.error('Failed to generate wallet')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAccessWallet = () => {
|
||||||
|
router.push({ name: 'home' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAll() {
|
||||||
|
step.value = 1
|
||||||
|
clearWallet()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="auth-container">
|
||||||
|
<div class="auth-card">
|
||||||
|
<!-- Step 1: Create Password -->
|
||||||
|
<CreatePasswordStep
|
||||||
|
v-if="step === 1"
|
||||||
|
@next="handleNextFromPassword"
|
||||||
|
@navigate-to-recover-wallet="handleNavigateToRecoverWallet"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Step 2: Recovery Seed -->
|
||||||
|
<SeedPhraseDisplayComponent v-else-if="step === 2" @next="handleNextToConfirmSeed" />
|
||||||
|
|
||||||
|
<!-- Step 3: Confirm Seed -->
|
||||||
|
<ConfirmSeedComponent v-else-if="step === 3" @next="handleNextToWalletCreated" />
|
||||||
|
|
||||||
|
<!-- Step 4: Success -->
|
||||||
|
<WalletCreatedStep
|
||||||
|
v-else-if="step === 4"
|
||||||
|
@access-wallet="handleAccessWallet"
|
||||||
|
@create-another="resetAll"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Fallback slot -->
|
||||||
|
<template v-else>
|
||||||
|
<slot></slot>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.auth-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
background: var(--bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
@include card-base;
|
||||||
|
max-width: 720px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@include screen(mobile) {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,12 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonCommon } from '@/components'
|
import { ButtonCommon } from '@/components'
|
||||||
|
import { useNeptuneStore } from '@/stores/neptuneStore';
|
||||||
|
|
||||||
|
const neptuneStore = useNeptuneStore()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
accessWallet: []
|
accessWallet: []
|
||||||
createAnother: []
|
createAnother: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const handleAccessWallet = () => {
|
const handleAccessWallet = async () => {
|
||||||
|
const seedPhrase = neptuneStore.getSeedPhraseString
|
||||||
|
const password = neptuneStore.getPassword!
|
||||||
|
const encrypted = (window as any).walletApi.createKeystore(seedPhrase, password)
|
||||||
|
console.log('encrypted keystore sample:', encrypted)
|
||||||
|
// TODO: save keystore file, update settings.json, clear RAM... (implement in later steps)
|
||||||
emit('accessWallet')
|
emit('accessWallet')
|
||||||
}
|
}
|
||||||
|
|
||||||
4
src/views/Auth/components/create/index.ts
Normal file
4
src/views/Auth/components/create/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { default as CreatePasswordStep } from './CreatePasswordStep.vue'
|
||||||
|
export { default as CreateWalletComponent } from './CreateWalletComponent.vue'
|
||||||
|
export { default as ConfirmSeedComponent } from './ConfirmSeedComponent.vue'
|
||||||
|
export { default as WalletCreatedStep } from './WalletCreatedStep.vue'
|
||||||
@ -1,18 +1,13 @@
|
|||||||
// Tabs
|
// Tabs
|
||||||
export { default as LoginTab } from './LoginTab.vue'
|
export { default as LoginTab } from './LoginTab.vue'
|
||||||
export { default as CreateTab } from './CreateTab.vue'
|
export { default as CreateTab } from './CreateTab.vue'
|
||||||
export { default as RecoveryTab } from './RecoveryTab.vue'
|
|
||||||
export { default as ConfirmTab } from './ConfirmTab.vue'
|
export { default as ConfirmTab } from './ConfirmTab.vue'
|
||||||
export { default as SeedPhraseTab } from './SeedPhraseTab.vue'
|
export { default as RecoverSeedTab } from './RecoverSeedTab.vue'
|
||||||
export { default as KeystoreTab } from './KeystoreTab.vue'
|
|
||||||
|
|
||||||
// Auth Components
|
// Auth Components
|
||||||
export { default as OnboardingComponent } from './OnboardingComponent.vue'
|
export { default as OnboardingTab } from './OnboardingTab.vue'
|
||||||
export { default as CreateWalletComponent } from './CreateWalletComponent.vue'
|
export { default as SeedPhraseDisplayComponent } from './SeedPhraseDisplayComponent.vue'
|
||||||
export { default as RecoverySeedComponent } from './RecoverySeedComponent.vue'
|
|
||||||
export { default as ConfirmSeedComponent } from './ConfirmSeedComponent.vue'
|
|
||||||
export { default as ImportWalletComponent } from './ImportWalletComponent.vue'
|
|
||||||
export { default as KeystoreDownloadComponent } from './KeystoreDownloadComponent.vue'
|
|
||||||
|
|
||||||
// Steps
|
// Nested Components
|
||||||
export * from './steps'
|
export * from './create'
|
||||||
|
export * from './recover'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { validateSeedPhrase18 } from '@/utils'
|
import { validateSeedPhrase18 } from '@/utils'
|
||||||
import { ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:valid', valid: boolean): void
|
(e: 'update:valid', valid: boolean): void
|
||||||
@ -10,11 +10,14 @@ const emit = defineEmits<{
|
|||||||
const seedWords = ref<string[]>(Array.from({ length: 18 }, () => ''))
|
const seedWords = ref<string[]>(Array.from({ length: 18 }, () => ''))
|
||||||
const seedError = ref('')
|
const seedError = ref('')
|
||||||
|
|
||||||
const updateValidity = () => {
|
const isValid = computed(() => {
|
||||||
const words = seedWords.value.filter((w) => w.trim())
|
const words = seedWords.value.filter((w) => w.trim())
|
||||||
const isValid = validateSeedPhrase18(words) && !seedError.value
|
return validateSeedPhrase18(words) && !seedError.value
|
||||||
emit('update:valid', isValid)
|
})
|
||||||
}
|
|
||||||
|
watch(isValid, (newVal) => {
|
||||||
|
emit('update:valid', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
const inputBoxFocus = (idx: number) => {
|
const inputBoxFocus = (idx: number) => {
|
||||||
document.getElementById('input-' + idx)?.focus()
|
document.getElementById('input-' + idx)?.focus()
|
||||||
@ -22,7 +25,6 @@ const inputBoxFocus = (idx: number) => {
|
|||||||
|
|
||||||
const handleGridInput = (index: number, value: string) => {
|
const handleGridInput = (index: number, value: string) => {
|
||||||
seedWords.value[index] = value
|
seedWords.value[index] = value
|
||||||
updateValidity()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePaste = (event: ClipboardEvent) => {
|
const handlePaste = (event: ClipboardEvent) => {
|
||||||
@ -37,22 +39,12 @@ const handlePaste = (event: ClipboardEvent) => {
|
|||||||
|
|
||||||
seedWords.value = words
|
seedWords.value = words
|
||||||
seedError.value = ''
|
seedError.value = ''
|
||||||
updateValidity()
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateSeed = () => {
|
|
||||||
const words = seedWords.value.filter((w) => w.trim())
|
|
||||||
return validateSeedPhrase18(words)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (validateSeed()) seedError.value = ''
|
const words = seedWords.value.filter((w) => w.trim())
|
||||||
emit(
|
if (validateSeedPhrase18(words)) seedError.value = ''
|
||||||
'submit',
|
emit('submit', words)
|
||||||
seedWords.value.filter((w) => w.trim())
|
|
||||||
)
|
|
||||||
|
|
||||||
updateValidity()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@ -61,14 +53,14 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="seed-phrase-tab">
|
<div class="recover-seed-form">
|
||||||
<div class="seed-row-radio">
|
<div class="recover-seed-row-radio">
|
||||||
<div class="radio active">18 words</div>
|
<div class="radio active">18 words</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Individual input grid -->
|
<!-- Individual input grid -->
|
||||||
<div class="seed-inputs">
|
<div class="recover-seed-inputs">
|
||||||
<div class="seed-input-grid">
|
<div class="recover-seed-input-grid">
|
||||||
<div v-for="(word, i) in seedWords" :key="i" class="seed-box">
|
<div v-for="(word, i) in seedWords" :key="i" class="seed-box">
|
||||||
<input
|
<input
|
||||||
:id="'input-' + i"
|
:id="'input-' + i"
|
||||||
@ -94,8 +86,8 @@ defineExpose({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.seed-phrase-tab {
|
.recover-seed-form {
|
||||||
.seed-row-radio {
|
.recover-seed-row-radio {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -117,10 +109,10 @@ defineExpose({
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.seed-inputs {
|
.recover-seed-inputs {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.seed-input-grid {
|
.recover-seed-input-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
76
src/views/Auth/components/recover/RecoverWalletComponent.vue
Normal file
76
src/views/Auth/components/recover/RecoverWalletComponent.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ButtonCommon } from '@/components'
|
||||||
|
import { RecoverSeedComponent } from '..'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'import-success', data: { type: 'seed'; value: string[] }): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const recoverSeedComponentRef = ref<InstanceType<typeof RecoverSeedComponent>>()
|
||||||
|
const isSeedPhraseValid = ref(false)
|
||||||
|
|
||||||
|
const handleSeedPhraseSubmit = (words: string[]) => {
|
||||||
|
emit('import-success', {
|
||||||
|
type: 'seed',
|
||||||
|
value: words,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleContinue = () => {
|
||||||
|
recoverSeedComponentRef.value?.handleSubmit?.()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="import-wallet dark-card">
|
||||||
|
<h2 class="title">Import Wallet</h2>
|
||||||
|
<div class="desc">Enter your recovery seed phrase</div>
|
||||||
|
<RecoverSeedComponent
|
||||||
|
ref="recoverSeedComponentRef"
|
||||||
|
@update:valid="isSeedPhraseValid = $event"
|
||||||
|
@submit="handleSeedPhraseSubmit"
|
||||||
|
/>
|
||||||
|
<ButtonCommon
|
||||||
|
class="mt-lg"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
:disabled="!isSeedPhraseValid"
|
||||||
|
@click="handleContinue"
|
||||||
|
>Continue</ButtonCommon
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.import-wallet {
|
||||||
|
max-width: 420px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 24px auto;
|
||||||
|
background: var(--bg-light);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
box-shadow: var(--shadow-primary);
|
||||||
|
padding: 32px 28px 24px 28px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.45rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.mt-lg {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@include screen(mobile) {
|
||||||
|
.import-wallet {
|
||||||
|
padding: 16px 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
2
src/views/Auth/components/recover/index.ts
Normal file
2
src/views/Auth/components/recover/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as RecoverSeedComponent } from './RecoverSeedComponent.vue'
|
||||||
|
export { default as RecoverWalletComponent } from './RecoverWalletComponent.vue'
|
||||||
@ -1,46 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ButtonCommon } from '@/components'
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
chooseMethod: [method: 'seed' | 'keystore']
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleChooseMethod = (method: 'seed' | 'keystore') => {
|
|
||||||
emit('chooseMethod', method)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="choose-backup-method">
|
|
||||||
<h2>Choose your method</h2>
|
|
||||||
<div class="choose-btns">
|
|
||||||
<ButtonCommon type="primary" size="large" @click="() => handleChooseMethod('seed')">
|
|
||||||
Mnemonic Phrase
|
|
||||||
</ButtonCommon>
|
|
||||||
<ButtonCommon type="default" size="large" @click="() => handleChooseMethod('keystore')">
|
|
||||||
Keystore File
|
|
||||||
</ButtonCommon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.choose-backup-method {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px 8px;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: var(--primary-color);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.07em;
|
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.choose-btns {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export { default as ChooseBackupMethodStep } from './ChooseBackupMethodStep.vue'
|
|
||||||
export { default as CreatePasswordStep } from './CreatePasswordStep.vue'
|
|
||||||
export { default as WalletCreatedStep } from './WalletCreatedStep.vue'
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import PasswordKeystoreComponent from '@/views/Auth/components/PasswordKeystoreComponent.vue'
|
|
||||||
|
|
||||||
const handleSuccess = () => {
|
|
||||||
console.log('Wallet accessed successfully')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleError = (message: string) => {
|
|
||||||
console.error('Error accessing wallet:', message)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="password-keystore-view">
|
|
||||||
<div class="auth-container">
|
|
||||||
<PasswordKeystoreComponent @success="handleSuccess" @error="handleError" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.password-keystore-view {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
background: var(--bg-white);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
box-shadow: var(--shadow-primary);
|
|
||||||
padding: var(--spacing-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@include screen(mobile) {
|
|
||||||
.password-keystore-view {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-container {
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
export const Home = () => import('@/views/Home/HomeView.vue')
|
export const Home = () => import('@/views/Home/HomeView.vue')
|
||||||
export const NotFound = () => import('@/views/NotFound/NotFoundView.vue')
|
export const NotFound = () => import('@/views/NotFound/NotFoundView.vue')
|
||||||
export const Auth = () => import('@/views/Auth/AuthView.vue')
|
export const Auth = () => import('@/views/Auth/AuthView.vue')
|
||||||
export const PasswordKeystore = () => import('@/views/PasswordKeystore/PasswordKeystoreView.vue')
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user