TT#32987 Call: As a Customer I want to get notified about an incoming call

Change-Id: I9ae9d37d7551260181a08666cf3819a068d0057b
changes/28/19128/4
Hans-Peter Herzog 7 years ago
parent 3475daa3dd
commit b81d974eeb

@ -19,7 +19,11 @@ export function login(username, password) {
subscriberId: subscriberId,
});
}).catch((err)=>{
reject(err);
if(err.status && err.status >= 400) {
reject(new Error(err.body.message));
} else {
reject(err);
}
});
});
}

@ -15,9 +15,9 @@
<span v-else-if="isIncoming" class="text">{{ $t('call.incoming') }}</span>
<span v-else class="text">{{ $t('call.call') }}</span>
<q-btn v-if="isFullscreenEnabled" round :small="!isFullscreenEnabled" slot="right"
<q-btn v-if="isFullscreenEnabled && !isMobile" round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="toggleFullscreen()" icon="fullscreen exit"/>
<q-btn v-else round :small="!isFullscreenEnabled" slot="right"
<q-btn v-else-if="!isMobile" round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="toggleFullscreen()" icon="fullscreen"/>
<q-btn round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="close()" icon="clear"/>
@ -70,8 +70,11 @@
import { mapState, mapGetters } from 'vuex'
import CscMedia from './CscMedia'
import { QLayout, QCard, QCardTitle, QCardSeparator, QCardMain, QField, QInput,
QCardActions, QBtn, QIcon, Loading, Alert, QSpinnerRings, Dialog } from 'quasar-framework'
QCardActions, QBtn, QIcon, Loading, Alert, QSpinnerRings, Dialog, Platform } from 'quasar-framework'
import { normalizeNumber, rawNumber } from '../filters/number-format'
import numberFormat from '../filters/number-format'
import { showCallNotification } from '../helpers/ui'
export default {
name: 'csc-call',
props: ['region', 'fullscreen'],
@ -270,6 +273,16 @@
]),
hasLocalVideo() {
return this.getLocalMediaType !== null && this.getLocalMediaType.match(/(v|V)ideo/) !== null;
},
isMobile() {
return Platform.is.mobile;
}
},
watch: {
isIncoming(value) {
if(value) {
showCallNotification(numberFormat(this.getNumber));
}
}
}
}

@ -32,9 +32,12 @@
</template>
<script>
import { mapGetters } from 'vuex'
import { startLoading, stopLoading, showGlobalError } from '../helpers/ui'
import { QLayout, QCard, QCardTitle, QCardSeparator, QCardMain, QField, QInput,
QCardActions, QBtn, QIcon, Loading, Alert, Platform } from 'quasar-framework'
export default {
name: 'login',
components: {
@ -65,21 +68,37 @@
classes.push('mobile');
}
return classes;
}
},
...mapGetters('user', [
'loginRequesting',
'loginSucceeded',
'loginError'
]),
},
methods: {
login() {
startLoading();
this.$store.dispatch('user/login', {
username: this.username,
password: this.password
}).then(()=>{
stopLoading();
});
}
},
watch: {
loginRequesting(logging) {
if(logging) {
startLoading();
}
},
loginSucceeded(loggedIn) {
if(loggedIn) {
this.$router.push({path : '/'});
}).catch((err)=>{
}
},
loginError(error) {
if(error) {
stopLoading();
showGlobalError(this.$i18n.t('pages.login.error'));
});
}
}
}
}

@ -87,13 +87,13 @@
<q-fixed-position id="global-action-btn" corner="top-right" :offset="fabOffset" class="page-button transition-generic">
<q-fab v-if="hasCommunicationCapabilities" color="primary" icon="question answer" active-icon="clear" direction="down" flat>
<q-fab-action v-if="hasFaxCapability && hasSendFaxFeature" color="primary" @click="" icon="fa-fax">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('sendFax') }}</q-tooltip>
<q-tooltip v-if="isDesktop" anchor="center right" self="center left" :offset="[15, 0]">{{ $t('sendFax') }}</q-tooltip>
</q-fab-action>
<q-fab-action v-if="hasSmsCapability && hasSendSmsFeature" color="primary" @click="" icon="fa-send">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('sendSms') }}</q-tooltip>
<q-tooltip v-if="isDesktop" anchor="center right" self="center left" :offset="[15, 0]">{{ $t('sendSms') }}</q-tooltip>
</q-fab-action>
<q-fab-action v-if="isCallAvailable" color="primary" @click="call()" icon="fa-phone">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('startCall') }}</q-tooltip>
<q-tooltip v-if="isDesktop" anchor="center right" self="center left" :offset="[15, 0]">{{ $t('startCall') }}</q-tooltip>
</q-fab-action>
</q-fab>
</q-fixed-position>
@ -105,7 +105,8 @@
<script>
import _ from 'lodash';
import { startLoading, stopLoading, showGlobalError, showToast } from '../../helpers/ui'
import { startLoading, stopLoading, showGlobalError,
showToast, showGlobalWarning, enableIncomingCallNotifications} from '../../helpers/ui'
import { mapState, mapGetters } from 'vuex'
import CscCall from '../CscCall'
import {
@ -140,17 +141,7 @@
this.$store.commit('layout/showLeft');
}
this.applyLayout();
if(!this.hasUser) {
startLoading();
this.$store.dispatch('user/initUser').then(()=>{
stopLoading();
this.showInitialToasts();
}).catch(()=>{
this.logout();
});
} else {
this.showInitialToasts();
}
this.$store.dispatch('user/initUser');
},
components: {
QLayout,
@ -186,13 +177,16 @@
'hasCallInitFailure'
]),
...mapGetters('user', [
'isLogged',
'hasUser',
'getUsername',
'isPbxAdmin',
'hasSmsCapability',
'hasFaxCapability',
'hasSendSmsFeature',
'hasSendFaxFeature'
'hasSendFaxFeature',
'userDataRequesting',
'userDataSucceeded'
]),
...mapState({
isCallForward: state => _.startsWith(state.route.path, '/user/call-forward'),
@ -227,6 +221,9 @@
} else {
return [48, 17];
}
},
isDesktop() {
return Platform.is.desktop;
}
},
methods: {
@ -235,15 +232,6 @@
toggleFullscreen() {
this.$store.commit('layout/toggleFullscreen');
},
showInitialToasts() {
if(this.isCallAvailable) {
showToast(this.$i18n.t('toasts.callAvailable'));
}
if(this.hasCallInitFailure) {
showToast(this.$i18n.t('toasts.callNotAvailable'));
}
},
call() {
this.$store.commit('layout/showRight');
this.$refs.cscCall.init();
@ -293,6 +281,27 @@
} else {
this.$refs.layout.hideLeft();
}
},
userDataRequesting(value) {
if(value) {
startLoading();
}
},
userDataSucceeded(value) {
if(value) {
stopLoading();
enableIncomingCallNotifications();
}
},
isCallAvailable(value) {
if(value) {
showToast(this.$i18n.t('toasts.callAvailable'));
}
},
hasCallInitFailure(value) {
if(value) {
showToast(this.$i18n.t('toasts.callNotAvailable'));
}
}
}
}

@ -1,5 +1,6 @@
import { Loading, Alert, Toast } from 'quasar-framework'
import { i18n } from '../i18n';
export function startLoading() {
Loading.show({ delay: 0 });
@ -14,11 +15,34 @@ export function showGlobalError(message) {
html: message,
position: 'top-center',
enter: 'bounceIn',
leave: 'fadeOut'
leave: 'fadeOut',
color: 'negative'
});
setTimeout(()=>{ alert.dismiss(); }, 2000);
}
export function showGlobalWarning(message) {
const alert = Alert.create({
html: message,
position: 'top-center',
enter: 'bounceIn',
leave: 'fadeOut',
color: 'warning'
});
setTimeout(()=>{ alert.dismiss(); }, 2000);
}
export function showPermanentGlobalWarning(message) {
const alert = Alert.create({
html: message,
position: 'top-center',
enter: 'bounceIn',
leave: 'fadeOut',
color: 'warning'
});
}
export function showToast(message) {
Toast.create({
html: message,
@ -27,6 +51,60 @@ export function showToast(message) {
});
}
export function removeDialog(options) {
export function askForNotificationPermission() {
return new Promise((resolve, reject)=>{
if(_.isObject(Notification)) {
Notification.requestPermission().then((perms)=>{
if(perms === 'denied' || perms === 'default') {
showPermanentGlobalWarning(i18n.t('call.notificationBlocked'));
}
resolve();
}).catch((err)=>{
reject(err);
});
} else {
showPermanentGlobalWarning(i18n.t('call.notificationNotSupported'));
resolve();
}
});
}
var serviceWorkerPath = '/csc/statics/service-worker.js';
export function enableIncomingCallNotifications() {
return new Promise((resolve, reject)=>{
Promise.resolve().then(()=>{
if(navigator.serviceWorker) {
return navigator.serviceWorker.register(serviceWorkerPath);
} else {
showPermanentGlobalWarning(i18n.t('call.notificationNotSupported'));
resolve();
}
}).then(()=>{
return askForNotificationPermission();
}).then(()=>{
resolve();
}).catch((err)=>{
showPermanentGlobalWarning(i18n.t('call.notificationFailed'));
console.error(err);
});
});
}
export function showCallNotification(number) {
if(navigator.serviceWorker) {
navigator.serviceWorker.getRegistration(serviceWorkerPath).then((registration)=>{
if(registration && registration.showNotification) {
registration.showNotification(i18n.t('call.notificationTitle', {
number: number
}), {
requireInteraction: true,
vibrate: [300, 200, 300, 200, 300],
tag: 'call-notification',
data: {
url: document.location.href
}
});
}
});
}
}

@ -159,7 +159,11 @@
"inputValidNumber": "Input a valid phone number",
"number": "Number",
"endCall": "End Call",
"endCallDialog": "You are about to end the current call. Are you sure?"
"endCallDialog": "You are about to end the current call. Are you sure?",
"notificationTitle": "Incoming call from {number}",
"notificationBlocked": "You have blocked incoming call notifications.",
"notificationFailed": "Could not enable incoming call notifications.",
"notificationNotSupported": "Incoming call notifications are not supported."
},
"pbxConfig": {
"seat": "Seat",

Binary file not shown.

@ -0,0 +1,18 @@
self.addEventListener('notificationclick', function(event) {
event.notification.close();
var promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true
}).then((windowClients) => {
var matchingClient = null;
for (var i = 0; i < windowClients.length; i++) {
var windowClient = windowClients[i];
if(windowClient.url === event.notification.data.url) {
return windowClient.focus();
}
}
});
event.waitUntil(promiseChain);
});

@ -13,7 +13,13 @@ export default {
features: {
sendFax: false,
sendSms: false
}
},
loginRequesting: false,
loginSucceeded: false,
loginError: null,
userDataRequesting: false,
userDataSucceeded: false,
userDataError: null
},
getters: {
isLogged(state, getters) {
@ -59,66 +65,101 @@ export default {
},
getSubscriberId(state, getters) {
return state.subscriberId;
}
},
loginRequesting(state, getters) {
return state.loginRequesting;
},
loginSucceeded(state) {
return state.loginSucceeded;
},
loginError(state) {
return state.loginError;
},
userDataRequesting(state, getters) {
return state.userDataRequesting;
},
userDataSucceeded(state) {
return state.userDataSucceeded;
},
},
mutations: {
login(state, options) {
loginRequesting(state) {
state.loginRequesting = true;
state.loginSucceeded = false;
state.loginError = null;
},
loginSucceeded(state, options) {
state.jwt = options.jwt;
state.subscriberId = options.subscriberId;
state.loginRequesting = false;
state.loginSucceeded = true;
state.loginError = null;
},
loginFailed(state, error) {
state.loginRequesting = false;
state.loginSucceeded = false;
state.loginError = error;
},
setUserData(state, options) {
userDataRequesting(state) {
state.userDataRequesting = true;
state.userDataSucceeded = false;
state.userDataError = null;
},
userDataSucceeded(state, options) {
state.subscriber = options.subscriber;
state.capabilities = options.capabilities;
state.userDataSucceeded = true;
state.userDataRequesting = false;
state.userDataError = null;
},
userDataFailed(state, error) {
state.userDataError = error;
state.userDataSucceeded = false;
state.userDataRequesting = false;
},
logout(state) {
state.jwt = null;
state.subscriberId = null;
state.subscriber = null;
state.capabilities = null;
state.loginRequesting = false;
state.loginSucceeded = false;
state.loginError = null;
state.userDataRequesting = false;
state.userDataSucceeded = false;
state.userDataError = null;
}
},
actions: {
login(context, options) {
return new Promise((resolve, reject)=>{
login(options.username, options.password).then((result)=>{
localStorage.setItem('jwt', result.jwt);
localStorage.setItem('subscriberId', result.subscriberId);
context.commit('login', {
jwt: localStorage.getItem('jwt'),
subscriberId: localStorage.getItem('subscriberId'),
});
}).then(()=>{
return context.dispatch('initUser');
}).then(()=>{
resolve();
}).catch((err)=>{
reject(err);
context.commit('loginRequesting');
login(options.username, options.password).then((result)=>{
localStorage.setItem('jwt', result.jwt);
localStorage.setItem('subscriberId', result.subscriberId);
context.commit('loginSucceeded', {
jwt: localStorage.getItem('jwt'),
subscriberId: localStorage.getItem('subscriberId')
});
}).catch((err)=>{
context.commit('loginFailed', err.message);
});
},
logout(context) {
return new Promise((resolve, reject)=>{
localStorage.removeItem('jwt');
localStorage.removeItem('subscriberId');
context.commit('logout');
resolve();
});
localStorage.removeItem('jwt');
localStorage.removeItem('subscriberId');
document.location.href = '/csc';
},
initUser(context) {
return new Promise((resolve, reject)=>{
getUserData(localStorage.getItem('subscriberId')).then((result)=>{
context.commit('setUserData', {
subscriber: result.subscriber,
capabilities: result.capabilities
});
context.dispatch('call/initialize', null, { root: true }).then(()=>{
resolve();
}).catch((err)=>{
resolve();
});
}).catch((err)=>{
reject(err);
context.commit('userDataRequesting');
getUserData(localStorage.getItem('subscriberId')).then((result)=>{
context.commit('userDataSucceeded', {
subscriber: result.subscriber,
capabilities: result.capabilities
});
context.dispatch('call/initialize', null, { root: true });
}).catch((err)=>{
context.commit('userDataFailed', err.message);
context.dispatch('logout');
});
}
}

@ -7,7 +7,7 @@ describe('UserModule', ()=>{
it('should login', ()=>{
var state = {};
UserModule.mutations.login(state, {
UserModule.mutations.loginSucceeded(state, {
jwt: 'abc123',
subscriberId: 123
});

Loading…
Cancel
Save