fix: FE base login
This commit is contained in:
parent
c5b12065bf
commit
689fc629a7
@ -31,3 +31,7 @@ h2 {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
--text-muted: #8b95a5;
|
--text-muted: #8b95a5;
|
||||||
--text-light: #ffffff;
|
--text-light: #ffffff;
|
||||||
|
|
||||||
|
|
||||||
// Background Colors
|
// Background Colors
|
||||||
--bg-gradient-start: #F0F8FF;
|
--bg-gradient-start: #F0F8FF;
|
||||||
--bg-gradient-end: #E6F2FF;
|
--bg-gradient-end: #E6F2FF;
|
||||||
|
|||||||
436
src/components/auth/ConfirmSeedComponent.vue
Normal file
436
src/components/auth/ConfirmSeedComponent.vue
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineEmits, onMounted } from 'vue'
|
||||||
|
import { ButtonCommon } from '@/components'
|
||||||
|
import { useSeedStore } from '@/stores'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
next: []
|
||||||
|
back: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const seedStore = useSeedStore()
|
||||||
|
|
||||||
|
const seedWords = ref<string[]>([])
|
||||||
|
const currentQuestionIndex = ref(0)
|
||||||
|
const selectedAnswer = ref('')
|
||||||
|
const isCorrect = ref(false)
|
||||||
|
const showResult = ref(false)
|
||||||
|
|
||||||
|
const generateQuiz = (): {
|
||||||
|
position: number
|
||||||
|
correctWord: string
|
||||||
|
options: string[]
|
||||||
|
} | null => {
|
||||||
|
if (seedWords.value.length === 0) return null
|
||||||
|
|
||||||
|
const randomPosition = Math.floor(Math.random() * 12) + 1
|
||||||
|
currentQuestionIndex.value = randomPosition - 1
|
||||||
|
|
||||||
|
const correctWord = seedWords.value[randomPosition - 1]
|
||||||
|
const options = [correctWord]
|
||||||
|
|
||||||
|
const BIP39_WORDS = [
|
||||||
|
'abandon',
|
||||||
|
'ability',
|
||||||
|
'able',
|
||||||
|
'about',
|
||||||
|
'above',
|
||||||
|
'absent',
|
||||||
|
'absorb',
|
||||||
|
'abstract',
|
||||||
|
'absurd',
|
||||||
|
'abuse',
|
||||||
|
'access',
|
||||||
|
'accident',
|
||||||
|
'account',
|
||||||
|
'accuse',
|
||||||
|
'achieve',
|
||||||
|
'acid',
|
||||||
|
'acoustic',
|
||||||
|
'acquire',
|
||||||
|
'across',
|
||||||
|
'act',
|
||||||
|
'action',
|
||||||
|
'actor',
|
||||||
|
'actress',
|
||||||
|
'actual',
|
||||||
|
'adapt',
|
||||||
|
'add',
|
||||||
|
'addict',
|
||||||
|
'address',
|
||||||
|
'adjust',
|
||||||
|
'admit',
|
||||||
|
'adult',
|
||||||
|
'advance',
|
||||||
|
'advice',
|
||||||
|
'aerobic',
|
||||||
|
'affair',
|
||||||
|
'afford',
|
||||||
|
'afraid',
|
||||||
|
'again',
|
||||||
|
'age',
|
||||||
|
'agent',
|
||||||
|
'agree',
|
||||||
|
'ahead',
|
||||||
|
'aim',
|
||||||
|
'air',
|
||||||
|
'airport',
|
||||||
|
'aisle',
|
||||||
|
'alarm',
|
||||||
|
'album',
|
||||||
|
'alcohol',
|
||||||
|
'alert',
|
||||||
|
'alien',
|
||||||
|
'all',
|
||||||
|
'alley',
|
||||||
|
'allow',
|
||||||
|
'almost',
|
||||||
|
'alone',
|
||||||
|
'alpha',
|
||||||
|
'already',
|
||||||
|
'also',
|
||||||
|
'alter',
|
||||||
|
'always',
|
||||||
|
'amateur',
|
||||||
|
'amazing',
|
||||||
|
'among',
|
||||||
|
'amount',
|
||||||
|
'amused',
|
||||||
|
'analyst',
|
||||||
|
'anchor',
|
||||||
|
'ancient',
|
||||||
|
'anger',
|
||||||
|
'angle',
|
||||||
|
'angry',
|
||||||
|
'animal',
|
||||||
|
'ankle',
|
||||||
|
'announce',
|
||||||
|
'annual',
|
||||||
|
'another',
|
||||||
|
'answer',
|
||||||
|
'antenna',
|
||||||
|
'antique',
|
||||||
|
'anxiety',
|
||||||
|
'any',
|
||||||
|
'apart',
|
||||||
|
'apology',
|
||||||
|
'appear',
|
||||||
|
'apple',
|
||||||
|
'approve',
|
||||||
|
'april',
|
||||||
|
'arch',
|
||||||
|
'arctic',
|
||||||
|
'area',
|
||||||
|
'arena',
|
||||||
|
'argue',
|
||||||
|
'arm',
|
||||||
|
'armed',
|
||||||
|
'armor',
|
||||||
|
'army',
|
||||||
|
'around',
|
||||||
|
'arrange',
|
||||||
|
'arrest',
|
||||||
|
]
|
||||||
|
|
||||||
|
while (options.length < 4) {
|
||||||
|
const randomWord = BIP39_WORDS[Math.floor(Math.random() * BIP39_WORDS.length)]
|
||||||
|
if (!options.includes(randomWord)) {
|
||||||
|
options.push(randomWord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options.sort(() => Math.random() - 0.5)
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: randomPosition,
|
||||||
|
correctWord,
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const quizData = ref<{
|
||||||
|
position: number
|
||||||
|
correctWord: string
|
||||||
|
options: string[]
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
const handleAnswerSelect = (answer: string) => {
|
||||||
|
selectedAnswer.value = answer
|
||||||
|
isCorrect.value = answer === quizData.value?.correctWord
|
||||||
|
showResult.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (isCorrect.value) {
|
||||||
|
emit('next')
|
||||||
|
} else {
|
||||||
|
showResult.value = false
|
||||||
|
selectedAnswer.value = ''
|
||||||
|
const newQuiz = generateQuiz()
|
||||||
|
if (newQuiz) {
|
||||||
|
quizData.value = newQuiz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const words = seedStore.getSeedWords()
|
||||||
|
if (words.length > 0) {
|
||||||
|
seedWords.value = words
|
||||||
|
const newQuiz = generateQuiz()
|
||||||
|
if (newQuiz) {
|
||||||
|
quizData.value = newQuiz
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const sampleWords = [
|
||||||
|
'abandon',
|
||||||
|
'ability',
|
||||||
|
'able',
|
||||||
|
'about',
|
||||||
|
'above',
|
||||||
|
'absent',
|
||||||
|
'absorb',
|
||||||
|
'abstract',
|
||||||
|
'absurd',
|
||||||
|
'abuse',
|
||||||
|
'access',
|
||||||
|
'accident',
|
||||||
|
]
|
||||||
|
seedWords.value = sampleWords
|
||||||
|
const newQuiz = generateQuiz()
|
||||||
|
if (newQuiz) {
|
||||||
|
quizData.value = newQuiz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="confirm-container">
|
||||||
|
<div class="confirm-card">
|
||||||
|
<div class="confirm-header">
|
||||||
|
<h1 class="confirm-title">Recovery Seed</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="confirm-content">
|
||||||
|
<div class="progress-indicators">
|
||||||
|
<div class="progress-circle"></div>
|
||||||
|
<div class="progress-circle active"></div>
|
||||||
|
<div class="progress-circle"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instruction-text">
|
||||||
|
<p>
|
||||||
|
Make sure you wrote the phrase down correctly by answering this quick
|
||||||
|
checkup.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quiz-section">
|
||||||
|
<h2 class="quiz-question">What is the {{ quizData?.position }}th word?</h2>
|
||||||
|
|
||||||
|
<div class="answer-options">
|
||||||
|
<button
|
||||||
|
v-for="(option, index) in quizData?.options"
|
||||||
|
:key="index"
|
||||||
|
class="answer-button"
|
||||||
|
:class="{
|
||||||
|
selected: selectedAnswer === option,
|
||||||
|
correct: showResult && option === quizData?.correctWord,
|
||||||
|
incorrect:
|
||||||
|
showResult &&
|
||||||
|
selectedAnswer === option &&
|
||||||
|
option !== quizData?.correctWord,
|
||||||
|
}"
|
||||||
|
@click="handleAnswerSelect(option)"
|
||||||
|
:disabled="showResult"
|
||||||
|
>
|
||||||
|
{{ option }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showResult" class="result-message">
|
||||||
|
<p v-if="isCorrect" class="success-message">✓ Correct! You can proceed.</p>
|
||||||
|
<p v-else class="error-message">✗ Incorrect. Please try again.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="confirm-actions">
|
||||||
|
<ButtonCommon
|
||||||
|
v-if="showResult && isCorrect"
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
@click="handleNext"
|
||||||
|
>
|
||||||
|
CONTINUE
|
||||||
|
</ButtonCommon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.confirm-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
background: var(--bg-light);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-card {
|
||||||
|
@include card-base;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
padding-bottom: var(--spacing-xl);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
.confirm-title {
|
||||||
|
font-size: var(--font-2xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-content {
|
||||||
|
.progress-indicators {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
|
||||||
|
.progress-circle {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--border-light);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-section {
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
|
||||||
|
.quiz-question {
|
||||||
|
font-size: var(--font-lg);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
|
||||||
|
.answer-button {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border: 2px solid var(--border-light);
|
||||||
|
background: var(--bg-white);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.correct {
|
||||||
|
border-color: var(--success-color);
|
||||||
|
background: var(--success-light);
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.incorrect {
|
||||||
|
border-color: var(--error-color);
|
||||||
|
background: var(--error-light);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
color: var(--success-color);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.confirm-container {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-card {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-section {
|
||||||
|
.answer-options {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,13 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, defineEmits } from 'vue'
|
||||||
import { ButtonCommon, FormCommon } from '@/components'
|
import { ButtonCommon, FormCommon } from '@/components'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
navigateToOpenWallet: [event: Event]
|
||||||
|
navigateToRecoverySeed: []
|
||||||
|
}>()
|
||||||
|
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const confirmPassword = ref('')
|
const confirmPassword = ref('')
|
||||||
const passwordError = ref('')
|
const passwordError = ref('')
|
||||||
const confirmPasswordError = ref('')
|
const confirmPasswordError = ref('')
|
||||||
|
|
||||||
// Password strength calculation
|
|
||||||
const passwordStrength = computed(() => {
|
const passwordStrength = computed(() => {
|
||||||
if (!password.value) return { level: 0, text: '', color: '' }
|
if (!password.value) return { level: 0, text: '', color: '' }
|
||||||
|
|
||||||
@ -42,15 +46,9 @@ const canProceed = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleIHaveWallet = (e: Event) => {
|
||||||
password.value = ''
|
e.preventDefault()
|
||||||
confirmPassword.value = ''
|
emit('navigateToOpenWallet', e)
|
||||||
passwordError.value = ''
|
|
||||||
confirmPasswordError.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleIHaveWallet = () => {
|
|
||||||
console.log('Navigate to open wallet')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
@ -63,14 +61,13 @@ const handleNext = () => {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('Proceed to next step')
|
emit('navigateToRecoverySeed')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
<div class="auth-card">
|
<div class="auth-card">
|
||||||
<!-- Header -->
|
|
||||||
<div class="auth-card-header">
|
<div class="auth-card-header">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<div class="logo-circle">
|
<div class="logo-circle">
|
||||||
@ -79,7 +76,6 @@ const handleNext = () => {
|
|||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
class="neptune-logo"
|
class="neptune-logo"
|
||||||
>
|
>
|
||||||
<!-- Neptune planet with ring -->
|
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="neptuneGradient"
|
id="neptuneGradient"
|
||||||
@ -113,10 +109,8 @@ const handleNext = () => {
|
|||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<!-- Planet -->
|
|
||||||
<circle cx="50" cy="50" r="28" fill="url(#neptuneGradient)" />
|
<circle cx="50" cy="50" r="28" fill="url(#neptuneGradient)" />
|
||||||
|
|
||||||
<!-- Surface details -->
|
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="50"
|
cx="50"
|
||||||
cy="45"
|
cy="45"
|
||||||
@ -126,7 +120,6 @@ const handleNext = () => {
|
|||||||
/>
|
/>
|
||||||
<ellipse cx="50" cy="55" rx="20" ry="5" fill="rgba(0, 0, 0, 0.1)" />
|
<ellipse cx="50" cy="55" rx="20" ry="5" fill="rgba(0, 0, 0, 0.1)" />
|
||||||
|
|
||||||
<!-- Ring -->
|
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="50"
|
cx="50"
|
||||||
cy="50"
|
cy="50"
|
||||||
@ -138,7 +131,6 @@ const handleNext = () => {
|
|||||||
opacity="0.8"
|
opacity="0.8"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Highlight -->
|
|
||||||
<circle cx="42" cy="42" r="6" fill="rgba(255, 255, 255, 0.4)" />
|
<circle cx="42" cy="42" r="6" fill="rgba(255, 255, 255, 0.4)" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@ -152,7 +144,6 @@ const handleNext = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-card-content">
|
<div class="auth-card-content">
|
||||||
<!-- Password Input -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<FormCommon
|
<FormCommon
|
||||||
v-model="password"
|
v-model="password"
|
||||||
@ -165,7 +156,6 @@ const handleNext = () => {
|
|||||||
@input="passwordError = ''"
|
@input="passwordError = ''"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Password Strength Indicator -->
|
|
||||||
<div v-if="password" class="password-strength">
|
<div v-if="password" class="password-strength">
|
||||||
<div class="strength-bar">
|
<div class="strength-bar">
|
||||||
<div
|
<div
|
||||||
@ -182,7 +172,6 @@ const handleNext = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Password Input -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<FormCommon
|
<FormCommon
|
||||||
v-model="confirmPassword"
|
v-model="confirmPassword"
|
||||||
@ -195,7 +184,6 @@ const handleNext = () => {
|
|||||||
@input="confirmPasswordError = ''"
|
@input="confirmPasswordError = ''"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Password Match Indicator -->
|
|
||||||
<div
|
<div
|
||||||
v-if="confirmPassword"
|
v-if="confirmPassword"
|
||||||
class="password-match"
|
class="password-match"
|
||||||
@ -206,12 +194,10 @@ const handleNext = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Helper Text -->
|
|
||||||
<p class="helper-text">
|
<p class="helper-text">
|
||||||
Password must be at least 8 characters with uppercase, lowercase, and numbers.
|
Password must be at least 8 characters with uppercase, lowercase, and numbers.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="auth-button-group">
|
<div class="auth-button-group">
|
||||||
<ButtonCommon
|
<ButtonCommon
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -227,8 +213,6 @@ const handleNext = () => {
|
|||||||
<button class="link-button" @click="handleIHaveWallet">
|
<button class="link-button" @click="handleIHaveWallet">
|
||||||
Already have a wallet?
|
Already have a wallet?
|
||||||
</button>
|
</button>
|
||||||
<span class="separator">•</span>
|
|
||||||
<button class="link-button" @click="handleCancel">Cancel</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -238,7 +222,6 @@ const handleNext = () => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.auth-container {
|
.auth-container {
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -330,7 +313,6 @@ const handleNext = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password Strength Indicator
|
|
||||||
.password-strength {
|
.password-strength {
|
||||||
margin-top: var(--spacing-sm);
|
margin-top: var(--spacing-sm);
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -358,7 +340,6 @@ const handleNext = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password Match Indicator
|
|
||||||
.password-match {
|
.password-match {
|
||||||
margin-top: var(--spacing-sm);
|
margin-top: var(--spacing-sm);
|
||||||
font-size: var(--font-xs);
|
font-size: var(--font-xs);
|
||||||
@ -372,7 +353,6 @@ const handleNext = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper Text
|
|
||||||
.helper-text {
|
.helper-text {
|
||||||
font-size: var(--font-xs);
|
font-size: var(--font-xs);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@ -380,7 +360,6 @@ const handleNext = () => {
|
|||||||
line-height: var(--leading-normal);
|
line-height: var(--leading-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action Buttons
|
|
||||||
.auth-button-group {
|
.auth-button-group {
|
||||||
margin-top: var(--spacing-2xl);
|
margin-top: var(--spacing-2xl);
|
||||||
|
|
||||||
@ -413,7 +392,6 @@ const handleNext = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsive Design
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.auth-container {
|
.auth-container {
|
||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md);
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonCommon } from '@/components'
|
import { ButtonCommon } from '@/components'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
onboardingComplete: []
|
||||||
|
}>()
|
||||||
|
|
||||||
const goToNewWallet = () => {
|
const goToNewWallet = () => {
|
||||||
window.open('https://kaspa-ng.org', '_blank')
|
window.open('https://kaspa-ng.org', '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToLegacyWallet = () => {
|
const goToLegacyWallet = () => {
|
||||||
window.open('https://wallet.kaspanet.io', '_blank')
|
// Emit event to parent to show tab interface
|
||||||
|
emit('onboardingComplete')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -40,13 +45,13 @@ const goToLegacyWallet = () => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.welcome-page {
|
.welcome-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: var(--vt-c-white);
|
background-color: var(--bg-light);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.welcome-card {
|
.welcome-card {
|
||||||
background-color: var(--vt-c-white);
|
background-color: var(--bg-white);
|
||||||
box-shadow: 0 0 15px var(--vt-c-badge-caption-shadow);
|
box-shadow: var(--shadow-md);
|
||||||
border-radius: 10px;
|
border-radius: var(--radius-md);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -59,4 +64,15 @@ const goToLegacyWallet = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1rem;
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,44 +1,172 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { ButtonCommon, FormCommon } from '@/components'
|
import { ButtonCommon, FormCommon } from '@/components'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
|
const passwordError = ref('')
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
const handleOpenWallet = () => {}
|
const handleOpenWallet = async () => {
|
||||||
|
if (!password.value) {
|
||||||
|
passwordError.value = 'Please enter your password'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const handleNewWallet = () => {}
|
isLoading.value = true
|
||||||
|
passwordError.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||||
|
router.push('/')
|
||||||
|
} catch (error) {
|
||||||
|
passwordError.value = 'Invalid password. Please try again.'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
navigateToCreate: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const navigateToNewWallet = () => {
|
||||||
|
emit('navigateToCreate')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
<div class="auth-card">
|
<div class="auth-card">
|
||||||
<div class="auth-card-header">
|
|
||||||
<h2>Open Wallet</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-card-content">
|
<div class="auth-card-content">
|
||||||
<div class="wallet-icon">
|
<div class="wallet-icon">
|
||||||
<div class="icon-circle"></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="password-section">
|
<div class="form-group">
|
||||||
<FormCommon
|
<FormCommon
|
||||||
v-model="password"
|
v-model="password"
|
||||||
type="password"
|
type="password"
|
||||||
label="Unlock the wallet with your password:"
|
label="Enter your password"
|
||||||
placeholder="Enter your password"
|
placeholder="Password"
|
||||||
show-password-toggle
|
show-password-toggle
|
||||||
required
|
required
|
||||||
|
:error="passwordError"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@input="passwordError = ''"
|
||||||
|
@keyup.enter="handleOpenWallet"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-button-group">
|
<div class="security-notice">
|
||||||
<ButtonCommon class="auth-btn secondary" @click="handleNewWallet">
|
<div class="notice-icon">
|
||||||
NEW WALLET
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
</ButtonCommon>
|
<path
|
||||||
|
d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9 12l2 2 4-4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span>Your password is encrypted and stored locally</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ButtonCommon class="auth-btn primary" @click="handleOpenWallet">
|
<div class="auth-button-group">
|
||||||
OPEN WALLET
|
<ButtonCommon
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
:disabled="!password || isLoading"
|
||||||
|
:loading="isLoading"
|
||||||
|
@click="handleOpenWallet"
|
||||||
|
>
|
||||||
|
{{ isLoading ? 'Opening...' : 'Open Wallet' }}
|
||||||
|
</ButtonCommon>
|
||||||
|
<ButtonCommon type="default" size="large" block @click="navigateToNewWallet">
|
||||||
|
New Wallet
|
||||||
</ButtonCommon>
|
</ButtonCommon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -46,4 +174,173 @@ const handleNewWallet = () => {}
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<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: 420px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card-content {
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-notice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background: var(--bg-light);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
|
||||||
|
.notice-icon {
|
||||||
|
color: var(--success-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help Links
|
||||||
|
.help-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding-top: var(--spacing-lg);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
.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:not(:disabled) {
|
||||||
|
color: var(--primary-hover);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.auth-container {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-icon {
|
||||||
|
.icon-circle {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
207
src/components/auth/RecoverySeedComponent.vue
Normal file
207
src/components/auth/RecoverySeedComponent.vue
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineEmits, onMounted } from 'vue'
|
||||||
|
import { ButtonCommon } from '@/components'
|
||||||
|
import { generateSeedPhrase } from '@/utils'
|
||||||
|
import { useSeedStore } from '@/stores'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
next: []
|
||||||
|
back: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const seedStore = useSeedStore()
|
||||||
|
|
||||||
|
const seedWords = ref<string[]>([])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const words = generateSeedPhrase()
|
||||||
|
seedWords.value = words
|
||||||
|
seedStore.setSeedWords(words)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
emit('next')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="recovery-container">
|
||||||
|
<div class="recovery-card">
|
||||||
|
<div class="recovery-header">
|
||||||
|
<h1 class="recovery-title">Recovery Seed</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recovery-content">
|
||||||
|
<div class="instruction-text">
|
||||||
|
<p>
|
||||||
|
Your wallet is accessible by a seed phrase. The seed phrase is an ordered
|
||||||
|
12-word secret phrase.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Make sure no one is looking, as anyone with your seed phrase can access your
|
||||||
|
wallet your funds. Write it down and keep it safe.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="seed-words-container">
|
||||||
|
<div class="seed-words-grid">
|
||||||
|
<div v-for="(word, index) in seedWords" :key="index" class="seed-word-item">
|
||||||
|
<span class="word-number">{{ index + 1 }}</span>
|
||||||
|
<span class="word-text">{{ word }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cool-fact">
|
||||||
|
<p>
|
||||||
|
Cool fact: there are more 12-word phrase combinations than nanoseconds since
|
||||||
|
the big bang!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recovery-actions">
|
||||||
|
<ButtonCommon type="default" size="large" @click="handleBack">
|
||||||
|
BACK
|
||||||
|
</ButtonCommon>
|
||||||
|
<ButtonCommon type="primary" size="large" @click="handleNext">
|
||||||
|
NEXT
|
||||||
|
</ButtonCommon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.recovery-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
background: var(--bg-light);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-card {
|
||||||
|
@include card-base;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
padding-bottom: var(--spacing-xl);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
.recovery-title {
|
||||||
|
font-size: var(--font-2xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-content {
|
||||||
|
.instruction-text {
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seed-words-container {
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
|
||||||
|
.seed-words-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
|
||||||
|
.seed-word-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
background: var(--bg-white);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
|
||||||
|
.word-number {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-muted);
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-text {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cool-fact {
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.recovery-container {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-card {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seed-words-container {
|
||||||
|
.seed-words-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
|
||||||
|
.seed-word-item {
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
|
||||||
|
.word-number {
|
||||||
|
min-width: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -46,7 +46,7 @@ const handleClick = () => {
|
|||||||
transition: var(--transition-all);
|
transition: var(--transition-all);
|
||||||
border-radius: var(--btn-radius);
|
border-radius: var(--btn-radius);
|
||||||
letter-spacing: var(--tracking-wide);
|
letter-spacing: var(--tracking-wide);
|
||||||
|
transition: all 2s ease-in-out;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--primary-hover);
|
background: var(--primary-hover);
|
||||||
border-color: var(--primary-hover);
|
border-color: var(--primary-hover);
|
||||||
|
|||||||
@ -89,19 +89,19 @@ const handleBlur = (e: FocusEvent) => {
|
|||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid var(--vt-c-gray-6);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
&.focused {
|
&.focused {
|
||||||
border-color: var(--vt-c-main-color);
|
border-color: var(--border-primary);
|
||||||
box-shadow: var(--vt-input-shadow-focus);
|
box-shadow: var(--shadow-primary);
|
||||||
}
|
}
|
||||||
&.error {
|
&.error {
|
||||||
border-color: var(--vt-c-red-v3);
|
border-color: var(--error-color);
|
||||||
background-color: var();
|
background-color: var();
|
||||||
}
|
}
|
||||||
&.disabled {
|
&.disabled {
|
||||||
background-color: var(--vt-c-gray-4);
|
background-color: var(--bg-hover);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,17 +109,18 @@ const handleBlur = (e: FocusEvent) => {
|
|||||||
.form-input {
|
.form-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 40px 12px 12px;
|
padding: 12px 40px 12px 12px;
|
||||||
border: none;
|
border: var(--border-color);
|
||||||
background: transparent;
|
background: var(--text-light);
|
||||||
font-size: 14px;
|
font-size: var(--font-base);
|
||||||
color: var(--vt-c-black-bold);
|
color: var(--text-primary);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--vt-c-gray-8);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
color: var(--vt-c-gray-8);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,10 +132,10 @@ const handleBlur = (e: FocusEvent) => {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--vt-c-gray-8);
|
color: var(--text-muted);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--vt-c-black-bold);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@ -145,7 +146,7 @@ const handleBlur = (e: FocusEvent) => {
|
|||||||
.error-message {
|
.error-message {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--vt-c-red-v3);
|
color: var(--error-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,201 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IconProps } from '@/interface';
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import {
|
|
||||||
EyeOutlined,
|
|
||||||
EyeInvisibleOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
RightOutlined,
|
|
||||||
LeftOutlined,
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import type { IconProps } from '@/interface'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<IconProps>(), {
|
const props = withDefaults(defineProps<IconProps>(), {
|
||||||
icon: '',
|
size: 16,
|
||||||
|
color: 'currentColor',
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconMap: Record<string, any> = {
|
const iconSize = computed(() => {
|
||||||
eye: EyeOutlined,
|
if (typeof props.size === 'number') {
|
||||||
'eye-off': EyeInvisibleOutlined,
|
return `${props.size}px`
|
||||||
search: SearchOutlined,
|
|
||||||
close: CloseOutlined,
|
|
||||||
'arrow-right': RightOutlined,
|
|
||||||
'arrow-left': LeftOutlined,
|
|
||||||
}
|
}
|
||||||
|
return props.size
|
||||||
const IconComponent = computed(() => iconMap[props.icon])
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<component :is="IconComponent" :style="{ fontSize: size, color }" :class="props.class" />
|
<svg
|
||||||
|
:width="iconSize"
|
||||||
|
:height="iconSize"
|
||||||
|
:class="props.class"
|
||||||
|
:style="{ color: props.color }"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<!-- Eye Icon -->
|
||||||
|
<path
|
||||||
|
v-if="icon === 'eye'"
|
||||||
|
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
v-if="icon === 'eye'"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="3"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Eye Off Icon -->
|
||||||
|
<path
|
||||||
|
v-if="icon === 'eye-off'"
|
||||||
|
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
v-if="icon === 'eye-off'"
|
||||||
|
x1="1"
|
||||||
|
y1="1"
|
||||||
|
x2="23"
|
||||||
|
y2="23"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Wallet Icon -->
|
||||||
|
<path
|
||||||
|
v-if="icon === 'wallet'"
|
||||||
|
d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
v-if="icon === 'wallet'"
|
||||||
|
d="M9 12L11 14L15 10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Shield Icon -->
|
||||||
|
<path
|
||||||
|
v-if="icon === 'shield'"
|
||||||
|
d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
v-if="icon === 'shield'"
|
||||||
|
d="M9 12l2 2 4-4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Lock Icon -->
|
||||||
|
<rect
|
||||||
|
v-if="icon === 'lock'"
|
||||||
|
x="3"
|
||||||
|
y="11"
|
||||||
|
width="18"
|
||||||
|
height="11"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
v-if="icon === 'lock'"
|
||||||
|
d="M7 11V7a5 5 0 0 1 10 0v4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Key Icon -->
|
||||||
|
<path
|
||||||
|
v-if="icon === 'key'"
|
||||||
|
d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Plus Icon -->
|
||||||
|
<line
|
||||||
|
v-if="icon === 'plus'"
|
||||||
|
x1="12"
|
||||||
|
y1="5"
|
||||||
|
x2="12"
|
||||||
|
y2="19"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
v-if="icon === 'plus'"
|
||||||
|
x1="5"
|
||||||
|
y1="12"
|
||||||
|
x2="19"
|
||||||
|
y2="12"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Check Icon -->
|
||||||
|
<path
|
||||||
|
v-if="icon === 'check'"
|
||||||
|
d="M20 6L9 17l-5-5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- X Icon -->
|
||||||
|
<line
|
||||||
|
v-if="icon === 'x'"
|
||||||
|
x1="18"
|
||||||
|
y1="6"
|
||||||
|
x2="6"
|
||||||
|
y2="18"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
v-if="icon === 'x'"
|
||||||
|
x1="6"
|
||||||
|
y1="6"
|
||||||
|
x2="18"
|
||||||
|
y2="18"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Arrow Right Icon -->
|
||||||
|
<path
|
||||||
|
v-if="icon === 'arrow-right'"
|
||||||
|
d="M5 12h14m-7-7l7 7-7 7"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Copy Icon -->
|
||||||
|
<rect
|
||||||
|
v-if="icon === 'copy'"
|
||||||
|
x="9"
|
||||||
|
y="9"
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
v-if="icon === 'copy'"
|
||||||
|
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import FormCommon from './common/FormCommon.vue'
|
|||||||
import OnboardingComponent from './auth/OnboardingComponent.vue'
|
import OnboardingComponent from './auth/OnboardingComponent.vue'
|
||||||
import OpenWalletComponent from './auth/OpenWalletComponent.vue'
|
import OpenWalletComponent from './auth/OpenWalletComponent.vue'
|
||||||
import CreateWalletComponent from './auth/CreateWalletComponent.vue'
|
import CreateWalletComponent from './auth/CreateWalletComponent.vue'
|
||||||
|
import RecoverySeedComponent from './auth/RecoverySeedComponent.vue'
|
||||||
|
import ConfirmSeedComponent from './auth/ConfirmSeedComponent.vue'
|
||||||
import { IconCommon } from './icon'
|
import { IconCommon } from './icon'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -13,5 +15,7 @@ export {
|
|||||||
OnboardingComponent,
|
OnboardingComponent,
|
||||||
OpenWalletComponent,
|
OpenWalletComponent,
|
||||||
CreateWalletComponent,
|
CreateWalletComponent,
|
||||||
|
RecoverySeedComponent,
|
||||||
|
ConfirmSeedComponent,
|
||||||
IconCommon,
|
IconCommon,
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/interface/icon.ts
Normal file
10
src/interface/icon.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface IconProps {
|
||||||
|
icon: string
|
||||||
|
size?: number | string
|
||||||
|
color?: string
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HighlightProps {
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
@ -26,7 +26,19 @@ export const routes: any = [
|
|||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: Page.Login,
|
component: Page.Auth,
|
||||||
|
beforeEnter: ifNotAuthenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/recovery-seed',
|
||||||
|
name: 'recovery-seed',
|
||||||
|
component: Page.Auth,
|
||||||
|
beforeEnter: ifNotAuthenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/confirm-seed',
|
||||||
|
name: 'confirm-seed',
|
||||||
|
component: Page.Auth,
|
||||||
beforeEnter: ifNotAuthenticated,
|
beforeEnter: ifNotAuthenticated,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
85
src/stores/authStore.ts
Normal file
85
src/stores/authStore.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// Auth flow states
|
||||||
|
export type AuthState = 'onboarding' | 'login' | 'create' | 'recovery' | 'confirm' | 'complete'
|
||||||
|
|
||||||
|
// Auth store to manage the flow
|
||||||
|
const currentState = ref<AuthState>('onboarding')
|
||||||
|
|
||||||
|
export const useAuthStore = () => {
|
||||||
|
const getCurrentState = () => currentState.value
|
||||||
|
|
||||||
|
const setState = (state: AuthState) => {
|
||||||
|
currentState.value = state
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
switch (currentState.value) {
|
||||||
|
case 'onboarding':
|
||||||
|
setState('login')
|
||||||
|
break
|
||||||
|
case 'login':
|
||||||
|
// Stay in login, user chooses create or open
|
||||||
|
break
|
||||||
|
case 'create':
|
||||||
|
setState('recovery')
|
||||||
|
break
|
||||||
|
case 'recovery':
|
||||||
|
setState('confirm')
|
||||||
|
break
|
||||||
|
case 'confirm':
|
||||||
|
setState('complete')
|
||||||
|
break
|
||||||
|
case 'complete':
|
||||||
|
// Flow complete
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousStep = () => {
|
||||||
|
switch (currentState.value) {
|
||||||
|
case 'onboarding':
|
||||||
|
// Can't go back from onboarding
|
||||||
|
break
|
||||||
|
case 'login':
|
||||||
|
setState('onboarding')
|
||||||
|
break
|
||||||
|
case 'create':
|
||||||
|
setState('login')
|
||||||
|
break
|
||||||
|
case 'recovery':
|
||||||
|
setState('create')
|
||||||
|
break
|
||||||
|
case 'confirm':
|
||||||
|
setState('recovery')
|
||||||
|
break
|
||||||
|
case 'complete':
|
||||||
|
setState('confirm')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToCreate = () => {
|
||||||
|
setState('create')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToLogin = () => {
|
||||||
|
setState('login')
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetFlow = () => {
|
||||||
|
setState('onboarding')
|
||||||
|
localStorage.removeItem('onboarding-completed')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentState: currentState.value,
|
||||||
|
getCurrentState,
|
||||||
|
setState,
|
||||||
|
nextStep,
|
||||||
|
previousStep,
|
||||||
|
goToCreate,
|
||||||
|
goToLogin,
|
||||||
|
resetFlow,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1,2 @@
|
|||||||
export {}
|
export * from './seedStore'
|
||||||
|
export * from './authStore'
|
||||||
|
|||||||
33
src/stores/seedStore.ts
Normal file
33
src/stores/seedStore.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const seedWords = ref<string[]>([])
|
||||||
|
const isSeedGenerated = ref(false)
|
||||||
|
|
||||||
|
export const useSeedStore = () => {
|
||||||
|
const setSeedWords = (words: string[]) => {
|
||||||
|
seedWords.value = words
|
||||||
|
isSeedGenerated.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSeedWords = () => {
|
||||||
|
return seedWords.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSeedWords = () => {
|
||||||
|
seedWords.value = []
|
||||||
|
isSeedGenerated.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSeedWords = () => {
|
||||||
|
return isSeedGenerated.value && seedWords.value.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
seedWords: seedWords.value,
|
||||||
|
isSeedGenerated: isSeedGenerated.value,
|
||||||
|
setSeedWords,
|
||||||
|
getSeedWords,
|
||||||
|
clearSeedWords,
|
||||||
|
hasSeedWords,
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/utils/helpers/seedPhrase.ts
Normal file
128
src/utils/helpers/seedPhrase.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// BIP39 English wordlist (first 100 words for demo)
|
||||||
|
const BIP39_WORDS = [
|
||||||
|
'abandon',
|
||||||
|
'ability',
|
||||||
|
'able',
|
||||||
|
'about',
|
||||||
|
'above',
|
||||||
|
'absent',
|
||||||
|
'absorb',
|
||||||
|
'abstract',
|
||||||
|
'absurd',
|
||||||
|
'abuse',
|
||||||
|
'access',
|
||||||
|
'accident',
|
||||||
|
'account',
|
||||||
|
'accuse',
|
||||||
|
'achieve',
|
||||||
|
'acid',
|
||||||
|
'acoustic',
|
||||||
|
'acquire',
|
||||||
|
'across',
|
||||||
|
'act',
|
||||||
|
'action',
|
||||||
|
'actor',
|
||||||
|
'actress',
|
||||||
|
'actual',
|
||||||
|
'adapt',
|
||||||
|
'add',
|
||||||
|
'addict',
|
||||||
|
'address',
|
||||||
|
'adjust',
|
||||||
|
'admit',
|
||||||
|
'adult',
|
||||||
|
'advance',
|
||||||
|
'advice',
|
||||||
|
'aerobic',
|
||||||
|
'affair',
|
||||||
|
'afford',
|
||||||
|
'afraid',
|
||||||
|
'again',
|
||||||
|
'age',
|
||||||
|
'agent',
|
||||||
|
'agree',
|
||||||
|
'ahead',
|
||||||
|
'aim',
|
||||||
|
'air',
|
||||||
|
'airport',
|
||||||
|
'aisle',
|
||||||
|
'alarm',
|
||||||
|
'album',
|
||||||
|
'alcohol',
|
||||||
|
'alert',
|
||||||
|
'alien',
|
||||||
|
'all',
|
||||||
|
'alley',
|
||||||
|
'allow',
|
||||||
|
'almost',
|
||||||
|
'alone',
|
||||||
|
'alpha',
|
||||||
|
'already',
|
||||||
|
'also',
|
||||||
|
'alter',
|
||||||
|
'always',
|
||||||
|
'amateur',
|
||||||
|
'amazing',
|
||||||
|
'among',
|
||||||
|
'amount',
|
||||||
|
'amused',
|
||||||
|
'analyst',
|
||||||
|
'anchor',
|
||||||
|
'ancient',
|
||||||
|
'anger',
|
||||||
|
'angle',
|
||||||
|
'angry',
|
||||||
|
'animal',
|
||||||
|
'ankle',
|
||||||
|
'announce',
|
||||||
|
'annual',
|
||||||
|
'another',
|
||||||
|
'answer',
|
||||||
|
'antenna',
|
||||||
|
'antique',
|
||||||
|
'anxiety',
|
||||||
|
'any',
|
||||||
|
'apart',
|
||||||
|
'apology',
|
||||||
|
'appear',
|
||||||
|
'apple',
|
||||||
|
'approve',
|
||||||
|
'april',
|
||||||
|
'arch',
|
||||||
|
'arctic',
|
||||||
|
'area',
|
||||||
|
'arena',
|
||||||
|
'argue',
|
||||||
|
'arm',
|
||||||
|
'armed',
|
||||||
|
'armor',
|
||||||
|
'army',
|
||||||
|
'around',
|
||||||
|
'arrange',
|
||||||
|
'arrest',
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random seed phrase with 12 words
|
||||||
|
* In a real application, you would use a proper BIP39 library
|
||||||
|
*/
|
||||||
|
export const generateSeedPhrase = (): string[] => {
|
||||||
|
const words: string[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * BIP39_WORDS.length)
|
||||||
|
words.push(BIP39_WORDS[randomIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if a seed phrase is valid (basic validation)
|
||||||
|
*/
|
||||||
|
export const validateSeedPhrase = (words: string[]): boolean => {
|
||||||
|
if (words.length !== 12) return false
|
||||||
|
|
||||||
|
// Check if all words are in the BIP39 wordlist
|
||||||
|
return words.every((word) => BIP39_WORDS.includes(word.toLowerCase()))
|
||||||
|
}
|
||||||
@ -2,3 +2,4 @@ export * from './constants/code'
|
|||||||
export * from './constants/constants'
|
export * from './constants/constants'
|
||||||
export * from './helpers/format'
|
export * from './helpers/format'
|
||||||
export * from './helpers/localStorage'
|
export * from './helpers/localStorage'
|
||||||
|
export * from './helpers/seedPhrase'
|
||||||
|
|||||||
88
src/views/Auth/AuthView.vue
Normal file
88
src/views/Auth/AuthView.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { OnboardingComponent } from '@/components'
|
||||||
|
import { useAuthStore } from '@/stores'
|
||||||
|
import { LoginTab, CreateTab, RecoveryTab, ConfirmTab } from './components'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const currentState = computed(() => authStore.getCurrentState())
|
||||||
|
|
||||||
|
const handleOnboardingComplete = () => {
|
||||||
|
authStore.nextStep()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGoToCreate = () => {
|
||||||
|
authStore.goToCreate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGoToLogin = () => {
|
||||||
|
authStore.goToLogin()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
authStore.nextStep()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
authStore.previousStep()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="auth-container">
|
||||||
|
<OnboardingComponent
|
||||||
|
v-if="currentState === 'onboarding'"
|
||||||
|
@onboarding-complete="handleOnboardingComplete"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoginTab v-else-if="currentState === 'login'" @go-to-create="handleGoToCreate" />
|
||||||
|
|
||||||
|
<CreateTab
|
||||||
|
v-else-if="currentState === 'create'"
|
||||||
|
@go-to-login="handleGoToLogin"
|
||||||
|
@next="handleNext"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RecoveryTab
|
||||||
|
v-else-if="currentState === 'recovery'"
|
||||||
|
@back="handleBack"
|
||||||
|
@next="handleNext"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmTab v-else-if="currentState === 'confirm'" @back="handleBack" @next="handleNext" />
|
||||||
|
|
||||||
|
<div v-else-if="currentState === 'complete'" class="complete-state">
|
||||||
|
<h2>Wallet Setup Complete!</h2>
|
||||||
|
<p>Your wallet has been successfully created.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.auth-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-light);
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.complete-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: var(--success-color);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { CreateWalletComponent } from '@/components'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div></div>
|
|
||||||
<CreateWalletComponent />
|
|
||||||
</template>
|
|
||||||
28
src/views/Auth/components/ConfirmTab.vue
Normal file
28
src/views/Auth/components/ConfirmTab.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ConfirmSeedComponent } from '@/components'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
next: []
|
||||||
|
back: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
emit('next')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="confirm-tab">
|
||||||
|
<ConfirmSeedComponent @next="handleNext" @back="handleBack" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.confirm-tab {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
src/views/Auth/components/CreateTab.vue
Normal file
31
src/views/Auth/components/CreateTab.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CreateWalletComponent } from '@/components'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
goToLogin: []
|
||||||
|
next: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleNavigateToOpenWallet = () => {
|
||||||
|
emit('goToLogin')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNavigateToRecoverySeed = () => {
|
||||||
|
emit('next')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="create-tab">
|
||||||
|
<CreateWalletComponent
|
||||||
|
@navigateToOpenWallet="handleNavigateToOpenWallet"
|
||||||
|
@navigateToRecoverySeed="handleNavigateToRecoverySeed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.create-tab {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
23
src/views/Auth/components/LoginTab.vue
Normal file
23
src/views/Auth/components/LoginTab.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { OpenWalletComponent } from '@/components'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
goToCreate: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleNavigateToCreate = () => {
|
||||||
|
emit('goToCreate')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="login-tab">
|
||||||
|
<OpenWalletComponent @navigateToCreate="handleNavigateToCreate" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.login-tab {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
28
src/views/Auth/components/RecoveryTab.vue
Normal file
28
src/views/Auth/components/RecoveryTab.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RecoverySeedComponent } from '@/components'
|
||||||
|
|
||||||
|
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>
|
||||||
4
src/views/Auth/components/index.ts
Normal file
4
src/views/Auth/components/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { default as LoginTab } from './LoginTab.vue'
|
||||||
|
export { default as CreateTab } from './CreateTab.vue'
|
||||||
|
export { default as RecoveryTab } from './RecoveryTab.vue'
|
||||||
|
export { default as ConfirmTab } from './ConfirmTab.vue'
|
||||||
@ -1,3 +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 Login = () => import('@/views/Auth/LoginView.vue')
|
export const Auth = () => import('@/views/Auth/AuthView.vue')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user