import firebase from 'firebase/app'; import 'firebase/messaging'; import { TTAPI } from '@/services/TimeTrexClientAPI'; import 'bootstrap'; import TTEventBus from '@/services/TTEventBus'; class NotificationConsumer { constructor() { const firebaseConfig = { apiKey: "AIzaSyB9tM0QYb1D3JF07RqpeG-14ADGhezGRws", authDomain: "timetrex-app.firebaseapp.com", databaseURL: "https://timetrex-app.firebaseio.com", projectId: "timetrex-app", storageBucket: "timetrex-app.appspot.com", messagingSenderId: "462133047262", appId: "1:462133047262:web:1705b6bfca364bcd99b74f" }; // Initialize Firebase firebase.initializeApp( firebaseConfig ); this.browser_supported = firebase.messaging.isSupported(); this.messaging = firebase.messaging.isSupported() ? firebase.messaging() : null; this.user_notification_device_token_api = TTAPI.APINotificationDeviceToken; this.notification_api = TTAPI.APINotification; this.user_preference_api = TTAPI.APIUserPreference; this.notification_holder = document.querySelector( '#notification-holder' ); this.notification_duration = 120000; this.token = ''; this.notification_total = 0; this.notification_on_screen_total = 0; // How many notification pop ups are currently on screen. this.create_event_listeners = true; this.notification_sound = null; this.sound_timer = null; this.title_timer = null; this.previous_page_title = document.title; this.pending_events = []; this.event_bus = new TTEventBus({ view_id: 'notification_consumer' }); } setupUser( request_permission, refresh_token ) { if ( refresh_token === true ) { this.deleteNotificationDeniedCookie(); } if ( this.isBrowserSupported( refresh_token ) === false ) { if ( refresh_token === true ) { TAlertManager.showAlert( $.i18n._( 'Sorry, this is browser does not support push notifications.' ), $.i18n._( 'Push Notifications' ) ); } return false; } //If impersonating other users do not show notification permission request pop ups or notifications. var alternate_session_data = getCookie( 'AlternateSessionData' ); if ( alternate_session_data && !refresh_token ) { return false; } //User has either repeatedly denied permissions or has disabled notifications for this browser in user preferences. if ( this.getNotificationDeniedCookie().asked_count >= 2 && refresh_token === false ) { return false; } if ( LocalCacheData.getLoginUser() && APIGlobal.pre_login_data.production === true && APIGlobal.pre_login_data.demo_mode !== true && APIGlobal.pre_login_data.sandbox !== true ) { this.notification_duration = parseInt( LocalCacheData.getLoginUserPreference().notification_duration ) * 1000; if ( Notification.permission === 'granted' && LocalCacheData.getLoginUserPreference().notification_status_id !== 2 ) { this.registerWorkerAndGetToken( true ); this.createNotificationListeners(); if ( refresh_token === true ) { // Provide feedback to user when they click refresh push notifications button. TAlertManager.showAlert( $.i18n._( 'Push Notifications Enabled' ), $.i18n._( 'Push Notifications' ) ); } } else if ( request_permission && Notification.permission === 'default' ) { //Do not show mobile browsers notification permission alert unless they manually refresh notifications in My Account -> Preferences. if ( APIGlobal.pre_login_data.user_agent_data.is_mobile === true && refresh_token === false ) { return false; } this.showNotificationPermissionAlert(); } else if ( refresh_token === true ) { this.showNotificationPermissionAlert(); } } else { // Else user has declined permissions. if ( refresh_token === true ) { TAlertManager.showAlert( $.i18n._( 'Sorry, push notifications are disabled on this server.' ), $.i18n._( 'Push Notifications' ) ); } } } // Register our service worker and vapidKey. // Safari and Firefox require this to trigger on a user action and not just randomly ask. registerWorkerAndGetToken( send_token ) { try { navigator.serviceWorker.register( './services/firebase-messaging-sw.js' ) .then( ( registration ) => { this.messaging.getToken( { serviceWorkerRegistration: registration, vapidKey: 'BAIFamGLNE689DChvdL8bWrvgiPFUMGzPwBrxuDiKQNTzpbQu-VZ3urH3SdIOSQ4DUYAOmeTrhmGTNQaNdtW-2I' } ).then( ( currentToken ) => { if ( currentToken ) { this.token = currentToken; if ( send_token ) { this.sendDeviceToken( this.token ); } //If permission alert exists, remove it. if ( $( '.modal-alert' ).length ) { $( '.modal-alert' ).remove(); Global.setUIReady(); this.sendAnalytics( 'allow-confirm' ); //Only trigger this when the employee is actually asked to allow permissions. Not everytime the service worker is registered. } } else { //request permission window happens } } ).catch( ( err ) => { // unexpected error } ); } ); } catch ( err ) { Debug.Text( 'Error attempting to register firebase service workers and push notification service: ' + err.message, 'NotificationConsumer.js', 'NotificationConsumer', 'registerWorkerAndGetToken', 9 ); } } isBrowserSupported( show_message ) { if ( window.location.protocol !== 'https:' ) { Debug.Text( 'Not on a HTTPS connection. Push Notifications disabled.', 'NotificationConsumer.js', 'NotificationConsumer', 'checkBrowserSupported', 9 ); if ( show_message === true ) { // Provide feedback to user why push notifications are not working if they clicked to refresh push notifications in My Account -> Preferences. TAlertManager.showAlert( $.i18n._( 'Push Notification are only available on HTTPS connections.' ), $.i18n._( 'Push Notifications' ) ); } return false; } else if ( this.browser_supported === false ) { Debug.Text( 'User on an unsupported browser.', 'NotificationConsumer.js', 'NotificationConsumer', 'checkBrowserSupported', 9 ); if ( show_message === true ) { // Provide feedback to user why push notifications are not working if they clicked to refresh push notifications in My Account -> Preferences. TAlertManager.showAlert( $.i18n._( 'Push Notification are not supported on this browser.' ), $.i18n._( 'Push Notifications' ) ); } return false; } return true; } sendDeviceToken( device_token ) { this.user_notification_device_token_api.checkAndSetNotificationDeviceToken( device_token, 100, { onResult: function( res ) { let result = res.getResult(); } } ); } deleteToken() { var $this = this; if ( this.token ) { this.messaging.deleteToken().then( ( result ) => { let data = {}; data.device_token = this.token; this.user_notification_device_token_api.deleteNotificationDeviceToken( [data], { onResult: function( res ) { $this.token = ''; var result = res.getResult(); if ( result ) { Debug.Text( 'Successfully deleted device token.', 'NotificationConsumer.js', 'NotificationConsumer', 'deleteToken', 9 ); } else { Debug.Text( 'Failed to delete device token.', 'NotificationConsumer.js', 'NotificationConsumer', 'deleteToken', 9 ); } } } ); } ).catch( ( err ) => { Debug.Text( 'ERROR: While attempting to delete notification device token.', 'NotificationConsumer.js', 'NotificationConsumer', 'deleteToken', 9 ); } ); } } deleteAllTokens() { //This deletion path does necessarily mean the current session has a device token, but will attempt to delete all device tokens for the current user. var $this = this; this.user_notification_device_token_api.deleteAllNotificationDeviceTokens( { onResult: function( res ) { $this.token = ''; var result = res.getResult(); if ( result ) { Debug.Text( 'Successfully deleted all device tokens.', 'NotificationConsumer.js', 'NotificationConsumer', 'DeleteAllDeviceTokens', 9 ); } else { Debug.Text( 'Failed to delete all device tokens, none may exist.', 'NotificationConsumer.js', 'NotificationConsumer', 'DeleteAllDeviceTokens', 9 ); } } } ); } createNotificationListeners() { if ( this.create_event_listeners === false ) { return; } var $this = this; this.create_event_listeners = false; // Handles foreground, background and background notification-clicked events. navigator.serviceWorker.addEventListener( 'message', payload => { this.handlePushNotificationEvent( payload.data.messageType === 'push-received', payload.data ); } ); this.notification_holder.addEventListener( 'click', event => { var element = event.target; if ( element.className === 'notification-close' ) { // Ignored notifications are not marked as read. this.removeNotification( element.parentNode.parentNode.parentNode ); } else if ( element.className === 'notification-link' ) { // Mark notification as read when user clicks "view details" and is sent to the notificwtion link. this.setNotificationAsRead( element.id ); this.removeNotification( element.parentNode.parentNode ); //If notification has an open_view event attached, trigger that event and then delete it. for ( var i = this.pending_events.length - 1; i >= 0; i-- ) { if ( this.pending_events[i].id === element.id && this.pending_events[i].event === 'open_view' ) { this.openViewLinkedToNotification( this.pending_events[i].event_data ); event.preventDefault(); //Stop default href from being followed. this.pending_events.splice( i, 1 ); //Delete event from pending events. break; } } } } ); window.addEventListener( 'focus', function( event ) { $this.cancelTimers(); }, false ); } handlePushNotificationEvent( foreground, payload ) { let timetrex_data = JSON.parse( payload.data.timetrex ); Debug.Arr( payload, 'Push notification received.', 'NotificationConsumer.js', 'NotificationConsumer', 'handlePushNotificationEvent', 9 ); this.event_bus.emit( 'tt_topbar', 'profile_pending_counts', { //When push notification received update all "My Profile" badges. object_types: [] } ); // If on foreground displays the notification. if ( payload.messageType !== 'notification-clicked' && timetrex_data.user_id !== undefined && LocalCacheData.getLoginUser() && LocalCacheData.getLoginUser().id === timetrex_data.user_id && payload.notification && payload.notification.title ) { // Increment total on bell everytime a new notification comes in. this.updateBell( true, this.notification_total + 1 ); this.showNotification( payload.notification.title, payload.notification.body, payload.notification.click_action, timetrex_data.id, timetrex_data.priority, payload.data.link_target ? payload.data.link_target : '' ); Debug.Text( 'Showing Notification on UI as user_id matches and notification was not a system click.', 'NotificationConsumer.js', 'NotificationConsumer', 'handlePushNotificationEvent', 9 ); } if ( payload.messageType === 'notification-clicked' ) { Debug.Text( 'Notification was clicked on from desktop.', 'NotificationConsumer.js', 'NotificationConsumer', 'handlePushNotificationEvent', 9 ); // Set notification as read as we about to redirect the user to the notification. this.setNotificationAsRead( timetrex_data.id ); // If the notification toast is still on screen remove it. this.removeNotificationById( timetrex_data.id ); // User clicked desktop notification, redirect them directly to the notification if a click_action was given. if ( payload.notification.click_action !== undefined && payload.notification.click_action !== '' ) { window.location = payload.notification.click_action; } else { window.location = Global.getBaseURL() + '#!m=Notification'; } } else { //Handle background events if any are in the payload. if ( timetrex_data.event !== undefined && timetrex_data.event.length > 0 ) { this.handleBackgroundEvent( timetrex_data ); } } } handleBackgroundEvent( timetrex_data ) { // Handles timetrex specific data of notification payload. Debug.Text( 'Background action was supplied in the push notification.', 'NotificationConsumer.js', 'NotificationConsumer', 'handlePushNotificationEvent', 9 ); for ( let i = 0; i < timetrex_data.event.length; i++ ) { switch ( timetrex_data.event[i].type ) { case 'clean_cache': LocalCacheData.cleanNecessaryCache(); break; case 'open_view': //Only triggered if user clicks the notification. this.pending_events.push( { id: timetrex_data.id, event: timetrex_data.event[i].type, event_data: timetrex_data.event[i] } ); break; case 'open_view_immediate': this.openViewLinkedToNotification( timetrex_data.event[i] ); break; case 'redirect': if ( timetrex_data.event[i].ask === 1 ) { TAlertManager.showConfirmAlert( $.i18n._( timetrex_data.event[i].text ), $.i18n._( 'Redirect Confirmation' ), ( flag ) => { if ( flag === true ) { if ( timetrex_data.event[i].target && timetrex_data.event[i].target === '_blank' ) { window.open( timetrex_data.event[i].link, '_blank' ); } else { window.location = timetrex_data.event[i].link; } } } ); } else { if ( timetrex_data.event[i].target && timetrex_data.event[i].target === '_blank' ) { window.open( timetrex_data.event[i].link, '_blank' ); } else { window.location = timetrex_data.event[i].link; } } break; case 'refresh_job_queue': this.event_bus.emit( 'tt_topbar', 'toggle_job_queue_spinner', { //Boolean events for job queue spinner. show: timetrex_data.event[i].show, //Show the job queue spinner get_job_data: timetrex_data.event[i].get_job_data, //Update job queue panel data check_completed: timetrex_data.event[i].check_completed //Check if job queue is completed and hide the job queue spinner if no pending tasks. } ); //Update TimeSheet is user is on it. if ( LocalCacheData.current_open_primary_controller && LocalCacheData.current_open_primary_controller.viewId === 'TimeSheet' ) { LocalCacheData.current_open_primary_controller.search(); } break; } } } openViewLinkedToNotification( event_data ) { //Open a view with the option of pre-filling fields. LocalCacheData.setAutoFillData( event_data.data ); // This is taking them to listview in some cases so a on a onAdd/onEdit click can be clicked afterwards. IndexViewController.goToViewByViewLabel( event_data.view_name ); // Ignore edit only views that don't have list views. if ( event_data.view_name !== 'InOut' && event_data.view_name !== 'Contact Information' ) { // Need to add the promise before onTabShow is called where it's originally intended to be added otherwise the below wait is not triggered. TTPromise.add( 'BaseViewController', 'onTabShow' ); TTPromise.wait( 'BaseViewController', 'onTabShow', function() { if ( event_data.action === 'add' ) { LocalCacheData.current_open_primary_controller.onAddClick(); } else if ( event_data.action === 'edit' ) { LocalCacheData.current_open_primary_controller.onEditClick( event_data.view_id ); } else if ( event_data.action === 'view' ) { LocalCacheData.current_open_primary_controller.onViewClick( event_data.view_id ); } } ); } } setNotificationDeniedCookie( cookie_value ) { cookie_value.asked_count++; cookie_value.last_asked = new Date().getTime(); setCookie( 'disable_push_notification_ask', JSON.stringify( cookie_value ), 10000, APIGlobal.pre_login_data.cookie_base_url ); } deleteNotificationDeniedCookie() { deleteCookie( 'disable_push_notification_ask' ); } getNotificationDeniedCookie() { if ( getCookie( 'disable_push_notification_ask' ) ) { return JSON.parse( getCookie( 'disable_push_notification_ask' ) ); } var cookie_value = {}; cookie_value.asked_count = 0; cookie_value.last_asked = 0; return cookie_value; } showNotificationPermissionAlert() { //Only ever ask twice to enable notification permissions for this browser. //If we have only asked once before and it has been 180 days since then, ask again. var notification_denied_cookie = this.getNotificationDeniedCookie(); if ( notification_denied_cookie.asked_count > 1 || notification_denied_cookie.last_asked + ( 180 * 24 * 60 * 60 * 1000 ) > new Date().getTime() ) { return false; } TAlertManager.showModalAlert( 'push_notification', 'ask', ( flag ) => { if ( flag === true ) { this.showPermissionHelp(); this.setUserPreferencePushNotification( 1 ); this.sendAnalytics( 'allow' ); } else { this.setUserPreferencePushNotification( 0 ); this.sendAnalytics( 'deny' ); this.setNotificationDeniedCookie( notification_denied_cookie ); } } ); } showPermissionHelp() { this.registerWorkerAndGetToken( true ); this.createNotificationListeners(); TAlertManager.showModalAlert( 'push_notification', 'wait_for_permission', ( flag ) => { if ( flag === true ) { TAlertManager.showModalAlert( 'push_notification', 'help_text', '' ); this.showArrowToEnablePushNotifications(); this.sendAnalytics( 'unsure' ); } } ); } showArrowToEnablePushNotifications() { const arrow = $( '
' ); $( '.modal-alert' ).append( arrow ); } setUserPreferencePushNotification( status ) { if ( LocalCacheData.getLoginUser() && LocalCacheData.getLoginUserPreference() ) { var data = {}; data.user_id = LocalCacheData.getLoginUser().id; data.id = LocalCacheData.getLoginUserPreference().id; data.browser_permission_ask_date = Math.round( new Date().getTime() / 1000 ); //If user agrees set notifications to enabled. Else only set last browser_permission_ask_date. if ( status === 1 ) { data.notification_status_id = status; } this.user_preference_api.setUserPreference( data, { onResult: function( res ) { let result = res.getResult(); } } ); } } sendAnalytics( choice ) { Global.sendAnalyticsEvent( 'push_notifications', 'click', 'click:push_notifications:' + choice ); } getUnreadNotifications() { this.notification_api.getUnreadNotifications( { onResult: ( result ) => { this.notification_total = parseInt( result.getResult() ); this.updateBell( false, this.notification_total ); } } ); } getSystemNotifications( target ) { this.notification_api.getSystemNotification( target, { onResult: ( result ) => { var new_system_notifications = parseInt( result.getResult() ); if ( new_system_notifications > 0 ) { this.updateBell( true, this.notification_total + new_system_notifications ); } } } ); } setNotificationAsRead( id ) { this.notification_api.setNotificationStatus( [id], 20, { onResult: ( result ) => { this.updateBell( true, this.notification_total - 1 ); } } ); } updateBell( manual, amount ) { if ( manual ) { this.notification_total = amount; } this.event_bus.emit( 'tt_topbar', 'notification_bell', { notification_count: this.notification_total }); } playSound() { // Only load notification sound when we first need it. Then reuse from then on. if ( this.notification_sound === null ) { this.notification_sound = new Audio(); this.notification_sound.src = Global.getBaseURL( '../' ) + 'sounds/notification.mp3'; this.notification_sound.load(); } const playPromise = this.notification_sound.play(); if ( playPromise !== undefined ) { //Older browsers play() does not return anything. playPromise.then( () => { //Notification audio is playing. } ) .catch( error => { console.log( error ); } ); } } startTimers( notification ) { var $this = this; if ( document.hasFocus() === false && this.sound_timer === null ) { //Change page title and repeat notififation sound if page does not have focus and no timer is already set. this.sound_timer = setInterval( function() { if ( document.hasFocus() ) { //If tab is in focus cancel the timer. $this.cancelTimers(); } else { $this.playSound(); } }, 5000 ); this.title_timer = setInterval( function() { if ( document.hasFocus() ) { //If tab is in focus cancel the timer. $this.cancelTimers(); } else { if ( document.title === '!!!!!!!!!!!!!' ) { document.title = $.i18n._( 'NOTICE!' ); } else { document.title = '!!!!!!!!!!!!!'; } } }, 2000 ); } } cancelTimers() { if ( this.sound_timer !== null ) { clearInterval( this.sound_timer ); this.sound_timer = null; } if ( this.title_timer !== null ) { document.title = this.previous_page_title; clearInterval( this.title_timer ); this.title_timer = null; } } showNotification( title, body, url, notification_id, priority, target ) { if ( priority == 10 ) { //User is not notified nor receives toast for low priority notifications. return false; } //All notifications other than low play notification sound. this.playSound(); // To stop infinite stacking notifications on screen we only show up to 5 at a time. // Allow high and critical priority notifications with priority 1 or 2 through. if ( this.notification_on_screen_total >= 5 && priority > 2 ) { return false; } const notification = document.createElement( 'div' ); notification.className = 'toast show toast-spacing'; notification.style = 'width: 22rem; background-color: hsla(0, 0%, 100%, 1) !important; margin-bottom: 0.4rem !important;'; const notification_header = document.createElement( 'div' ); notification_header.className = 'toast-header'; const notification_title = document.createElement( 'strong' ); notification_title.className = 'mr-auto'; notification_title.textContent = title; const notification_close_button = document.createElement( 'button' ); notification_close_button.className = 'ml-2 mb-1 close'; notification_close_button.setAttribute( 'aria-label', 'close' ); notification_close_button.innerHTML = ' '; const notification_body = document.createElement( 'div' ); notification_body.className = 'toast-body'; const notification_body_text = document.createElement( 'p' ); notification_body_text.textContent = body; const notification_body_link = document.createElement( 'a' ); notification_body_link.textContent = 'View Details'; notification_body_link.id = notification_id; notification_body_link.className = 'notification-link'; if ( url !== undefined && url !== '' ) { notification_body_link.href = url; } else { notification_body_link.href = Global.getBaseURL() + '#!m=Notification'; } if ( target === '_blank' ) { notification_body_link.target = '_blank'; } notification_header.appendChild( notification_title ); notification_header.appendChild( notification_close_button ); notification.appendChild( notification_header ); notification_body.appendChild( notification_body_text ); notification_body.appendChild( notification_body_link ); notification.appendChild( notification_body ); this.notification_holder.appendChild( notification ); this.notification_on_screen_total++; if ( priority == 2 ) { //High priority notifications flash border twice around notification. notification.className += ' notification-outline-repeat'; } else if ( priority == 1 ) { //Critical priority notification repeat the notification sound, change document title and continuously flash border around toast. this.startTimers( notification ); notification.className += ' notification-outline-infinite'; } if ( this.notification_duration !== 0 && priority != 1 ) { // User notification preferences with 0 delay or critical notifications with priority 1 are never automatically removed. setTimeout( () => { this.removeNotification( notification ); }, this.notification_duration ); } } removeNotification( notification ) { if ( notification ) { this.notification_on_screen_total--; notification.remove(); } } removeNotificationById( id ) { var notification_link = document.getElementById( id ); if ( notification_link ) { this.removeNotification( notification_link.parentNode.parentNode ); } } removeAllNotifications() { var notifications = document.querySelectorAll( ".toast" ); for ( var i = 0; i < notifications.length; i++ ) { this.removeNotification( notifications[i] ); } this.cancelTimers(); } detectBrowserNeedsExtraPermission() { // Some browsers by default block push notification permission so we need to detect them to show user a different prompt if ( Global.getBrowserVendor() === 'Edge' ) { return true; } else { return false; } } } export const NotificationConsumerObj = new NotificationConsumer();