mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
feat: backup and security settings pages, nostr restore onboarding, passkey unlock entry
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="modelValue.length" class="row q-gutter-xs q-mb-sm">
|
||||
<q-chip
|
||||
v-for="relay in modelValue"
|
||||
:key="relay"
|
||||
removable
|
||||
dense
|
||||
color="secondary"
|
||||
text-color="primary"
|
||||
:aria-label="`Remove ${relay}`"
|
||||
@remove="remove(relay)"
|
||||
>
|
||||
{{ relay }}
|
||||
</q-chip>
|
||||
</div>
|
||||
<div v-else class="text-caption text-warning q-mb-sm">
|
||||
No relays configured - a backup has nowhere to go.
|
||||
</div>
|
||||
<q-input
|
||||
v-model="draft"
|
||||
dark
|
||||
outlined
|
||||
dense
|
||||
color="primary"
|
||||
label="Add a relay"
|
||||
placeholder="wss://relay.example.com"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
:error="error !== ''"
|
||||
:error-message="error"
|
||||
class="q-mb-sm"
|
||||
@keyup.enter="add"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
color="primary"
|
||||
label="Add relay"
|
||||
:disable="draft.trim() === ''"
|
||||
@click="add"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { normalizeRelay } from '@/stores/nostrBackup';
|
||||
|
||||
const props = defineProps<{ modelValue: string[] }>();
|
||||
const emit = defineEmits<{ 'update:modelValue': [relays: string[]] }>();
|
||||
|
||||
const draft = ref('');
|
||||
const error = ref('');
|
||||
|
||||
const add = () => {
|
||||
error.value = '';
|
||||
try {
|
||||
const relay = normalizeRelay(draft.value);
|
||||
if (!props.modelValue.includes(relay)) {
|
||||
emit('update:modelValue', [...props.modelValue, relay]);
|
||||
}
|
||||
draft.value = '';
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'That is not a relay address.';
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (relay: string) => {
|
||||
emit(
|
||||
'update:modelValue',
|
||||
props.modelValue.filter((r) => r !== relay),
|
||||
);
|
||||
};
|
||||
</script>
|
||||
@@ -39,6 +39,17 @@
|
||||
@click="unlock"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
v-if="passkeyAvailable"
|
||||
outline
|
||||
color="primary"
|
||||
icon="fingerprint"
|
||||
label="Unlock with passkey"
|
||||
class="full-width q-mt-sm"
|
||||
:loading="passkeyBusy"
|
||||
@click="unlockViaPasskey"
|
||||
/>
|
||||
|
||||
<div class="text-center q-mt-md">
|
||||
<q-btn
|
||||
flat
|
||||
@@ -57,6 +68,7 @@
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { hasPasskeySlots } from '@/lnurlcash/passkeys';
|
||||
import { useWalletStore } from '@/stores/wallet';
|
||||
|
||||
const emit = defineEmits<{ unlocked: [] }>();
|
||||
@@ -69,6 +81,25 @@ const showPassword = ref(false);
|
||||
const busy = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
// slots live in plain localStorage - a sync read at setup is enough; they
|
||||
// can only change from the security page while unlocked
|
||||
const passkeyAvailable = hasPasskeySlots();
|
||||
const passkeyBusy = ref(false);
|
||||
|
||||
const unlockViaPasskey = async () => {
|
||||
if (passkeyBusy.value) return;
|
||||
passkeyBusy.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await wallet.unlockWithPasskey();
|
||||
emit('unlocked');
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Passkey unlock failed.';
|
||||
} finally {
|
||||
passkeyBusy.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const unlock = async () => {
|
||||
if (busy.value || password.value === '') return;
|
||||
busy.value = true;
|
||||
|
||||
Reference in New Issue
Block a user