When showing my web push notifications, I'm adding an action in my options so user can ask to get notified again in 30 minutes.
const options = {
...,
actions: [
{
action: 'remind-action',
title: 'Remind again in 30',
icon: '/remind.png'
}
],
....
}
and then I handle the action in service worker:
self.addEventListener('notificationclick', function(event) {
if (!event.action) return
switch (event.action) {
...
case 'remind-action':
event.notification.close()
event.waitUntil(
new Promise((resolve, reject) => {
setTimeout(() => {
const options = {
...
actions: [
{
action: 'remind-action',
title: 'Remind again in 30',
icon: '/remind.png'
}
],
...
}
self.registration.showNotification('Reminder', options)
resolve(true)
}, 30 * 60 * 1000)
})
)
break
}
})
If only there was a way to re-emit the push event.
The issue with the above code is that it won't show the notification after 30 minutes, however if I set the timeout to 1 minute it works just fine.
I think some where Chrome is ignoring event.waitUntill and killing the setTimeout.
What is causing this and how can I resolve this?
The browser will automatically and on purpose throttle your setTimeouts to prevent abuse and eg. tracking of the user. Put it another way, you cannot implement what you want solely on the client side.
To show the notification again (reliably), you need to send it again from the server. You would ofc inform the server that the user wants to be reminded in x minutes or so.
Related
recently I started learning about service workers, background syncs... I implemented service worker and in install step I cached some files I want to show when offline.
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE)
.then((cache) => {
return cache.addAll([navigationIcon, offlineImage, offlineFallbackPage]);
})
);
});
I am listening to fetch event to catch when there is no internet connection so I can show offline page when then.
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate' || (event.request.method === 'GET'
&& event.request.headers.get('accept')
.includes('text/html'))) {
event.respondWith(
fetch(event.request.url)
.catch(() => {
// Return the offline page
return caches.match(offlineFallbackPage);
})
);
} else {
event.respondWith(caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
}));
}
});
I also added background sync, so I can go back online when there is internet connection.
After registering service worker I added:
.then(swRegistration => swRegistration.sync.register('backOnline'))
And I listen to sync event in my service worker.
When I'm offline and go back online nothing happens. BUT when I delete my fetch event (don't show previously cached offline page) then page goes back online by itself (which I want to do when I have fetch event)
Does anyone know what should I add so my page can go back online by itself?
You can use navigator, Include it in your main js file that is cached or in your service-worker js file, just ensure it's cached
let onlineStatus = locate => {
if(navigator.onLine){
location.replace(locate)
}
}
let isOfflinePage = window.location.pathname == offlineFallbackPage ? true : false;
// kindly edit isOfflinePage to return true if it's offline page
if(isOfflinePage){
onlineStatus('index.html')
}
You can simply use location.reload() instead
I am using service worker to achieve background sync functionality. Following is my code:
importScripts( 'https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js' )
const queue = new workbox.backgroundSync.Queue('registerQueue', { callbacks: {
queueDidReplay: function(requestArray) {
let requestSynced = 0
requestArray.forEach(item => {
if (!item.error) {
requestSynced++
}
})
if (requestSynced > 0) {
new BroadcastChannel('backgroundSynBroadCastChannel').postMessage(
requestSynced
)
}
} } }) const GraphQLMatch = /graphql(\S+)?/
self.addEventListener('fetch', event => { if (
null !== event.request.url.match(GraphQLMatch) &&
navigator.onLine === false ) {
const promiseChain = fetch(event.request.clone()).catch(err => {
return queue.addRequest(event.request)
})
event.waitUntil(promiseChain) } })
self.addEventListener('message', event => { if (!event.data) {
return }
switch (event.data) {
case 'skipWaiting':
self.skipWaiting()
break
default:
break } })
workbox.precaching.precacheAndRoute([])
/* * Alternate for navigateFallback & navigateFallbackBlacklist */ workbox.routing.registerNavigationRoute('/index.html', { blacklist: [/^\/__.*$/] })
On internet disconnect, the requests are queued on the indexed DB. But the problem is After acquiring the connection back, the background sync is made at least 5-10 mins later. Is there any way to do the background sync immediately upon internet re-connection or at least reduce the time for syncing.
Thanks in advance.
You could manually trigger a replay of a queue as soon as your connection is back by sending an event to the service worker.
In your service worker:
self.addEventListener('message', (event) => {
if (event.data.type === 'replayQueue') {
queue.replayRequests();
}
});
In your app (using workbox-window):
if ('serviceWorker' in navigator) this.wb = new Workbox('/service-worker.js');
window.addEventListener(‘online’, function(event){
this.wb.messageSW({type: 'replayQueue'});
});
Unfortunately, doesn't look like it's possible right now to change the timing for the sync. According to Google's Workbox documentation:
Browsers that support the BackgroundSync API will automatically replay
failed requests on your behalf at an interval managed by the browser,
likely using exponential backoff between replay attempts.
If Google's documentation is correct (at least for Chrome) that also means that the longer the user has been offline, the probability of a longer wait for the sync event increases.
#cyril-hanquez's idea is good as long as the user is still utilizing your site when they come back online. You might also want to add a "fetchDidFail" callback to handle more network outage edge cases. Along those lines: you might want to avoid relying on the status of "navigator.onLine", since it doesn't always do what one would expect.
in iOS, in order to allow push notifications, the user must do 1 of the following 2 options.
answer the original prompt which asks permission for notifications. If they don't do this, you can't bring up the permission request again, so you must redirect them to the settings page.
Go to the settings page and manually change the permissions.
Here's my flow now:
Check permissions: if they have them, move to the next screen.
if they don't, show an alert stating why notifications are important for our app. If they click ok, it shows the real notification screen, if not it just waits to ask later.
If the user already saw the request for notifications screen, I want to show a dialog that asks them to go the settings to allow notifications.
Is there a way of knowing whether the user said yes or no to the original permission box? Is there a way to know when they've answered it?
The way I'm checking now doesn't wait for the user to click on an option in the original dialogue box, so it doesn't wait to check permissions and find s them the exact same as they were before.
is there any way to check whether they've had the request for permissions already and whether they said yes or no?
Here's the code I'm using:
(only relevant imports)
import {PushNotificationIOS} from 'react-native
import PushNotification 'react-native-push-notification'
const requestPermissionsIOS = navigateForward => {
PushNotification.requestPermissions().then( ({alert}) => {
if (!alert) {
settingsAlertIOS(navigateForward)
} else {
configurePushNotification()
scheduleNotifications()
navigateForward()
}
}).catch( err => {
settingsAlertIOS(navigateForward)
})
}
const configurePushNotification = () => {
PushNotification.configure({
onRegister: (token) => {
},
onNotification: (notification) => {
onNotification: notification => handleNotification(notification)
},
permissions: {
alert: true,
badge: true,
sound: true
},
popInitialNotification: true,
requestPermissions: false,
});
}
const settingsAlertIOS = (navigateForward) => {
Alert.alert(
'Go to settings page?',
'In order to receive important notifications, please enable them in the settings page \(leaves app\)',
[
{text: 'Cancel', onPress: () => {Mixpanel.track('Decline change notifications from settings alert'); scheduleNotifications(); navigateForward()}},
{text: 'Settings', onPress: () => {configurePushNotification(); scheduleNotifications(); navigateForward(); Linking.openURL('app-settings:')}},
]
)
}
To listen to the user's decision.
PushNotification.requestPermissions().then((response: any) => {
if (response && response.alert !== 0) {
// Allow
return;
}
// Decline
});
Credit - jose920405
I've created an html file with embedded Watson Virtual Agent chat bot, code similar below, with WVA strictly using the building core capabilities:
IBMChat.init({
el: 'ibm_chat_root',
baseURL: 'https://api.ibm.com/virtualagent/run/api/v1',
botID: '',
XIBMClientID: '',
XIBMClientSecret: ''
});
What I noticed is if I run the WVA in Preview mode, and have input "pay bill", the WVA can come back with two piece response, with first:
Accessing your account information...
and second the make payment:
Your account balance is $42.01 due on 5/17/2017. What would you like to do? (More options coming soon!)
However, if I enter the same in my HTML chatbot, the response only comes back with the first part:
Accessing your account information...
and second part never comes out.
Does anyone else experience the same problem?
The version in the "Preview" mode has some mock "action" handlers setup. Obviously, not every one of you users would owe $42! In the sample code on the github, the mock action handlers are not setup. There are examples on how to subscribe to those action events with handlers here: https://github.com/watson-virtual-agents/chat-widget/tree/master/examples/basic-actions-example
As of 5/31/17 you can cover all the built in actions using the code snippet below...
const config = { instance: null };
const getUserProfileVariablesMap = {
'bill_amount': '42.01',
'payment_due_date': (() => {
const currentDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
return `${currentDate.getMonth() + 1}/${currentDate.getDate()}/${currentDate.getFullYear()}`;
})(),
'authorized_users': 'Bob Everyman and Jane Doe'
};
const getUserProfileVariables = (data) => {
const variables = data.message.action.args.variables;
variables.forEach(v => {
const value = getUserProfileVariablesMap[v];
(value) ? config.instance.profile.set(v, value) : config.instance.profile.set(v, '[sample data]');
});
config.instance.sendSilently('success');
};
const success = () => config.instance.sendSilently('success');
const agent = () => config.instance.receive('On your own site you would run code to connect to an agent now.');
const accountSettings = () => config.instance.receive('On your own site you would run code to open the Account Settings page now.');
function registerActions(instance) {
config.instance = instance;
instance.subscribe('action:getUserProfileVariables', getUserProfileVariables);
instance.subscribe('action:updateAddress', success);
instance.subscribe('action:updateUserName', success);
instance.subscribe('action:updatePhoneNumber', success);
instance.subscribe('action:updateEmail', success);
instance.subscribe('action:payBill', success);
instance.subscribe('action:sendPaymentReceipt', success);
instance.subscribe('action:agent', agent);
instance.subscribe('action:openAccountSettingsPage', accountSettings);
};
window.IBMChatActions = {
registerActions: registerActions
};
// window.IBMChatActions.registerActions(window.IBMChat);
On the Administrative Preview, you are getting fake code stubs that handle action requests from the agent.
When one of these actions are invoked, the widget will print the "Processing..." message and then invoke all registered subscribers for that action. It is up to these registered subscribers to continue the conversation flow by silently sending "success", "failure", or "cancel" back to the server.
For example, the agent might pass down the "payBill" action. You would want to call your payment gateway, determine if it was successful, and then notify the agent of the result:
IBMChat.init(/* Settings */);
IBMChat.subscribe('action:payBill', function() {
var data = {
amount: IBMChat.profile.get('amount'),
card: {
number: IBMChat.profile.get('cc_number'),
// ... other private card data
}
};
$.post('https://www.myserver.com/payment-gateway', data)
.done( function() {
IBMChat.sendSilently('success');
})
.fail( function() {
IBMChat.sendSilently('failure');
});
});
Actions Documentation
https://github.com/watson-virtual-agents/chat-widget/blob/master/docs/DOCS.md#actions
I am installing a service worker for the first time, and following the tutorial at: https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
My service worker behaves as expected when installing and updating, but fetch requests are not triggered as expected.
var CACHE_NAME = 'test-cache-v1'
var urlsToCache = [
'/',
'/public/scripts/app.js'
]
self.addEventListener('install', function (event) {
console.log('Installing new service worker', event)
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function (cache) {
return cache.addAll(urlsToCache)
})
.catch(err => console.log('Error Caching', err))
)
})
self.addEventListener('fetch', function (event) {
console.log('Fetch req', event)
event.respondWith(
caches.match(event.request)
.then(function (response) {
console.log('Cache hit', response)
// Cache hit - return response
if (response) {
return response
}
return fetch(event.request)
.catch(e => console.log('Error matching cache', e))
}
)
)
})
I see 'Installing new service worker' outputted to the console when expected, but not 'Fetch req'. I am using Chrome devtools and have accessed the "Inspect" option next to the ServiceWorker under the Application tab.
If you listen for the activate event, and add in a call to clients.claim() inside that event, then your newly active service worker will take control over existing web pages in its scope, including the page that registered it. There's more information in this article on the service worker lifecycle. The following code is sufficient:
self.addEventListener('activate', () => self.clients.claim());
If you don't call clients.claim(), then the service worker will activate, but not control any of the currently open pages. It won't be until you navigate to the next page under its scope (or reload a current page) that the service worker will take control, and start intercepting network requests via its fetch handler.
On dynamic websites, be careful!
If service worker has scope: example.com/weather/
It does not have scope: example.com/weather
Especially on firebase which by default removes trailing slash
In this case, service worker will install, activate, and even cache files, but not receive ‘fetch’ events! Very hard to debug.
Add “trailingSlash”: true to firebase.json under ‘hosting’. This will solve the problem. Make sure to modify rewrite from:
{
"source": "/weather", "function": "weather"
}
To :
{
"source": "/weather/", "function": "weather"
}
As well as manifest.json
I found that Jeff Posnick's "clients.claim()" in the activate event handler was useful, but it was not enough to cache resources the first time the JS app runs. That is because on the first run the service worker has not finished activating when the JS starts loading its resources.
The following function lets the main app register the SW and then waits for it to activate before continuing to load resources:
/**
* Registers service worker and waits until it is activated or failed.
* #param js URI of service worker JS
* #param onReady function to call when service worker is activated or failed
* #param maxWait maximum time to wait in milliseconds
*/
function registerServiceWorkerAndWaitForActivated(js, onReady, maxWait) {
let bReady = false;
function setReady() {
if (!bReady) {
bReady = true;
onReady();
}
}
if ('serviceWorker' in navigator) {
setTimeout(setReady, maxWait || 1000);
navigator.serviceWorker.register(js).then((reg) => {
let serviceWorker = reg.installing || reg.waiting;
if (serviceWorker) {
serviceWorker.addEventListener("statechange", (e) => {
if (serviceWorker.state == "activated")
setReady();
});
} else {
if (!reg.active)
console.log("Unknown service worker state");
setReady();
}
}, () => setReady());
} else {
let msg = "ServiceWorker not available. App will not run offline."
if (document.location.protocol != "https:")
msg = "Please use HTTPS so app can run offline later.";
console.warn(msg);
alert(msg);
setReady();
}
}