mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 11:18:24 +00:00
Handling of remote notifications and fixes
This commit is contained in:
@@ -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)
|
||||
|
||||
+37
-14
@@ -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<NwcError>
|
||||
|
||||
// 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() {
|
||||
|
||||
@@ -83,6 +83,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
transactionsStore,
|
||||
paymentRequestsStore,
|
||||
userSettingsStore,
|
||||
nwcStore
|
||||
} = useStores()
|
||||
|
||||
const pagerRef = useRef<PagerView>(null)
|
||||
@@ -160,6 +161,8 @@ export const WalletScreen: FC<WalletScreenProps> = 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)
|
||||
|
||||
@@ -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(
|
||||
`<b>⚡${formatCurrency(amount, currencyCode)} ${currencyCode}</b> incoming!`,
|
||||
`${zapSenderProfile ? 'Zap' : 'Payment'} from <b>${zapSenderProfile?.nip05 || 'unknown payer'}</b> 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(
|
||||
`<b>⚡${formatCurrency(amount, currencyCode)} ${currencyCode}</b> incoming!`,
|
||||
`${zapSenderProfile ? 'Zap' : 'Payment'} from <b>${zapSenderProfile?.nip05 || 'unknown payer'}</b> 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 <b>Nostr Wallet Connect</b> 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<boolean> {
|
||||
}
|
||||
|
||||
export const NotificationService = {
|
||||
onReceiveRemoteNotification,
|
||||
onForegroundNotification,
|
||||
onBackgroundNotification,
|
||||
createLocalNotification,
|
||||
// updateLocalNotification,
|
||||
// cancelNotification,
|
||||
|
||||
Reference in New Issue
Block a user