feat: 311025/login_by_password_and_download_keystore_file_when_creating_wallet

This commit is contained in:
NguyenAnhQuan 2025-10-31 21:56:47 +07:00
parent 08ed2814c9
commit f23a20df10
21 changed files with 682 additions and 126 deletions

View File

@ -1 +1,2 @@
VITE_APP_API=
VITE_NODE_NETWORK=

2
.gitignore vendored
View File

@ -13,7 +13,7 @@ dist
dist-ssr
coverage
*.local
.vite/build/*
.vite/*/**
wallets/*
/cypress/videos/

View File

@ -1,24 +1,56 @@
import { ipcMain } from 'electron';
import type { HDNodeWallet } from 'ethers';
import { Wallet } from 'ethers';
import fs from 'fs';
import path from 'path';
import { ipcMain } from 'electron'
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);
try {
const wallet = Wallet.fromPhrase(seed)
const keystore = await wallet.encrypt(password)
const savePath = path.join(process.cwd(), 'wallets');
fs.mkdirSync(savePath, { recursive: true });
const savePath = path.join(process.cwd(), 'wallets')
fs.mkdirSync(savePath, { recursive: true })
const filePath = path.join(savePath, `${wallet.address}.json`);
fs.writeFileSync(filePath, keystore);
const filePath = path.join(savePath, `${wallet.address}.json`)
fs.writeFileSync(filePath, keystore)
return { address: wallet.address, filePath };
});
return { address: wallet.address, filePath, error: null }
} catch (error) {
console.error('Error creating keystore:', error)
return { address: null, filePath: null, error: String(error) }
}
})
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 };
});
try {
const json = fs.readFileSync(filePath, 'utf-8')
const wallet = await Wallet.fromEncryptedJson(json, password)
let phrase: string | undefined
if ('mnemonic' in wallet && wallet.mnemonic) {
phrase = wallet.mnemonic.phrase
}
return { address: wallet.address, phrase, error: null }
} catch (error) {
console.error('Error decrypting keystore ipc:', error)
return { address: null, phrase: null, error: String(error) }
}
})
ipcMain.handle('wallet:checkKeystore', async () => {
try {
const walletDir = path.join(process.cwd(), 'wallets')
if (!fs.existsSync(walletDir))
return { exists: false, filePath: null, error: 'Wallet directory not found' }
const file = fs.readdirSync(walletDir).find((f) => f.endsWith('.json'))
if (!file) return { exists: false, filePath: null, error: 'Keystore file not found' }
const filePath = path.join(walletDir, file)
return { exists: true, filePath, error: null }
} catch (error) {
console.error('Error checking keystore:', error)
return { exists: false, filePath: null, error: String(error) }
}
})

View File

@ -7,4 +7,5 @@ contextBridge.exposeInMainWorld('walletApi', {
ipcRenderer.invoke('wallet:createKeystore', seed, password),
decryptKeystore: (filePath: string, password: string) =>
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password),
checkKeystore: () => ipcRenderer.invoke('wallet:checkKeystore'),
})

View File

@ -15,6 +15,8 @@ const currentDaaScore = ref(0)
const isLoadingData = ref(false)
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
const isAddressExpanded = ref(false)
const walletStatus = computed(() => {
if (neptuneStore.getLoading) return 'Loading...'
if (neptuneStore.getError) return 'Error'
@ -22,6 +24,10 @@ const walletStatus = computed(() => {
return 'Offline'
})
const toggleAddressExpanded = () => {
isAddressExpanded.value = !isAddressExpanded.value
}
const copyAddress = async () => {
if (!receiveAddress.value) {
message.error('No address available')
@ -109,7 +115,11 @@ onMounted(() => {
<!-- Receive Address Section -->
<div class="receive-section">
<div class="address-label">Receive Address:</div>
<div class="address-value" @click="copyAddress">
<div
class="address-value"
:class="{ expanded: isAddressExpanded, collapsed: !isAddressExpanded }"
@click="copyAddress"
>
{{ receiveAddress || 'No address available' }}
<svg
class="copy-icon"
@ -123,6 +133,13 @@ onMounted(() => {
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</div>
<button
v-if="receiveAddress && receiveAddress.length > 80"
class="toggle-address-btn"
@click.stop="toggleAddressExpanded"
>
{{ isAddressExpanded ? 'Show less' : 'Show more' }}
</button>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
@ -231,9 +248,26 @@ onMounted(() => {
cursor: pointer;
transition: var(--transition-all);
display: flex;
align-items: center;
align-items: flex-start;
gap: var(--spacing-sm);
border: 2px solid transparent;
line-height: 1.5;
position: relative;
&.collapsed {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
&.expanded {
display: flex;
flex-direction: column;
word-break: break-all;
}
&:hover {
background: var(--bg-hover);
@ -245,6 +279,29 @@ onMounted(() => {
height: 18px;
flex-shrink: 0;
color: var(--primary-color);
margin-top: 2px;
align-self: flex-start;
}
}
.toggle-address-btn {
margin-top: var(--spacing-md);
padding: var(--spacing-xs) var(--spacing-md);
background: none;
border: none;
color: var(--primary-color);
font-size: var(--font-sm);
font-weight: var(--font-medium);
cursor: pointer;
text-decoration: underline;
transition: color 0.2s ease;
&:hover {
color: var(--primary-hover);
}
&:active {
opacity: 0.8;
}
}
}

View File

@ -22,7 +22,12 @@ const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
const showPassword = ref(false)
const isFocused = ref(false)
const inputType = computed(() => props.type)
const inputType = computed(() => {
if (props.type === 'password' ) {
return showPassword.value ? 'text' : 'password'
}
return props.type
})
const togglePassword = () => (showPassword.value = !showPassword.value)
const handleInput = (e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value)
@ -56,14 +61,13 @@ const handleBlur = (e: FocusEvent) => {
@blur="handleBlur"
/>
<button
<div
v-if="type === 'password' && showPasswordToggle"
type="button"
class="password-toggle flex-center"
@click="togglePassword"
>
<IconCommon :size="20" :icon="showPassword ? 'eye-off' : 'eye'" />
</button>
</div>
</div>
<div v-if="error" class="error-message">{{ error }}</div>

View File

@ -0,0 +1,133 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ButtonCommon, FormCommon } from '@/components'
interface Props {
title?: string
subtitle?: string
buttonText?: string
hideBackButton?: boolean
backButtonText?: string
placeholder?: string
label?: string
loading?: boolean
error?: boolean
errorMessage?: string
}
const props = withDefaults(defineProps<Props>(), {
title: 'Access Wallet',
subtitle: 'Enter your password to unlock your wallet',
buttonText: 'Unlock Wallet',
backButtonText: 'Back',
hideBackButton: false,
placeholder: 'Enter your password',
label: 'Password',
loading: false,
error: false,
errorMessage: 'Invalid password',
})
const emit = defineEmits<{
submit: [password: string]
back: []
}>()
const password = ref('')
const passwordError = ref('')
const canProceed = computed(() => {
return password.value.length > 0 && !passwordError.value
})
const handleSubmit = () => {
if (!canProceed.value) {
if (!password.value) {
passwordError.value = 'Please enter your password'
}
return
}
passwordError.value = ''
emit('submit', password.value)
}
const handleBack = () => {
emit('back')
password.value = ''
passwordError.value = ''
}
</script>
<template>
<div class="auth-card-content">
<div class="form-group">
<FormCommon
v-model="password"
type="password"
:label="props.label"
:placeholder="props.placeholder"
show-password-toggle
required
:error="passwordError"
@input="passwordError = ''"
@keyup.enter="handleSubmit"
/>
<span v-if="error" class="error-message">{{ errorMessage }}</span>
</div>
<div class="auth-button-group">
<ButtonCommon
v-if="!props.hideBackButton"
type="default"
size="large"
class="auth-button"
block
@click="handleBack"
>
{{ props.backButtonText }}
</ButtonCommon>
<ButtonCommon
type="primary"
size="large"
class="auth-button"
block
:disabled="!canProceed"
:loading="props.loading"
@click="handleSubmit"
>
{{ props.buttonText }}
</ButtonCommon>
</div>
</div>
</template>
<style lang="scss" scoped>
.auth-card-content {
.form-group {
margin-bottom: var(--spacing-xl);
}
}
.auth-button {
width: fit-content;
margin: 0 auto;
}
.auth-button-group {
width: 50%;
margin: 0 auto;
margin-top: var(--spacing-2xl);
@include center_flex;
gap: var(--spacing-md);
}
.error-message {
display: block;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--error-light);
color: var(--error-color);
border-radius: var(--radius-sm);
font-size: var(--font-sm);
text-align: center;
}
</style>

View File

@ -1,7 +1,8 @@
import LayoutVue from './common/LayoutVue.vue'
import ButtonCommon from './common/ButtonCommon.vue'
import FormCommon from './common/FormCommon.vue'
import PasswordForm from './common/PasswordForm.vue'
import SpinnerCommon from './common/SpinnerCommon.vue'
import { IconCommon } from './icon'
export { LayoutVue, ButtonCommon, FormCommon, SpinnerCommon, IconCommon }
export { LayoutVue, ButtonCommon, FormCommon, PasswordForm, SpinnerCommon, IconCommon }

View File

@ -1,4 +1,3 @@
import { computed } from 'vue'
import { useNeptuneStore } from '@/stores/neptuneStore'
import * as API from '@/api/neptuneApi'
import type { GenerateSeedResult, ViewKeyResult } from '@/interface'
@ -7,7 +6,6 @@ import initWasm, {
get_viewkey,
address_from_seed,
validate_seed_phrase,
decode_viewkey,
} from '@neptune/wasm'
let wasmInitialized = false
@ -57,13 +55,11 @@ export function useNeptuneWallet() {
const resultJson = generate_seed()
const result: GenerateSeedResult = JSON.parse(resultJson)
console.log('result :>> ', result);
store.setSeedPhrase(result.seed_phrase)
store.setReceiverId(result.receiver_identifier)
const viewKeyResult = await getViewKeyFromSeed(result.seed_phrase, store.getNetwork)
const viewKeyResult = await getViewKeyFromSeed(result.seed_phrase)
store.setViewKey(viewKeyResult.view_key)
store.setAddress(viewKeyResult.address)
@ -77,20 +73,14 @@ export function useNeptuneWallet() {
}
}
const getViewKeyFromSeed = async (
seedPhrase: string[],
network: 'mainnet' | 'testnet'
): Promise<ViewKeyResult> => {
const getViewKeyFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
await ensureWasmInitialized()
const seedPhraseJson = JSON.stringify(seedPhrase)
const resultJson = get_viewkey(seedPhraseJson, network)
const resultJson = get_viewkey(seedPhraseJson, store.getNetwork)
return JSON.parse(resultJson)
}
const importWallet = async (
seedPhrase: string[],
network: 'mainnet' | 'testnet' = 'testnet'
): Promise<ViewKeyResult> => {
const importWallet = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
try {
store.setLoading(true)
store.setError(null)
@ -100,13 +90,12 @@ export function useNeptuneWallet() {
throw new Error('Invalid seed phrase')
}
const result = await getViewKeyFromSeed(seedPhrase, network)
const result = await getViewKeyFromSeed(seedPhrase)
store.setSeedPhrase(seedPhrase)
store.setReceiverId(result.receiver_identifier)
store.setViewKey(result.view_key)
store.setAddress(result.address)
store.setNetwork(network)
return result
} catch (err) {
@ -118,13 +107,10 @@ export function useNeptuneWallet() {
}
}
const getAddressFromSeed = async (
seedPhrase: string[],
network: 'mainnet' | 'testnet'
): Promise<string> => {
const getAddressFromSeed = async (seedPhrase: string[]): Promise<string> => {
await ensureWasmInitialized()
const seedPhraseJson = JSON.stringify(seedPhrase)
return address_from_seed(seedPhraseJson, network)
return address_from_seed(seedPhraseJson, store.getNetwork)
}
const validateSeedPhrase = async (seedPhrase: string[]): Promise<boolean> => {
@ -138,10 +124,27 @@ export function useNeptuneWallet() {
}
}
const decodeViewKey = async (viewKeyHex: string): Promise<{ receiver_identifier: string }> => {
await ensureWasmInitialized()
const resultJson = decode_viewkey(viewKeyHex)
return JSON.parse(resultJson)
const decryptKeystore = async (password: string): Promise<void> => {
try {
const result = await (window as any).walletApi.decryptKeystore(
store.getKeystorePath || '',
password
)
if (result.error) {
console.error('Error decrypting keystore composable:', result.error)
return
}
const seedPhrase = result.phrase.trim().split(/\s+/)
const viewKeyResult = await getViewKeyFromSeed(seedPhrase)
store.setSeedPhrase(seedPhrase)
store.setAddress(viewKeyResult.address)
store.setViewKey(viewKeyResult.view_key)
store.setReceiverId(viewKeyResult.receiver_identifier)
} catch (err) {
console.error('Error decrypting keystore composable:', err)
}
}
// ===== API METHODS =====
@ -256,7 +259,7 @@ export function useNeptuneWallet() {
store.setNetwork(network)
if (store.getSeedPhrase) {
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase, network)
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase)
store.setAddress(viewKeyResult.address)
store.setViewKey(viewKeyResult.view_key)
}
@ -268,24 +271,19 @@ export function useNeptuneWallet() {
}
return {
walletState: computed(() => store.getWallet),
isLoading: computed(() => store.getLoading),
error: computed(() => store.getError),
hasWallet: computed(() => store.hasWallet),
initWasm: ensureWasmInitialized,
generateWallet,
importWallet,
getViewKeyFromSeed,
getAddressFromSeed,
validateSeedPhrase,
decodeViewKey,
getUtxos,
getBalance,
getBlockHeight,
getNetworkInfo,
sendTransaction,
decryptKeystore,
clearWallet,
setNetwork,

View File

@ -1,19 +1,30 @@
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 ifAuthenticated = (to: any, from: any, next: any) => {
const neptuneStore = useNeptuneStore()
const authStore = useAuthStore()
if (neptuneStore.getReceiverId) {
if (neptuneStore.hasWallet) {
next()
return
}
authStore.setState('login')
next('/auth')
}
const ifNotAuthenticated = async (to: any, from: any, next: any) => {
const neptuneStore = useNeptuneStore()
const authStore = useAuthStore()
const keystoreFile = await (window as any).walletApi.checkKeystore()
if (keystoreFile.exists) {
neptuneStore.setKeystorePath(keystoreFile.filePath)
authStore.setState('login')
}
next()
}
export const routes: any = [
{
path: '/',
@ -25,6 +36,7 @@ export const routes: any = [
path: '/auth',
name: 'auth',
component: Page.Auth,
beforeEnter: ifNotAuthenticated,
},
{
path: '/:pathMatch(.*)*',

View File

@ -3,6 +3,8 @@ import { ref, computed } from 'vue'
import type { WalletState } from '@/interface'
export const useNeptuneStore = defineStore('neptune', () => {
const defaultNetwork = (import.meta.env.VITE_NODE_NETWORK || 'testnet') as 'mainnet' | 'testnet'
// ===== STATE =====
const wallet = ref<WalletState>({
seedPhrase: null,
@ -10,11 +12,13 @@ export const useNeptuneStore = defineStore('neptune', () => {
receiverId: null,
viewKey: null,
address: null,
network: 'testnet',
network: defaultNetwork,
balance: null,
utxos: [],
})
const keystorePath = ref<null | string>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
@ -64,6 +68,10 @@ export const useNeptuneStore = defineStore('neptune', () => {
wallet.value = { ...wallet.value, ...walletData }
}
const setKeystorePath = (path: string | null) => {
keystorePath.value = path
}
const clearWallet = () => {
wallet.value = {
seedPhrase: null,
@ -71,7 +79,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
receiverId: null,
viewKey: null,
address: null,
network: 'testnet',
network: defaultNetwork,
balance: null,
utxos: [],
}
@ -92,7 +100,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
const hasWallet = computed(() => wallet.value.address !== null)
const getLoading = computed(() => isLoading.value)
const getError = computed(() => error.value)
const getKeystorePath = computed(() => keystorePath.value)
return {
getWallet,
getSeedPhrase,
@ -107,7 +115,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
hasWallet,
getLoading,
getError,
getKeystorePath,
setSeedPhrase,
setPassword,
setReceiverId,
@ -119,6 +127,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
setLoading,
setError,
setWallet,
setKeystorePath,
clearWallet,
}
})

View File

@ -2,11 +2,11 @@
declare global {
interface Window {
electron: {
send: (channel: string, data: any) => void;
receive: (channel: string, func: (...args: any[]) => void) => void;
removeAllListeners: (channel: string) => void;
};
send: (channel: string, data: any) => void
receive: (channel: string, func: (...args: any[]) => void) => void
removeAllListeners: (channel: string) => void
}
}
}
export {};
export {}

View File

@ -12,9 +12,7 @@ const handleNavigateToRecoverWallet = () => {
<template>
<div class="create-tab">
<CreateWalletComponent
@navigate-to-recover-wallet="handleNavigateToRecoverWallet"
/>
<CreateWalletComponent @navigate-to-recover-wallet="handleNavigateToRecoverWallet" />
</div>
</template>

View File

@ -1,12 +1,236 @@
<script setup lang="ts">
import { ref } from 'vue'
import { PasswordForm } from '@/components'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/authStore'
const authStore = useAuthStore()
const router = useRouter()
const neptuneWallet = useNeptuneWallet()
const isLoading = ref(false)
const error = ref(false)
const handlePasswordSubmit = async (password: string) => {
try {
isLoading.value = true
await neptuneWallet.decryptKeystore(password)
router.push({name: 'home'})
} catch (err) {
error.value = true
console.error('Password Error')
}
finally {
isLoading.value = false
}
}
const handleNewWallet = () => {
authStore.goToCreate()
router.push({name: 'auth'})
}
</script>
<template>
<div class="login-tab">
<div class="login-password-form">
<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="neptuneGradientLogin"
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="ringGradientLogin"
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(#neptuneGradientLogin)" />
<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(#ringGradientLogin)"
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">NPT</span>
</div>
</div>
<h1 class="auth-title">Access Wallet</h1>
<p class="auth-subtitle">Enter your password to unlock your wallet</p>
</div>
<PasswordForm
button-text="Unlock Wallet"
placeholder="Enter your password"
back-button-text="New Wallet"
label="Password"
:loading="isLoading"
@submit="handlePasswordSubmit"
@back="handleNewWallet"
:error="error"
:errorMessage="'Invalid password'"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.login-tab {
padding: var(--spacing-lg);
display: flex;
align-items: center;
justify-content: center;
min-height: 100%;
}
.login-password-form {
width: 100%;
max-width: 480px;
}
.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;
}
}
@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

@ -83,28 +83,28 @@ const handleAnswerSelect = (answer: string) => {
const handleNext = () => {
emit('next')
// if (isCorrect.value) {
// correctCount.value++
// askedPositions.value.add(quizData.value!.position)
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 = () => {
@ -216,11 +216,7 @@ onMounted(() => {
>
CONTINUE
</ButtonCommon> -->
<ButtonCommon
type="primary"
size="large"
@click="handleNext"
>
<ButtonCommon type="primary" size="large" @click="handleNext">
CONTINUE
</ButtonCommon>
</div>

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ButtonCommon, FormCommon } from '@/components'
import { useNeptuneStore } from '@/stores';
import { useNeptuneStore } from '@/stores'
const emit = defineEmits<{
next: []
navigateToOpenWallet: [event: Event]
navigateToRecoverWallet: []
}>()
const neptuneStore = useNeptuneStore()
@ -58,13 +58,11 @@ const handleNext = () => {
return
}
neptuneStore.setPassword(password.value)
// console.log('neptuneStore.getPassword :>> ', neptuneStore.getPassword)
emit('next')
}
const handleIHaveWallet = (e: Event) => {
e.preventDefault()
emit('navigateToOpenWallet', e)
const handleIHaveWallet = () => {
emit('navigateToRecoverWallet')
}
</script>
@ -192,9 +190,14 @@ const handleIHaveWallet = (e: Event) => {
Create Wallet
</ButtonCommon>
<div class="secondary-actions">
<button class="link-button" @click="handleIHaveWallet">
<ButtonCommon
type="link"
size="small"
class="link-button"
@click="handleIHaveWallet"
>
Already have a wallet?
</button>
</ButtonCommon>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore';
import { useNeptuneStore } from '@/stores/neptuneStore'
const neptuneStore = useNeptuneStore()
@ -13,8 +13,6 @@ 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')
}

View File

@ -1,29 +1,51 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ButtonCommon } from '@/components'
import { Modal } from 'ant-design-vue'
import { ButtonCommon, PasswordForm } from '@/components'
import { RecoverSeedComponent } from '..'
const emit = defineEmits<{
(e: 'import-success', data: { type: 'seed'; value: string[] }): void
(e: 'import-success', data: { type: 'seed'; value: string[]; password: string }): void
}>()
const recoverSeedComponentRef = ref<InstanceType<typeof RecoverSeedComponent>>()
const isSeedPhraseValid = ref(false)
const showPasswordModal = ref(false)
const seedPhrase = ref<string[]>([])
const isLoading = ref(false)
const passwordError = ref(false)
const handleSeedPhraseSubmit = (words: string[]) => {
emit('import-success', {
type: 'seed',
value: words,
})
seedPhrase.value = words
showPasswordModal.value = true
}
const handleContinue = () => {
recoverSeedComponentRef.value?.handleSubmit?.()
}
const handlePasswordSubmit = (password: string) => {
showPasswordModal.value = false
emit('import-success', {
type: 'seed',
value: seedPhrase.value,
password,
})
}
const handlePasswordBack = () => {
showPasswordModal.value = false
passwordError.value = false
}
const handleModalCancel = () => {
showPasswordModal.value = false
passwordError.value = false
}
</script>
<template>
<div class="import-wallet dark-card">
<h2 class="title">Import Wallet</h2>
<h2 class="title">Recover Wallet</h2>
<div class="desc">Enter your recovery seed phrase</div>
<RecoverSeedComponent
ref="recoverSeedComponentRef"
@ -40,6 +62,27 @@ const handleContinue = () => {
>Continue</ButtonCommon
>
</div>
<Modal
v-model:open="showPasswordModal"
title="Enter Password"
:footer="null"
:width="480"
:mask-closable="false"
:keyboard="false"
@cancel="handleModalCancel"
>
<PasswordForm
button-text="Continue"
placeholder="Enter password to encrypt seed phrase"
label="Password"
:loading="isLoading"
:error="passwordError"
:error-message="'Invalid password'"
@submit="handlePasswordSubmit"
@back="handlePasswordBack"
/>
</Modal>
</template>
<style lang="scss" scoped>
.import-wallet {

View File

@ -5,6 +5,7 @@ import { SpinnerCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
import { get_network_info } from '@neptune/wasm'
const neptuneStore = useNeptuneStore()
const { getBlockHeight, getNetworkInfo } = useNeptuneWallet()
@ -22,7 +23,6 @@ const network = computed(() => neptuneStore.getNetwork)
const loadNetworkData = async () => {
try {
error.value = ''
const [heightResult, infoResult] = await Promise.all([getBlockHeight(), getNetworkInfo()])
if (heightResult !== undefined) {

View File

@ -1,13 +1,31 @@
<script setup lang="ts">
import { Divider } from 'ant-design-vue'
import { ref } from 'vue'
import { Divider, Modal } from 'ant-design-vue'
import ButtonCommon from '@/components/common/ButtonCommon.vue'
import SeedPhraseDisplayComponent from '@/views/Auth/components/SeedPhraseDisplayComponent.vue'
const showSeedModal = ref(false)
const handleBackupFile = () => {
// TODO: Implement backup file functionality
}
const handleBackupSeed = () => {
// TODO: Implement backup seed functionality
showSeedModal.value = true
}
const handleCloseModal = () => {
showSeedModal.value = false
}
const handleModalNext = () => {
// SeedPhraseDisplayComponent emits 'next' but in modal context, we just close
handleCloseModal()
}
const handleModalBack = () => {
// SeedPhraseDisplayComponent emits 'back' but in modal context, we just close
handleCloseModal()
}
</script>
@ -28,6 +46,20 @@ const handleBackupSeed = () => {
<Divider />
</div>
<Modal
v-model:open="showSeedModal"
title="Backup Seed Phrase"
:footer="null"
:width="600"
:mask-closable="false"
:keyboard="false"
@cancel="handleCloseModal"
>
<div class="seed-modal-content">
<SeedPhraseDisplayComponent @next="handleModalNext" @back="handleModalBack" />
</div>
</Modal>
</template>
<style lang="scss" scoped>
@ -91,4 +123,18 @@ const handleBackupSeed = () => {
}
}
}
.seed-modal-content {
:deep(.recovery-container) {
padding: 0;
min-height: auto;
background: transparent;
}
:deep(.recovery-card) {
border: none;
box-shadow: none;
padding: 0;
}
}
</style>