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, subscriberId: subscriberId,
}); });
}).catch((err)=>{ }).catch((err)=>{
if(err.status && err.status >= 400) {
reject(new Error(err.body.message));
} else {
reject(err); reject(err);
}
}); });
}); });
} }

@ -15,9 +15,9 @@
<span v-else-if="isIncoming" class="text">{{ $t('call.incoming') }}</span> <span v-else-if="isIncoming" class="text">{{ $t('call.incoming') }}</span>
<span v-else class="text">{{ $t('call.call') }}</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"/> 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"/> class="no-shadow" @click="toggleFullscreen()" icon="fullscreen"/>
<q-btn round :small="!isFullscreenEnabled" slot="right" <q-btn round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="close()" icon="clear"/> class="no-shadow" @click="close()" icon="clear"/>
@ -70,8 +70,11 @@
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import CscMedia from './CscMedia' import CscMedia from './CscMedia'
import { QLayout, QCard, QCardTitle, QCardSeparator, QCardMain, QField, QInput, 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 { normalizeNumber, rawNumber } from '../filters/number-format'
import numberFormat from '../filters/number-format'
import { showCallNotification } from '../helpers/ui'
export default { export default {
name: 'csc-call', name: 'csc-call',
props: ['region', 'fullscreen'], props: ['region', 'fullscreen'],
@ -270,6 +273,16 @@
]), ]),
hasLocalVideo() { hasLocalVideo() {
return this.getLocalMediaType !== null && this.getLocalMediaType.match(/(v|V)ideo/) !== null; 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> </template>
<script> <script>
import { mapGetters } from 'vuex'
import { startLoading, stopLoading, showGlobalError } from '../helpers/ui' import { startLoading, stopLoading, showGlobalError } from '../helpers/ui'
import { QLayout, QCard, QCardTitle, QCardSeparator, QCardMain, QField, QInput, import { QLayout, QCard, QCardTitle, QCardSeparator, QCardMain, QField, QInput,
QCardActions, QBtn, QIcon, Loading, Alert, Platform } from 'quasar-framework' QCardActions, QBtn, QIcon, Loading, Alert, Platform } from 'quasar-framework'
export default { export default {
name: 'login', name: 'login',
components: { components: {
@ -65,21 +68,37 @@
classes.push('mobile'); classes.push('mobile');
} }
return classes; return classes;
} },
...mapGetters('user', [
'loginRequesting',
'loginSucceeded',
'loginError'
]),
}, },
methods: { methods: {
login() { login() {
startLoading();
this.$store.dispatch('user/login', { this.$store.dispatch('user/login', {
username: this.username, username: this.username,
password: this.password password: this.password
}).then(()=>{ });
stopLoading(); }
},
watch: {
loginRequesting(logging) {
if(logging) {
startLoading();
}
},
loginSucceeded(loggedIn) {
if(loggedIn) {
this.$router.push({path : '/'}); this.$router.push({path : '/'});
}).catch((err)=>{ }
},
loginError(error) {
if(error) {
stopLoading(); stopLoading();
showGlobalError(this.$i18n.t('pages.login.error')); 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-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 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-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>
<q-fab-action v-if="hasSmsCapability && hasSendSmsFeature" color="primary" @click="" icon="fa-send"> <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>
<q-fab-action v-if="isCallAvailable" color="primary" @click="call()" icon="fa-phone"> <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-action>
</q-fab> </q-fab>
</q-fixed-position> </q-fixed-position>
@ -105,7 +105,8 @@
<script> <script>
import _ from 'lodash'; 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 { mapState, mapGetters } from 'vuex'
import CscCall from '../CscCall' import CscCall from '../CscCall'
import { import {
@ -140,17 +141,7 @@
this.$store.commit('layout/showLeft'); this.$store.commit('layout/showLeft');
} }
this.applyLayout(); this.applyLayout();
if(!this.hasUser) { this.$store.dispatch('user/initUser');
startLoading();
this.$store.dispatch('user/initUser').then(()=>{
stopLoading();
this.showInitialToasts();
}).catch(()=>{
this.logout();
});
} else {
this.showInitialToasts();
}
}, },
components: { components: {
QLayout, QLayout,
@ -186,13 +177,16 @@
'hasCallInitFailure' 'hasCallInitFailure'
]), ]),
...mapGetters('user', [ ...mapGetters('user', [
'isLogged',
'hasUser', 'hasUser',
'getUsername', 'getUsername',
'isPbxAdmin', 'isPbxAdmin',
'hasSmsCapability', 'hasSmsCapability',
'hasFaxCapability', 'hasFaxCapability',
'hasSendSmsFeature', 'hasSendSmsFeature',
'hasSendFaxFeature' 'hasSendFaxFeature',
'userDataRequesting',
'userDataSucceeded'
]), ]),
...mapState({ ...mapState({
isCallForward: state => _.startsWith(state.route.path, '/user/call-forward'), isCallForward: state => _.startsWith(state.route.path, '/user/call-forward'),
@ -227,6 +221,9 @@
} else { } else {
return [48, 17]; return [48, 17];
} }
},
isDesktop() {
return Platform.is.desktop;
} }
}, },
methods: { methods: {
@ -235,15 +232,6 @@
toggleFullscreen() { toggleFullscreen() {
this.$store.commit('layout/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() { call() {
this.$store.commit('layout/showRight'); this.$store.commit('layout/showRight');
this.$refs.cscCall.init(); this.$refs.cscCall.init();
@ -293,6 +281,27 @@
} else { } else {
this.$refs.layout.hideLeft(); 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 { Loading, Alert, Toast } from 'quasar-framework'
import { i18n } from '../i18n';
export function startLoading() { export function startLoading() {
Loading.show({ delay: 0 }); Loading.show({ delay: 0 });
@ -14,11 +15,34 @@ export function showGlobalError(message) {
html: message, html: message,
position: 'top-center', position: 'top-center',
enter: 'bounceIn', enter: 'bounceIn',
leave: 'fadeOut' leave: 'fadeOut',
color: 'negative'
}); });
setTimeout(()=>{ alert.dismiss(); }, 2000); 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) { export function showToast(message) {
Toast.create({ Toast.create({
html: message, 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", "inputValidNumber": "Input a valid phone number",
"number": "Number", "number": "Number",
"endCall": "End Call", "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": { "pbxConfig": {
"seat": "Seat", "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: { features: {
sendFax: false, sendFax: false,
sendSms: false sendSms: false
} },
loginRequesting: false,
loginSucceeded: false,
loginError: null,
userDataRequesting: false,
userDataSucceeded: false,
userDataError: null
}, },
getters: { getters: {
isLogged(state, getters) { isLogged(state, getters) {
@ -59,66 +65,101 @@ export default {
}, },
getSubscriberId(state, getters) { getSubscriberId(state, getters) {
return state.subscriberId; 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: { mutations: {
login(state, options) { loginRequesting(state) {
state.loginRequesting = true;
state.loginSucceeded = false;
state.loginError = null;
},
loginSucceeded(state, options) {
state.jwt = options.jwt; state.jwt = options.jwt;
state.subscriberId = options.subscriberId; 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;
},
userDataRequesting(state) {
state.userDataRequesting = true;
state.userDataSucceeded = false;
state.userDataError = null;
}, },
setUserData(state, options) { userDataSucceeded(state, options) {
state.subscriber = options.subscriber; state.subscriber = options.subscriber;
state.capabilities = options.capabilities; 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) { logout(state) {
state.jwt = null; state.jwt = null;
state.subscriberId = null; state.subscriberId = null;
state.subscriber = null; state.subscriber = null;
state.capabilities = null; state.capabilities = null;
state.loginRequesting = false;
state.loginSucceeded = false;
state.loginError = null;
state.userDataRequesting = false;
state.userDataSucceeded = false;
state.userDataError = null;
} }
}, },
actions: { actions: {
login(context, options) { login(context, options) {
return new Promise((resolve, reject)=>{ context.commit('loginRequesting');
login(options.username, options.password).then((result)=>{ login(options.username, options.password).then((result)=>{
localStorage.setItem('jwt', result.jwt); localStorage.setItem('jwt', result.jwt);
localStorage.setItem('subscriberId', result.subscriberId); localStorage.setItem('subscriberId', result.subscriberId);
context.commit('login', { context.commit('loginSucceeded', {
jwt: localStorage.getItem('jwt'), jwt: localStorage.getItem('jwt'),
subscriberId: localStorage.getItem('subscriberId'), subscriberId: localStorage.getItem('subscriberId')
}); });
}).then(()=>{
return context.dispatch('initUser');
}).then(()=>{
resolve();
}).catch((err)=>{ }).catch((err)=>{
reject(err); context.commit('loginFailed', err.message);
});
}); });
}, },
logout(context) { logout(context) {
return new Promise((resolve, reject)=>{
localStorage.removeItem('jwt'); localStorage.removeItem('jwt');
localStorage.removeItem('subscriberId'); localStorage.removeItem('subscriberId');
context.commit('logout'); document.location.href = '/csc';
resolve();
});
}, },
initUser(context) { initUser(context) {
return new Promise((resolve, reject)=>{ context.commit('userDataRequesting');
getUserData(localStorage.getItem('subscriberId')).then((result)=>{ getUserData(localStorage.getItem('subscriberId')).then((result)=>{
context.commit('setUserData', { context.commit('userDataSucceeded', {
subscriber: result.subscriber, subscriber: result.subscriber,
capabilities: result.capabilities capabilities: result.capabilities
}); });
context.dispatch('call/initialize', null, { root: true }).then(()=>{ context.dispatch('call/initialize', null, { root: true });
resolve();
}).catch((err)=>{
resolve();
});
}).catch((err)=>{ }).catch((err)=>{
reject(err); context.commit('userDataFailed', err.message);
}); context.dispatch('logout');
}); });
} }
} }

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

Loading…
Cancel
Save