Merge pull request #3 from HiamQuan/fix/template_login
fix: FE base login
This commit is contained in:
commit
dcb36ab411
@ -31,3 +31,7 @@ h2 {
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
--text-muted: #8b95a5;
|
||||
--text-light: #ffffff;
|
||||
|
||||
|
||||
// Background Colors
|
||||
--bg-gradient-start: #F0F8FF;
|
||||
--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">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, defineEmits } from 'vue'
|
||||
import { ButtonCommon, FormCommon } from '@/components'
|
||||
|
||||
const emit = defineEmits<{
|
||||
navigateToOpenWallet: [event: Event]
|
||||
navigateToRecoverySeed: []
|
||||
}>()
|
||||
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const passwordError = ref('')
|
||||
const confirmPasswordError = ref('')
|
||||
|
||||
// Password strength calculation
|
||||
const passwordStrength = computed(() => {
|
||||
if (!password.value) return { level: 0, text: '', color: '' }
|
||||
|
||||
@ -42,15 +46,9 @@ const canProceed = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
password.value = ''
|
||||
confirmPassword.value = ''
|
||||
passwordError.value = ''
|
||||
confirmPasswordError.value = ''
|
||||
}
|
||||
|
||||
const handleIHaveWallet = () => {
|
||||
console.log('Navigate to open wallet')
|
||||
const handleIHaveWallet = (e: Event) => {
|
||||
e.preventDefault()
|
||||
emit('navigateToOpenWallet', e)
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
@ -63,14 +61,13 @@ const handleNext = () => {
|
||||
}
|
||||
return
|
||||
}
|
||||
console.log('Proceed to next step')
|
||||
emit('navigateToRecoverySeed')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<!-- Header -->
|
||||
<div class="auth-card-header">
|
||||
<div class="logo-container">
|
||||
<div class="logo-circle">
|
||||
@ -79,7 +76,6 @@ const handleNext = () => {
|
||||
viewBox="0 0 100 100"
|
||||
class="neptune-logo"
|
||||
>
|
||||
<!-- Neptune planet with ring -->
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="neptuneGradient"
|
||||
@ -113,10 +109,8 @@ const handleNext = () => {
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Planet -->
|
||||
<circle cx="50" cy="50" r="28" fill="url(#neptuneGradient)" />
|
||||
|
||||
<!-- Surface details -->
|
||||
<ellipse
|
||||
cx="50"
|
||||
cy="45"
|
||||
@ -126,7 +120,6 @@ const handleNext = () => {
|
||||
/>
|
||||
<ellipse cx="50" cy="55" rx="20" ry="5" fill="rgba(0, 0, 0, 0.1)" />
|
||||
|
||||
<!-- Ring -->
|
||||
<ellipse
|
||||
cx="50"
|
||||
cy="50"
|
||||
@ -138,7 +131,6 @@ const handleNext = () => {
|
||||
opacity="0.8"
|
||||
/>
|
||||
|
||||
<!-- Highlight -->
|
||||
<circle cx="42" cy="42" r="6" fill="rgba(255, 255, 255, 0.4)" />
|
||||
</svg>
|
||||
</div>
|
||||
@ -152,7 +144,6 @@ const handleNext = () => {
|
||||
</div>
|
||||
|
||||
<div class="auth-card-content">
|
||||
<!-- Password Input -->
|
||||
<div class="form-group">
|
||||
<FormCommon
|
||||
v-model="password"
|
||||
@ -165,7 +156,6 @@ const handleNext = () => {
|
||||
@input="passwordError = ''"
|
||||
/>
|
||||
|
||||
<!-- Password Strength Indicator -->
|
||||
<div v-if="password" class="password-strength">
|
||||
<div class="strength-bar">
|
||||
<div
|
||||
@ -182,7 +172,6 @@ const handleNext = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Input -->
|
||||
<div class="form-group">
|
||||
<FormCommon
|
||||
v-model="confirmPassword"
|
||||
@ -195,7 +184,6 @@ const handleNext = () => {
|
||||
@input="confirmPasswordError = ''"
|
||||
/>
|
||||
|
||||
<!-- Password Match Indicator -->
|
||||
<div
|
||||
v-if="confirmPassword"
|
||||
class="password-match"
|
||||
@ -206,12 +194,10 @@ const handleNext = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Helper Text -->
|
||||
<p class="helper-text">
|
||||
Password must be at least 8 characters with uppercase, lowercase, and numbers.
|
||||
</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="auth-button-group">
|
||||
<ButtonCommon
|
||||
type="primary"
|
||||
@ -227,8 +213,6 @@ const handleNext = () => {
|
||||
<button class="link-button" @click="handleIHaveWallet">
|
||||
Already have a wallet?
|
||||
</button>
|
||||
<span class="separator">•</span>
|
||||
<button class="link-button" @click="handleCancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -238,7 +222,6 @@ const handleNext = () => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.auth-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -330,7 +313,6 @@ const handleNext = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Password Strength Indicator
|
||||
.password-strength {
|
||||
margin-top: var(--spacing-sm);
|
||||
display: flex;
|
||||
@ -358,7 +340,6 @@ const handleNext = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Password Match Indicator
|
||||
.password-match {
|
||||
margin-top: var(--spacing-sm);
|
||||
font-size: var(--font-xs);
|
||||
@ -372,7 +353,6 @@ const handleNext = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper Text
|
||||
.helper-text {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-muted);
|
||||
@ -380,7 +360,6 @@ const handleNext = () => {
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
.auth-button-group {
|
||||
margin-top: var(--spacing-2xl);
|
||||
|
||||
@ -413,7 +392,6 @@ const handleNext = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 640px) {
|
||||
.auth-container {
|
||||
padding: var(--spacing-md);
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonCommon } from '@/components'
|
||||
|
||||
const emit = defineEmits<{
|
||||
onboardingComplete: []
|
||||
}>()
|
||||
|
||||
const goToNewWallet = () => {
|
||||
window.open('https://kaspa-ng.org', '_blank')
|
||||
}
|
||||
|
||||
const goToLegacyWallet = () => {
|
||||
window.open('https://wallet.kaspanet.io', '_blank')
|
||||
// Emit event to parent to show tab interface
|
||||
emit('onboardingComplete')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -40,13 +45,13 @@ const goToLegacyWallet = () => {
|
||||
<style lang="scss" scoped>
|
||||
.welcome-page {
|
||||
min-height: 100vh;
|
||||
background-color: var(--vt-c-white);
|
||||
background-color: var(--bg-light);
|
||||
position: relative;
|
||||
|
||||
.welcome-card {
|
||||
background-color: var(--vt-c-white);
|
||||
box-shadow: 0 0 15px var(--vt-c-badge-caption-shadow);
|
||||
border-radius: 10px;
|
||||
background-color: var(--bg-white);
|
||||
box-shadow: var(--shadow-md);
|
||||
border-radius: var(--radius-md);
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
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>
|
||||
|
||||
@ -1,44 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ButtonCommon, FormCommon } from '@/components'
|
||||
|
||||
const router = useRouter()
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-card-header">
|
||||
<h2>Open Wallet</h2>
|
||||
</div>
|
||||
|
||||
<div class="auth-card-content">
|
||||
<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 class="password-section">
|
||||
<div class="form-group">
|
||||
<FormCommon
|
||||
v-model="password"
|
||||
type="password"
|
||||
label="Unlock the wallet with your password:"
|
||||
placeholder="Enter your password"
|
||||
label="Enter your password"
|
||||
placeholder="Password"
|
||||
show-password-toggle
|
||||
required
|
||||
:error="passwordError"
|
||||
:disabled="isLoading"
|
||||
@input="passwordError = ''"
|
||||
@keyup.enter="handleOpenWallet"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="auth-button-group">
|
||||
<ButtonCommon class="auth-btn secondary" @click="handleNewWallet">
|
||||
NEW WALLET
|
||||
</ButtonCommon>
|
||||
<div class="security-notice">
|
||||
<div class="notice-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<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">
|
||||
OPEN WALLET
|
||||
<div class="auth-button-group">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,4 +174,173 @@ const handleNewWallet = () => {}
|
||||
</div>
|
||||
</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);
|
||||
border-radius: var(--btn-radius);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
|
||||
transition: all 2s ease-in-out;
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
|
||||
@ -89,19 +89,19 @@ const handleBlur = (e: FocusEvent) => {
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
border: 1px solid var(--vt-c-gray-6);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
&.focused {
|
||||
border-color: var(--vt-c-main-color);
|
||||
box-shadow: var(--vt-input-shadow-focus);
|
||||
border-color: var(--border-primary);
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
&.error {
|
||||
border-color: var(--vt-c-red-v3);
|
||||
border-color: var(--error-color);
|
||||
background-color: var();
|
||||
}
|
||||
&.disabled {
|
||||
background-color: var(--vt-c-gray-4);
|
||||
background-color: var(--bg-hover);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
@ -109,17 +109,18 @@ const handleBlur = (e: FocusEvent) => {
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px 40px 12px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
color: var(--vt-c-black-bold);
|
||||
border: var(--border-color);
|
||||
background: var(--text-light);
|
||||
font-size: var(--font-base);
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
border-radius: var(--radius-md);
|
||||
&::placeholder {
|
||||
color: var(--vt-c-gray-8);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--vt-c-gray-8);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,10 +132,10 @@ const handleBlur = (e: FocusEvent) => {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--vt-c-gray-8);
|
||||
color: var(--text-muted);
|
||||
padding: 4px;
|
||||
&:hover {
|
||||
color: var(--vt-c-black-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
@ -145,7 +146,7 @@ const handleBlur = (e: FocusEvent) => {
|
||||
.error-message {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--vt-c-red-v3);
|
||||
color: var(--error-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@ -1,31 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconProps } from '@/interface';
|
||||
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>(), {
|
||||
icon: '',
|
||||
size: 16,
|
||||
color: 'currentColor',
|
||||
})
|
||||
|
||||
const iconMap: Record<string, any> = {
|
||||
eye: EyeOutlined,
|
||||
'eye-off': EyeInvisibleOutlined,
|
||||
search: SearchOutlined,
|
||||
close: CloseOutlined,
|
||||
'arrow-right': RightOutlined,
|
||||
'arrow-left': LeftOutlined,
|
||||
const iconSize = computed(() => {
|
||||
if (typeof props.size === 'number') {
|
||||
return `${props.size}px`
|
||||
}
|
||||
|
||||
const IconComponent = computed(() => iconMap[props.icon])
|
||||
return props.size
|
||||
})
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@ -4,6 +4,8 @@ import FormCommon from './common/FormCommon.vue'
|
||||
import OnboardingComponent from './auth/OnboardingComponent.vue'
|
||||
import OpenWalletComponent from './auth/OpenWalletComponent.vue'
|
||||
import CreateWalletComponent from './auth/CreateWalletComponent.vue'
|
||||
import RecoverySeedComponent from './auth/RecoverySeedComponent.vue'
|
||||
import ConfirmSeedComponent from './auth/ConfirmSeedComponent.vue'
|
||||
import { IconCommon } from './icon'
|
||||
|
||||
export {
|
||||
@ -13,5 +15,7 @@ export {
|
||||
OnboardingComponent,
|
||||
OpenWalletComponent,
|
||||
CreateWalletComponent,
|
||||
RecoverySeedComponent,
|
||||
ConfirmSeedComponent,
|
||||
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',
|
||||
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,
|
||||
},
|
||||
{
|
||||
|
||||
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 './helpers/format'
|
||||
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 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