Feature: Add web push notification permission in frontend (#766)

Add webpush notification permission in frontend

Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
Pranav Raj S
2020-05-06 00:10:56 +05:30
committed by GitHub
parent 5bd7a4c511
commit e9131ea558
37 changed files with 651 additions and 318 deletions

View File

@@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class NotificationSubscriptions extends ApiClient {
constructor() {
super('notification_subscriptions');
}
}
export default new NotificationSubscriptions();

View File

@@ -1,6 +1,6 @@
<template>
<button
type="submit"
:type="type"
:disabled="disabled"
:class="computedClass"
@click="onClick"
@@ -39,6 +39,10 @@ export default {
type: String,
default: '',
},
type: {
type: String,
default: 'submit',
},
},
computed: {
computedClass() {

View File

@@ -0,0 +1,93 @@
/* eslint-disable no-console */
import NotificationSubscriptions from '../api/notificationSubscription';
import auth from '../api/auth';
export const verifyServiceWorkerExistence = (callback = () => {}) => {
if (!('serviceWorker' in navigator)) {
// Service Worker isn't supported on this browser, disable or hide UI.
return;
}
if (!('PushManager' in window)) {
// Push isn't supported on this browser, disable or hide UI.
return;
}
navigator.serviceWorker
.register('/sw.js')
.then(registration => callback(registration))
.catch(registrationError => {
// eslint-disable-next-line
console.log('SW registration failed: ', registrationError);
});
};
export const hasPushPermissions = () => {
if ('Notification' in window) {
return Notification.permission === 'granted';
}
return false;
};
const generateKeys = str =>
btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-')
.replace(/\//g, '_');
export const getPushSubscriptionPayload = subscription => ({
subscription_type: 'browser_push',
subscription_attributes: {
endpoint: subscription.endpoint,
p256dh: generateKeys(subscription.getKey('p256dh')),
auth: generateKeys(subscription.getKey('auth')),
},
});
export const sendRegistrationToServer = subscription => {
if (auth.isLoggedIn()) {
return NotificationSubscriptions.create(
getPushSubscriptionPayload(subscription)
);
}
return null;
};
export const registerSubscription = (onSuccess = () => {}) => {
if (!window.chatwootConfig.vapidPublicKey) {
return;
}
navigator.serviceWorker.ready
.then(serviceWorkerRegistration =>
serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: window.chatwootConfig.vapidPublicKey,
})
)
.then(sendRegistrationToServer)
.then(() => {
onSuccess();
})
.catch(() => {
window.bus.$emit(
'newToastMessage',
'This browser does not support desktop notification'
);
});
};
export const requestPushPermissions = ({ onSuccess }) => {
if (!('Notification' in window)) {
window.bus.$emit(
'newToastMessage',
'This browser does not support desktop notification'
);
} else if (Notification.permission === 'granted') {
registerSubscription(onSuccess);
} else if (Notification.permission !== 'denied') {
Notification.requestPermission(permission => {
if (permission === 'granted') {
registerSubscription(onSuccess);
}
});
}
};

View File

@@ -10,11 +10,11 @@
"REMOVE_IMAGE": "Remove",
"UPLOAD_IMAGE": "Upload image",
"UPDATE_IMAGE": "Update image",
"PROFILE_SECTION" : {
"PROFILE_SECTION": {
"TITLE": "Profile",
"NOTE": "Your email address is your identity and is used to log in."
},
"PASSWORD_SECTION" : {
"PASSWORD_SECTION": {
"TITLE": "Password",
"NOTE": "Updating your password would reset your logins in multiple devices."
},
@@ -22,15 +22,25 @@
"TITLE": "Access Token",
"NOTE": "This token can be used if you are building an API based integration"
},
"EMAIL_NOTIFICATIONS_SECTION" : {
"EMAIL_NOTIFICATIONS_SECTION": {
"TITLE": "Email Notifications",
"NOTE": "Update your email notification preferences here",
"CONVERSATION_ASSIGNMENT": "Send email notifications when a conversation is assigned to me",
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created",
"UPDATE_SUCCESS": "Your email notification preferences are updated successfully",
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created"
},
"API": {
"UPDATE_SUCCESS": "Your notification preferences are updated successfully",
"UPDATE_ERROR": "There is an error while updating the preferences, please try again"
},
"PROFILE_IMAGE":{
"PUSH_NOTIFICATIONS_SECTION": {
"TITLE": "Push Notifications",
"NOTE": "Update your push notification preferences here",
"CONVERSATION_ASSIGNMENT": "Send push notifications when a conversation is assigned to me",
"CONVERSATION_CREATION": "Send push notifications when a new conversation is created",
"HAS_ENABLED_PUSH": "You have enabled push for this browser.",
"REQUEST_PUSH": "Enable push notifications"
},
"PROFILE_IMAGE": {
"LABEL": "Profile Image"
},
"NAME": {

View File

@@ -1,114 +0,0 @@
<template>
<div class="profile--settings--row row">
<div class="columns small-3 ">
<h4 class="block-title">
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
</h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.NOTE') }}</p>
</div>
<div class="columns small-9">
<div>
<input
v-model="selectedNotifications"
class="email-notification--checkbox"
type="checkbox"
value="email_conversation_creation"
@input="handleInput"
/>
<label for="conversation_creation">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
)
}}
</label>
</div>
<div>
<input
v-model="selectedNotifications"
class="email-notification--checkbox"
type="checkbox"
value="email_conversation_assignment"
@input="handleInput"
/>
<label for="conversation_assignment">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
)
}}
</label>
</div>
</div>
</div>
</template>
<script>
/* global bus */
import { mapGetters } from 'vuex';
export default {
data() {
return {
selectedNotifications: [],
};
},
computed: {
...mapGetters({
selectedEmailFlags: 'userNotificationSettings/getSelectedEmailFlags',
}),
},
watch: {
selectedEmailFlags(value) {
this.selectedNotifications = value;
},
},
mounted() {
this.$store.dispatch('userNotificationSettings/get');
},
methods: {
async handleInput(e) {
const selectedValue = e.target.value;
if (this.selectedEmailFlags.includes(e.target.value)) {
const selectedEmailFlags = this.selectedEmailFlags.filter(
flag => flag !== selectedValue
);
this.selectedNotifications = selectedEmailFlags;
} else {
this.selectedNotifications = [
...this.selectedEmailFlags,
selectedValue,
];
}
try {
this.$store.dispatch(
'userNotificationSettings/update',
this.selectedNotifications
);
bus.$emit(
'newToastMessage',
this.$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.UPDATE_SUCCESS'
)
);
} catch (error) {
bus.$emit(
'newToastMessage',
this.$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.UPDATE_ERROR'
)
);
}
},
},
};
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables.scss';
.email-notification--checkbox {
font-size: $font-size-large;
}
</style>

View File

@@ -82,7 +82,7 @@
</label>
</div>
</div>
<email-notifications />
<notification-settings />
<div class="profile--settings--row row">
<div class="columns small-3 ">
<h4 class="block-title">
@@ -111,11 +111,11 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import { required, minLength, email } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import { clearCookiesOnLogout } from '../../../../store/utils/api';
import EmailNotifications from './EmailNotifications';
import NotificationSettings from './NotificationSettings';
export default {
components: {
EmailNotifications,
NotificationSettings,
Thumbnail,
},
data() {

View File

@@ -0,0 +1,226 @@
<template>
<div>
<div class="profile--settings--row row">
<div class="columns small-3 ">
<h4 class="block-title">
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
</h4>
<p>
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.NOTE') }}
</p>
</div>
<div class="columns small-9">
<div>
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_conversation_creation"
@input="handleEmailInput"
/>
<label for="conversation_creation">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
)
}}
</label>
</div>
<div>
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_conversation_assignment"
@input="handleEmailInput"
/>
<label for="conversation_assignment">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
)
}}
</label>
</div>
</div>
</div>
<div v-if="vapidPublicKey" class="profile--settings--row row push-row">
<div class="columns small-3 ">
<h4 class="block-title">
{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.TITLE') }}
</h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.NOTE') }}</p>
</div>
<div class="columns small-9">
<p v-if="hasEnabledPushPermissions">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.HAS_ENABLED_PUSH'
)
}}
</p>
<div v-else>
<woot-submit-button
:button-text="
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.REQUEST_PUSH'
)
"
class="button nice small"
type="button"
@click="onRequestPermissions"
/>
</div>
<div>
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_conversation_creation"
@input="handlePushInput"
/>
<label for="conversation_creation">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
)
}}
</label>
</div>
<div>
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_conversation_assignment"
@input="handlePushInput"
/>
<label for="conversation_assignment">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
)
}}
</label>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import configMixin from 'shared/mixins/configMixin';
import {
hasPushPermissions,
requestPushPermissions,
verifyServiceWorkerExistence,
} from '../../../../helper/pushHelper';
export default {
mixins: [alertMixin, configMixin],
data() {
return {
selectedEmailFlags: [],
selectedPushFlags: [],
hasEnabledPushPermissions: false,
};
},
computed: {
...mapGetters({
emailFlags: 'userNotificationSettings/getSelectedEmailFlags',
pushFlags: 'userNotificationSettings/getSelectedPushFlags',
}),
},
watch: {
emailFlags(value) {
this.selectedEmailFlags = value;
},
pushFlags(value) {
this.selectedPushFlags = value;
},
},
mounted() {
if (hasPushPermissions()) {
this.getPushSubscription();
}
this.$store.dispatch('userNotificationSettings/get');
},
methods: {
onRegistrationSuccess() {
this.hasEnabledPushPermissions = true;
},
onRequestPermissions() {
requestPushPermissions({
onSuccess: this.onRegistrationSuccess,
});
},
getPushSubscription() {
verifyServiceWorkerExistence(registration =>
registration.pushManager
.getSubscription()
.then(subscription => {
console.log(subscription);
if (!subscription) {
this.hasEnabledPushPermissions = false;
} else {
this.hasEnabledPushPermissions = true;
}
})
.catch(error => console.log(error))
);
},
async updateNotificationSettings() {
try {
this.$store.dispatch('userNotificationSettings/update', {
selectedEmailFlags: this.selectedEmailFlags,
selectedPushFlags: this.selectedPushFlags,
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
} catch (error) {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_ERROR'));
}
},
handleEmailInput(e) {
this.selectedEmailFlags = this.toggleInput(
this.selectedEmailFlags,
e.target.value
);
this.updateNotificationSettings();
},
handlePushInput(e) {
this.selectedPushFlags = this.toggleInput(
this.selectedPushFlags,
e.target.value
);
this.updateNotificationSettings();
},
toggleInput(selected, current) {
if (selected.includes(current)) {
const newSelectedFlags = selected.filter(flag => flag !== current);
return newSelectedFlags;
}
return [...selected, current];
},
},
};
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables.scss';
.notification--checkbox {
font-size: $font-size-large;
}
// Hide on Safari
.push-row:not(:root:root) {
display: none;
}
</style>

View File

@@ -17,6 +17,9 @@ export const getters = {
getSelectedEmailFlags: $state => {
return $state.record.selected_email_flags;
},
getSelectedPushFlags: $state => {
return $state.record.selected_push_flags;
},
};
export const actions = {
@@ -35,12 +38,13 @@ export const actions = {
}
},
update: async ({ commit }, params) => {
update: async ({ commit }, { selectedEmailFlags, selectedPushFlags }) => {
commit(types.default.SET_USER_NOTIFICATION_UI_FLAG, { isUpdating: true });
try {
const response = await UserNotificationSettings.update({
notification_settings: {
selected_email_flags: params,
selected_email_flags: selectedEmailFlags,
selected_push_flags: selectedPushFlags,
},
});
commit(types.default.SET_USER_NOTIFICATION, response.data);

View File

@@ -26,6 +26,10 @@ import router from '../dashboard/routes';
import store from '../dashboard/store';
import vueActionCable from '../dashboard/helper/actionCable';
import constants from '../dashboard/constants';
import {
verifyServiceWorkerExistence,
registerSubscription,
} from '../dashboard/helper/pushHelper';
Vue.config.env = process.env;
@@ -66,15 +70,12 @@ window.onload = () => {
vueActionCable.init();
};
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then(registration => {
console.log('SW registered: ', registration);
})
.catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
window.addEventListener('load', () => {
verifyServiceWorkerExistence(registration =>
registration.pushManager.getSubscription().then(subscription => {
if (subscription) {
registerSubscription();
}
})
);
});

View File

@@ -6,5 +6,8 @@ export default {
twilioCallbackURL() {
return `${this.hostURL}/twilio/callback`;
},
vapidPublicKey() {
return window.chatwootConfig.vapidPublicKey;
},
},
};