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_APP_API=
VITE_NODE_NETWORK=

2
.gitignore vendored
View File

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

View File

@ -1,24 +1,56 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron'
import type { HDNodeWallet } from 'ethers'; import { Wallet } from 'ethers'
import { Wallet } from 'ethers'; import fs from 'fs'
import fs from 'fs'; import path from 'path'
import path from 'path';
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => { ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
const wallet = Wallet.fromPhrase(seed); try {
const keystore = await wallet.encrypt(password); const wallet = Wallet.fromPhrase(seed)
const keystore = await wallet.encrypt(password)
const savePath = path.join(process.cwd(), 'wallets'); const savePath = path.join(process.cwd(), 'wallets')
fs.mkdirSync(savePath, { recursive: true }); fs.mkdirSync(savePath, { recursive: true })
const filePath = path.join(savePath, `${wallet.address}.json`); const filePath = path.join(savePath, `${wallet.address}.json`)
fs.writeFileSync(filePath, keystore); 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) => { ipcMain.handle('wallet:decryptKeystore', async (_event, filePath, password) => {
const json = fs.readFileSync(filePath, 'utf-8'); try {
const wallet = await Wallet.fromEncryptedJson(json, password) as HDNodeWallet; const json = fs.readFileSync(filePath, 'utf-8')
return { address: wallet.address, phrase: wallet.mnemonic }; 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), ipcRenderer.invoke('wallet:createKeystore', seed, password),
decryptKeystore: (filePath: string, password: string) => decryptKeystore: (filePath: string, password: string) =>
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password), 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 isLoadingData = ref(false)
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '') const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
const isAddressExpanded = ref(false)
const walletStatus = computed(() => { const walletStatus = computed(() => {
if (neptuneStore.getLoading) return 'Loading...' if (neptuneStore.getLoading) return 'Loading...'
if (neptuneStore.getError) return 'Error' if (neptuneStore.getError) return 'Error'
@ -22,6 +24,10 @@ const walletStatus = computed(() => {
return 'Offline' return 'Offline'
}) })
const toggleAddressExpanded = () => {
isAddressExpanded.value = !isAddressExpanded.value
}
const copyAddress = async () => { const copyAddress = async () => {
if (!receiveAddress.value) { if (!receiveAddress.value) {
message.error('No address available') message.error('No address available')
@ -109,7 +115,11 @@ onMounted(() => {
<!-- Receive Address Section --> <!-- Receive Address Section -->
<div class="receive-section"> <div class="receive-section">
<div class="address-label">Receive Address:</div> <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' }} {{ receiveAddress || 'No address available' }}
<svg <svg
class="copy-icon" 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> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg> </svg>
</div> </div>
<button
v-if="receiveAddress && receiveAddress.length > 80"
class="toggle-address-btn"
@click.stop="toggleAddressExpanded"
>
{{ isAddressExpanded ? 'Show less' : 'Show more' }}
</button>
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="action-buttons"> <div class="action-buttons">
@ -231,9 +248,26 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
transition: var(--transition-all); transition: var(--transition-all);
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: var(--spacing-sm); gap: var(--spacing-sm);
border: 2px solid transparent; 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 { &:hover {
background: var(--bg-hover); background: var(--bg-hover);
@ -245,6 +279,29 @@ onMounted(() => {
height: 18px; height: 18px;
flex-shrink: 0; flex-shrink: 0;
color: var(--primary-color); 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 showPassword = ref(false)
const isFocused = 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 togglePassword = () => (showPassword.value = !showPassword.value)
const handleInput = (e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value) const handleInput = (e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value)
@ -56,14 +61,13 @@ const handleBlur = (e: FocusEvent) => {
@blur="handleBlur" @blur="handleBlur"
/> />
<button <div
v-if="type === 'password' && showPasswordToggle" v-if="type === 'password' && showPasswordToggle"
type="button"
class="password-toggle flex-center" class="password-toggle flex-center"
@click="togglePassword" @click="togglePassword"
> >
<IconCommon :size="20" :icon="showPassword ? 'eye-off' : 'eye'" /> <IconCommon :size="20" :icon="showPassword ? 'eye-off' : 'eye'" />
</button> </div>
</div> </div>
<div v-if="error" class="error-message">{{ error }}</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 LayoutVue from './common/LayoutVue.vue'
import ButtonCommon from './common/ButtonCommon.vue' import ButtonCommon from './common/ButtonCommon.vue'
import FormCommon from './common/FormCommon.vue' import FormCommon from './common/FormCommon.vue'
import PasswordForm from './common/PasswordForm.vue'
import SpinnerCommon from './common/SpinnerCommon.vue' import SpinnerCommon from './common/SpinnerCommon.vue'
import { IconCommon } from './icon' 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 { useNeptuneStore } from '@/stores/neptuneStore'
import * as API from '@/api/neptuneApi' import * as API from '@/api/neptuneApi'
import type { GenerateSeedResult, ViewKeyResult } from '@/interface' import type { GenerateSeedResult, ViewKeyResult } from '@/interface'
@ -7,7 +6,6 @@ import initWasm, {
get_viewkey, get_viewkey,
address_from_seed, address_from_seed,
validate_seed_phrase, validate_seed_phrase,
decode_viewkey,
} from '@neptune/wasm' } from '@neptune/wasm'
let wasmInitialized = false let wasmInitialized = false
@ -57,13 +55,11 @@ export function useNeptuneWallet() {
const resultJson = generate_seed() const resultJson = generate_seed()
const result: GenerateSeedResult = JSON.parse(resultJson) const result: GenerateSeedResult = JSON.parse(resultJson)
console.log('result :>> ', result);
store.setSeedPhrase(result.seed_phrase) store.setSeedPhrase(result.seed_phrase)
store.setReceiverId(result.receiver_identifier) 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.setViewKey(viewKeyResult.view_key)
store.setAddress(viewKeyResult.address) store.setAddress(viewKeyResult.address)
@ -77,20 +73,14 @@ export function useNeptuneWallet() {
} }
} }
const getViewKeyFromSeed = async ( const getViewKeyFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
seedPhrase: string[],
network: 'mainnet' | 'testnet'
): Promise<ViewKeyResult> => {
await ensureWasmInitialized() await ensureWasmInitialized()
const seedPhraseJson = JSON.stringify(seedPhrase) const seedPhraseJson = JSON.stringify(seedPhrase)
const resultJson = get_viewkey(seedPhraseJson, network) const resultJson = get_viewkey(seedPhraseJson, store.getNetwork)
return JSON.parse(resultJson) return JSON.parse(resultJson)
} }
const importWallet = async ( const importWallet = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
seedPhrase: string[],
network: 'mainnet' | 'testnet' = 'testnet'
): Promise<ViewKeyResult> => {
try { try {
store.setLoading(true) store.setLoading(true)
store.setError(null) store.setError(null)
@ -100,13 +90,12 @@ export function useNeptuneWallet() {
throw new Error('Invalid seed phrase') throw new Error('Invalid seed phrase')
} }
const result = await getViewKeyFromSeed(seedPhrase, network) const result = await getViewKeyFromSeed(seedPhrase)
store.setSeedPhrase(seedPhrase) store.setSeedPhrase(seedPhrase)
store.setReceiverId(result.receiver_identifier) store.setReceiverId(result.receiver_identifier)
store.setViewKey(result.view_key) store.setViewKey(result.view_key)
store.setAddress(result.address) store.setAddress(result.address)
store.setNetwork(network)
return result return result
} catch (err) { } catch (err) {
@ -118,13 +107,10 @@ export function useNeptuneWallet() {
} }
} }
const getAddressFromSeed = async ( const getAddressFromSeed = async (seedPhrase: string[]): Promise<string> => {
seedPhrase: string[],
network: 'mainnet' | 'testnet'
): Promise<string> => {
await ensureWasmInitialized() await ensureWasmInitialized()
const seedPhraseJson = JSON.stringify(seedPhrase) 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> => { const validateSeedPhrase = async (seedPhrase: string[]): Promise<boolean> => {
@ -138,10 +124,27 @@ export function useNeptuneWallet() {
} }
} }
const decodeViewKey = async (viewKeyHex: string): Promise<{ receiver_identifier: string }> => { const decryptKeystore = async (password: string): Promise<void> => {
await ensureWasmInitialized() try {
const resultJson = decode_viewkey(viewKeyHex) const result = await (window as any).walletApi.decryptKeystore(
return JSON.parse(resultJson) 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 ===== // ===== API METHODS =====
@ -256,7 +259,7 @@ export function useNeptuneWallet() {
store.setNetwork(network) store.setNetwork(network)
if (store.getSeedPhrase) { if (store.getSeedPhrase) {
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase, network) const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase)
store.setAddress(viewKeyResult.address) store.setAddress(viewKeyResult.address)
store.setViewKey(viewKeyResult.view_key) store.setViewKey(viewKeyResult.view_key)
} }
@ -268,24 +271,19 @@ export function useNeptuneWallet() {
} }
return { return {
walletState: computed(() => store.getWallet),
isLoading: computed(() => store.getLoading),
error: computed(() => store.getError),
hasWallet: computed(() => store.hasWallet),
initWasm: ensureWasmInitialized, initWasm: ensureWasmInitialized,
generateWallet, generateWallet,
importWallet, importWallet,
getViewKeyFromSeed, getViewKeyFromSeed,
getAddressFromSeed, getAddressFromSeed,
validateSeedPhrase, validateSeedPhrase,
decodeViewKey,
getUtxos, getUtxos,
getBalance, getBalance,
getBlockHeight, getBlockHeight,
getNetworkInfo, getNetworkInfo,
sendTransaction, sendTransaction,
decryptKeystore,
clearWallet, clearWallet,
setNetwork, setNetwork,

View File

@ -1,19 +1,30 @@
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' import { useAuthStore } from '@/stores/authStore'
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.hasWallet) {
next() next()
return return
} }
authStore.setState('login')
next('/auth') 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 = [ export const routes: any = [
{ {
path: '/', path: '/',
@ -25,6 +36,7 @@ export const routes: any = [
path: '/auth', path: '/auth',
name: 'auth', name: 'auth',
component: Page.Auth, component: Page.Auth,
beforeEnter: ifNotAuthenticated,
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',

View File

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

View File

@ -1,12 +1,12 @@
// Global type definitions for Electron API // Global type definitions for Electron API
declare global { declare global {
interface Window { interface Window {
electron: { electron: {
send: (channel: string, data: any) => void; send: (channel: string, data: any) => void
receive: (channel: string, func: (...args: any[]) => void) => void; receive: (channel: string, func: (...args: any[]) => void) => void
removeAllListeners: (channel: string) => void; removeAllListeners: (channel: string) => void
}; }
} }
} }
export {}; export {}

View File

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

View File

@ -1,12 +1,236 @@
<script setup lang="ts"> <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> </script>
<template> <template>
<div class="login-tab"> <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> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.login-tab { .login-tab {
padding: var(--spacing-lg); 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> </style>

View File

@ -31,7 +31,7 @@ const handleRecover = () => {
Create new wallet Create new wallet
</ButtonCommon> </ButtonCommon>
<ButtonCommon type="default" size="large" @click="handleRecover"> <ButtonCommon type="default" size="large" @click="handleRecover">
Recover 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

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

View File

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

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ButtonCommon } from '@/components' import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore'; import { useNeptuneStore } from '@/stores/neptuneStore'
const neptuneStore = useNeptuneStore() const neptuneStore = useNeptuneStore()
@ -13,8 +13,6 @@ const handleAccessWallet = async () => {
const seedPhrase = neptuneStore.getSeedPhraseString const seedPhrase = neptuneStore.getSeedPhraseString
const password = neptuneStore.getPassword! const password = neptuneStore.getPassword!
const encrypted = (window as any).walletApi.createKeystore(seedPhrase, password) 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

@ -1,29 +1,51 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { ButtonCommon } from '@/components' import { Modal } from 'ant-design-vue'
import { ButtonCommon, PasswordForm } from '@/components'
import { RecoverSeedComponent } from '..' import { RecoverSeedComponent } from '..'
const emit = defineEmits<{ 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 recoverSeedComponentRef = ref<InstanceType<typeof RecoverSeedComponent>>()
const isSeedPhraseValid = ref(false) const isSeedPhraseValid = ref(false)
const showPasswordModal = ref(false)
const seedPhrase = ref<string[]>([])
const isLoading = ref(false)
const passwordError = ref(false)
const handleSeedPhraseSubmit = (words: string[]) => { const handleSeedPhraseSubmit = (words: string[]) => {
emit('import-success', { seedPhrase.value = words
type: 'seed', showPasswordModal.value = true
value: words,
})
} }
const handleContinue = () => { const handleContinue = () => {
recoverSeedComponentRef.value?.handleSubmit?.() 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> </script>
<template> <template>
<div class="import-wallet dark-card"> <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> <div class="desc">Enter your recovery seed phrase</div>
<RecoverSeedComponent <RecoverSeedComponent
ref="recoverSeedComponentRef" ref="recoverSeedComponentRef"
@ -40,6 +62,27 @@ const handleContinue = () => {
>Continue</ButtonCommon >Continue</ButtonCommon
> >
</div> </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> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.import-wallet { .import-wallet {

View File

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

View File

@ -1,13 +1,31 @@
<script setup lang="ts"> <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 ButtonCommon from '@/components/common/ButtonCommon.vue'
import SeedPhraseDisplayComponent from '@/views/Auth/components/SeedPhraseDisplayComponent.vue'
const showSeedModal = ref(false)
const handleBackupFile = () => { const handleBackupFile = () => {
// TODO: Implement backup file functionality // TODO: Implement backup file functionality
} }
const handleBackupSeed = () => { 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> </script>
@ -28,6 +46,20 @@ const handleBackupSeed = () => {
<Divider /> <Divider />
</div> </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> </template>
<style lang="scss" scoped> <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> </style>