Skip to content

Commit fccf0e5

Browse files
committed
feat: in app banner notifications and tests
1 parent 0a5e820 commit fccf0e5

File tree

15 files changed

+634
-3
lines changed

15 files changed

+634
-3
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"WELCOME_DEVELOPER": {
3+
"DANGER_SHOW_ON_EVERY_BOOT" : false,
4+
"HTML_CONTENT": "<div>Welcome to Phoenix Code dev community! Click here to <a class='notification_ack' href='https://discord.com/invite/rBpTBPttca'> chat with our Discord Community.</a></div>",
5+
"FOR_VERSIONS": ">=3.0.0",
6+
"PLATFORM" : "all"
7+
}
8+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}

src/brackets.config.dist.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"coreAnalyticsAppName" : "phoenix-prod",
66
"environment" : "production",
77
"buildtype" : "production",
8-
"bugsnagEnv" : "production"
8+
"bugsnagEnv" : "production",
9+
"app_notification_url" : "https://updates.phcode.io/appNotifications/prod/"
910
}

src/brackets.config.staging.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"coreAnalyticsAppName" : "phoenix-stage",
66
"environment" : "stage",
77
"buildtype" : "staging",
8-
"bugsnagEnv" : "staging"
8+
"bugsnagEnv" : "staging",
9+
"app_notification_url" : "https://updates.phcode.io/appNotifications/staging/"
910
}

src/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"extension_registry_popularity": "https://extensions.phcode.dev/popularity.json",
2121
"extension_url": "https://extensions.phcode.dev/extensions/",
2222
"extension_store_url": "https://store.core.ai/src/",
23+
"app_notification_url": "assets/notifications/dev/",
2324
"linting.enabled_by_default": true,
2425
"build_timestamp": "",
2526
"googleAnalyticsID": "G-P4HJFPDB76",
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Copyright (c) 2019 - present Adobe. All rights reserved.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a
5+
* copy of this software and associated documentation files (the "Software"),
6+
* to deal in the Software without restriction, including without limitation
7+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
8+
* and/or sell copies of the Software, and to permit persons to whom the
9+
* Software is furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20+
* DEALINGS IN THE SOFTWARE.
21+
*
22+
*/
23+
/*global Phoenix*/
24+
/**
25+
* module for displaying in-app banner notifications
26+
*
27+
*/
28+
define(function (require, exports, module) {
29+
30+
const AppInit = require("utils/AppInit"),
31+
PreferencesManager = require("preferences/PreferencesManager"),
32+
ExtensionUtils = require("utils/ExtensionUtils"),
33+
Metrics = require("utils/Metrics"),
34+
utils = require("./utils"),
35+
NotificationBarHtml = require("text!./htmlContent/notificationContainer.html");
36+
37+
ExtensionUtils.loadStyleSheet(module, "styles/styles.css");
38+
39+
// duration of one day in milliseconds
40+
const ONE_DAY = 1000 * 60 * 60 * 24;
41+
const IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE = "InAppNotificationsBannerShown";
42+
const NOTIFICATION_ACK_CLASS = "notification_ack";
43+
44+
// Init default last notification number
45+
PreferencesManager.stateManager.definePreference(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE,
46+
"object", {});
47+
48+
/**
49+
* If there are multiple notifications, thew will be shown one after the other and not all at once.
50+
* A sample notifications is as follows:
51+
* {
52+
* "SAMPLE_NOTIFICATION_NAME": {
53+
* "DANGER_SHOW_ON_EVERY_BOOT" : false,
54+
* "HTML_CONTENT": "<div>hello world <a class='notification_ack'>Click to acknowledge.</a></div>",
55+
* "FOR_VERSIONS": "1.x || >=2.5.0 || 5.0.0 - 7.2.3",
56+
* "PLATFORM" : "allDesktop"
57+
* },
58+
* "ANOTHER_SAMPLE_NOTIFICATION_NAME": {etc}
59+
* }
60+
* By default, a notification is shown only once except if `DANGER_SHOW_ON_EVERY_BOOT` is set
61+
* or there is an html element with class `notification_ack`.
62+
*
63+
* 1. `SAMPLE_NOTIFICATION_NAME` : This is a unique ID. It is used to check if the notification was shown to user.
64+
* 2. `DANGER_SHOW_ON_EVERY_BOOT` : (Default false) Setting this to true will cause the
65+
* notification to be shown on every boot. This is bad ux and only be used if there is a critical security issue
66+
* that we want the version not to be used.
67+
* 3. `HTML_CONTENT`: The actual html content to show to the user. It can have an optional `notification_ack` class.
68+
* Setting this class will cause the notification to be shown once a day until the user explicitly clicks
69+
* on any html element with class `notification_ack` or explicitly click the close button.
70+
* If such a class is not present, then the notification is shown only once ever.
71+
* 4. `FOR_VERSIONS` : [Semver compatible version filter](https://www.npmjs.com/package/semver).
72+
* The notification will be shown to all versions satisfying this.
73+
* 5. `PLATFORM`: A comma seperated list of all platforms in which the message will be shown.
74+
* allowed values are: `mac,win,linux,allDesktop,firefox,chrome,safari,allBrowser,all`
75+
* @param notifications
76+
* @returns {false|*}
77+
* @private
78+
*/
79+
async function _renderNotifications(notifications) {
80+
if(!notifications) {
81+
return; // nothing to show here
82+
}
83+
84+
const _InAppBannerShownAndDone = PreferencesManager.getViewState(
85+
IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE);
86+
87+
for(const notificationID of Object.keys(notifications)){
88+
if(!_InAppBannerShownAndDone[notificationID]) {
89+
const notification = notifications[notificationID];
90+
if(!utils.isValidForThisVersion(notification.FOR_VERSIONS)){
91+
continue;
92+
}
93+
if(!utils.isValidForThisPlatform(notification.PLATFORM)){
94+
continue;
95+
}
96+
if(!notification.HTML_CONTENT.includes(NOTIFICATION_ACK_CLASS)
97+
&& !notification.DANGER_SHOW_ON_EVERY_BOOT){
98+
// One time notification. mark as shown and never show again
99+
_markAsShownAndDone(notificationID);
100+
}
101+
await showBannerAndWaitForDismiss(notification.HTML_CONTENT, notificationID, notification);
102+
if(!notification.DANGER_SHOW_ON_EVERY_BOOT){
103+
_markAsShownAndDone(notificationID);
104+
}
105+
}
106+
}
107+
}
108+
109+
function _markAsShownAndDone(notificationID) {
110+
const _InAppBannersShownAndDone = PreferencesManager.getViewState(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE);
111+
_InAppBannersShownAndDone[notificationID] = true;
112+
PreferencesManager.setViewState(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE,
113+
_InAppBannersShownAndDone);
114+
}
115+
116+
function fetchJSON(url) {
117+
return fetch(url)
118+
.then(response => {
119+
if (!response.ok) {
120+
return null;
121+
}
122+
return response.json();
123+
});
124+
}
125+
126+
let inProgress = false;
127+
function _fetchAndRenderNotifications() {
128+
if(inProgress){
129+
return;
130+
}
131+
inProgress = true;
132+
const locale = brackets.getLocale(); // en-US default
133+
const fetchURL = `${brackets.config.app_notification_url}${locale}/banner.json`;
134+
const defaultFetchURL = `${brackets.config.app_notification_url}root/banner.json`;
135+
// Fetch data from fetchURL first
136+
fetchJSON(fetchURL)
137+
.then(fetchedJSON => {
138+
// Check if fetchedJSON is empty or undefined
139+
if (fetchedJSON === null) {
140+
// Fetch data from defaultFetchURL if fetchURL didn't provide data
141+
return fetchJSON(defaultFetchURL);
142+
}
143+
return fetchedJSON;
144+
})
145+
.then(_renderNotifications) // Call the render function with the fetched JSON data
146+
.catch(error => {
147+
console.error(`Error fetching and rendering banner.json`, error);
148+
})
149+
.finally(()=>{
150+
inProgress = false;
151+
});
152+
}
153+
154+
155+
/**
156+
* Removes and cleans up the notification bar from DOM
157+
*/
158+
function cleanNotificationBanner() {
159+
const $notificationBar = $('#notification-bar');
160+
if ($notificationBar.length > 0) {
161+
$notificationBar.remove();
162+
}
163+
}
164+
165+
/**
166+
* Displays the Notification Bar UI
167+
*
168+
*/
169+
async function showBannerAndWaitForDismiss(html, notificationID) {
170+
let resolved = false;
171+
return new Promise((resolve)=>{
172+
const $htmlContent = $(html),
173+
$notificationBarElement = $(NotificationBarHtml);
174+
175+
// Remove any SCRIPT tag to avoid secuirity issues
176+
$htmlContent.find('script').remove();
177+
178+
// Remove any STYLE tag to avoid styling impact on Brackets DOM
179+
$htmlContent.find('style').remove();
180+
181+
cleanNotificationBanner(); //Remove an already existing notification bar, if any
182+
$notificationBarElement.prependTo(".content");
183+
184+
var $notificationBar = $('#notification-bar'),
185+
$notificationContent = $notificationBar.find('.content-container'),
186+
$closeIcon = $notificationBar.find('.close-icon');
187+
188+
$notificationContent.append($htmlContent);
189+
Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-"+notificationID,
190+
"shown");
191+
192+
// Click handlers on actionable elements
193+
if ($closeIcon.length > 0) {
194+
$closeIcon.click(function () {
195+
cleanNotificationBanner();
196+
Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-"+notificationID,
197+
"closeClick");
198+
!resolved && resolve($htmlContent);
199+
resolved = true;
200+
});
201+
}
202+
203+
$notificationBar.find(`.${NOTIFICATION_ACK_CLASS}`).click(function() {
204+
// Your click event handler logic here
205+
cleanNotificationBanner();
206+
Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-"+notificationID,
207+
"ackClick");
208+
!resolved && resolve($htmlContent);
209+
resolved = true;
210+
});
211+
});
212+
}
213+
214+
215+
AppInit.appReady(function () {
216+
if(Phoenix.isTestWindow) {
217+
return;
218+
}
219+
_fetchAndRenderNotifications();
220+
setInterval(_fetchAndRenderNotifications, ONE_DAY);
221+
});
222+
223+
if(Phoenix.isTestWindow){
224+
exports.cleanNotificationBanner = cleanNotificationBanner;
225+
exports._renderNotifications = _renderNotifications;
226+
}
227+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div id="notification-bar" tabindex="0">
2+
<div class="content-container">
3+
</div>
4+
<div class="close-icon-container">
5+
<button type="button" class="close-icon" tabIndex="0">&times;</button>
6+
</div>
7+
</div>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) 2019 - present Adobe. All rights reserved.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a
5+
* copy of this software and associated documentation files (the "Software"),
6+
* to deal in the Software without restriction, including without limitation
7+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
8+
* and/or sell copies of the Software, and to permit persons to whom the
9+
* Software is furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20+
* DEALINGS IN THE SOFTWARE.
21+
*
22+
*/
23+
/**
24+
* module for displaying in-app notifications
25+
*
26+
*/
27+
define(function (require, exports, module) {
28+
require("./banner");
29+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#notification-bar {
2+
display: block;
3+
background-color: #105F9C;
4+
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.53);
5+
padding: 5px 0px;
6+
width: 100%;
7+
min-height: 39px;
8+
position: absolute;
9+
z-index: 16;
10+
left: 0px;
11+
bottom: 25px;
12+
outline: none;
13+
overflow: hidden;
14+
color: rgb(51, 51, 51);
15+
background: rgb(223, 226, 226);
16+
}
17+
18+
.dark #notification-bar {
19+
color: #ccc;
20+
background: #2c2c2c;
21+
}
22+
23+
#notification-bar .content-container {
24+
padding: 5px 10px;
25+
float: left;
26+
width: 100%;
27+
}
28+
29+
#notification-bar .close-icon-container {
30+
height: auto;
31+
position: absolute;
32+
float: right;
33+
text-align: center;
34+
width: auto;
35+
min-width: 66px;
36+
right: 20px;
37+
top: 10px;
38+
}
39+
40+
#notification-bar .close-icon-container .close-icon {
41+
display: block;
42+
font-size: 18px;
43+
line-height: 18px;
44+
text-decoration: none;
45+
width: 18px;
46+
height: 18px;
47+
background-color: transparent;
48+
border: none;
49+
padding: 0px; /*This is needed to center the icon*/
50+
float: right;
51+
}
52+
53+
.dark #notification-bar .close-icon-container .close-icon {
54+
color: #ccc;
55+
}
56+

0 commit comments

Comments
 (0)