diff --git a/index.js b/index.js index 8c9807a2..e3bc33c2 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,7 @@ function BootstrapApp() { // Setup notification listeners and handlers -messaging().onMessage(NotificationService.onReceiveRemoteNotification) -messaging().setBackgroundMessageHandler(NotificationService.onReceiveRemoteNotification) +messaging().onMessage(NotificationService.onForegroundNotification) +messaging().setBackgroundMessageHandler(NotificationService.onBackgroundNotification) AppRegistry.registerComponent(appName, () => BootstrapApp) diff --git a/src/models/NwcStore.ts b/src/models/NwcStore.ts index be220e09..5b6c6d19 100644 --- a/src/models/NwcStore.ts +++ b/src/models/NwcStore.ts @@ -70,7 +70,7 @@ export const NwcConnectionModel = types.model('NwcConnection', { return NostrClient.getMinibitsRelays() }, get supportedMethods() { - return ['pay_invoice', 'get_balance', 'get_info', 'list_transactions'] + return ['pay_invoice', 'get_balance', 'get_info'] } })) .views(self => ({ @@ -111,9 +111,6 @@ export const NwcConnectionModel = types.model('NwcConnection', { resetMeltQuoteInFlight() { self.meltQuoteInFlight = undefined }, - /*setListenerExists(exists: boolean) { - self.listenerExists = exists - }*/ })) .actions(self => ({ sendResponse: flow(function* sendResponse(nwcResponse: NwcResponse | NwcError) { @@ -309,10 +306,10 @@ export const NwcConnectionModel = types.model('NwcConnection', { log.trace('[Nwc.handleRequest] request method', {method: nwcRequest.method}) switch (nwcRequest.method) { - case 'get_info': + case 'get_info': nwcResponse = self.handleGetInfo(nwcRequest) break - case 'get_balance': + case 'get_balance': nwcResponse = self.handleGetBalance(nwcRequest) break case 'pay_invoice': @@ -326,13 +323,13 @@ export const NwcConnectionModel = types.model('NwcConnection', { break } - - self.setEventInFlight(requestEvent) + // only early errors are immediately returned, transfer result is handled via event handler nwcResponse = yield self.handlePayInvoice(nwcRequest) as Promise // no early error, transfer initiated, exit and create response in transfer result event handler if(!nwcResponse) { + self.setEventInFlight(requestEvent) return } break @@ -346,13 +343,19 @@ export const NwcConnectionModel = types.model('NwcConnection', { log.error(message, {nwcRequest}) } + self.setEventInFlight(requestEvent) yield self.sendResponse(nwcResponse) }), })) .actions(self => ({ - receiveNwcEvents: flow(function* receiveNwcEvents() { - log.trace('[receiveNwcEvents] start for connection', {name: self.name}) + receiveNwcEvents () { + log.trace('[receiveNwcEvents] start listening for NWC events', { + name: self.name, + connectionPubkey: self.connectionPubkey, + walletPubkey: self.walletPubkey + }) + self.resetEventInFlight() self.resetMeltQuoteInFlight() @@ -431,7 +434,7 @@ export const NwcConnectionModel = types.model('NwcConnection', { log.error(e.name, e.message) return } - }) + } })) @@ -458,7 +461,7 @@ export const NwcStoreModel = types return self.nwcConnections }, get supportedMethods() { - return ['pay_invoice', 'get_balance', 'get_info', 'list_transactions'] + return ['pay_invoice', 'get_balance', 'get_info'] } })) .actions(self => ({ @@ -534,8 +537,28 @@ export const NwcStoreModel = types conn.receiveNwcEvents() } - EventEmitter.on('ev_transferTask_result', self.handleTransferTaskResult) - } + if (self.all.length > 0) { + EventEmitter.on('ev_transferTask_result', self.handleTransferTaskResult) + } + }, + handleNwcRequestFromNotification: flow(function* handleNwcRequestFromNotification(requestEvent: NostrEvent) { + // We need to select the connection the request belongs to + const connection = self.nwcConnections.find(c => c.connectionPubkey === requestEvent.pubkey) + + if(connection) { + const decryptedContent = yield NostrClient.decryptNip04(requestEvent.pubkey, requestEvent.content) + const nwcRequest: NwcRequest = JSON.parse(decryptedContent) + + if(nwcRequest.method === 'pay_invoice') { + EventEmitter.on('ev_transferTask_result', self.handleTransferTaskResult) + } + + yield connection.handleRequest(requestEvent) + return nwcRequest + } else { + log.warn('[handleNwcRequestFromNotification] No connectionPubkey matches the event pubkey', {requestEvent}) + } + }) })) .views(self => ({ get all() { diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx index 4299bc7b..e379369a 100644 --- a/src/screens/WalletScreen.tsx +++ b/src/screens/WalletScreen.tsx @@ -83,6 +83,7 @@ export const WalletScreen: FC = observer( transactionsStore, paymentRequestsStore, userSettingsStore, + nwcStore } = useStores() const pagerRef = useRef(null) @@ -160,6 +161,8 @@ export const WalletScreen: FC = observer( WalletTask.handleInFlight().catch(e => false) // Create websocket subscriptions to receive tokens or payment requests by NOSTR DMs WalletTask.receiveEventsFromRelays().catch(e => false) + // Create websocket subscriptions to receive NWC requests from remote wallets (if any) + nwcStore.receiveNwcEvents() // log.trace('[getInitialData]', 'walletProfile', walletProfileStore) const preferredUnit: MintUnit = userSettingsStore.preferredUnit const pageIndex = groupedMints.findIndex((m) => m.unit === preferredUnit) diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index e6d1cad9..bf5ac9d8 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -6,10 +6,10 @@ import { } from '@env' import { FirebaseMessagingTypes } from '@react-native-firebase/messaging' import { MintUnit, formatCurrency, getCurrency } from './wallet/currency' -import { NostrClient, NostrProfile } from './nostrService' +import { NostrClient, NostrEvent, NostrProfile } from './nostrService' import AppError, { Err } from '../utils/AppError' import { Platform } from 'react-native' -import { rootStoreInstance } from '../models'; +import { RootStoreModel, rootStoreInstance, setupRootStore } from '../models'; export type NotifyReceiveToLnurlData = { type: 'NotifyReceiveToLnurlData', @@ -21,52 +21,30 @@ export type NotifyReceiveToLnurlData = { } } -const { - proofsStore, - relaysStore, - walletStore, -} = rootStoreInstance +export type NotifyNwcRequestData = { + type: 'NotifyNwcRequestData', + data: { + requestEvent: NostrEvent, + } +} // Remote notification receive handler -const onReceiveRemoteNotification = async function(remoteMessage: FirebaseMessagingTypes.RemoteMessage) { - log.debug('[onReceiveRemoteNotification]', {remoteMessage}) +const onForegroundNotification = async function(remoteMessage: FirebaseMessagingTypes.RemoteMessage) { + log.trace('[onForegroundReceiveNotification]', {remoteMessage}) try { - const {encrypted} = remoteMessage.data! + const remoteData = await _getRemoteData(remoteMessage) - if (!encrypted) { - throw new AppError(Err.VALIDATION_ERROR, 'Unknown remote message data received', {data: remoteMessage.data}) + // Process notification about incoming lightning address / zap payment + if(remoteData.type === 'NotifyReceiveToLnurlData') { + return _receiveToLnurlHandler(remoteData) } - const serverPubkey = MINIBIT_SERVER_NOSTR_PUBKEY - const decrypted = await NostrClient.decryptNip04(serverPubkey, encrypted as string) - - if (!decrypted) { - throw new AppError(Err.VALIDATION_ERROR, 'Unknown remote message data received', {data: remoteMessage.data}) - } - - const remoteData = JSON.parse(decrypted) - - // Remote notification building and showing per remoteData.type - if(remoteData.type === 'NotifyReceiveToLnurlData' || remoteData.type === 'RemoteMessageReceiveToLnurl') { - const {amount, unit, comment, zapSenderProfile} = remoteData.data - const currencyCode = getCurrency(unit as MintUnit).code - - - /* const mint = await walletStore.getMint('https://mint.minibits.cash/Bitcoin') - const {keysets} = await mint.getKeySets() - - log.trace('[onReceiveRemoteNotification]', {mint: mint.mintUrl, keysets}) */ - - await createLocalNotification( - `⚡${formatCurrency(amount, currencyCode)} ${currencyCode} incoming!`, - `${zapSenderProfile ? 'Zap' : 'Payment'} from ${zapSenderProfile?.nip05 || 'unknown payer'} is ready to be received.${comment ? ' Message from sender: ' + comment : ''}`, - zapSenderProfile?.picture - ) - - - - return + // Process NWC request notified by FCM message + if(remoteData.type === 'NotifyNwcRequestData') { + // Ingnore when in foreground as we should be connected to relays + log.trace('[onForegroundNotification] App is in foreground, skipping NWC requestHandler') + return } throw new AppError(Err.VALIDATION_ERROR, 'Unknown remoteData.type', {remoteData}) @@ -77,6 +55,85 @@ const onReceiveRemoteNotification = async function(remoteMessage: FirebaseMessag } +const onBackgroundNotification = async function(remoteMessage: FirebaseMessagingTypes.RemoteMessage) { + log.trace('[onBackgroundReceiveNotification]', {remoteMessage}) + try { + + const remoteData = await _getRemoteData(remoteMessage) + + // Process notification about incoming lightning address / zap payment + if(remoteData.type === 'NotifyReceiveToLnurlData') { + return _receiveToLnurlHandler(remoteData) + } + + // Process NWC request notified by FCM message + if(remoteData.type === 'NotifyNwcRequestData') { + return _nwcRequestHandler(remoteData) + } + + throw new AppError(Err.VALIDATION_ERROR, 'Unknown remoteData.type', {remoteData}) + + } catch (e: any) { + log.error(e.name, e.message) + } +} + +const _receiveToLnurlHandler = async function(remoteData: NotifyReceiveToLnurlData) { + const {amount, unit, comment, zapSenderProfile} = remoteData.data + const currencyCode = getCurrency(unit as MintUnit).code + + await createLocalNotification( + `⚡${formatCurrency(amount, currencyCode)} ${currencyCode} incoming!`, + `${zapSenderProfile ? 'Zap' : 'Payment'} from ${zapSenderProfile?.nip05 || 'unknown payer'} is ready to be received.${comment ? ' Message from sender: ' + comment : ''}`, + zapSenderProfile?.picture + ) +} + +const _nwcRequestHandler = async function(remoteData: NotifyNwcRequestData) { + const {requestEvent} = remoteData.data + const {nwcStore} = rootStoreInstance + + // If app is killed, state is not loaded + if(nwcStore.all.length === 0) { + await setupRootStore(rootStoreInstance) + } + + log.trace('[_nwcRequestHandler]', {connections: nwcStore.all}) + + const nwcRequest = await nwcStore.handleNwcRequestFromNotification(requestEvent) + + if(nwcRequest) { + const body = nwcRequest.method === 'pay_invoice' ? 'Pay invoice' : nwcRequest.method === 'get_balance' ? 'Get wallet balance' : null + + if(body) { + await createLocalNotification( + `Received Nostr Wallet Connect request`, + body, + ) + } + } else { + log.warn('[_nwcRequestHandler] Could not get nwcRequest from requestEvent', {remoteData}) + } +} + +const _getRemoteData = async function(remoteMessage: FirebaseMessagingTypes.RemoteMessage) { + + const {encrypted} = remoteMessage.data! + + if (!encrypted) { + throw new AppError(Err.VALIDATION_ERROR, 'Unknown remote message data received', {data: remoteMessage.data}) + } + + const serverPubkey = MINIBIT_SERVER_NOSTR_PUBKEY + const decrypted = await NostrClient.decryptNip04(serverPubkey, encrypted as string) + + if (!decrypted) { + throw new AppError(Err.VALIDATION_ERROR, 'Unknown remote message data received', {data: remoteMessage.data}) + } + + return JSON.parse(decrypted) +} + // Local notification creation const createLocalNotification = async function (title: string, body: string, largeIcon?: string) { @@ -170,7 +227,8 @@ const areNotificationsEnabled = async function (): Promise { } export const NotificationService = { - onReceiveRemoteNotification, + onForegroundNotification, + onBackgroundNotification, createLocalNotification, // updateLocalNotification, // cancelNotification,