feat: deep link routing for lightning and lnurl schemes plus association files

This commit is contained in:
2026-08-20 08:31:59 +02:00
parent e59b722b5e
commit 229e9fcf38
6 changed files with 72 additions and 8 deletions
@@ -0,0 +1,14 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "REPLACE_WITH_APPLE_TEAM_ID.eu.protom.sattle",
"paths": ["*"]
}
]
},
"webcredentials": {
"apps": ["REPLACE_WITH_APPLE_TEAM_ID.eu.protom.sattle"]
}
}
+12
View File
@@ -0,0 +1,12 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "eu.protom.sattle",
"sha256_cert_fingerprints": [
"REPLACE_WITH_RELEASE_KEYSTORE_SHA256_CERT_FINGERPRINT"
]
}
}
]
+1 -1
View File
@@ -11,7 +11,7 @@ export default defineConfig((ctx) => {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: ['i18n', 'kit-fetch', 'wallet'],
boot: ['i18n', 'kit-fetch', 'wallet', 'deeplinks'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
css: ['app.scss'],
+6
View File
@@ -5,6 +5,12 @@
"id": "sattle",
"display_override": ["fullscreen", "minimal-ui"],
"display": "standalone",
"protocol_handlers": [
{
"protocol": "web+lightning",
"url": "/?uri=%s"
}
],
"icons": [
{
"src": "icons/icon-128x128.png",
+16
View File
@@ -0,0 +1,16 @@
import { defineBoot } from '#q-app';
import { initDeepLinks } from '@/capabilities/deepLinks';
// Deep-link bootstrap (capabilities/deepLinks.ts): native appUrlOpen /
// cold-start launch URLs and the PWA protocol-handler ?uri= landing all
// funnel into pendingExternalInput; the home screen consumes it and opens
// the matching receive/pay dialog. Every accepted link routes home first -
// the dialogs live there.
export default defineBoot(({ router }) => {
void initDeepLinks(() => {
void router.push('/').catch(() => {
// already on '/' - a redundant navigation is not an error
});
});
});
+23 -7
View File
@@ -6,9 +6,7 @@
class="sattle-card full-width q-mt-xl q-pa-lg text-center welcome-card"
>
<div class="text-h5 text-primary q-mb-sm">Welcome to sattle</div>
<div class="text-grey-5 q-mb-md">
A simple wallet for Lightning bearer notes.
</div>
<div class="text-grey-5 q-mb-md">A simple wallet for Lightning bearer notes.</div>
<q-btn
unelevated
color="primary"
@@ -43,9 +41,7 @@
class="sattle-card q-pa-sm q-mt-md row items-center lock-warning"
>
<q-icon name="lock_clock" color="warning" class="q-mx-sm" />
<div class="col text-grey-4">
Locking in {{ wallet.lockWarningSecondsLeft }}s
</div>
<div class="col text-grey-4">Locking in {{ wallet.lockWarningSecondsLeft }}s</div>
<q-btn
flat
dense
@@ -128,10 +124,11 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { isValidNoteInput } from 'lnurlcash-kit';
import { consumePendingExternalInput, pendingExternalInput } from '@/capabilities/deepLinks';
import { useWalletStore } from '@/stores/wallet';
import UnlockForm from '@/components/UnlockForm.vue';
import HistoryList from '@/components/HistoryList.vue';
@@ -174,6 +171,25 @@ const onScanError = (message: string) => {
showScan.value = false;
$q.notify({ type: 'negative', message, position: 'top' });
};
// Deep links (capabilities/deepLinks.ts): an inbound lightning:/lnurlw:
// link waits as pendingExternalInput until the wallet is unlocked (the
// dialogs need the keys), then opens the same dialogs a scan would.
watch(
[pendingExternalInput, () => wallet.state],
() => {
if (!pendingExternalInput.value || wallet.state !== 'unlocked') return;
const input = consumePendingExternalInput();
if (!input) return;
scanned.value = input.value;
if (input.kind === 'note') {
showReceiveToken.value = true;
} else {
showPayInvoice.value = true;
}
},
{ immediate: true },
);
</script>
<style lang="scss" scoped>