2024-05-20 11:54:13 +02:00
import AsyncStorage from '@react-native-async-storage/async-storage' ;
2020-07-18 21:33:43 +02:00
import PushNotificationIOS from '@react-native-community/push-notification-ios' ;
2024-11-12 02:42:38 +01:00
import { AppState , findNodeHandle , Platform } from 'react-native' ;
2024-05-20 11:54:13 +02:00
import { getApplicationName , getSystemName , getSystemVersion , getVersion , hasGmsSync , hasHmsSync } from 'react-native-device-info' ;
2024-11-12 02:42:38 +01:00
import { checkNotifications , requestNotifications } from 'react-native-permissions' ;
2024-03-08 00:00:31 +01:00
import PushNotification from 'react-native-push-notification' ;
2024-05-20 11:54:13 +02:00
import loc from '../loc' ;
2024-03-21 00:21:05 +01:00
import ActionSheet from '../screen/ActionSheet' ;
2024-04-09 18:14:14 +02:00
import { groundControlUri } from './constants' ;
2021-01-23 03:46:33 +01:00
2020-07-18 21:33:43 +02:00
const PUSH _TOKEN = 'PUSH_TOKEN' ;
2020-07-31 15:43:55 +02:00
const GROUNDCONTROL _BASE _URI = 'GROUNDCONTROL_BASE_URI' ;
2020-08-10 16:17:50 +02:00
const NOTIFICATIONS _STORAGE = 'NOTIFICATIONS_STORAGE' ;
2020-11-11 18:52:08 +01:00
const NOTIFICATIONS _NO _AND _DONT _ASK _FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK_FLAG' ;
2020-07-18 21:33:43 +02:00
let alreadyConfigured = false ;
2024-04-09 18:14:14 +02:00
let baseURI = groundControlUri ;
2020-07-18 21:33:43 +02:00
2024-11-12 02:42:38 +01:00
// Function to check notification permission status at the system level
export const checkNotificationPermissionStatus = async ( ) => {
try {
const { status } = await checkNotifications ( ) ;
return status ;
} catch ( error ) {
console . error ( 'Failed to check notification permissions:' , error ) ;
return 'unavailable' ; // Return 'unavailable' if the status cannot be retrieved
}
} ;
// Listener to monitor notification permission status changes while app is running
let currentPermissionStatus = 'unavailable' ;
const handleAppStateChange = async nextAppState => {
if ( nextAppState === 'active' ) {
const newPermissionStatus = await checkNotificationPermissionStatus ( ) ;
if ( newPermissionStatus !== currentPermissionStatus ) {
currentPermissionStatus = newPermissionStatus ;
if ( newPermissionStatus === 'granted' ) {
// Re-initialize notifications if permissions are granted
await initializeNotifications ( ) ;
} else {
// Optionally, handle the case where permissions are revoked (e.g., disable in-app notifications)
console . warn ( 'Notifications have been disabled at the system level.' ) ;
}
}
}
} ;
AppState . addEventListener ( 'change' , handleAppStateChange ) ;
2024-11-11 23:29:45 +01:00
export const cleanUserOptOutFlag = async ( ) => {
return AsyncStorage . removeItem ( NOTIFICATIONS _NO _AND _DONT _ASK _FLAG ) ;
} ;
2020-07-18 21:33:43 +02:00
2024-11-11 23:29:45 +01:00
/ * *
* Should be called when user is most interested in receiving push notifications .
* If we dont have a token it will show alert asking whether
* user wants to receive notifications , and if yes - will configure push notifications .
* FYI , on Android permissions are acquired when app is installed , so basically we dont need to ask ,
* we can just call ` configure ` . On iOS its different , and calling ` configure ` triggers system ' s dialog box .
*
* @ returns { Promise < boolean > } TRUE if permissions were obtained , FALSE otherwise
* /
export const tryToObtainPermissions = async anchor => {
if ( ! isNotificationsCapable ) return false ;
2020-07-18 21:33:43 +02:00
2024-11-11 23:29:45 +01:00
try {
if ( await getPushToken ( ) ) {
// we already have a token, no sense asking again, just configure pushes to register callbacks and we are done
if ( ! alreadyConfigured ) configureNotifications ( ) ; // no await so it executes in background while we return TRUE and use token
return true ;
}
} catch ( error ) {
console . error ( 'Failed to get push token:' , error ) ;
return false ;
}
2020-11-11 18:52:08 +01:00
2024-11-11 23:29:45 +01:00
if ( await AsyncStorage . getItem ( NOTIFICATIONS _NO _AND _DONT _ASK _FLAG ) ) {
// user doesn't want them
return false ;
}
2024-11-11 22:50:51 +01:00
2024-11-11 23:29:45 +01:00
return new Promise ( function ( resolve ) {
const buttons = [ loc . notifications . no _and _dont _ask , loc . notifications . ask _me _later , loc . _ . ok ] ;
const options = {
title : loc . settings . notifications ,
message : ` ${ loc . notifications . would _you _like _to _receive _notifications } \n ${ loc . settings . push _notifications _explanation } ` ,
options : buttons ,
cancelButtonIndex : 0 , // Assuming 'no and don't ask' is still treated as the cancel action
} ;
if ( anchor ) {
options . anchor = findNodeHandle ( anchor . current ) ;
}
ActionSheet . showActionSheetWithOptions ( options , buttonIndex => {
switch ( buttonIndex ) {
case 0 :
AsyncStorage . setItem ( NOTIFICATIONS _NO _AND _DONT _ASK _FLAG , '1' ) . then ( ( ) => resolve ( false ) ) ;
break ;
case 1 :
resolve ( false ) ;
break ;
case 2 :
configureNotifications ( ) . then ( resolve ) ;
break ;
2024-11-11 22:50:51 +01:00
}
2024-11-11 23:29:45 +01:00
} ) ;
} ) ;
} ;
/ * *
* Submits onchain bitcoin addresses and ln invoice preimage hashes to GroundControl server , so later we could
* be notified if they were paid
*
* @ param addresses { string [ ] }
* @ param hashes { string [ ] }
* @ param txids { string [ ] }
* @ returns { Promise < object > } Response object from API rest call
* /
export const majorTomToGroundControl = async ( addresses , hashes , txids ) => {
try {
if ( ! Array . isArray ( addresses ) || ! Array . isArray ( hashes ) || ! Array . isArray ( txids ) ) {
throw new Error ( 'No addresses, hashes, or txids provided' ) ;
2020-10-24 19:20:59 +02:00
}
2024-11-11 23:29:45 +01:00
const pushToken = await getPushToken ( ) ;
if ( ! pushToken || ! pushToken . token || ! pushToken . os ) {
return ;
2020-11-11 18:52:08 +01:00
}
2024-11-11 23:29:45 +01:00
const requestBody = JSON . stringify ( {
addresses ,
hashes ,
txids ,
token : pushToken . token ,
os : pushToken . os ,
2020-10-24 19:20:59 +02:00
} ) ;
2020-07-31 15:43:55 +02:00
2024-11-11 23:29:45 +01:00
let response ;
2024-10-24 04:09:22 +02:00
try {
2024-11-11 23:29:45 +01:00
response = await fetch ( ` ${ baseURI } /majorTomToGroundControl ` , {
method : 'POST' ,
headers : _getHeaders ( ) ,
body : requestBody ,
2024-10-24 04:09:22 +02:00
} ) ;
2024-11-11 23:29:45 +01:00
} catch ( networkError ) {
console . error ( 'Network request failed:' , networkError ) ;
throw networkError ;
}
2024-08-15 17:46:32 +02:00
2024-11-11 23:29:45 +01:00
if ( ! response . ok ) {
2024-11-12 03:20:29 +01:00
throw new Error ( ` Ground Control request failed with status ${ response . status } : ${ response . statusText } ` ) ;
2024-11-11 23:29:45 +01:00
}
2024-10-24 04:09:22 +02:00
2024-11-11 23:29:45 +01:00
const responseText = await response . text ( ) ;
if ( responseText ) {
try {
return JSON . parse ( responseText ) ;
} catch ( jsonError ) {
console . error ( 'Error parsing response JSON:' , jsonError ) ;
throw jsonError ;
2024-10-24 04:09:22 +02:00
}
2024-11-11 23:29:45 +01:00
} else {
return { } ; // Return an empty object if there is no response body
2024-10-24 04:09:22 +02:00
}
2024-11-11 23:29:45 +01:00
} catch ( error ) {
console . error ( 'Error in majorTomToGroundControl:' , error ) ;
2024-11-12 00:07:57 +01:00
throw error ;
2024-11-11 23:29:45 +01:00
}
} ;
2020-10-24 19:20:59 +02:00
2024-11-11 23:29:45 +01:00
/ * *
* Returns a permissions object :
* alert : boolean
* badge : boolean
* sound : boolean
*
* @ returns { Promise < Object > }
* /
export const checkPermissions = async ( ) => {
return new Promise ( function ( resolve ) {
PushNotification . checkPermissions ( result => {
resolve ( result ) ;
2020-10-24 19:20:59 +02:00
} ) ;
2024-11-11 23:29:45 +01:00
} ) ;
} ;
2020-10-24 19:20:59 +02:00
2024-11-11 23:29:45 +01:00
/ * *
* Posts to groundcontrol info whether we want to opt in or out of specific notifications level
*
* @ param levelAll { Boolean }
* @ returns { Promise < * > }
* /
export const setLevels = async levelAll => {
const pushToken = await getPushToken ( ) ;
if ( ! pushToken || ! pushToken . token || ! pushToken . os ) return ;
2020-10-24 19:20:59 +02:00
2024-11-11 23:29:45 +01:00
try {
const response = await fetch ( ` ${ baseURI } /setTokenConfiguration ` , {
method : 'POST' ,
headers : _getHeaders ( ) ,
body : JSON . stringify ( {
level _all : ! ! levelAll ,
token : pushToken . token ,
os : pushToken . os ,
} ) ,
} ) ;
if ( ! response . ok ) {
2024-11-12 00:08:21 +01:00
throw new Error ( 'Failed to set token configuration: ' + response . statusText ) ;
2024-11-11 22:50:51 +01:00
}
2024-11-11 23:51:09 +01:00
if ( ! levelAll ) {
console . debug ( 'Abandoning notifications Permissions...' ) ;
PushNotification . abandonPermissions ( ) ;
console . debug ( 'Abandoned notifications Permissions...' ) ;
}
2024-11-11 23:29:45 +01:00
} catch ( e ) {
console . error ( e ) ;
}
} ;
2020-10-24 19:20:59 +02:00
2024-11-11 23:29:45 +01:00
export const addNotification = async notification => {
let notifications = [ ] ;
try {
const stringified = await AsyncStorage . getItem ( NOTIFICATIONS _STORAGE ) ;
notifications = JSON . parse ( stringified ) ;
if ( ! Array . isArray ( notifications ) ) notifications = [ ] ;
} catch ( e ) {
console . error ( e ) ;
// Start fresh with just the new notification
notifications = [ ] ;
}
2021-01-01 20:15:40 +01:00
2024-11-11 23:29:45 +01:00
notifications . push ( notification ) ;
await AsyncStorage . setItem ( NOTIFICATIONS _STORAGE , JSON . stringify ( notifications ) ) ;
} ;
2021-01-01 20:15:40 +01:00
2024-11-11 23:29:45 +01:00
const postTokenConfig = async ( ) => {
const pushToken = await getPushToken ( ) ;
if ( ! pushToken || ! pushToken . token || ! pushToken . os ) return ;
2020-10-24 19:20:59 +02:00
2024-11-11 23:29:45 +01:00
try {
const lang = ( await AsyncStorage . getItem ( 'lang' ) ) || 'en' ;
const appVersion = getSystemName ( ) + ' ' + getSystemVersion ( ) + ';' + getApplicationName ( ) + ' ' + getVersion ( ) ;
2020-10-24 19:20:59 +02:00
2024-11-11 23:29:45 +01:00
await fetch ( ` ${ baseURI } /setTokenConfiguration ` , {
method : 'POST' ,
headers : _getHeaders ( ) ,
body : JSON . stringify ( {
token : pushToken . token ,
os : pushToken . os ,
lang ,
app _version : appVersion ,
} ) ,
} ) ;
} catch ( e ) {
console . error ( e ) ;
await AsyncStorage . setItem ( 'lang' , 'en' ) ;
throw e ;
}
} ;
2020-10-24 19:20:59 +02:00
2024-11-11 23:29:45 +01:00
const _setPushToken = async token => {
token = JSON . stringify ( token ) ;
return AsyncStorage . setItem ( PUSH _TOKEN , token ) ;
} ;
2020-10-24 19:20:59 +02:00
2024-11-11 23:29:45 +01:00
/ * *
* Calls ` configure ` , which tries to obtain push token , save it , and registers all associated with
* notifications callbacks
*
* @ returns { Promise < boolean > } TRUE if acquired token , FALSE if not
* /
export const configureNotifications = async onProcessNotifications => {
return new Promise ( function ( resolve ) {
requestNotifications ( [ 'alert' , 'sound' , 'badge' ] ) . then ( ( { status , _ } ) => {
if ( status === 'granted' ) {
PushNotification . configure ( {
// (optional) Called when Token is generated (iOS and Android)
onRegister : async token => {
console . debug ( 'TOKEN:' , token ) ;
alreadyConfigured = true ;
await _setPushToken ( token ) ;
resolve ( true ) ;
} ,
// (required) Called when a remote is received or opened, or local notification is opened
onNotification : async notification => {
// since we do not know whether we:
// 1) received notification while app is in background (and storage is not decrypted so wallets are not loaded)
// 2) opening this notification right now but storage is still unencrypted
// 3) any of the above but the storage is decrypted, and app wallets are loaded
//
// ...we save notification in internal notifications queue thats gona be processed later (on unsuspend with decrypted storage)
const payload = Object . assign ( { } , notification , notification . data ) ;
if ( notification . data && notification . data . data ) Object . assign ( payload , notification . data . data ) ;
delete payload . data ;
// ^^^ weird, but sometimes payload data is not in `data` but in root level
2024-11-12 02:42:38 +01:00
console . debug ( 'Received Push Notification Payload: ' , payload ) ;
2024-11-11 23:29:45 +01:00
await addNotification ( payload ) ;
2020-10-24 19:20:59 +02:00
2024-11-11 23:29:45 +01:00
// (required) Called when a remote is received or opened, or local notification is opened
notification . finish ( PushNotificationIOS . FetchResult . NoData ) ;
// if user is staring at the app when he receives the notification we process it instantly
// so app refetches related wallet
if ( payload . foreground && onProcessNotifications ) {
await onProcessNotifications ( ) ;
}
} ,
// (optional) Called when Registered Action is pressed and invokeApp is false, if true onNotification will be called (Android)
onAction : notification => {
console . debug ( 'ACTION:' , notification . action ) ;
console . debug ( 'NOTIFICATION:' , notification ) ;
// process the action
} ,
// (optional) Called when the user fails to register for remote notifications. Typically occurs when APNS is having issues, or the device is a simulator. (iOS)
onRegistrationError : function ( err ) {
console . error ( err . message , err ) ;
resolve ( false ) ;
} ,
// IOS ONLY (optional): default: all - Permissions to register.
permissions : {
alert : true ,
badge : true ,
sound : true ,
} ,
// Should the initial notification be popped automatically
// default: true
popInitialNotification : true ,
/ * *
* ( optional ) default : true
* - Specified if permissions ( ios ) and token ( android and ios ) will requested or not ,
* - if not , you must call PushNotificationsHandler . requestPermissions ( ) later
* - if you are not using remote notification or do not have Firebase installed , use this :
* requestPermissions : Platform . OS === 'ios'
* /
2024-11-12 02:55:38 +01:00
requestPermissions : true ,
2024-11-11 23:29:45 +01:00
} ) ;
}
} ) ;
} ) ;
// …
} ;
2020-10-24 19:20:59 +02:00
2024-11-11 22:45:21 +01:00
const _sleep = async ms => {
return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
} ;
2024-11-11 23:29:45 +01:00
/ * *
* Validates whether the provided GroundControl URI is valid by pinging it .
*
* @ param uri { string }
* @ returns { Promise < boolean > } TRUE if valid , FALSE otherwise
* /
export const isGroundControlUriValid = async uri => {
let response ;
try {
response = await Promise . race ( [ fetch ( ` ${ uri } /ping ` , { headers : _getHeaders ( ) } ) , _sleep ( 2000 ) ] ) ;
} catch ( _ ) { }
if ( ! response ) return false ;
const json = await response . json ( ) ;
return ! ! json . description ;
} ;
2024-09-18 04:00:58 +02:00
export const isNotificationsCapable = hasGmsSync ( ) || hasHmsSync ( ) || Platform . OS !== 'android' ;
2024-11-11 22:33:50 +01:00
export const getPushToken = async ( ) => {
try {
let token = await AsyncStorage . getItem ( PUSH _TOKEN ) ;
token = JSON . parse ( token ) ;
return token ;
} catch ( e ) {
console . error ( e ) ;
AsyncStorage . removeItem ( PUSH _TOKEN ) ;
throw e ;
}
} ;
2024-11-11 22:45:21 +01:00
/ * *
* Queries groundcontrol for token configuration , which contains subscriptions to notification levels
*
* @ returns { Promise < { } | * > }
* /
const getLevels = async ( ) => {
const pushToken = await getPushToken ( ) ;
if ( ! pushToken || ! pushToken . token || ! pushToken . os ) return ;
let response ;
try {
response = await Promise . race ( [
fetch ( ` ${ baseURI } /getTokenConfiguration ` , {
method : 'POST' ,
headers : _getHeaders ( ) ,
body : JSON . stringify ( {
token : pushToken . token ,
os : pushToken . os ,
} ) ,
} ) ,
_sleep ( 3000 ) ,
] ) ;
} catch ( _ ) { }
if ( ! response ) return { } ;
return await response . json ( ) ;
} ;
2024-11-11 19:11:59 +01:00
/ * *
* The opposite of ` majorTomToGroundControl ` call .
*
* @ param addresses { string [ ] }
* @ param hashes { string [ ] }
* @ param txids { string [ ] }
* @ returns { Promise < object > } Response object from API rest call
* /
export const unsubscribe = async ( addresses , hashes , txids ) => {
2024-11-11 22:33:50 +01:00
if ( ! Array . isArray ( addresses ) || ! Array . isArray ( hashes ) || ! Array . isArray ( txids ) ) {
2024-11-11 19:42:55 +01:00
throw new Error ( 'No addresses, hashes, or txids provided' ) ;
2024-11-11 22:33:50 +01:00
}
const token = await getPushToken ( ) ;
if ( ! token ? . token || ! token ? . os ) {
console . error ( 'No push token or OS found' ) ;
return ;
}
const body = JSON . stringify ( {
addresses ,
hashes ,
txids ,
token : token . token ,
os : token . os ,
} ) ;
2024-11-11 19:11:59 +01:00
2024-11-11 19:42:55 +01:00
try {
const response = await fetch ( ` ${ baseURI } /unsubscribe ` , {
method : 'POST' ,
headers : _getHeaders ( ) ,
2024-11-11 22:33:50 +01:00
body ,
2024-11-11 19:42:55 +01:00
} ) ;
if ( ! response . ok ) {
2024-11-11 22:33:50 +01:00
console . error ( 'Failed to unsubscribe:' , response . statusText ) ;
2024-11-11 19:42:55 +01:00
return ;
}
2024-11-11 22:33:50 +01:00
return response ;
2024-11-11 19:42:55 +01:00
} catch ( error ) {
2024-11-11 22:33:50 +01:00
console . error ( 'Error during unsubscribe:' , error ) ;
2024-11-11 19:42:55 +01:00
throw error ;
}
2024-11-11 19:11:59 +01:00
} ;
2024-11-11 22:45:21 +01:00
const _getHeaders = ( ) => {
2024-11-11 19:11:59 +01:00
return {
'Access-Control-Allow-Origin' : '*' ,
'Content-Type' : 'application/json' ,
} ;
2024-11-11 22:45:21 +01:00
} ;
export const clearStoredNotifications = async ( ) => {
try {
await AsyncStorage . setItem ( NOTIFICATIONS _STORAGE , JSON . stringify ( [ ] ) ) ;
} catch ( _ ) { }
} ;
export const getDeliveredNotifications = ( ) => {
return new Promise ( resolve => {
PushNotification . getDeliveredNotifications ( notifications => resolve ( notifications ) ) ;
} ) ;
} ;
export const removeDeliveredNotifications = ( identifiers = [ ] ) => {
PushNotification . removeDeliveredNotifications ( identifiers ) ;
} ;
export const setApplicationIconBadgeNumber = function ( badges ) {
PushNotification . setApplicationIconBadgeNumber ( badges ) ;
} ;
export const removeAllDeliveredNotifications = ( ) => {
PushNotification . removeAllDeliveredNotifications ( ) ;
} ;
2024-11-11 22:33:50 +01:00
export const getDefaultUri = ( ) => {
return groundControlUri ;
} ;
export const saveUri = async uri => {
baseURI = uri || groundControlUri ; // setting the url to use currently. if not set - use default
try {
await AsyncStorage . setItem ( GROUNDCONTROL _BASE _URI , baseURI ) ;
} catch ( storageError ) {
console . error ( 'Failed to reset URI:' , storageError ) ;
throw storageError ;
}
} ;
export const getSavedUri = async ( ) => {
try {
const baseUriStored = await AsyncStorage . getItem ( GROUNDCONTROL _BASE _URI ) ;
if ( baseUriStored ) {
baseURI = baseUriStored ;
}
return baseUriStored ;
} catch ( e ) {
console . error ( e ) ;
try {
await AsyncStorage . setItem ( GROUNDCONTROL _BASE _URI , groundControlUri ) ;
} catch ( storageError ) {
console . error ( 'Failed to reset URI:' , storageError ) ;
}
throw e ;
}
} ;
2024-11-11 22:45:21 +01:00
export const isNotificationsEnabled = async ( ) => {
const levels = await getLevels ( ) ;
return ! ! ( await getPushToken ( ) ) && ! ! levels . level _all ;
} ;
export const getStoredNotifications = async ( ) => {
let notifications = [ ] ;
try {
const stringified = await AsyncStorage . getItem ( NOTIFICATIONS _STORAGE ) ;
notifications = JSON . parse ( stringified ) ;
if ( ! Array . isArray ( notifications ) ) notifications = [ ] ;
} catch ( e ) {
if ( e instanceof SyntaxError ) {
console . error ( 'Invalid notifications format:' , e ) ;
notifications = [ ] ;
await AsyncStorage . setItem ( NOTIFICATIONS _STORAGE , '[]' ) ;
} else {
console . error ( 'Error accessing notifications:' , e ) ;
throw e ;
}
}
return notifications ;
} ;
2024-11-11 23:29:45 +01:00
// on app launch (load module):
export const initializeNotifications = async onProcessNotifications => {
try {
const baseUriStored = await AsyncStorage . getItem ( GROUNDCONTROL _BASE _URI ) ;
baseURI = baseUriStored || groundControlUri ;
} catch ( e ) {
console . error ( 'Failed to load custom URI, falling back to default' , e ) ;
baseURI = groundControlUri ;
await AsyncStorage . setItem ( GROUNDCONTROL _BASE _URI , groundControlUri ) . catch ( err => console . error ( 'Failed to reset URI:' , err ) ) ;
}
setApplicationIconBadgeNumber ( 0 ) ;
try {
2024-11-12 02:42:38 +01:00
currentPermissionStatus = await checkNotificationPermissionStatus ( ) ;
if ( currentPermissionStatus === 'granted' && ( await getPushToken ( ) ) ) {
2024-11-11 23:29:45 +01:00
await configureNotifications ( onProcessNotifications ) ;
await postTokenConfig ( ) ;
2024-11-12 02:42:38 +01:00
} else {
console . warn ( 'Notifications are disabled at the system level.' ) ;
2024-11-11 23:29:45 +01:00
}
} catch ( error ) {
console . error ( 'Failed to initialize notifications:' , error ) ;
}
} ;