refactor: 3311025/recoding_create_wallet_flow

This commit is contained in:
NguyenAnhQuan 2025-10-31 01:22:35 +07:00
parent a39f306f8d
commit 08ed2814c9
39 changed files with 470 additions and 1348 deletions

2
.gitignore vendored
View File

@ -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
View 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 };
});

View File

@ -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) {

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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
} }

View File

@ -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)
}

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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,
} }
} }

View File

@ -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,

View File

@ -1 +1 @@
export const validateSeedPhrase18 = (words: string[]): boolean => words.length !== 18 export const validateSeedPhrase18 = (words: string[]): boolean => words.length === 18

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">&#9888;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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')
} }

View 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>

View File

@ -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')
} }

View 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'

View File

@ -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'

View File

@ -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;

View 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>

View File

@ -0,0 +1,2 @@
export { default as RecoverSeedComponent } from './RecoverSeedComponent.vue'
export { default as RecoverWalletComponent } from './RecoverWalletComponent.vue'

View File

@ -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>

View File

@ -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'

View File

@ -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>

View File

@ -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')