Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented: in-app notifications state and UI #279

Merged
merged 21 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
07cfe86
Implemented: static UI and logic
k2maan Aug 25, 2023
4ea5a52
Implemented: logic and functionality to show notifications and manage…
k2maan Sep 12, 2023
6902e3c
Improved: code for utils and added toast when notification is added
k2maan Sep 13, 2023
c8285fe
Merge branch 'main' of https://github.com/hotwax/bopis into bopis/not…
k2maan Sep 13, 2023
ca0df0c
Fixed: URLs and removed logs in firebase-messaging-sw file
k2maan Sep 13, 2023
e3d098f
Improved: used firebase vapid key from env
k2maan Sep 13, 2023
f89b9ec
Improved: cleared notifications state on logout
k2maan Sep 13, 2023
a9e416c
Implemented: in-app notifications state and UI
k2maan Sep 14, 2023
11e2897
Improved: showed toast for foreground notifications only
k2maan Sep 14, 2023
9322e85
Fixed: focus if notifications tab is already open
k2maan Sep 15, 2023
4125372
Improved: indentation and removed unused return from fetchNotificatio…
k2maan Sep 15, 2023
478e5ae
Improved: handled case if no prefs are found
k2maan Sep 15, 2023
9f55058
Improved: handling based on OMS service responses
k2maan Sep 15, 2023
be855f3
Fixed: build failure and updated entries for DXP and OMS API
k2maan Sep 15, 2023
e858969
Merge branch 'main' of https://github.com/hotwax/bopis into bopis/not…
k2maan Sep 18, 2023
5f05518
Improved: code to disable save button in preference update modal if i…
k2maan Sep 18, 2023
8620247
Improved: variable and function naming and removed unused imports
k2maan Sep 20, 2023
c7f73a8
Improved: handling for showing toast if preference update fails and c…
k2maan Sep 21, 2023
7f70348
Improved: code for showing toast and parameter type in isDisabled com…
k2maan Sep 21, 2023
0c57a8d
Improved: removed client registration token directly instead of action
k2maan Sep 22, 2023
45277de
Improved: used getter instead of computed property for changing notif…
k2maan Sep 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ VUE_APP_ALIAS=
VUE_APP_CURRENCY_FORMATS={"en": {"currency": {"style": "currency","currency": "USD"}}, "ja": {"currency": {"style": "currency", "currency": "JPY"}}, "es": {"currency": {"style": "currency","currency": "ESP"}}}
VUE_APP_RF_CNFG_MPNG={ "allowDeliveryMethodUpdate": "CUST_DLVRMTHD_UPDATE", "allowDeliveryAddressUpdate": "CUST_DLVRADR_UPDATE", "allowPickupUpdate": "CUST_PCKUP_UPDATE", "allowCancel": "CUST_ALLOW_CNCL", "shippingMethod": "RF_SHIPPING_METHOD"}
VUE_APP_DEFAULT_LOG_LEVEL="error"
VUE_APP_LOGIN_URL="http://launchpad.hotwax.io/login"
VUE_APP_LOGIN_URL="http://launchpad.hotwax.io/login"
VUE_APP_NOTIF_APP_ID=BOPIS
VUE_APP_NOTIF_ENUM_TYPE_ID=NOTIF_BOPIS
VUE_APP_FIREBASE_CONFIG={"apiKey": "","authDomain": "","databaseURL": "","projectId": "","storageBucket": "","messagingSenderId": "","appId": ""}
VUE_APP_FIREBAE_VAPID_KEY=""
2,293 changes: 2,033 additions & 260 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
"@casl/ability": "^6.0.0",
"@hotwax/app-version-info": "^1.0.0",
"@hotwax/apps-theme": "^1.1.0",
"@hotwax/dxp-components": "^1.3.4",
"@hotwax/dxp-components": "^1.6.0",
"@hotwax/oms-api": "^1.10.0",
"@ionic/core": "6.7.5",
"@ionic/vue": "6.7.5",
"@ionic/vue-router": "6.7.5",
"@shopify/app-bridge-utils": "^2.0.4",
"boon-js": "^2.0.3",
"core-js": "^3.6.5",
"firebase": "^10.3.1",
"luxon": "^3.2.0",
"mitt": "^2.1.0",
"register-service-worker": "^1.7.1",
Expand Down
65 changes: 65 additions & 0 deletions public/firebase-messaging-sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here. Other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js');

// Initialize a default click_action URL
const clickActionURL = '/notifications';
const iconURL = 'img/icons/msapplication-icon-144x144.png';

const firebaseConfig = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
}

// Initialize the Firebase app in the service worker by passing in
// your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp(firebaseConfig);

// Retrieve an instance of Firebase Messaging so that it can handle background
// messages.
const messaging = firebase.messaging();
messaging.onBackgroundMessage(payload => {
// Customize notification here
const notificationTitle = payload.data.title;
const notificationOptions = {
body: payload.data.body,
icon: iconURL,
data: {
click_action: clickActionURL
}
};
self.registration.showNotification(notificationTitle, notificationOptions);

// broadcast background message on FB_BG_MESSAGES so that app can receive that message
const broadcast = new BroadcastChannel('FB_BG_MESSAGES');
broadcast.postMessage(payload);
});

self.addEventListener('notificationclick', event => {
event.notification.close();
const deepLink = event.notification.data.click_action;
event.waitUntil(
clients.matchAll({ includeUncontrolled: true, type: 'window' }).then(windowClients => {
// Check if the app window is already open
for (let client of windowClients) {
const clientPath = (new URL(client.url)).pathname;
if (clientPath === deepLink && 'focus' in client) {
return client.focus();
}
}

// If the app window is not open, open a new one
if (clients.openWindow) {
return clients.openWindow(deepLink);
}
})
);
});
29 changes: 25 additions & 4 deletions src/adapter/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import { api, client, hasError, initialise, resetConfig, updateInstanceUrl, updateToken, getUserFacilities } from '@hotwax/oms-api'
import {
api,
client,
hasError,
getUserFacilities,
getNotificationEnumIds,
getNotificationUserPrefTypeIds,
initialise,
resetConfig,
removeClientRegistrationToken,
storeClientRegistrationToken,
subscribeTopic,
unsubscribeTopic,
updateInstanceUrl,
updateToken
} from '@hotwax/oms-api'

export {
api,
client,
initialise,
hasError,
getUserFacilities,
getNotificationEnumIds,
getNotificationUserPrefTypeIds,
initialise,
resetConfig,
removeClientRegistrationToken,
storeClientRegistrationToken,
subscribeTopic,
unsubscribeTopic,
updateInstanceUrl,
updateToken,
getUserFacilities
updateToken
}
200 changes: 200 additions & 0 deletions src/components/NotificationPreferenceModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="closeModal">
<ion-icon slot="icon-only" :icon="closeOutline" />
</ion-button>
</ion-buttons>
<ion-title>{{ $t("Notification Preference") }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<div v-if="!notificationPrefs.length" class="ion-text-center">
<p>{{ $t("Notification preferences not found.")}}</p>
</div>
<ion-list v-else>
<ion-item :key="pref.enumId" v-for="pref in notificationPrefs">
<ion-label class="ion-text-wrap">{{ pref.description }}</ion-label>
<ion-toggle @click="toggleNotificationPref(pref.enumId, $event)" :checked="pref.isEnabled" slot="end" />
</ion-item>
</ion-list>
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button :disabled="isButtonDisabled" @click="confirmSave()">
<ion-icon :icon="save" />
</ion-fab-button>
</ion-fab>
</ion-content>
</template>

<script lang="ts">
import {
IonButtons,
IonButton,
IonContent,
IonFab,
IonFabButton,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonTitle,
IonToggle,
IonToolbar,
modalController,
alertController,
} from "@ionic/vue";
import { defineComponent } from "vue";
import { closeOutline, save } from "ionicons/icons";
import { mapGetters, useStore } from "vuex";
import { translate } from "@/i18n";
import { showToast } from "@/utils";
import emitter from "@/event-bus"
import { generateTopicName } from "@/utils/firebase";
import {
subscribeTopic,
unsubscribeTopic
} from '@/adapter';

export default defineComponent({
name: "NotificationPreferenceModal",
components: {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonFab,
IonFabButton,
IonIcon,
IonItem,
IonLabel,
IonList,
IonTitle,
IonToggle,
IonToolbar
},
data() {
return {
notificationPrefState: {} as any,
notificationPrefToUpate: {
subscribe: [],
unsubscribe: []
} as any,
initialNotificationPrefState: {} as any
}
},
computed: {
...mapGetters({
currentFacility: 'user/getCurrentFacility',
instanceUrl: 'user/getInstanceUrl',
notificationPrefs: 'user/getNotificationPrefs'
}),
// checks initial and final state of prefs to enable/disable the save button
isButtonDisabled(): boolean {
const enumTypeIds = Object.keys(this.initialNotificationPrefState);
return enumTypeIds.every((enumTypeId: string) => this.notificationPrefState[enumTypeId] === this.initialNotificationPrefState[enumTypeId]);
},
},
async beforeMount() {
await this.store.dispatch('user/fetchNotificationPreferences')
this.notificationPrefState = this.notificationPrefs.reduce((prefs: any, pref: any) => {
prefs[pref.enumId] = pref.isEnabled
return prefs
}, {})
this.initialNotificationPrefState = JSON.parse(JSON.stringify(this.notificationPrefState))
},
methods: {
closeModal() {
modalController.dismiss({ dismissed: true });
},
toggleNotificationPref(enumId: string, event: any) {
// used click event and extracted value this way as ionChange was
// running when the ion-toggle hydrates and hence, updated the
// initialNotificationPrefState here
const value = !event.target.checked
// updates the notificationPrefToUpate to check which pref
// values were updated from their initial values
if (value !== this.initialNotificationPrefState[enumId]) {
value
? this.notificationPrefToUpate.subscribe.push(enumId)
: this.notificationPrefToUpate.unsubscribe.push(enumId)
} else {
!value
? this.notificationPrefToUpate.subscribe.splice(this.notificationPrefToUpate.subscribe.indexOf(enumId), 1)
: this.notificationPrefToUpate.unsubscribe.splice(this.notificationPrefToUpate.subscribe.indexOf(enumId), 1)
}

// updating this.notificationPrefState as it is used to
// determine the save button disable state, hence, updating
// is necessary to recompute isButtonDisabled property
this.notificationPrefState[enumId] = value
},
async updateNotificationPref() {
// TODO disbale button if initial and final are same
// added loader as the API call is in pending state for too long, blocking the flow
emitter.emit("presentLoader");
try {
await this.handleTopicSubscription()
} catch (error) {
console.error(error)
} finally {
emitter.emit("dismissLoader")
}
},
async handleTopicSubscription() {
const oms = this.instanceUrl
const facilityId = (this.currentFacility as any).facilityId
const subscribeRequests = [] as any
this.notificationPrefToUpate.subscribe.map(async (enumId: string) => {
const topicName = generateTopicName(oms, facilityId, enumId)
await subscribeRequests.push(subscribeTopic(topicName, process.env.VUE_APP_NOTIF_APP_ID))
})

const unsubscribeRequests = [] as any
this.notificationPrefToUpate.unsubscribe.map(async (enumId: string) => {
const topicName = generateTopicName(oms, facilityId, enumId)
await unsubscribeRequests.push(unsubscribeTopic(topicName, process.env.VUE_APP_NOTIF_APP_ID))
})

const responses = await Promise.allSettled([...subscribeRequests, ...unsubscribeRequests])
const hasFailedResponse = responses.some((response: any) => response.status === "rejected")
showToast(
hasFailedResponse
? translate('Notification preferences not updated. Please try again.')
: translate('Notification preferences updated.')
)
},
async confirmSave() {
const message = this.$t("Are you sure you want to update the notification preferences?");
const alert = await alertController.create({
header: this.$t("Update notification preferences"),
message,
buttons: [
{
text: this.$t("Cancel"),
},
{
text: this.$t("Confirm"),
handler: async () => {
await this.updateNotificationPref();
modalController.dismiss({ dismissed: true });
}
}
],
});
return alert.present();
},
},
setup() {
const store = useStore();

return {
closeOutline,
save,
store
};
},
});
</script>
9 changes: 9 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"An email notification will be sent to that their order is ready for pickup. This order will also be moved to the packed orders tab.": "An email notification will be sent to { customerName } that their order is ready for pickup.{ space } This order will also be moved to the packed orders tab.",
"An email notification will be sent to that their order is ready for pickup.": "An email notification will be sent to { customerName } that their order is ready for pickup.",
"Are you sure you want to change the time zone to?": "Are you sure you want to change the time zone to?",
"Are you sure you want to update the notification preferences?": "Are you sure you want to update the notification preferences?",
"Arrived": "Arrived",
"Authenticating": "Authenticating",
"Assign Pickers": "Assign Pickers",
Expand Down Expand Up @@ -56,14 +57,21 @@
"Logout": "Logout",
"Mismatch": "Mismatch",
"More": "More",
"New notification received.": "New notification received.",
"No inventory details found": "No inventory details found",
"Not in stock": "Not in stock",
"Not in Stock": "Not in Stock",
"No products found": "No products found",
"No reason": "No reason",
"No notifications to show": "No notifications to show",
"No picker assigned.": "No picker assigned.",
"No picker found": "No picker found",
"No time zone found": "No time zone found",
"Notifications": "Notifications",
"Notification Preference": "Notification Preference",
"Notification preferences not found.": "Notification preferences not found.",
"Notification preferences updated.": "Notification preferences updated.",
"Notification preferences not updated. Please try again.": "Notification preferences not updated. Please try again.",
"Open": "Open",
"OMS": "OMS",
"OMS instance": "OMS instance",
Expand Down Expand Up @@ -144,6 +152,7 @@
"Warehouse": "Warehouse",
"Worn Display": "Worn Display",
"This order will be removed from your dashboard. This action cannot be undone.": "This order will be removed from your dashboard.{ space } This action cannot be undone.",
"Update notification preferences": "Update notification preferences",
"View shipping orders along with pickup orders.": "View shipping orders along with pickup orders.",
"You do not have permission to access this page": "You do not have permission to access this page",
"Zipcode": "Zipcode"
Expand Down
Loading