feat: capability layer for clipboard, share and deep links with tests

This commit is contained in:
2026-08-20 08:31:59 +02:00
parent 12851a2b1c
commit f525b06966
11 changed files with 446 additions and 33 deletions
@@ -130,8 +130,8 @@
<div v-else class="column items-center q-gutter-sm">
<div class="text-caption text-grey-5 text-center">
Not watching right now if the invoice gets paid, the sats are still
claimed into your wallet automatically.
Not watching right now if the invoice gets paid, the sats are still claimed into your
wallet automatically.
</div>
<q-btn
outline
@@ -157,8 +157,8 @@
<template #avatar>
<q-icon name="warning" color="warning" />
</template>
The note is in your wallet, but it could not be fully secured yet the
sender may still hold a copy. You can secure it later.
The note is in your wallet, but it could not be fully secured yet the sender may still
hold a copy. You can secure it later.
</q-banner>
<q-btn
@@ -182,8 +182,8 @@
<template v-if="trustNodeAlias"> ({{ trustNodeAlias }})</template>
</div>
<div class="text-caption text-grey-5 q-mb-md">
Trusting saves the mint so it is offered next time. You can manage trusted
mints in Settings.
Trusting saves the mint so it is offered next time. You can manage trusted mints in
Settings.
</div>
<div class="row q-gutter-sm justify-end">
<q-btn flat no-caps color="grey-5" label="Just this once" @click="skipTrust" />
@@ -203,9 +203,10 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { Notify, copyToClipboard } from 'quasar';
import { Notify } from 'quasar';
import QrCode from '../QrCode.vue';
import { writeClipboard } from '@/capabilities/clipboard';
import { prepareMint, claimMintedNote } from '@/lnurlcash/ops';
import type { ClaimedNote, PreparedMint } from '@/lnurlcash/ops';
import type { NewBearer } from '@/lnurlcash/types';
@@ -277,7 +278,9 @@ const defaultChoice = (): string => {
const formValid = computed(() => {
if (!Number.isInteger(amountSats.value) || (amountSats.value ?? 0) < 1) return false;
return mintChoice.value === CUSTOM_MINT ? customMint.value.trim() !== '' : mintChoice.value !== '';
return mintChoice.value === CUSTOM_MINT
? customMint.value.trim() !== ''
: mintChoice.value !== '';
});
const createInvoice = async () => {
@@ -326,7 +329,7 @@ const feeSats = computed(() => grossSats.value - netSats.value);
const copyInvoice = async () => {
if (!prepared.value) return;
try {
await copyToClipboard(prepared.value.invoice);
await writeClipboard(prepared.value.invoice);
Notify.create({ type: 'positive', message: 'Invoice copied.' });
} catch (err) {
Notify.create({ type: 'negative', message: errorMessage(err) });
@@ -380,7 +383,10 @@ const onClaimed = async (claimed: ClaimedNote, from: PreparedMint) => {
receivedSats.value = displaySats(claimed.note.amount);
receivedServer.value = server;
rotationWarning.value = claimed.rotationError ?? '';
activity.log('mint', `Received ${receivedSats.value.toLocaleString()} sats from ${server} over Lightning.`);
activity.log(
'mint',
`Received ${receivedSats.value.toLocaleString()} sats from ${server} over Lightning.`,
);
const nodeInfo = mintAddressCacheInfo(from.nodeInfo, from.username);
if (nodeInfo) mints.cacheNodeInfo(server, nodeInfo);
Notify.create({
@@ -420,7 +426,8 @@ const skipTrust = () => {
showTrust.value = false;
Notify.create({
type: 'warning',
message: 'Note added, but this mint is not in your trusted list yet — you can review it in Settings.',
message:
'Note added, but this mint is not in your trusted list yet — you can review it in Settings.',
});
};
+4 -10
View File
@@ -12,6 +12,7 @@ import { useQuasar } from 'quasar';
import { decodeBolt11AmountMsat, isBolt11Invoice, resolveLnurlInput } from 'lnurlcash-kit';
import QrScanner from '@/components/QrScanner.vue';
import { readClipboard } from '@/capabilities/clipboard';
import { payWithBearers, UncertainOutcomeError } from '@/lnurlcash/ops';
import type { CarveResult, PayOutcome } from '@/lnurlcash/ops';
import type { NewBearer } from '@/lnurlcash/types';
@@ -121,7 +122,7 @@ const onScanError = (message: string) => {
const paste = async () => {
try {
const text = await navigator.clipboard.readText();
const text = await readClipboard();
if (text) input.value = text.trim();
} catch {
toast('negative', "Couldn't read the clipboard - paste manually.");
@@ -349,18 +350,11 @@ const closeResult = () => {
</q-item>
</q-list>
<div class="text-caption text-grey-5 q-mt-md">
If the mint charges a fee, it comes out of your change - you pay exactly the amount
shown.
If the mint charges a fee, it comes out of your change - you pay exactly the amount shown.
</div>
<div class="row justify-end q-gutter-sm q-mt-lg">
<q-btn flat label="Back" color="grey-5" @click="step = 'input'" />
<q-btn
unelevated
color="primary"
text-color="dark"
label="Pay now"
@click="pay"
/>
<q-btn unelevated color="primary" text-color="dark" label="Pay now" @click="pay" />
</div>
</q-card-section>
+13 -8
View File
@@ -12,6 +12,8 @@ import { useQuasar } from 'quasar';
import { toBech32Lnurl } from 'lnurlcash-kit';
import QrCode from '@/components/QrCode.vue';
import { writeClipboard } from '@/capabilities/clipboard';
import { canShareText, shareText } from '@/capabilities/share';
import { ensureExactAmount, UncertainOutcomeError } from '@/lnurlcash/ops';
import type { CarveResult } from '@/lnurlcash/ops';
import type { Bearer, NewBearer } from '@/lnurlcash/types';
@@ -71,12 +73,9 @@ const canPrepare = computed(
() => parsedAmount.value !== null && amountError.value === null && !preparing.value,
);
const noteDisplayValue = computed(() =>
prepared.value ? toBech32Lnurl(prepared.value.url) : '',
);
const noteDisplayValue = computed(() => (prepared.value ? toBech32Lnurl(prepared.value.url) : ''));
const canShare =
typeof navigator !== 'undefined' && typeof navigator.share === 'function';
const canShare = canShareText();
const reset = () => {
step.value = 'amount';
@@ -153,7 +152,7 @@ const prepare = async () => {
const copyNote = async () => {
try {
await navigator.clipboard.writeText(noteDisplayValue.value);
await writeClipboard(noteDisplayValue.value);
toast('positive', 'Note copied to clipboard.');
} catch {
toast('negative', "Couldn't copy - reveal the note and copy it manually.");
@@ -162,7 +161,7 @@ const copyNote = async () => {
const shareNote = async () => {
try {
await navigator.share({ title: 'sattle bearer note', text: noteDisplayValue.value });
await shareText('sattle bearer note', noteDisplayValue.value);
} catch (err) {
// the user dismissing the share sheet is not an error
if (err instanceof DOMException && err.name === 'AbortError') return;
@@ -302,7 +301,13 @@ const finishKeep = () => {
:loading="removing"
@click="finishRemove"
/>
<q-btn flat color="grey-5" label="Keep in wallet" :disable="removing" @click="finishKeep" />
<q-btn
flat
color="grey-5"
label="Keep in wallet"
:disable="removing"
@click="finishKeep"
/>
</div>
</q-card-section>
</q-card>