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
|
||||
coverage
|
||||
*.local
|
||||
.vite/build/*
|
||||
wallets/*
|
||||
|
||||
/cypress/videos/
|
||||
/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 path from 'node:path'
|
||||
import started from 'electron-squirrel-startup'
|
||||
import './ipcHandlers'
|
||||
|
||||
if (started) {
|
||||
app.quit()
|
||||
@ -8,10 +9,12 @@ if (started) {
|
||||
|
||||
const createWindow = () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
width: 1000,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
})
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
|
||||
@ -1,2 +1,10 @@
|
||||
// See the Electron documentation for details on how to use 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",
|
||||
"dayjs": "^1.11.10",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"ethers": "^6.15.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
@ -52,6 +53,12 @@
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz",
|
||||
@ -2526,6 +2533,30 @@
|
||||
"resolved": "packages/neptune-wasm",
|
||||
"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": {
|
||||
"version": "2.1.5",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -6697,6 +6734,49 @@
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
@ -11271,6 +11351,12 @@
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@ -12095,6 +12181,27 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
"axios": "^1.6.8",
|
||||
"dayjs": "^1.11.10",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"ethers": "^6.15.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
|
||||
@ -8,22 +8,8 @@ const instance = axios.create({
|
||||
headers: {
|
||||
'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) => {
|
||||
instance.defaults.headers.common['lang'] = locale
|
||||
}
|
||||
|
||||
@ -43,19 +43,3 @@ export const sendTransaction = async (
|
||||
export const broadcastSignedTransaction = async (signedTxData: any): Promise<any> => {
|
||||
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)
|
||||
}
|
||||
|
||||
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 =====
|
||||
|
||||
const getUtxos = async (
|
||||
@ -333,8 +276,6 @@ export function useNeptuneWallet() {
|
||||
initWasm: ensureWasmInitialized,
|
||||
generateWallet,
|
||||
importWallet,
|
||||
importFromViewKey,
|
||||
importFromKeystore,
|
||||
getViewKeyFromSeed,
|
||||
getAddressFromSeed,
|
||||
validateSeedPhrase,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export interface WalletState {
|
||||
seedPhrase: string[] | null
|
||||
password: string | null
|
||||
receiverId: string | null
|
||||
viewKey: string | null
|
||||
address: string | null
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import * as Page from '@/views'
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
export const ifAuthenticated = (to: any, from: any, next: any) => {
|
||||
const neptuneStore = useNeptuneStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (neptuneStore.getReceiverId) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
next('/login')
|
||||
authStore.setState('login')
|
||||
next('/auth')
|
||||
}
|
||||
|
||||
export const routes: any = [
|
||||
@ -18,15 +22,10 @@ export const routes: any = [
|
||||
beforeEnter: ifAuthenticated,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
path: '/auth',
|
||||
name: 'auth',
|
||||
component: Page.Auth,
|
||||
},
|
||||
{
|
||||
path: '/password',
|
||||
name: 'password',
|
||||
component: Page.PasswordKeystore,
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: Page.NotFound,
|
||||
|
||||
@ -13,52 +13,6 @@ export const useAuthStore = () => {
|
||||
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 = () => {
|
||||
setState('create')
|
||||
}
|
||||
@ -67,19 +21,21 @@ export const useAuthStore = () => {
|
||||
setState('login')
|
||||
}
|
||||
|
||||
const goToRecover = () => {
|
||||
setState('recovery')
|
||||
}
|
||||
|
||||
const resetFlow = () => {
|
||||
setState('onboarding')
|
||||
localStorage.removeItem('onboarding-completed')
|
||||
}
|
||||
|
||||
return {
|
||||
currentState: currentState.value,
|
||||
getCurrentState,
|
||||
setState,
|
||||
nextStep,
|
||||
previousStep,
|
||||
goToCreate,
|
||||
goToLogin,
|
||||
goToRecover,
|
||||
resetFlow,
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
// ===== STATE =====
|
||||
const wallet = ref<WalletState>({
|
||||
seedPhrase: null,
|
||||
password: null,
|
||||
receiverId: null,
|
||||
viewKey: null,
|
||||
address: null,
|
||||
@ -23,6 +24,10 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
wallet.value.seedPhrase = seedPhrase
|
||||
}
|
||||
|
||||
const setPassword = (password: string | null) => {
|
||||
wallet.value.password = password
|
||||
}
|
||||
|
||||
const setReceiverId = (receiverId: string | null) => {
|
||||
wallet.value.receiverId = receiverId
|
||||
}
|
||||
@ -62,6 +67,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
const clearWallet = () => {
|
||||
wallet.value = {
|
||||
seedPhrase: null,
|
||||
password: null,
|
||||
receiverId: null,
|
||||
viewKey: null,
|
||||
address: null,
|
||||
@ -75,6 +81,8 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
// ===== GETTERS =====
|
||||
const getWallet = computed(() => wallet.value)
|
||||
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 getViewKey = computed(() => wallet.value.viewKey)
|
||||
const getAddress = computed(() => wallet.value.address)
|
||||
@ -88,6 +96,8 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
return {
|
||||
getWallet,
|
||||
getSeedPhrase,
|
||||
getSeedPhraseString,
|
||||
getPassword,
|
||||
getReceiverId,
|
||||
getViewKey,
|
||||
getAddress,
|
||||
@ -99,6 +109,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
getError,
|
||||
|
||||
setSeedPhrase,
|
||||
setPassword,
|
||||
setReceiverId,
|
||||
setViewKey,
|
||||
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">
|
||||
import { computed } from 'vue'
|
||||
import { OnboardingComponent } from './components'
|
||||
import { OnboardingTab } from './components'
|
||||
import { useAuthStore } from '@/stores'
|
||||
import { LoginTab, CreateTab, RecoveryTab, ConfirmTab } from './components'
|
||||
import { LoginTab, CreateTab, RecoverSeedTab } from './components'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
@ -12,47 +12,21 @@ const handleGoToCreate = () => {
|
||||
authStore.goToCreate()
|
||||
}
|
||||
|
||||
const handleGoToLogin = () => {
|
||||
authStore.goToLogin()
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
authStore.nextStep()
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
authStore.previousStep()
|
||||
const handleGoToRecover = () => {
|
||||
authStore.goToRecover()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-container">
|
||||
<OnboardingComponent
|
||||
<OnboardingTab
|
||||
v-if="currentState === 'onboarding'"
|
||||
@go-to-create="handleGoToCreate"
|
||||
@go-to-login="handleGoToLogin"
|
||||
@go-to-recover="handleGoToRecover"
|
||||
/>
|
||||
|
||||
<LoginTab v-else-if="currentState === 'login'" @go-to-create="handleGoToCreate" />
|
||||
|
||||
<CreateTab
|
||||
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>
|
||||
<CreateTab v-else-if="currentState === 'create'" @go-to-recover="handleGoToRecover" />
|
||||
<RecoverSeedTab v-else-if="currentState === 'recovery'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -2,24 +2,18 @@
|
||||
import { CreateWalletComponent } from '.'
|
||||
|
||||
const emit = defineEmits<{
|
||||
goToLogin: []
|
||||
next: []
|
||||
goToRecover: []
|
||||
}>()
|
||||
|
||||
const handleNavigateToOpenWallet = () => {
|
||||
emit('goToLogin')
|
||||
}
|
||||
|
||||
const handleNavigateToRecoverySeed = () => {
|
||||
emit('next')
|
||||
const handleNavigateToRecoverWallet = () => {
|
||||
emit('goToRecover')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="create-tab">
|
||||
<CreateWalletComponent
|
||||
@navigateToOpenWallet="handleNavigateToOpenWallet"
|
||||
@navigateToRecoverySeed="handleNavigateToRecoverySeed"
|
||||
@navigate-to-recover-wallet="handleNavigateToRecoverWallet"
|
||||
/>
|
||||
</div>
|
||||
</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">
|
||||
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>
|
||||
<template>
|
||||
<div class="login-tab">
|
||||
<ImportWalletComponent @import-success="handleImported" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -3,15 +3,15 @@ import { ButtonCommon } from '@/components'
|
||||
|
||||
const emit = defineEmits<{
|
||||
goToCreate: []
|
||||
goToLogin: []
|
||||
goToRecover: []
|
||||
}>()
|
||||
|
||||
const handleGoToCreate = () => {
|
||||
emit('goToCreate')
|
||||
}
|
||||
|
||||
const handleGoToLogin = () => {
|
||||
emit('goToLogin')
|
||||
const handleRecover = () => {
|
||||
emit('goToRecover')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -20,7 +20,7 @@ const handleGoToLogin = () => {
|
||||
<div class="welcome-card flex-center">
|
||||
<div class="welcome-box">
|
||||
<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>
|
||||
</div>
|
||||
<div
|
||||
@ -30,8 +30,8 @@ const handleGoToLogin = () => {
|
||||
<ButtonCommon type="primary" size="large" @click="handleGoToCreate">
|
||||
Create new wallet
|
||||
</ButtonCommon>
|
||||
<ButtonCommon type="default" size="large" @click="handleGoToLogin">
|
||||
Open existing wallet
|
||||
<ButtonCommon type="default" size="large" @click="handleRecover">
|
||||
Recover wallet
|
||||
</ButtonCommon>
|
||||
</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 = () => {
|
||||
if (isCorrect.value) {
|
||||
correctCount.value++
|
||||
askedPositions.value.add(quizData.value!.position)
|
||||
emit('next')
|
||||
// if (isCorrect.value) {
|
||||
// correctCount.value++
|
||||
// askedPositions.value.add(quizData.value!.position)
|
||||
|
||||
if (correctCount.value >= totalQuestions) {
|
||||
emit('next')
|
||||
} else {
|
||||
showResult.value = false
|
||||
selectedAnswer.value = ''
|
||||
const newQuiz = generateQuiz()
|
||||
if (newQuiz) {
|
||||
quizData.value = newQuiz
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showResult.value = false
|
||||
selectedAnswer.value = ''
|
||||
const newQuiz = generateQuiz()
|
||||
if (newQuiz) {
|
||||
quizData.value = newQuiz
|
||||
}
|
||||
}
|
||||
// if (correctCount.value >= totalQuestions) {
|
||||
// emit('next')
|
||||
// } else {
|
||||
// showResult.value = false
|
||||
// selectedAnswer.value = ''
|
||||
// const newQuiz = generateQuiz()
|
||||
// if (newQuiz) {
|
||||
// quizData.value = newQuiz
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// showResult.value = false
|
||||
// selectedAnswer.value = ''
|
||||
// const newQuiz = generateQuiz()
|
||||
// if (newQuiz) {
|
||||
// quizData.value = newQuiz
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
@ -207,13 +208,20 @@ onMounted(() => {
|
||||
>
|
||||
NEXT QUESTION
|
||||
</ButtonCommon>
|
||||
<ButtonCommon
|
||||
<!-- <ButtonCommon
|
||||
v-if="showResult && isCorrect && correctCount + 1 >= totalQuestions"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleNext"
|
||||
>
|
||||
CONTINUE
|
||||
</ButtonCommon> -->
|
||||
<ButtonCommon
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleNext"
|
||||
>
|
||||
CONTINUE
|
||||
</ButtonCommon>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,12 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ButtonCommon, FormCommon } from '@/components'
|
||||
import { useNeptuneStore } from '@/stores';
|
||||
|
||||
const emit = defineEmits<{
|
||||
next: []
|
||||
navigateToOpenWallet: [event: Event]
|
||||
}>()
|
||||
|
||||
const neptuneStore = useNeptuneStore()
|
||||
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const passwordError = ref('')
|
||||
@ -54,6 +57,8 @@ const handleNext = () => {
|
||||
}
|
||||
return
|
||||
}
|
||||
neptuneStore.setPassword(password.value)
|
||||
// console.log('neptuneStore.getPassword :>> ', neptuneStore.getPassword)
|
||||
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">
|
||||
import { ButtonCommon } from '@/components'
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore';
|
||||
|
||||
const neptuneStore = useNeptuneStore()
|
||||
|
||||
const emit = defineEmits<{
|
||||
accessWallet: []
|
||||
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')
|
||||
}
|
||||
|
||||
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
|
||||
export { default as LoginTab } from './LoginTab.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 SeedPhraseTab } from './SeedPhraseTab.vue'
|
||||
export { default as KeystoreTab } from './KeystoreTab.vue'
|
||||
export { default as RecoverSeedTab } from './RecoverSeedTab.vue'
|
||||
|
||||
// Auth Components
|
||||
export { default as OnboardingComponent } from './OnboardingComponent.vue'
|
||||
export { default as CreateWalletComponent } from './CreateWalletComponent.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'
|
||||
export { default as OnboardingTab } from './OnboardingTab.vue'
|
||||
export { default as SeedPhraseDisplayComponent } from './SeedPhraseDisplayComponent.vue'
|
||||
|
||||
// Steps
|
||||
export * from './steps'
|
||||
// Nested Components
|
||||
export * from './create'
|
||||
export * from './recover'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { validateSeedPhrase18 } from '@/utils'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:valid', valid: boolean): void
|
||||
@ -10,11 +10,14 @@ const emit = defineEmits<{
|
||||
const seedWords = ref<string[]>(Array.from({ length: 18 }, () => ''))
|
||||
const seedError = ref('')
|
||||
|
||||
const updateValidity = () => {
|
||||
const isValid = computed(() => {
|
||||
const words = seedWords.value.filter((w) => w.trim())
|
||||
const isValid = validateSeedPhrase18(words) && !seedError.value
|
||||
emit('update:valid', isValid)
|
||||
}
|
||||
return validateSeedPhrase18(words) && !seedError.value
|
||||
})
|
||||
|
||||
watch(isValid, (newVal) => {
|
||||
emit('update:valid', newVal)
|
||||
})
|
||||
|
||||
const inputBoxFocus = (idx: number) => {
|
||||
document.getElementById('input-' + idx)?.focus()
|
||||
@ -22,7 +25,6 @@ const inputBoxFocus = (idx: number) => {
|
||||
|
||||
const handleGridInput = (index: number, value: string) => {
|
||||
seedWords.value[index] = value
|
||||
updateValidity()
|
||||
}
|
||||
|
||||
const handlePaste = (event: ClipboardEvent) => {
|
||||
@ -37,22 +39,12 @@ const handlePaste = (event: ClipboardEvent) => {
|
||||
|
||||
seedWords.value = words
|
||||
seedError.value = ''
|
||||
updateValidity()
|
||||
}
|
||||
|
||||
const validateSeed = () => {
|
||||
const words = seedWords.value.filter((w) => w.trim())
|
||||
return validateSeedPhrase18(words)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (validateSeed()) seedError.value = ''
|
||||
emit(
|
||||
'submit',
|
||||
seedWords.value.filter((w) => w.trim())
|
||||
)
|
||||
|
||||
updateValidity()
|
||||
const words = seedWords.value.filter((w) => w.trim())
|
||||
if (validateSeedPhrase18(words)) seedError.value = ''
|
||||
emit('submit', words)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@ -61,14 +53,14 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="seed-phrase-tab">
|
||||
<div class="seed-row-radio">
|
||||
<div class="recover-seed-form">
|
||||
<div class="recover-seed-row-radio">
|
||||
<div class="radio active">18 words</div>
|
||||
</div>
|
||||
|
||||
<!-- Individual input grid -->
|
||||
<div class="seed-inputs">
|
||||
<div class="seed-input-grid">
|
||||
<div class="recover-seed-inputs">
|
||||
<div class="recover-seed-input-grid">
|
||||
<div v-for="(word, i) in seedWords" :key="i" class="seed-box">
|
||||
<input
|
||||
:id="'input-' + i"
|
||||
@ -94,8 +86,8 @@ defineExpose({
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.seed-phrase-tab {
|
||||
.seed-row-radio {
|
||||
.recover-seed-form {
|
||||
.recover-seed-row-radio {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
@ -117,10 +109,10 @@ defineExpose({
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.seed-inputs {
|
||||
.recover-seed-inputs {
|
||||
width: 100%;
|
||||
}
|
||||
.seed-input-grid {
|
||||
.recover-seed-input-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
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 NotFound = () => import('@/views/NotFound/NotFoundView.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