Skip to content

Commit

Permalink
Web Push prompt template implementation (#269) (#271)
Browse files Browse the repository at this point in the history
* Web Push prompt template implementation

* Handling the optional hover text

* added preview flag

* Updated the function processWebPushConfig

* Updated styles, and prompt api

* remove bell icon on notifications allow

* added z-index for bell icon

* updated the key for bell background color

* preview handling

* updated gif link and color for close button

* preview handling for box and bell prompt

* removed payload from tr

* Taking swPath as input and code optimisation

* Removed unused code and updated package.json and chengelog

* Added constants file for push prompt constants

* Handled style for prompt button, title and description

* Removed opcaity for tooltip

* Updated changelog

Co-authored-by: Yusuf Khan <154309421+kkyusuftk@users.noreply.github.com>
  • Loading branch information
KambleSonam and kkyusuftk authored Oct 10, 2024
1 parent 345d491 commit bfdd59a
Show file tree
Hide file tree
Showing 13 changed files with 2,496 additions and 1,714 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Change Log
All notable changes to this project will be documented in this file.

## [1.10.0] - 8 Oct, 2024
- Adds new api to handle rendering of customized web push prompt

## [1.9.6] - 23 Sept, 2024
- Shopify support for visual builder

Expand Down
3,732 changes: 2,029 additions & 1,703 deletions clevertap.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion clevertap.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion clevertap.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clevertap-web-sdk",
"version": "1.9.6",
"version": "1.10.0",
"description": "",
"main": "clevertap.js",
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions src/clevertap.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { hasWebInboxSettingsInLS, checkAndRegisterWebInboxElements, initializeWe
import { Variable } from './modules/variables/variable'
import VariableStore from './modules/variables/variableStore'
import { checkBuilder, addAntiFlicker } from './modules/visualBuilder/pageBuilder'
import { setServerKey } from './modules/webPushPrompt/prompt'

export default class CleverTap {
#logger
Expand Down Expand Up @@ -509,6 +510,7 @@ export default class CleverTap {
closeIframe(campaignId, divIdIgnored, this.#session.sessionId)
}
api.enableWebPush = (enabled, applicationServerKey) => {
setServerKey(applicationServerKey)
this.notifications._enableWebPush(enabled, applicationServerKey)
}
api.tr = (msg) => {
Expand Down
22 changes: 16 additions & 6 deletions src/modules/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import {
urlBase64ToUint8Array
} from '../util/encoder'
import { enablePush } from './webPushPrompt/prompt'

export default class NotificationHandler extends Array {
#oldValues
Expand Down Expand Up @@ -35,6 +36,11 @@ export default class NotificationHandler extends Array {
return 0
}

enable (options = {}) {
const { swPath } = options
enablePush(this.#logger, this.#account, this.#request, swPath)
}

_processOldValues () {
if (this.#oldValues) {
this.#setUpWebPush(this.#oldValues)
Expand All @@ -53,15 +59,15 @@ export default class NotificationHandler extends Array {
}
}

#setUpWebPushNotifications (subscriptionCallback, serviceWorkerPath, apnsWebPushId, apnsServiceUrl) {
setUpWebPushNotifications (subscriptionCallback, serviceWorkerPath, apnsWebPushId, apnsServiceUrl) {
if (navigator.userAgent.indexOf('Chrome') !== -1 || navigator.userAgent.indexOf('Firefox') !== -1) {
this.#setUpChromeFirefoxNotifications(subscriptionCallback, serviceWorkerPath)
} else if (navigator.userAgent.indexOf('Safari') !== -1) {
this.#setUpSafariNotifications(subscriptionCallback, apnsWebPushId, apnsServiceUrl)
}
}

#setApplicationServerKey (applicationServerKey) {
setApplicationServerKey (applicationServerKey) {
this.#fcmPublicKey = applicationServerKey
}

Expand Down Expand Up @@ -153,6 +159,10 @@ export default class NotificationHandler extends Array {
if (typeof subscriptionCallback !== 'undefined' && typeof subscriptionCallback === 'function') {
subscriptionCallback()
}
const existingBellWrapper = document.getElementById('bell_wrapper')
if (existingBellWrapper) {
existingBellWrapper.parentNode.removeChild(existingBellWrapper)
}
}).catch((error) => {
// unsubscribe from webpush if error
serviceWorkerRegistration.pushManager.getSubscription().then((subscription) => {
Expand Down Expand Up @@ -285,15 +295,15 @@ export default class NotificationHandler extends Array {
// handle migrations from other services -> chrome notifications may have already been asked for before
if (Notification.permission === 'granted') {
// skip the dialog and register
this.#setUpWebPushNotifications(subscriptionCallback, serviceWorkerPath, apnsWebPushId, apnsWebPushServiceUrl)
this.setUpWebPushNotifications(subscriptionCallback, serviceWorkerPath, apnsWebPushId, apnsWebPushServiceUrl)
return
} else if (Notification.permission === 'denied') {
// we've lost this profile :'(
return
}

if (skipDialog) {
this.#setUpWebPushNotifications(subscriptionCallback, serviceWorkerPath, apnsWebPushId, apnsWebPushServiceUrl)
this.setUpWebPushNotifications(subscriptionCallback, serviceWorkerPath, apnsWebPushId, apnsWebPushServiceUrl)
return
}
}
Expand Down Expand Up @@ -387,7 +397,7 @@ export default class NotificationHandler extends Array {
if (typeof okCallback === 'function') {
okCallback()
}
this.#setUpWebPushNotifications(subscriptionCallback, serviceWorkerPath, apnsWebPushId, apnsWebPushServiceUrl)
this.setUpWebPushNotifications(subscriptionCallback, serviceWorkerPath, apnsWebPushId, apnsWebPushServiceUrl)
} else {
if (typeof rejectCallback === 'function') {
rejectCallback()
Expand All @@ -402,7 +412,7 @@ export default class NotificationHandler extends Array {
_enableWebPush (enabled, applicationServerKey) {
$ct.webPushEnabled = enabled
if (applicationServerKey != null) {
this.#setApplicationServerKey(applicationServerKey)
this.setApplicationServerKey(applicationServerKey)
}
if ($ct.webPushEnabled && $ct.notifApi.notifEnabledFromApi) {
this.#handleNotificationRegistration($ct.notifApi.displayArgs)
Expand Down
251 changes: 251 additions & 0 deletions src/modules/webPushPrompt/prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { getBellIconStyles, getBoxPromptStyles } from './promptStyles.js'
import { WEBPUSH_CONFIG } from '../../util/constants.js'
import { StorageManager, $ct } from '../../util/storage.js'
import NotificationHandler from '../notification.js'
import { BELL_BASE64, PROMPT_BELL_BASE64 } from './promptConstants.js'

let appServerKey = null
let swPath = '/clevertap_sw.js'
let notificationHandler = null

export const processWebPushConfig = (webPushConfig, logger, request) => {
const _pushConfig = StorageManager.readFromLSorCookie(WEBPUSH_CONFIG) || {}

const updatePushConfig = () => {
$ct.pushConfig = webPushConfig
StorageManager.saveToLSorCookie(WEBPUSH_CONFIG, webPushConfig)
}

if (webPushConfig.isPreview) {
updatePushConfig()
enablePush(logger, null, request)
} else if (JSON.stringify(_pushConfig) !== JSON.stringify(webPushConfig)) {
updatePushConfig()
}
}

export const enablePush = (logger, account, request, customSwPath) => {
const _pushConfig = StorageManager.readFromLSorCookie(WEBPUSH_CONFIG) || {}
$ct.pushConfig = _pushConfig
if (!$ct.pushConfig) {
logger.error('Web Push config data not present')
return
}

if (customSwPath) { swPath = customSwPath }

notificationHandler = new NotificationHandler({ logger, session: {}, request, account })
const { showBox, boxType, showBellIcon, isPreview } = $ct.pushConfig

if (isPreview) {
if ($ct.pushConfig.boxConfig) createNotificationBox($ct.pushConfig)
if ($ct.pushConfig.bellIconConfig) createBellIcon($ct.pushConfig)
} else {
if (showBox && boxType === 'new') createNotificationBox($ct.pushConfig)
if (showBellIcon) createBellIcon($ct.pushConfig)
}
}

const createElementWithAttributes = (tag, attributes = {}) => {
const element = document.createElement(tag)
Object.entries(attributes).forEach(([key, value]) => {
element[key] = value
})
return element
}

export const createNotificationBox = (configData) => {
if (document.getElementById('pnWrapper')) return

const { boxConfig: { content, style } } = configData

// Create the wrapper div
const wrapper = createElementWithAttributes('div', { id: 'pnWrapper' })
const overlayDiv = createElementWithAttributes('div', { id: 'pnOverlay' })
const pnCard = createElementWithAttributes('div', { id: 'pnCard' })

const iconTitleDescWrapper = createElementWithAttributes('div', { id: 'iconTitleDescWrapper' })
const iconContainer = createElementWithAttributes('div', { id: 'iconContainer' })
const imgElement = createElementWithAttributes('img', {
id: 'imgElement',
src: content.icon.type === 'default' ? `data:image/svg+xml;base64,${PROMPT_BELL_BASE64}` : content.icon.url
})

iconContainer.appendChild(imgElement)
iconTitleDescWrapper.appendChild(iconContainer)

const titleDescWrapper = createElementWithAttributes('div', { id: 'titleDescWrapper' })
titleDescWrapper.appendChild(createElementWithAttributes('div', { id: 'title', textContent: content.title }))
titleDescWrapper.appendChild(createElementWithAttributes('div', { id: 'description', textContent: content.description }))

iconTitleDescWrapper.appendChild(titleDescWrapper)

const buttonsContainer = createElementWithAttributes('div', { id: 'buttonsContainer' })

const primaryButton = createElementWithAttributes('button', {
id: 'primaryButton',
textContent: content.buttons.primaryButtonText
})
const secondaryButton = createElementWithAttributes('button', {
id: 'secondaryButton',
textContent: content.buttons.secondaryButtonText
})
buttonsContainer.appendChild(secondaryButton)
buttonsContainer.appendChild(primaryButton)

pnCard.appendChild(iconTitleDescWrapper)
pnCard.appendChild(buttonsContainer)

// Apply styles
const styleElement = createElementWithAttributes('style', { textContent: getBoxPromptStyles(style) })

wrapper.appendChild(styleElement)
wrapper.appendChild(pnCard)
wrapper.appendChild(overlayDiv)

setElementPosition(pnCard, style.card.position)

const now = new Date().getTime() / 1000
const lastNotifTime = StorageManager.getMetaProp('webpush_last_notif_time')
const popupFrequency = content.popupFrequency || 7 * 24 * 60 * 60

if (!lastNotifTime || now - lastNotifTime >= popupFrequency * 24 * 60 * 60) {
document.body.appendChild(wrapper)
if (!configData.isPreview) { addEventListeners(wrapper) }
}
}

export const createBellIcon = (configData) => {
if (document.getElementById('bell_wrapper') || Notification.permission === 'granted') return

const { bellIconConfig: { content, style } } = configData

const bellWrapper = createElementWithAttributes('div', { id: 'bell_wrapper' })
const bellIcon = createElementWithAttributes('img', {
id: 'bell_icon',
src: content.icon.type === 'default' ? `data:image/svg+xml;base64,${BELL_BASE64}` : content.icon.url
})

// For playing gif
const gifModal = createElementWithAttributes('div', { id: 'gif_modal', style: 'display: none;' })
const gifImage = createElementWithAttributes('img', {
id: 'gif_image',
src: 'https://d2r1yp2w7bby2u.cloudfront.net/js/permission_grant.gif'
})
const closeModal = createElementWithAttributes('div', { id: 'close_modal', innerHTML: '&times;' })

gifModal.appendChild(gifImage)
gifModal.appendChild(closeModal)

bellWrapper.appendChild(bellIcon)
bellWrapper.appendChild(gifModal)
if (content.hoverText.enabled) {
const tooltip = createElementWithAttributes('div', {
id: 'bell_tooltip',
textContent: content.hoverText.text
})
bellWrapper.appendChild(tooltip)
}

setElementPosition(bellWrapper, style.card.position)
// Apply styles
const styleElement = createElementWithAttributes('style', { textContent: getBellIconStyles(style) })

document.head.appendChild(styleElement)
document.body.appendChild(bellWrapper)

if (!configData.isPreview) {
addBellEventListeners(bellWrapper)
}
return bellWrapper
}

export const setServerKey = (serverKey) => {
appServerKey = serverKey
}

export const addEventListeners = (wrapper) => {
const primaryButton = wrapper.querySelector('#primaryButton')
const secondaryButton = wrapper.querySelector('#secondaryButton')

const removeWrapper = () => wrapper.parentNode?.removeChild(wrapper)

primaryButton.addEventListener('click', () => {
removeWrapper()
notificationHandler.setApplicationServerKey(appServerKey)
notificationHandler.setUpWebPushNotifications(null, swPath, null, null)
})

secondaryButton.addEventListener('click', () => {
StorageManager.setMetaProp('webpush_last_notif_time', Date.now() / 1000)
removeWrapper()
})
}

export const addBellEventListeners = (bellWrapper) => {
const bellIcon = bellWrapper.querySelector('#bell_icon')
bellIcon.addEventListener('click', () => {
if (Notification.permission === 'denied') {
toggleGifModal(bellWrapper)
} else {
notificationHandler.setApplicationServerKey(appServerKey)
notificationHandler.setUpWebPushNotifications(null, swPath, null, null)
if (Notification.permission === 'granted') {
bellWrapper.remove()
}
}
})
bellIcon.addEventListener('mouseenter', () => displayTooltip(bellWrapper))
bellIcon.addEventListener('mouseleave', () => clearTooltip(bellWrapper))
bellWrapper.querySelector('#close_modal').addEventListener('click', () => toggleGifModal(bellWrapper))
}

export const setElementPosition = (element, position) => {
Object.assign(element.style, {
inset: 'auto',
transform: 'none'
})

const positions = {
'Top Right': { inset: '16px 16px auto auto' },
'Top Left': { inset: '16px auto auto 16px' },
'Bottom Right': { inset: 'auto 16px 16px auto' },
'Bottom Left': { inset: 'auto auto 16px 16px' },
Center: { inset: '50%', transform: 'translate(-50%, -50%)' },
Top: { inset: '16px auto auto 50%', transform: 'translateX(-50%)' },
Bottom: { inset: 'auto auto 16px 50%', transform: 'translateX(-50%)' }
}

Object.assign(element.style, positions[position] || positions['top-right'])
}

const displayTooltip = (bellWrapper) => {
const gifModal = bellWrapper.querySelector('#gif_modal')
if (gifModal.style.display === 'flex') {
return
}
const tooltip = bellWrapper.querySelector('#bell_tooltip')
if (tooltip) {
tooltip.style.display = 'flex'
}

const bellIcon = bellWrapper.querySelector('#bell_icon')
const bellRect = bellIcon.getBoundingClientRect()
var midX = window.innerWidth / 2
var midY = window.innerHeight / 2
bellWrapper.style['flex-direction'] = bellRect.y > midY ? 'column-reverse' : 'column'
bellWrapper.style['align-items'] = bellRect.x > midX ? 'flex-end' : 'flex-start'
}

const clearTooltip = (bellWrapper) => {
const tooltip = bellWrapper.querySelector('#bell_tooltip')
if (tooltip) {
tooltip.style.display = 'none'
}
}

const toggleGifModal = (bellWrapper) => {
clearTooltip(bellWrapper)
const gifModal = bellWrapper.querySelector('#gif_modal')
gifModal.style.display = gifModal.style.display === 'none' ? 'flex' : 'none'
}
2 changes: 2 additions & 0 deletions src/modules/webPushPrompt/promptConstants.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit bfdd59a

Please sign in to comment.