When i use my cordova app in the home page, i need to display message to user to ask him if he really want to exit app or not.
Than i need to exit app if user select "yes".
To do it, i use this code :
document.addEventListener('backbutton', function() {
$rootScope.back();
$rootScope.$apply();
}, false);
$rootScope.back = function(execute) {
var path = $location.$$path;
if (path !== '/') {
$location.path(path.split('/').slice(0, -1).join('/'));
} else {
$rootScope.exitApp();
}
}.bind(this);
$rootScope.exitApp = function() {
$rootScope.$emit('showPopover', {
message: $rootScope.LABELS.QUIT_APP,
confirmCallback: function() {
if (typeof navigator.app !== 'undefined') {
navigator.app.exitApp();
}
},
cancelCallback: doNothing
});
};
It's working in android and iOS, but not in Windows 10 app.
In W10, navigator.app is undefined.
I read that i'm supposed to suspend app and not exit it, so i tried this windows quirkswritten in cordova doc (https://cordova.apache.org/docs/en/latest/cordova/events/events.html#backbutton) :
$rootScope.exitApp = function() {
$rootScope.$emit('showPopover', {
message: $rootScope.LABELS.QUIT_APP,
confirmCallback: function() {
if (typeof navigator.app !== 'undefined') {
navigator.app.exitApp();
}
else if (platformUtils.isWindows()) {
throw new Error('Exit'); // This will suspend the app
}
},
cancelCallback: doNothing
});
};
throw new Error('Exit') is called and display error in logs, but app is not suspended.
Maybe because i'm in a angular app?
Does any one has an idea?
You could also use this:
if (device.platform.toLowerCase() === "windows") {
//this closes the app and leaves a message in the windows system eventlog if error object is provided
MSApp.terminateApp();
} else {
navigator.app.exitApp()
}
Problem is due to $emit used to display popover.
If i throw error on popover callback, this error is not returned to backbutton event callback.
But, on windows platform, we are not supposed to display popover to ask if user is sure to want to leave app, so if I add it to "back" function, it's working.
Final code used to Windows 10 app is :
document.addEventListener('backbutton', function() {
$rootScope.back();
$rootScope.$apply();
}, false);
$rootScope.back = function(execute) {
var path = $location.$$path;
if (path !== '/') {
$location.path(path.split('/').slice(0, -1).join('/'));
} else {
if (window.device.platform.toLowerCase() === 'windows') {
throw new Error('Exit'); // This will suspend the app
} else {
$rootScope.exitApp();
}
}
}.bind(this);
Related
I am building an app in ReactJS that requires the user's location. In the browser (Chrome) on my PC I get a prompt once. If I decline, the prompt will not show again upon a page refresh. However, I will get the option to manually enable location.
When I open the same web page on my android phone in Chrome I neither get a prompt nor the option to enable location access manually. I have looked for solutions through Google but all of them rely on a prompt being automatically shown upon a navigator.geolocation.getCurrentPosition call, but it doesn't.
This is my code:
function getUserLocation() {
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(
function (position) {
setLocationDidMount(true);
console.log("got position");
},
function errorCallback(error) {
console.log(error);
navigator.permissions
.query({ name: "geolocation" })
.then(function (result) {
if (result.state === "granted") {
console.log(result.state);
//If granted then you can directly call your function here
} else if (result.state === "prompt") {
console.log(result.state);
} else if (result.state === "denied") {
console.log("location access denied by user");
//If denied then you have to show instructions to enable location
}
result.onchange = function () {
console.log(result.state);
};
});
},
{
timeout: 5000,
}
);
} else {
console.log("Not Available");
}
}
How to handle this problem? Btw, I'm not supposed to use React-Native.
As is pointed out by the article shared by Sergey, mobile browsers will only handle location properly when there is an SSL certificate in place.
While causing a warning, installing a local certificate using OpenSSL did fix the problem.
I have a SPA PWA React app.
It is installed and running in standalone mode on the mobile device (Android+Chrome).
Let's say the app lists people and then when you click on a person it diplays details using /person route.
Now, I'm sending push notifications from the server and receiving them in the service worker attached to the app. The notification is about a person and I want to open that person's details when the user clicks on the notification.
The question is:
how do I activate the /person route on my app from the service worker
and pass data (e.g. person id, or person object)
without reloading the app
From what I understand, from the service worker notificationclick event handler I can:
focus on the app (but how do I pass data and activate a route)
open an url (but /person is not a physical route, and either way - I want avoid refreshing the page)
You can listen for click event for the Notification which you show to the user. And in the handler, you can open the URL for the corresponding person which comes from your server with push event.
notification.onclick = function(event) {
event.preventDefault();
// suppose you have an url property in the data
if (event.notification.data.url) {
self.clients.openWindow(event.notification.data.url);
}
}
Check these links:
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/notificationclick_event
https://developer.mozilla.org/en-US/docs/Web/API/Clients/openWindow
To answer my own question: I've used IndexedDB (can't use localStorage as it is synchronous) to communicate between SW and PWA, though I'm not too happy about it.
This is roughly how my service worker code looks (I'm using idb library):
self.addEventListener('notificationclick', function(event) {
const notif = event.notification;
notif.close();
if (notif.data) {
let db;
let p = idb.openDB('my-store', 1, {
upgrade(db) {
db.createObjectStore(OBJSTORENAME, {
keyPath: 'id'
});
}
}).then(function(idb) {
db = idb;
return db.clear(OBJSTORENAME);
}).then(function(rv) {
return db.put(OBJSTORENAME, notif.data);
}).then(function(res) {
clients.openWindow('/');
}).catch(function(err) {
console.log("Error spawning notif", err);
});
event.waitUntil(p);
}
});
and then, in the root of my react app ie in my AppNavBar component I always check if there is something to show:
componentWillMount() {
let self = this;
let db;
idb.openDB('my-store', 1)
.then(function (idb) {
db = idb;
return db.getAll(OBJSTORENAME);
}).then(function (items) {
if (items && items.length) {
axios.get(`/some-additional-info-optional/${items[0].id}`).then(res => {
if (res.data && res.data.success) {
self.props.history.push({
pathname: '/details',
state: {
selectedObject: res.data.data[0]
}
});
}
});
db.clear(OBJSTORENAME)
.then()
.catch(err => {
console.log("error clearing ", OBJSTORENAME);
});
}
}).catch(function (err) {
console.log("Error", err);
});
}
Have been toying with clients.openWindow('/?id=123'); and clients.openWindow('/#123'); but that was behaving strangely, sometimes the app would stall, so I reverted to the IndexedDB approach.
(clients.postMessage could also be the way to go though I'm not sure how to plug that into the react framework)
HTH someone else, and I'm still looking for a better solution.
I had a similar need in my project. Using your's postMessage tip, I was able to get an event on my component every time a user clicks on service worker notification, and then route the user to the desired path.
service-worker.js
self.addEventListener("notificationclick", async event => {
const notification = event.notification;
notification.close();
event.waitUntil(
self.clients.matchAll({ type: "window" }).then(clientsArr => {
if (clientsArr[0]) {
clientsArr[0].focus();
clientsArr[0].postMessage({
type: "NOTIFICATION_CLICK",
ticketId: notification.tag,
});
}
})
);
});
On your react component, add a new listener:
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", message => {
if (message.data.type === "NOTIFICATION_CLICK") {
history.push(`/tickets/${message.data.ticketId}`);
}
});
}
}, [history]);
I am currently using ApplicationInsights-JS in my progressive web app. It works in my react components as I can import what I need from the relevant npm packages.
In my service worker however, I can only import logic using importScripts.
I did manage to find a CDN for ApplicationInsights-JS on their Github page however it seems that in order to initialise app insights using this library you need to have access to window in order to store the appinsights, which you cannot do from a service worker.
I tried to use the web snippet approach since the CDN seemed to be
related to that particular library, but I can't use window and am not sure how else to implement this solution.
This is a copy paste of the suggested snippet to init the app insights object from: https://github.com/Microsoft/ApplicationInsights-JS
importScripts('https://az416426.vo.msecnd.net/beta/ai.2.min.js');
const sdkInstance = 'appInsightsSDK';
window[sdkInstance] = 'appInsights';
const aiName = window[sdkInstance];
const aisdk =
window[aiName] ||
(function(e) {
function n(e) {
i[e] = function() {
const n = arguments;
i.queue.push(function() {
i[e](...n);
});
};
}
let i = { config: e };
i.initialize = !0;
const a = document;
const t = window;
setTimeout(function() {
const n = a.createElement('script');
(n.src = e.url || 'https://az416426.vo.msecnd.net/next/ai.2.min.js'),
a.getElementsByTagName('script')[0].parentNode.appendChild(n);
});
try {
i.cookie = a.cookie;
} catch (e) {}
(i.queue = []), (i.version = 2);
for (
const r = [
'Event',
'PageView',
'Exception',
'Trace',
'DependencyData',
'Metric',
'PageViewPerformance'
];
r.length;
)
n(`track${r.pop()}`);
n('startTrackPage'), n('stopTrackPage');
const o = `Track${r[0]}`;
if (
(n(`start${o}`),
n(`stop${o}`),
!(
!0 === e.disableExceptionTracking ||
(e.extensionConfig &&
e.extensionConfig.ApplicationInsightsAnalytics &&
!0 ===
e.extensionConfig.ApplicationInsightsAnalytics
.disableExceptionTracking)
))
) {
n(`_${(r = 'onerror')}`);
const s = t[r];
(t[r] = function(e, n, a, t, o) {
const c = s && s(e, n, a, t, o);
return (
!0 !== c &&
i[`_${r}`]({
message: e,
url: n,
lineNumber: a,
columnNumber: t,
error: o
}),
c
);
}),
(e.autoExceptionInstrumented = !0);
}
return i;
})({ instrumentationKey: 'xxx-xxx-xxx-xxx-xxx' });
(window[aiName] = aisdk),
aisdk.queue && aisdk.queue.length === 0 && aisdk.trackPageView({});
I get window is not defined which is expected, but I'm not sure how else I can make use of this library from the service worker.
Has anyone else had a similar implementation in which they successfully logged telemetry using ApplicationInsights from a service worker?
I realised that I was over complicating this.
Since I only needed to track a custom event, and didn't need all the automated page tracking etc that appInsights does, I ended up doing a fetch from my service worker.
I just copied the header and body format from the requests that I made using my react pages.
The below successfully logged telemetry to my app insights dashboard:
fetch(url, {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify([
{
time: '2019-05-02T15:56:37.589Z',
iKey: 'INSTRUMENTATION_KEY',
name:
'Microsoft.ApplicationInsights.INSTRUMENTATION_KEY.Event',
tags: {
'ai.user.id': 'l6Tey',
'ai.session.id': 'TL+Ry',
'ai.device.id': 'browser',
'ai.device.type': 'Browser',
'ai.operation.id': 'HUfNE',
SampleRate: '100',
// eslint-disable-next-line no-script-url
'ai.internal.sdkVersion': 'javascript:2.0.0-rc4'
},
data: {
baseType: 'EventData',
baseData: {
ver: 2,
name: 'Testing manual event',
properties: {},
measurements: {}
}
}
}
])
})
.then(json)
.then(function(data) {
})
.catch(function(error) {
});
I've almost managed to use Microsoft Application Insights in our app's service worker.
The key parts are:
Using the lightweight version of appInsights (see this small remark at 4th step) with importScripts('https://az416426.vo.msecnd.net/next/aib.2.min.js').
Initialize an appInsights object:
appInsights = new Microsoft.AppInsights.AppInsights({ instrumentationKey: "[replace with your own key]" });
when track needed (during onpush event or onnotificationclick), go for appInsight.track({ eventItemFields }) then appInsights.flush().
I've said "almost" because the flush part seems to not working, I've got: "Sender was not initialized" internal error after enabling debugging.
I will publish here a working sample code if I successfully manage this issue.
References:
https://github.com/Azure-Samples/applicationinsights-web-sample1/blob/master/testlightsku.html
This response to the question: How to add analytics for Push notifications.
Using the Web SDK in a service worker is troublesome. The full version depends on a window object, while the basic SDK depends on Beacon or XmlHttpRequest for sending the messages (in file https://github.com/microsoft/ApplicationInsights-JS/blob/master/channels/applicationinsights-channel-js/src/Sender.ts):
if (!_self._senderConfig.isBeaconApiDisabled() && Util.IsBeaconApiSupported()) {
_self._sender = _beaconSender;
} else {
if (typeof XMLHttpRequest !== undefined) {
const xhr:any = getGlobalInst("XMLHttpRequest");
if(xhr) {
const testXhr = new xhr();
if ("withCredentials" in testXhr) {
_self._sender = _xhrSender;
_self._XMLHttpRequestSupported = true;
} else if (typeof XDomainRequest !== undefined) {
_self._sender = _xdrSender; // IE 8 and 9
}
}
}
}
At the moment Application Insights SDK does not seem to support service workers. Rajars solution seems to be the best option for now.
Update: There is an issue in the Github Repo about this: https://github.com/microsoft/ApplicationInsights-JS/issues/1436
A suggestion that works is by using the basic/lightweight version of Application Insights (as mentioned by Rajar) and adding a XMLHttpRequest polyfill (that uses the fetch api) before inititializing Application Insights. After that you can use the lightweight version.
An example can be found here: https://github.com/Pkiri/pwa-ai
I was trying to use AppInsightsSDK in E2E tests environment (pupeteer) and when I tried to log event or metric I got with "Sender was not initialized" error.
As #Pkiri mentioned one would need XMLHttpRequest polyfill to solve the issue. Although my scenario is not directly related to Service worker I wanted to mention that #Pkiri answer is not entirely true, because one can also use globalThis, self, window or global to get the same result according to SDK source code function getGlobalInst("XMLHttpRequest"); resolves to
function getGlobal() {
if (typeof globalThis !== strShimUndefined && globalThis) {
return globalThis;
}
if (typeof self !== strShimUndefined && self) {
return self;
}
if (typeof window !== strShimUndefined && window) {
return window;
}
if (typeof global !== strShimUndefined && global) {
return global;
}
return null;
}
And for my scenario this was a valid solution
const appInsights = new ApplicationInsights({
config: {
instrumentationKey: 'AppInsights_InstrumentationKey',
},
});
global.XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
global.appInsights = appInsights.loadAppInsights();
I'm using wkhtmltoimage. It works fine for e.g. http://stackoverflow.com
But when I try to run in on any website with AngularJs (e.g. https://material.angularjs.org/latest/):
wkhtmltoimage.exe --no-stop-slow-scripts --javascript-delay 5000 --enable-javascript --debug-javascript https://material.angularjs.org/latest/ C:\\dev\\temp\\0c8fca2c-0990-40c4-8672-12fc71ec6cf6.png
A get an error JS error (thanks to : --debug-javascript flag)
Loading page (1/2)
Warning: undefined:0 Error: [$injector:modulerr] http://errors.angularjs.org/1.5.5/$injector/modulerr?p0=ng&p1='undefined'%20is%20not%20an%20object
Rendering (2/2)
The result image is just a tiny white stripe.
How to ren
It seems that wkhtmltopdf doesn't play nicely with Function.prototype.bind (used in Angular >= 1.5), so you need to provide a "polyfill" to make it works.
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof fNOP
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
if (this.prototype) {
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
I have to check if the user disabled the location services i do this like so:
cordova.plugins.diagnostic.isLocationEnabled(function(enabled) {
if (enabled === false) {
$state.go("errorTwo");
}
}, function(error) {
alert("The following error occurred: " + error);
});
}
But the problem is, that if the user starts the app the first time, the error already runs. The function should wait until the user gave premission to use the location services. How can I do this?
Any help much appreciated!
The documentation states for isLocationEnabled():
Returns true if the device setting for location is on. On Android this returns true if Location Mode is switched on. On iOS this returns true if Location Services is switched on.
i.e. this only checks if the device setting is enabled.
However, you can either use isLocationAvailable()
On iOS and Windows 10 Mobile this returns true if both the device setting is enabled AND the application is authorized to use location. When location is enabled, the locations returned are by a mixture GPS hardware, network triangulation and Wifi network IDs.
On Android, this returns true if Location mode is enabled and any mode is selected (e.g. Battery saving, Device only, High accuracy) AND if the app is authorised to use location. When location is enabled, the locations returned are dependent on the location mode
This checks "if both the device setting is enabled AND the application is authorized to use location":
cordova.plugins.diagnostic.isLocationAvailable(
function(enabled) {
if (enabled === false) {
$state.go("errorTwo");
},
}, function(error) {
alert("The following error occurred: " + error);
}
});
Or, you can call isLocationAuthorized() to check if the app is authorized to use location, before checking the device setting:
cordova.plugins.diagnostic.isLocationAuthorized(function(authorized){
console.log("Location is " + (authorized ? "authorized" : "unauthorized"));
if(authorized){
cordova.plugins.diagnostic.isLocationEnabled(
function(enabled) {
if (enabled === false) {
$state.go("errorTwo");
},
}, function(error) {
alert("The following error occurred: " + error);
}
});
}
}, function(error){
console.error("The following error occurred: "+error);
});
To "wait" until the app has permission to use location before checking, you must take control of the request for location authorization:
function checkLocationSetting(){
cordova.plugins.diagnostic.isLocationEnabled(
function(enabled) {
if (enabled === false) {
$state.go("errorTwo");
},
}, function(error) {
alert("The following error occurred: " + error);
}
});
}
cordova.plugins.diagnostic.isLocationAuthorized(function(authorized){
console.log("Location is " + (authorized ? "authorized" : "unauthorized"));
if(authorized){
checkLocationSetting();
}else{
cordova.plugins.diagnostic.requestLocationAuthorization(function(status){
if(status == cordova.plugins.diagnostic.permissionStatus.GRANTED){
checkLocationSetting();
}else{
console.warn("Permission denied to use location");
}
}, function(error){
console.error(error);
});
}
}, function(error){
console.error("The following error occurred: "+error);
});
You must determine if this is th initial run of your application. To do this you can add this code and adjust to your needs.
//service
.factory('Application', function ($window) {
return {
setInitialRun = function (initial) {
$window.localStorage["initialRun"] = (initial ? "true" : "false");
},
isInitialRun = function () {
var value = $window.localStorage["initialRun"] || "true";
return value == "true";
}
};
});
in your app.js
.run(function ($ionicPlatform, $state, Application) {
var state = "mainPage"; // whatever, the main page of your app
if (Application.isInitialRun()) {
Application.setInitialRun(false);
state = "intro";
}
$state.go(state);
});