@zencemarketing/web-sdk
Version:
ZenceMarketing Web SDK for push notifications, popups, and custom event tracking.
432 lines (388 loc) • 15 kB
text/typescript
import apiRequest from "../api.js";
import { getSdkData } from "./init.js";
import { logger } from "../index.js";
import { getWebSocketInstance } from "../websocket.js";
import { SDKData } from "../types/types.js";
type Position =
| "Top Left"
| "Top Right"
| "Top Middle"
| "Bottom Left"
| "Bottom Right"
| "Bottom Middle";
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
async function subscribeUserMain(swFilePath:string, vapidPublicKey:string, websiteUrl:string) {
if ('serviceWorker' in navigator && 'PushManager' in window) {
try {
const registration : ServiceWorkerRegistration = await navigator.serviceWorker.register(swFilePath);
await new Promise(resolve => setTimeout(resolve, 1000));
if (registration.active) {
// logger.info(`A service worker is active: ${registration.active}`);
// const vapidPublicKey = publicVapidKey;
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey,
});
// logger.info("User is subscribed:", subscription);
const userID = "gaId";
await fetch("https://qa-ndjs-appwebnotification.erzqa.com/api/subscription/subscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ subscription, userID, websiteUrl }),
});
return subscription;
} else {
throw new Error("Service worker is not active yet.");
}
} catch (error) {
logger.error("Error in subscribing user:", error);
return null;
}
} else {
return null;
}
}
async function checkAndSubscribe(currentDomain:string, swFilePath:string, vapidPublicKey:string, sdkData:SDKData) {
let subNew: PushSubscription | null = null;
await navigator.serviceWorker.getRegistrations().then(async (registrations) => {
if (registrations.length === 0) {
// logger.info("No service workers registered.");
subNew = await subscribeUserMain(swFilePath, vapidPublicKey, sdkData.website_url);
return subNew;
} else {
let foundSubscription = false;
for (const reg of registrations) {
let subscription = await reg.pushManager.getSubscription();
if (subscription) {
// logger.info("Subscription details retrieved:", subscription);
const regDomain = new URL(reg.scope).hostname;
// const currentDomain = window.location.hostname;
const isMatchingDomain = regDomain === currentDomain || regDomain.endsWith(`.${currentDomain}`);
if (isMatchingDomain) {
subNew = subscription;
foundSubscription = true;
}
// logger.info("Domain Match:", isMatchingDomain, "Scope:", reg.scope);
}
}
if (foundSubscription) {
// logger.info("Subscription found for service worker...", subNew);
return subNew;
} else {
// logger.info("No subscription found for any service worker. Subscribing...");
subNew = await subscribeUserMain(swFilePath, vapidPublicKey, sdkData.website_url);
return subNew;
}
}
});
return subNew;
}
async function handleNotificationSubscription(sdkData: SDKData, swFilePath: string, triggerSource: string) {
const gaId = sdkData.userID;
if (triggerSource === 'Box' || triggerSource === 'Bell') {
const notificationPopup = document.querySelector('.popup-notification-er') as HTMLElement | null;;
if (notificationPopup) {
notificationPopup.style.display = 'none';
}
}
let subscription = null;
if ('serviceWorker' in navigator && 'PushManager' in window) {
subscription = await checkAndSubscribe(sdkData.website_url, swFilePath, sdkData.public_vapid_key, sdkData);
} else {
// logger.info("Service workers or push notifications are not supported in this browser.");
}
let webSocketInstance = getWebSocketInstance(gaId);
webSocketInstance.send("subscribedNotification", {
gaId: gaId,
website_url: sdkData.website_url,
permission: Notification.permission,
subscription: subscription ?? null,
send_web_push: true,
});
}
async function showPopupNotificationER(sdkData: SDKData, swFilePath: string) {
const config = sdkData.opt_in_prompt;
// Define default config if not passed
const defaultConfig = {
OptPromptType: "Box",
ShowOverlyHint: true,
Message: "Bee the first to know! Join us for instant updates on limited time offers, exclusive styles, and more.",
Position: "Top Right",
AllowButtonText: "Stay connected",
DontAllowButtonText: "I'll do this later",
BackgroundColor: "#fff",
TextColor: "#000",
UserClickDontAllow: "",
Minutes: "",
ShowOverlyBox: "true",
TextOnHover: ""
};
// logger.info(config);
// Merge the provided config with the default one
const settings = { ...defaultConfig, ...config };
if (settings.OptPromptType === "Native") {
handleNotificationSubscription(sdkData, swFilePath, "Native");
return;
}
if (settings.OptPromptType === "Bell") {
const bellIcon = document.createElement("div") as HTMLElement;
bellIcon.className = "popup-bell-icon";
bellIcon.innerHTML = "🔔";
document.body.appendChild(bellIcon);
const positionStyles: Record<Position, Partial<CSSStyleDeclaration>> = {
"Top Left": { top: "20px", left: "20px" },
"Top Right": { top: "20px", right: "20px" },
"Top Middle": { top: "20px", left: "50%", transform: "translateX(-50%)" },
"Bottom Left": { bottom: "20px", left: "20px" },
"Bottom Right": { bottom: "20px", right: "20px" },
"Bottom Middle": { bottom: "20px", left: "50%", transform: "translateX(-50%)" }
};
const positionKey = settings.Position as Position;
// Apply position styles
Object.assign(bellIcon.style, {
position: "fixed", // Ensure the bell icon is fixed
BackgroundColor: settings.BackgroundColor,
color: settings.TextColor || "#fff",
fontSize: "24px",
padding: "10px",
borderRadius: "50%",
cursor: "pointer",
boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.2)",
zIndex: "1000",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "50px",
height: "50px",
...positionStyles[positionKey]
});
bellIcon.addEventListener("mouseover", () => {
bellIcon.style.backgroundColor = settings.TextOnHover;
});
bellIcon.addEventListener("mouseout", () => {
bellIcon.style.backgroundColor = settings.BackgroundColor;
});
bellIcon.addEventListener("click", () => {
handleNotificationSubscription(sdkData, swFilePath, "Bell");
bellIcon.style.display = "none";
});
return;
}
// Create the notification popup
const notificationPopup = document.createElement("div") as HTMLElement;
notificationPopup.className = "popup-notification-er";
notificationPopup.innerHTML = `
<div class="popup-notification-er-message">${settings.Message}</div>
<div class="popup-notification-er-div">
<button class="popup-notification-er-button" id="requestPermissionButton-notification-er-doLaterButton">${settings.DontAllowButtonText}</button>
<button class="popup-notification-er-button" id="requestPermissionButton-notification-er-allow">${settings.AllowButtonText}</button>
</div>
`;
// Define styles for the popup
const styleElement = document.createElement("style") as HTMLElement;
styleElement.innerHTML = `
.popup-notification-er {
position: fixed;
background-color: ${settings.BackgroundColor};
color: ${settings.TextColor};
border-radius: 8px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.2);
padding: 15px;
max-width: 300px;
z-index: 1000;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 220px;
min-height: 80px;
}
/* Position Handling */
.popup-notification-er.top-left {
top: 20px;
left: 20px;
}
.popup-notification-er.top-right {
top: 20px;
right: 20px;
}
.popup-notification-er.top-middle {
top: 20px;
left: 50%;
transform: translateX(-50%);
}
.popup-notification-er.bottom-left {
bottom: 20px;
left: 20px;
}
.popup-notification-er.bottom-right {
bottom: 20px;
right: 20px;
}
.popup-notification-er.bottom-middle {
bottom: 20px;
left: 50%;
transform: translateX(-50%);
}
.popup-notification-er-message {
color: ${settings.TextColor};
font-size: 14px;
margin-bottom: 15px;
text-align: center;
}
.popup-notification-er-div {
display: flex;
justify-content: space-between;
gap: 5px;
}
.popup-notification-er-button {
background-color: #ffffff;
color: teal;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
flex: 1;
}
.popup-notification-er-button:focus {
outline: none;
}
.popup-notification-er-button:hover {
background-color: ${settings.TextOnHover};
}
`;
document.head.appendChild(styleElement);
document.body.appendChild(notificationPopup);
notificationPopup.style.display = 'block';
notificationPopup.classList.add(settings.Position.toLowerCase().replace(" ", "-"));
const allowBtn = document.getElementById('requestPermissionButton-notification-er-allow');
if (allowBtn) {
allowBtn.addEventListener('click', async function () {
handleNotificationSubscription(sdkData, swFilePath, "Box");
});
}
const doLaterBtn = document.getElementById('requestPermissionButton-notification-er-doLaterButton');
if (doLaterBtn) {
doLaterBtn.addEventListener('click', () => {
notificationPopup.style.display = 'none';
});
}
}
function showNotification(data: any) {
const {
title,
body,
icon,
badge,
image,
data: notificationData,
} = data;
const notificationOptions: any = {};
if (title) {
notificationOptions.title = title;
}
if (body) {
notificationOptions.body = body;
}
if (icon) {
notificationOptions.icon = icon;
}
if (badge) {
notificationOptions.badge = badge;
}
if (image) {
notificationOptions.image = image;
}
if (notificationData) {
notificationOptions.data = notificationData;
}
if (Notification.permission === "granted") {
new Notification(title || '', notificationOptions);
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
new Notification(title || '', notificationOptions);
}
});
}
}
async function subscribeUserPush(swFilePath: string, gaId: string) {
const sdkData = getSdkData();
sdkData.userID = gaId;
if (sdkData.show_web_push === "1") {
logger.info("✅ Web Push is enabled");
if (Notification.permission === "default") {
showPopupNotificationER(sdkData, swFilePath);
} else if (Notification.permission === "granted") {
let sub: any = null;
if ("serviceWorker" in navigator && "PushManager" in window) {
sub = await checkAndSubscribe(sdkData.website_url, swFilePath, sdkData.public_vapid_key, sdkData);
} else {
// logger.info("❌ Service workers or push notifications are not supported in this browser.");
}
// logger.info("📩 Subscription Data:", sub);
let webSocketInstance = getWebSocketInstance(gaId);
webSocketInstance.send("subscribedNotification", {
gaId: gaId,
website_url: sdkData.website_url,
permission: Notification.permission,
subscription: sub ?? null,
send_web_push: true,
});
if(sub && sub.endpoint){
sdkData.pushSubscribed = true;
sdkData.sub = sub;
}
}
} else {
logger.info("❌ Web Push is not enabled");
}
}
async function sendNotification(notificationData: any) {
const sdkData = getSdkData();
if (sdkData.pushSubscribed) {
return apiRequest("sendNotificationToUser", "POST", {
"subscription": sdkData.sub,
...notificationData
});
} else{
return {
"success": false,
"data": null,
"message": "Please Subscribe",
}
}
// return apiRequest("sendNotificationToUser", "POST", { notificationData });
}
// async function unsubscribeUser(gaId: string) {
// return apiRequest("subscription/unsubscribe", "POST", { gaId });
// }
async function getSubscriptionStatus() {
const sdkData = getSdkData();
if (sdkData.pushSubscribed) {
return {
"subscribed": true,
"sub": sdkData.sub,
}
} else{
return {
"subscribed": false,
"sub": null,
}
}
}
export { subscribeUserPush, sendNotification,
// unsubscribeUser,
getSubscriptionStatus, showNotification };