REF: Notifications module

This commit is contained in:
Marcos Rodriguez Velez 2024-11-11 18:29:45 -04:00
parent e5694ec4c3
commit ada287e3c9
12 changed files with 369 additions and 380 deletions

View file

@ -16,358 +16,324 @@ const NOTIFICATIONS_NO_AND_DONT_ASK_FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK_FLAG';
let alreadyConfigured = false;
let baseURI = groundControlUri;
function Notifications(props) {
const _setPushToken = async token => {
token = JSON.stringify(token);
return AsyncStorage.setItem(PUSH_TOKEN, token);
};
export const cleanUserOptOutFlag = async () => {
return AsyncStorage.removeItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
};
/**
* 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 () => {
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);
},
/**
* 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;
// (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)
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;
}
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
console.debug('got push notification', payload);
if (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) {
// user doesn't want them
return false;
}
await Notifications.addNotification(payload);
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
};
// (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) props.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'
*/
requestPermissions: true,
});
}
});
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;
}
});
// …
};
});
};
Notifications.cleanUserOptOutFlag = async () => {
return AsyncStorage.removeItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
};
/**
* 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 anchor => {
if (!isNotificationsCapable) return false;
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;
/**
* 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');
}
if (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) {
// user doesn't want them
return false;
const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) {
return;
}
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;
}
});
const requestBody = JSON.stringify({
addresses,
hashes,
txids,
token: pushToken.token,
os: pushToken.os,
});
};
/**
* 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 (addresses, hashes, txids) => {
try {
if (!Array.isArray(addresses) || !Array.isArray(hashes) || !Array.isArray(txids)) {
throw new Error('No addresses, hashes, or txids provided');
}
const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) {
return;
}
const requestBody = JSON.stringify({
addresses,
hashes,
txids,
token: pushToken.token,
os: pushToken.os,
});
let response;
try {
response = await fetch(`${baseURI}/majorTomToGroundControl`, {
method: 'POST',
headers: _getHeaders(),
body: requestBody,
});
} catch (networkError) {
console.error('Network request failed:', networkError);
throw networkError;
}
if (!response.ok) {
return;
}
const responseText = await response.text();
if (responseText) {
try {
return JSON.parse(responseText);
} catch (jsonError) {
console.error('Error parsing response JSON:', jsonError);
throw jsonError;
}
} else {
return {}; // Return an empty object if there is no response body
}
} catch (error) {
console.error('Error in majorTomToGroundControl:', error);
}
};
/**
* Validates whether the provided GroundControl URI is valid by pinging it.
*
* @param uri {string}
* @returns {Promise<boolean>} TRUE if valid, FALSE otherwise
*/
Notifications.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;
};
/**
* Returns a permissions object:
* alert: boolean
* badge: boolean
* sound: boolean
*
* @returns {Promise<Object>}
*/
Notifications.checkPermissions = async () => {
return new Promise(function (resolve) {
PushNotification.checkPermissions(result => {
resolve(result);
response = await fetch(`${baseURI}/majorTomToGroundControl`, {
method: 'POST',
headers: _getHeaders(),
body: requestBody,
});
} catch (networkError) {
console.error('Network request failed:', networkError);
throw networkError;
}
if (!response.ok) {
return;
}
const responseText = await response.text();
if (responseText) {
try {
return JSON.parse(responseText);
} catch (jsonError) {
console.error('Error parsing response JSON:', jsonError);
throw jsonError;
}
} else {
return {}; // Return an empty object if there is no response body
}
} catch (error) {
console.error('Error in majorTomToGroundControl:', error);
}
};
/**
* 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);
});
};
});
};
/**
* Posts to groundcontrol info whether we want to opt in or out of specific notifications level
*
* @param levelAll {Boolean}
* @returns {Promise<*>}
*/
Notifications.setLevels = async levelAll => {
const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) return;
/**
* 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;
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) {
throw Error('Failed to set token configuration:', response.statusText);
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) {
throw Error('Failed to set token configuration:', response.statusText);
}
} catch (e) {
console.error(e);
}
};
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 = [];
}
notifications.push(notification);
await AsyncStorage.setItem(NOTIFICATIONS_STORAGE, JSON.stringify(notifications));
};
const postTokenConfig = async () => {
const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) return;
try {
const lang = (await AsyncStorage.getItem('lang')) || 'en';
const appVersion = getSystemName() + ' ' + getSystemVersion() + ';' + getApplicationName() + ' ' + getVersion();
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;
}
};
const _setPushToken = async token => {
token = JSON.stringify(token);
return AsyncStorage.setItem(PUSH_TOKEN, token);
};
/**
* 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
console.debug('got push notification', payload);
await addNotification(payload);
// (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'
*/
requestPermissions: true,
});
}
console.debug('Abandoning notifications Permissions...');
PushNotification.abandonPermissions();
console.debug('Abandoned notifications Permissions...');
} catch (e) {
console.error(e);
}
};
Notifications.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 = [];
}
notifications.push(notification);
await AsyncStorage.setItem(NOTIFICATIONS_STORAGE, JSON.stringify(notifications));
};
const postTokenConfig = async () => {
const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) return;
try {
const lang = (await AsyncStorage.getItem('lang')) || 'en';
const appVersion = getSystemName() + ' ' + getSystemVersion() + ';' + getApplicationName() + ' ' + getVersion();
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;
}
};
// 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 (e) {
console.error(e);
console.warn('Failed to load custom URI, falling back to default');
baseURI = groundControlUri;
// Attempt to reset in background
AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri).catch(err => console.error('Failed to reset URI:', err));
}
// every launch should clear badges:
setApplicationIconBadgeNumber(0);
try {
if (!(await getPushToken())) return;
await configureNotifications();
// 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 postTokenConfig();
} catch (error) {
console.error('Failed to initialize notifications:', error);
}
})();
return null;
}
});
});
// …
};
const _sleep = async ms => {
return new Promise(resolve => setTimeout(resolve, ms));
};
/**
* 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;
};
export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android';
export const getPushToken = async () => {
@ -544,4 +510,28 @@ export const getStoredNotifications = async () => {
return notifications;
};
export default Notifications;
// on app launch (load module):
export const initializeNotifications = async onProcessNotifications => {
// Fetch custom GroundControl URI
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));
}
// Set the application icon badge to 0
setApplicationIconBadgeNumber(0);
try {
if (await getPushToken()) {
await configureNotifications(onProcessNotifications);
await postTokenConfig();
}
} catch (error) {
console.error('Failed to initialize notifications:', error);
}
};

View file

@ -7,10 +7,11 @@ import A from '../blue_modules/analytics';
import { getClipboardContent } from '../blue_modules/clipboard';
import { updateExchangeRate } from '../blue_modules/currency';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
import Notifications, {
import {
clearStoredNotifications,
getDeliveredNotifications,
getStoredNotifications,
initializeNotifications,
removeAllDeliveredNotifications,
setApplicationIconBadgeNumber,
} from '../blue_modules/notifications';
@ -43,7 +44,6 @@ const CompanionDelegates = () => {
const clipboardContent = useRef<undefined | string>();
useWatchConnectivity();
useWidgetCommunication();
useMenuElements();
@ -109,6 +109,10 @@ const CompanionDelegates = () => {
return false;
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]);
useEffect(() => {
initializeNotifications(processPushNotifications);
}, [processPushNotifications]);
const handleOpenURL = useCallback(
async (event: { url: string }): Promise<void> => {
const { url } = event;
@ -247,13 +251,10 @@ const CompanionDelegates = () => {
}, [addListeners]);
return (
<>
<Notifications onProcessNotifications={processPushNotifications} />
<Suspense fallback={null}>
{isQuickActionsEnabled && <DeviceQuickActions />}
{isHandOffUseEnabled && <HandOffComponentListener />}
</Suspense>
</>
<Suspense fallback={null}>
{isQuickActionsEnabled && <DeviceQuickActions />}
{isHandOffUseEnabled && <HandOffComponentListener />}
</Suspense>
);
};

View file

@ -1,7 +1,6 @@
import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { InteractionManager } from 'react-native';
import A from '../../blue_modules/analytics';
import Notifications from '../../blue_modules/notifications';
import { BlueApp as BlueAppClass, LegacyWallet, TCounterpartyMetadata, TTXMetadata, WatchOnlyWallet } from '../../class';
import type { TWallet } from '../../class/wallets/types';
import presentAlert from '../../components/Alert';
@ -9,6 +8,7 @@ import loc from '../../loc';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { startAndDecrypt } from '../../blue_modules/start-and-decrypt';
import { majorTomToGroundControl } from '../../blue_modules/notifications';
const BlueApp = BlueAppClass.getInstance();
@ -228,8 +228,7 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
message: w.type === WatchOnlyWallet.type ? loc.wallets.import_success_watchonly : loc.wallets.import_success,
});
// @ts-ignore: Notifications type is not defined
Notifications.majorTomToGroundControl(w.getAllExternalAddresses(), [], []);
majorTomToGroundControl(w.getAllExternalAddresses(), [], []);
await w.fetchBalance();
},
[wallets, addWallet, saveToDisk],

View file

@ -8,13 +8,13 @@ import {
useReachability,
watchEvents,
} from 'react-native-watch-connectivity';
import Notifications from '../blue_modules/notifications';
import { MultisigHDWallet } from '../class';
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
import { Chain } from '../models/bitcoinUnits';
import { FiatUnit } from '../models/fiatUnit';
import { useSettings } from '../hooks/context/useSettings';
import { useStorage } from '../hooks/context/useStorage';
import { isNotificationsEnabled, majorTomToGroundControl } from '../blue_modules/notifications';
interface Message {
request?: string;
@ -154,11 +154,9 @@ export function useWatchConnectivity() {
try {
if ('addInvoice' in wallet) {
const invoiceRequest = await wallet.addInvoice(amount, description);
// @ts-ignore: Notifications type is not defined
if (await Notifications.isNotificationsEnabled()) {
if (await isNotificationsEnabled()) {
const decoded = await wallet.decodeInvoice(invoiceRequest);
// @ts-ignore: Notifications type is not defined
Notifications.majorTomToGroundControl([], [decoded.payment_hash], []);
majorTomToGroundControl([], [decoded.payment_hash], []);
return invoiceRequest;
}
return invoiceRequest;

View file

@ -17,7 +17,6 @@ import { Icon } from '@rneui/themed';
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
import { btcToSatoshi, fiatToBTC, satoshiToBTC } from '../../blue_modules/currency';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import Notifications from '../../blue_modules/notifications';
import { BlueLoading } from '../../BlueComponents';
import Lnurl from '../../class/lnurl';
import presentAlert from '../../components/Alert';
@ -31,6 +30,7 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import * as NavigationService from '../../NavigationService';
import { useStorage } from '../../hooks/context/useStorage';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import { majorTomToGroundControl, tryToObtainPermissions } from '../../blue_modules/notifications';
const LNDCreateInvoice = () => {
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
@ -192,8 +192,8 @@ const LNDCreateInvoice = () => {
// lets decode payreq and subscribe groundcontrol so we can receive push notification when our invoice is paid
/** @type LightningCustodianWallet */
const decoded = await wallet.current.decodeInvoice(invoiceRequest);
await Notifications.tryToObtainPermissions(createInvoiceRef);
Notifications.majorTomToGroundControl([], [decoded.payment_hash], []);
await tryToObtainPermissions(createInvoiceRef);
majorTomToGroundControl([], [decoded.payment_hash], []);
// send to lnurl-withdraw callback url if that exists
if (lnurlParams) {

View file

@ -17,7 +17,6 @@ import Share from 'react-native-share';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import { fiatToBTC, satoshiToBTC } from '../../blue_modules/currency';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import Notifications from '../../blue_modules/notifications';
import { BlueButtonLink, BlueCard, BlueLoading, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import AmountInput from '../../components/AmountInput';
@ -38,6 +37,7 @@ import SegmentedControl from '../../components/SegmentControl';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import { useSettings } from '../../hooks/context/useSettings';
import { majorTomToGroundControl, tryToObtainPermissions } from '../../blue_modules/notifications';
const segmentControlValues = [loc.wallets.details_address, loc.bip47.payment_code];
@ -110,8 +110,8 @@ const ReceiveDetails = () => {
if (address) {
setAddressBIP21Encoded(address);
try {
await Notifications.tryToObtainPermissions(receiveAddressButton);
Notifications.majorTomToGroundControl([address], [], []);
await tryToObtainPermissions(receiveAddressButton);
majorTomToGroundControl([address], [], []);
} catch (error) {
console.error('Error obtaining notifications permissions:', error);
}
@ -144,8 +144,8 @@ const ReceiveDetails = () => {
}
setAddressBIP21Encoded(newAddress);
try {
await Notifications.tryToObtainPermissions(receiveAddressButton);
Notifications.majorTomToGroundControl([newAddress], [], []);
await tryToObtainPermissions(receiveAddressButton);
majorTomToGroundControl([newAddress], [], []);
} catch (error) {
console.error('Error obtaining notifications permissions:', error);
}

View file

@ -5,7 +5,6 @@ import { ActivityIndicator, Keyboard, Linking, StyleSheet, TextInput, View } fro
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import Notifications from '../../blue_modules/notifications';
import {
BlueBigCheckmark,
BlueButtonLink,
@ -24,6 +23,7 @@ import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useSettings } from '../../hooks/context/useSettings';
import { majorTomToGroundControl } from '../../blue_modules/notifications';
const BROADCAST_RESULT = Object.freeze({
none: 'Input transaction hex',
@ -76,8 +76,7 @@ const Broadcast: React.FC = () => {
setBroadcastResult(BROADCAST_RESULT.success);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
// @ts-ignore: fix later
Notifications.majorTomToGroundControl([], [], [txid]);
majorTomToGroundControl([], [], [txid]);
} else {
setBroadcastResult(BROADCAST_RESULT.error);
}

View file

@ -7,7 +7,6 @@ import * as bitcoin from 'bitcoinjs-lib';
import { BlueText, BlueCard } from '../../BlueComponents';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc';
import Notifications from '../../blue_modules/notifications';
import { useRoute, RouteProp } from '@react-navigation/native';
import presentAlert from '../../components/Alert';
import { useTheme } from '../../components/themes';
@ -26,6 +25,7 @@ import { ContactList } from '../../class/contact-list';
import { useStorage } from '../../hooks/context/useStorage';
import { HDSegwitBech32Wallet } from '../../class';
import { useSettings } from '../../hooks/context/useSettings';
import { majorTomToGroundControl } from '../../blue_modules/notifications';
enum ActionType {
SET_LOADING = 'SET_LOADING',
@ -216,8 +216,7 @@ const Confirm: React.FC = () => {
const txid = bitcoin.Transaction.fromHex(tx).getId();
txidsToWatch.push(txid);
// @ts-ignore: Notifications has to be TSed
Notifications.majorTomToGroundControl([], [], txidsToWatch);
majorTomToGroundControl([], [], txidsToWatch);
let amount = 0;
for (const recipient of recipients) {
if (recipient.value) {

View file

@ -7,7 +7,6 @@ import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import Notifications from '../../blue_modules/notifications';
import { BlueCard, BlueSpacing20, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
@ -21,6 +20,7 @@ import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { useSettings } from '../../hooks/context/useSettings';
import { majorTomToGroundControl } from '../../blue_modules/notifications';
const PsbtWithHardwareWallet = () => {
const { txMetadata, fetchAndSaveWalletTransactions, wallets } = useStorage();
@ -136,7 +136,7 @@ const PsbtWithHardwareWallet = () => {
setIsLoading(false);
const txDecoded = bitcoin.Transaction.fromHex(txHex);
const txid = txDecoded.getId();
Notifications.majorTomToGroundControl([], [], [txid]);
majorTomToGroundControl([], [], [txid]);
if (memo) {
txMetadata[txid] = { memo };
}

View file

@ -2,13 +2,18 @@ import React, { useCallback, useEffect, useState } from 'react';
import { I18nManager, Linking, ScrollView, StyleSheet, TextInput, View, Pressable } from 'react-native';
import { Button as ButtonRNElements } from '@rneui/themed';
// @ts-ignore: no declaration file
import Notifications, {
import {
getDefaultUri,
getPushToken,
getSavedUri,
getStoredNotifications,
saveUri,
isNotificationsEnabled,
setLevels,
tryToObtainPermissions,
cleanUserOptOutFlag,
isGroundControlUriValid,
checkPermissions,
} from '../../blue_modules/notifications';
import { BlueCard, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
@ -19,6 +24,7 @@ import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { Divider } from '@rneui/base';
import { openSettings } from 'react-native-permissions';
import PushNotification from 'react-native-push-notification';
const NotificationSettings: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
@ -53,21 +59,20 @@ const NotificationSettings: React.FC = () => {
setNotificationsEnabledState(value);
if (value) {
// User is enabling notifications
// @ts-ignore: refactor later
await Notifications.cleanUserOptOutFlag();
await cleanUserOptOutFlag();
if (await getPushToken()) {
// we already have a token, so we just need to reenable ALL level on groundcontrol:
// @ts-ignore: refactor later
await Notifications.setLevels(true);
await setLevels(true);
} else {
// ok, we dont have a token. we need to try to obtain permissions, configure callbacks and save token locally:
// @ts-ignore: refactor later
await Notifications.tryToObtainPermissions();
await tryToObtainPermissions();
}
} else {
// User is disabling notifications
// @ts-ignore: refactor later
await Notifications.setLevels(false);
await setLevels(false);
console.debug('Abandoning notifications Permissions...');
PushNotification.abandonPermissions();
console.debug('Abandoned notifications Permissions...');
}
setNotificationsEnabledState(await isNotificationsEnabled());
@ -87,8 +92,7 @@ const NotificationSettings: React.FC = () => {
'token: ' +
JSON.stringify(await getPushToken()) +
' permissions: ' +
// @ts-ignore: refactor later
JSON.stringify(await Notifications.checkPermissions()) +
JSON.stringify(await checkPermissions()) +
' stored notifications: ' +
JSON.stringify(await getStoredNotifications()),
);
@ -106,8 +110,7 @@ const NotificationSettings: React.FC = () => {
try {
if (URI) {
// validating only if its not empty. empty means use default
// @ts-ignore: refactor later
if (await Notifications.isGroundControlUriValid(URI)) {
if (await isGroundControlUriValid(URI)) {
await saveUri(URI);
presentAlert({ message: loc.settings.saved });
} else {

View file

@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
import { Text } from '@rneui/themed';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import Notifications from '../../blue_modules/notifications';
import { BlueCard, BlueSpacing, BlueSpacing20, BlueText } from '../../BlueComponents';
import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class';
import presentAlert, { AlertType } from '../../components/Alert';
@ -16,6 +15,7 @@ import loc from '../../loc';
import { StorageContext } from '../../components/Context/StorageProvider';
import { popToTop } from '../../NavigationService';
import ReplaceFeeSuggestions from '../../components/ReplaceFeeSuggestions';
import { majorTomToGroundControl } from '../../blue_modules/notifications';
const styles = StyleSheet.create({
root: {
@ -97,7 +97,7 @@ export default class CPFP extends Component {
onSuccessBroadcast() {
this.context.txMetadata[this.state.newTxid] = { memo: 'Child pays for parent (CPFP)' };
Notifications.majorTomToGroundControl([], [], [this.state.newTxid]);
majorTomToGroundControl([], [], [this.state.newTxid]);
this.context.sleep(4000).then(() => this.context.fetchAndSaveWalletTransactions(this.state.wallet.getID()));
this.props.navigation.navigate('Success', { onDonePressed: () => popToTop(), amount: undefined });
}

View file

@ -1,20 +1,20 @@
import assert from 'assert';
import { isGroundControlUriValid } from '../../blue_modules/notifications';
import Notifications from '../../blue_modules/notifications';
// Notifications.default = new Notifications();
describe('notifications', () => {
// yeah, lets rely less on external services...
// eslint-disable-next-line jest/no-disabled-tests
it.skip('can check groundcontrol server uri validity', async () => {
assert.ok(await Notifications.isGroundControlUriValid('https://groundcontrol-bluewallet.herokuapp.com'));
assert.ok(!(await Notifications.isGroundControlUriValid('https://www.google.com')));
assert.ok(await isGroundControlUriValid('https://groundcontrol-bluewallet.herokuapp.com'));
assert.ok(!(await isGroundControlUriValid('https://www.google.com')));
await new Promise(resolve => setTimeout(resolve, 2000));
});
// muted because it causes jest to hang waiting indefinitely
// eslint-disable-next-line jest/no-disabled-tests
it.skip('can check non-responding url', async () => {
assert.ok(!(await Notifications.isGroundControlUriValid('https://localhost.com')));
assert.ok(!(await isGroundControlUriValid('https://localhost.com')));
});
});