mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 15:04:50 +01:00
REF: Notifications module
This commit is contained in:
parent
e5694ec4c3
commit
ada287e3c9
12 changed files with 369 additions and 380 deletions
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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')));
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue