From b9b1f5e75bce52da87bbbe01fb70ab076c3a2c85 Mon Sep 17 00:00:00 2001 From: Vinny Fiano Date: Fri, 10 Jan 2025 14:39:40 -0500 Subject: [PATCH] Expose MakeInvoice and LookupInvoice as iOS AppIntents --- app.json | 41 ++++++++++++++++++++ app/_layout.tsx | 2 + lib/appIntents/LightningIntents.ts | 62 ++++++++++++++++++++++++++++++ lib/appIntents/types.ts | 16 ++++++++ package.json | 1 + 5 files changed, 122 insertions(+) create mode 100644 lib/appIntents/LightningIntents.ts create mode 100644 lib/appIntents/types.ts diff --git a/app.json b/app.json index 7b73ebd..17f8a3b 100644 --- a/app.json +++ b/app.json @@ -24,6 +24,47 @@ "faceIDPermission": "Allow Alby Go to use Face ID." } ], + [ + "expo-ios-app-intents", + { + "intents": [ + { + "name": "MakeInvoice", + "title": "Create Lightning Invoice", + "description": "Generate a new lightning network invoice", + "resultType": "String", + "parameters": [ + { + "name": "Amount", + "type": "Int", + "description": "Amount in satoshis", + "required": true + }, + { + "name": "Description", + "type": "String", + "description": "Invoice description", + "required": false + } + ] + }, + { + "name": "LookupInvoice", + "title": "Lookup Lightning Invoice", + "description": "Look up the status of a lightning network invoice", + "resultType": "String", + "parameters": [ + { + "name": "Invoice", + "type": "String", + "description": "The invoice", + "required": true + } + ] + } + ] + } + ], [ "expo-camera", { diff --git a/app/_layout.tsx b/app/_layout.tsx index bb2108c..ec58f0f 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -13,6 +13,7 @@ import { swrConfiguration } from "lib/swr"; import * as React from "react"; import { SafeAreaView } from "react-native-safe-area-context"; import Toast from "react-native-toast-message"; +import { setupLightningIntentHandlers } from "~/lib/appIntents/LightningIntents"; import { SWRConfig } from "swr"; import { toastConfig } from "~/components/ToastConfig"; import { UserInactivityProvider } from "~/context/UserInactivity"; @@ -82,6 +83,7 @@ export default function RootLayout() { React.useEffect(() => { const init = async () => { try { + setupLightningIntentHandlers(); await Promise.all([loadTheme(), loadFonts(), checkBiometricStatus()]); } finally { setResourcesLoaded(true); diff --git a/lib/appIntents/LightningIntents.ts b/lib/appIntents/LightningIntents.ts new file mode 100644 index 0000000..6d977ce --- /dev/null +++ b/lib/appIntents/LightningIntents.ts @@ -0,0 +1,62 @@ +import ExpoAppIntents from "expo-ios-app-intents"; +import { useAppStore } from "../state/appStore"; +import { Nip47Transaction } from "@getalby/sdk/dist/NWCClient"; + +export interface MakeInvoiceParameters { + _Amount: number; + _Description?: string; +} + +export interface LookupInvoiceParameters { + _Invoice: string; +} + +// Define the proper IntentEventPayload type +interface IntentEventPayload { + id: string; + name: string; + parameters: Record; +} + +async function handleIntent(event: IntentEventPayload) { + const { name, parameters, id } = event; + const nwcClient = useAppStore.getState().nwcClient; + + if (!nwcClient) { + ExpoAppIntents.failIntent(id, "NWC client not connected"); + return; + } + + try { + if (name === "MakeInvoice") { + const { _Amount, _Description } = parameters as MakeInvoiceParameters; + const response = (await nwcClient.makeInvoice({ + amount: _Amount, + ...(_Description ? { description: _Description } : {}), + })) as Nip47Transaction; + // Return [invoice, paymentHash] as string array + ExpoAppIntents.completeIntent(id, { value: JSON.stringify(response) }); + } else if (name === "LookupInvoice") { + const { _Invoice } = parameters as LookupInvoiceParameters; + const response = (await nwcClient.lookupInvoice({ + invoice: _Invoice, + })) as Nip47Transaction; + + // Return [paid, preimage, amount, description] as string array + // Determine paid status from presence of settled_at timestamp + ExpoAppIntents.completeIntent(id, { value: JSON.stringify(response) }); + } + } catch (error) { + console.error("App Intent error:", error); + ExpoAppIntents.failIntent( + id, + error instanceof Error ? error.message : "Unknown error", + ); + } +} + +export const setupLightningIntentHandlers = () => { + ExpoAppIntents.addListener("onIntent", (event: IntentEventPayload) => { + handleIntent(event); + }); +}; diff --git a/lib/appIntents/types.ts b/lib/appIntents/types.ts new file mode 100644 index 0000000..d134e68 --- /dev/null +++ b/lib/appIntents/types.ts @@ -0,0 +1,16 @@ +export interface MakeInvoiceResponse { + invoice: string; + paymentHash: string; +} + +export interface LookupInvoiceResponse { + paid: boolean; + preimage?: string; + amount: number; + description?: string; +} + +export interface NWCInvoiceError { + error: string; + message: string; +} diff --git a/package.json b/package.json index 2ebec16..31fc32b 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "expo-linear-gradient": "~14.0.1", "expo-linking": "~7.0.3", "expo-local-authentication": "~15.0.1", + "expo-ios-app-intents": "ynniv/expo-ios-app-intents", "expo-router": "~4.0.15", "expo-secure-store": "~14.0.0", "expo-splash-screen": "^0.29.18",