2020-07-18 21:33:43 +02:00
import PushNotificationIOS from '@react-native-community/push-notification-ios' ;
2020-07-20 15:42:22 +02:00
import { Alert , Platform } from 'react-native' ;
2020-07-18 21:33:43 +02:00
import Frisbee from 'frisbee' ;
2020-08-25 18:19:47 +02:00
import { getApplicationName , getVersion , getSystemName , getSystemVersion } from 'react-native-device-info' ;
2020-07-18 21:33:43 +02:00
import AsyncStorage from '@react-native-community/async-storage' ;
2020-07-23 16:20:40 +02:00
import loc from '../loc' ;
2020-07-18 21:33:43 +02:00
const PushNotification = require ( 'react-native-push-notification' ) ;
const constants = require ( './constants' ) ;
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-07-18 21:33:43 +02:00
let alreadyConfigured = false ;
2020-07-31 15:43:55 +02:00
let baseURI = constants . groundControlUri ;
2020-07-18 21:33:43 +02:00
2020-10-24 19:20:59 +02:00
function Notifications ( props ) {
async function _setPushToken ( token ) {
token = JSON . stringify ( token ) ;
return AsyncStorage . setItem ( PUSH _TOKEN , token ) ;
}
2020-07-18 21:33:43 +02:00
2020-10-24 19:20:59 +02:00
Notifications . getPushToken = async ( ) => {
try {
let token = await AsyncStorage . getItem ( PUSH _TOKEN ) ;
token = JSON . parse ( token ) ;
return token ;
} catch ( _ ) { }
return false ;
} ;
2020-07-18 21:33:43 +02:00
2020-10-24 19:20:59 +02: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
* /
const configureNotifications = async function ( ) {
return new Promise ( function ( resolve ) {
PushNotification . configure ( {
// (optional) Called when Token is generated (iOS and Android)
onRegister : async function ( token ) {
console . log ( 'TOKEN:' , token ) ;
alreadyConfigured = true ;
await _setPushToken ( token ) ;
resolve ( true ) ;
} ,
2020-07-18 21:33:43 +02:00
// (required) Called when a remote is received or opened, or local notification is opened
2020-10-24 19:20:59 +02:00
onNotification : async function ( 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)
if ( Platform . OS === 'ios' && notification . foreground === true && notification . userInteraction === false ) {
// iOS hack
// @see https://github.com/zo0r/react-native-push-notification/issues/1585
notification . userInteraction = true ;
// also, on iOS app is not suspending/unsuspending when user taps a notification bubble,so we simulate it
// since its where we actually handle notifications:
setTimeout ( ( ) => props . onProcessNotifications ( ) , 500 ) ;
}
let notifications = [ ] ;
try {
const stringified = await AsyncStorage . getItem ( NOTIFICATIONS _STORAGE ) ;
notifications = JSON . parse ( stringified ) ;
if ( ! Array . isArray ( notifications ) ) notifications = [ ] ;
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
notifications . push ( payload ) ;
await AsyncStorage . setItem ( NOTIFICATIONS _STORAGE , JSON . stringify ( notifications ) ) ;
} catch ( _ ) { }
// (required) Called when a remote is received or opened, or local notification is opened
notification . finish ( PushNotificationIOS . FetchResult . NoData ) ;
} ,
2020-07-18 21:33:43 +02:00
2020-10-24 19:20:59 +02:00
// (optional) Called when Registered Action is pressed and invokeApp is false, if true onNotification will be called (Android)
onAction : function ( notification ) {
console . log ( 'ACTION:' , notification . action ) ;
console . log ( 'NOTIFICATION:' , notification ) ;
2020-07-18 21:33:43 +02:00
2020-10-24 19:20:59 +02:00
// process the action
} ,
2020-07-18 21:33:43 +02:00
2020-10-24 19:20:59 +02:00
// (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 ) ;
} ,
2020-07-18 21:33:43 +02:00
2020-10-24 19:20:59 +02:00
// IOS ONLY (optional): default: all - Permissions to register.
permissions : {
alert : true ,
badge : true ,
sound : true ,
} ,
2020-07-18 21:33:43 +02:00
2020-10-24 19:20:59 +02:00
// 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'
* /
requestPermissions : true ,
} ) ;
2020-07-18 21:33:43 +02:00
} ) ;
2020-10-24 19:20:59 +02:00
} ;
2020-07-18 21:33:43 +02:00
2020-10-24 19:20:59 +02: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
* /
Notifications . tryToObtainPermissions = async function ( ) {
if ( await Notifications . 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 ;
}
return new Promise ( function ( resolve ) {
Alert . alert (
loc . settings . notifications ,
'Would you like to receive notifications when you get incoming payments?' ,
[
{
text : 'Ask Me Later' ,
onPress : ( ) => {
resolve ( false ) ;
} ,
style : 'cancel' ,
2020-07-18 21:33:43 +02:00
} ,
2020-10-24 19:20:59 +02:00
{
text : loc . _ . ok ,
onPress : async ( ) => {
resolve ( await configureNotifications ( ) ) ;
} ,
style : 'default' ,
2020-07-18 21:33:43 +02:00
} ,
2020-10-24 19:20:59 +02:00
] ,
{ cancelable : false } ,
) ;
} ) ;
2020-07-18 21:33:43 +02:00
} ;
2020-07-31 15:43:55 +02:00
2020-10-24 19:20:59 +02:00
function _getHeaders ( ) {
return {
headers : {
'Access-Control-Allow-Origin' : '*' ,
'Content-Type' : 'application/json' ,
2020-07-30 14:22:06 +02:00
} ,
2020-10-24 19:20:59 +02:00
} ;
}
async function _sleep ( ms ) {
return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
}
/ * *
* 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
* /
Notifications . majorTomToGroundControl = async function ( addresses , hashes , txids ) {
if ( ! Array . isArray ( addresses ) || ! Array . isArray ( hashes ) || ! Array . isArray ( txids ) )
throw new Error ( 'no addresses or hashes or txids provided' ) ;
const pushToken = await Notifications . getPushToken ( ) ;
if ( ! pushToken || ! pushToken . token || ! pushToken . os ) return ;
const api = new Frisbee ( { baseURI } ) ;
return await api . post (
'/majorTomToGroundControl' ,
2020-08-10 16:17:50 +02:00
Object . assign ( { } , _getHeaders ( ) , {
body : {
2020-10-24 19:20:59 +02:00
addresses ,
hashes ,
txids ,
2020-08-10 16:17:50 +02:00
token : pushToken . token ,
os : pushToken . os ,
} ,
} ) ,
) ;
2020-10-24 19:20:59 +02:00
} ;
/ * *
* The opposite of ` majorTomToGroundControl ` call .
*
* @ param addresses { string [ ] }
* @ param hashes { string [ ] }
* @ param txids { string [ ] }
* @ returns { Promise < object > } Response object from API rest call
* /
Notifications . unsubscribe = async function ( addresses , hashes , txids ) {
if ( ! Array . isArray ( addresses ) || ! Array . isArray ( hashes ) || ! Array . isArray ( txids ) )
throw new Error ( 'no addresses or hashes or txids provided' ) ;
const pushToken = await Notifications . getPushToken ( ) ;
if ( ! pushToken || ! pushToken . token || ! pushToken . os ) return ;
const api = new Frisbee ( { baseURI } ) ;
return await api . post (
'/unsubscribe' ,
2020-08-10 16:17:50 +02:00
Object . assign ( { } , _getHeaders ( ) , {
body : {
2020-10-24 19:20:59 +02:00
addresses ,
hashes ,
txids ,
2020-08-10 16:17:50 +02:00
token : pushToken . token ,
os : pushToken . os ,
} ,
} ) ,
) ;
2020-10-24 19:20:59 +02:00
} ;
Notifications . isNotificationsEnabled = async function ( ) {
const levels = await getLevels ( ) ;
return ! ! ( await Notifications . getPushToken ( ) ) && ! ! levels . level _all ;
} ;
Notifications . getDefaultUri = function ( ) {
return constants . groundControlUri ;
} ;
Notifications . saveUri = async function ( uri ) {
baseURI = uri || constants . groundControlUri ; // settign the url to use currently. if not set - use default
return AsyncStorage . setItem ( GROUNDCONTROL _BASE _URI , uri ) ;
} ;
Notifications . getSavedUri = async function ( ) {
return AsyncStorage . getItem ( GROUNDCONTROL _BASE _URI ) ;
} ;
Notifications . isGroundControlUriValid = async uri => {
const apiCall = new Frisbee ( {
baseURI : uri ,
} ) ;
let response ;
try {
response = await Promise . race ( [ apiCall . get ( '/ping' , _getHeaders ( ) ) , _sleep ( 2000 ) ] ) ;
} catch ( _ ) { }
if ( ! response || ! response . body ) return false ; // either sleep expired or apiCall threw an exception
const json = response . body ;
if ( json . description ) return true ;
return false ;
} ;
/ * *
* Returns a permissions object :
* alert : boolean
* badge : boolean
* sound : boolean
*
* @ returns { Promise < Object > }
* /
Notifications . checkPermissions = async function ( ) {
return new Promise ( function ( resolve ) {
PushNotification . checkPermissions ( result => {
resolve ( result ) ;
} ) ;
} ) ;
} ;
/ * *
* Posts to groundcontrol info whether we want to opt in or out of specific notifications level
*
* @ param levelAll { Boolean }
* @ returns { Promise < * > }
* /
Notifications . setLevels = async function ( levelAll ) {
const pushToken = await Notifications . getPushToken ( ) ;
if ( ! pushToken || ! pushToken . token || ! pushToken . os ) return ;
const api = new Frisbee ( { baseURI } ) ;
try {
await api . post (
'/setTokenConfiguration' ,
Object . assign ( { } , _getHeaders ( ) , {
body : {
level _all : ! ! levelAll ,
token : pushToken . token ,
os : pushToken . os ,
} ,
} ) ,
) ;
} catch ( _ ) { }
} ;
/ * *
* Queries groundcontrol for token configuration , which contains subscriptions to notification levels
*
* @ returns { Promise < { } | * > }
* /
const getLevels = async function ( ) {
const pushToken = await Notifications . getPushToken ( ) ;
if ( ! pushToken || ! pushToken . token || ! pushToken . os ) return ;
const api = new Frisbee ( { baseURI } ) ;
let response ;
try {
response = await Promise . race ( [
api . post ( '/getTokenConfiguration' , Object . assign ( { } , _getHeaders ( ) , { body : { token : pushToken . token , os : pushToken . os } } ) ) ,
_sleep ( 3000 ) ,
] ) ;
} catch ( _ ) { }
if ( ! response || ! response . body ) return { } ; // either sleep expired or apiCall threw an exception
return response . body ;
} ;
Notifications . getStoredNotifications = async function ( ) {
let notifications = [ ] ;
try {
const stringified = await AsyncStorage . getItem ( NOTIFICATIONS _STORAGE ) ;
notifications = JSON . parse ( stringified ) ;
if ( ! Array . isArray ( notifications ) ) notifications = [ ] ;
} catch ( _ ) { }
return notifications ;
} ;
const postTokenConfig = async function ( ) {
const pushToken = await Notifications . getPushToken ( ) ;
if ( ! pushToken || ! pushToken . token || ! pushToken . os ) return ;
const api = new Frisbee ( { baseURI } ) ;
try {
const lang = ( await AsyncStorage . getItem ( 'lang' ) ) || 'en' ;
const appVersion = getSystemName ( ) + ' ' + getSystemVersion ( ) + ';' + getApplicationName ( ) + ' ' + getVersion ( ) ;
await api . post (
'/setTokenConfiguration' ,
Object . assign ( { } , _getHeaders ( ) , {
body : {
token : pushToken . token ,
os : pushToken . os ,
lang ,
app _version : appVersion ,
} ,
} ) ,
) ;
} catch ( _ ) { }
} ;
Notifications . clearStoredNotifications = async function ( ) {
try {
await AsyncStorage . setItem ( NOTIFICATIONS _STORAGE , JSON . stringify ( [ ] ) ) ;
} catch ( _ ) { }
} ;
Notifications . setApplicationIconBadgeNumber = function ( badges ) {
PushNotification . setApplicationIconBadgeNumber ( badges ) ;
} ;
// on app launch (load module):
( async ( ) => {
// first, fetching to see if app uses custom GroundControl server, not the default one
try {
const baseUriStored = await AsyncStorage . getItem ( GROUNDCONTROL _BASE _URI ) ;
if ( baseUriStored ) {
baseURI = baseUriStored ;
}
} catch ( _ ) { }
// every launch should clear badges:
Notifications . setApplicationIconBadgeNumber ( 0 ) ;
if ( ! ( await Notifications . getPushToken ( ) ) ) return ;
// if we previously had token that means we already acquired permission from the user and it is safe to call
// `configure` to register callbacks etc
await configureNotifications ( ) ;
await postTokenConfig ( ) ;
} ) ( ) ;
return null ;
}
export default Notifications ;