diff --git a/src/services/app.ts b/src/services/app.ts index f175a1b..e1b71df 100644 --- a/src/services/app.ts +++ b/src/services/app.ts @@ -67,6 +67,7 @@ export class AppService extends EventEmitter { { onUpdated: (invitation: XOInvitation) => void; onStatusChanged: (status: string) => void; + onRemoved: () => void; } >(); @@ -241,13 +242,25 @@ export class AppService extends EventEmitter { invitationIdentifier, }); }; + const onRemoved = () => { + this.detachInvitationListeners(invitationIdentifier); + this.invitations.splice(this.invitations.indexOf(invitation), 1); + this.bumpInvitationRevision(invitationIdentifier); + this.emit("invitation-removed", invitation); + this.emit("wallet-state-changed", { + reason: "invitation-removed", + invitationIdentifier: invitationIdentifier, + }); + }; invitation.on("invitation-updated", onUpdated); invitation.on("invitation-status-changed", onStatusChanged); + invitation.on("invitation-removed", onRemoved); this.invitationEventCleanup.set(invitationIdentifier, { onUpdated, onStatusChanged, + onRemoved, }); } diff --git a/src/services/invitation.ts b/src/services/invitation.ts index ebe6630..7a08d7c 100644 --- a/src/services/invitation.ts +++ b/src/services/invitation.ts @@ -46,6 +46,7 @@ export type { ResolvedInvitationData } from "../utils/resolve-invitation-data.js export type InvitationEventMap = { "invitation-updated": XOInvitation; "invitation-status-changed": string; + "invitation-removed": void; error: Error; }; @@ -889,4 +890,21 @@ export class Invitation extends EventEmitter { return totalSats; } + + /** + * Removes the invitation from the Local SQLite db as well as the Engine's internal DB + * NOTE: This uses methods that are marked "DANGEROUSLY" inside the engine and behaviour may change + */ + public async delete() { + // Remove the invitation from our local db + this.storage.remove(this.data.invitationIdentifier); + + // Remove the invitation from the engine's internal db + await this.engine.DANGEROUS_deleteStoredInvitation(this.data.invitationIdentifier); + + this.emit("invitation-removed", this.data.invitationIdentifier); + + // Update the status of the invitation + await this.updateStatus(); + } } diff --git a/src/tui/screens/invitations/InvitationScreen.tsx b/src/tui/screens/invitations/InvitationScreen.tsx index 2a84517..28e3390 100644 --- a/src/tui/screens/invitations/InvitationScreen.tsx +++ b/src/tui/screens/invitations/InvitationScreen.tsx @@ -63,6 +63,7 @@ const actionItems: ListItemData[] = [ { key: 'sign', label: 'Sign Transaction', value: 'sign' }, { key: 'broadcast', label: 'Broadcast Transaction', value: 'broadcast' }, { key: 'copy', label: 'Copy Invitation ID', value: 'copy' }, + { key: 'delete', label: 'Delete Invitation', value: 'delete' }, ]; /** @@ -354,6 +355,28 @@ export function InvitationScreen(): React.ReactElement { } }, [selectedInvitation, showInfo, showError, setStatus]); + /** + * Delete the selected invitation from both our SQLite db and the engine's db + * NOTE: This uses methods marked "DANGEROUSLY" internally, and may change in the future. + */ + const deleteInvitation = useCallback(async () => { + if (!selectedInvitation) return; + + setIsLoading(true) + setStatus('Removing invitation...') + + try { + await selectedInvitation.delete(); + showInfo('Invitation successfully deleted') + setStatus('Ready') + } catch (error) { + showError(`Failed to delete invitation: ${error instanceof Error ? error.message : String(error)}`) + } finally { + setIsLoading(false) + setStatus('Ready') + } + }) + const copyId = useCallback(async () => { if (!selectedInvitation) { showError('No invitation selected'); @@ -509,6 +532,9 @@ export function InvitationScreen(): React.ReactElement { case 'broadcast': broadcastTransaction(); break; + case 'delete': + deleteInvitation(); + break; } }, [selectedInvitation, copyId, acceptInvitation, fillRequirements, signInvitation, broadcastTransaction, navigate]);