onesignal-web-sdk
Version:
Web push notifications from OneSignal.
490 lines (455 loc) • 22 kB
text/typescript
import {
AppUserConfig, AppConfig, AppUserConfigPromptOptions, ServerAppConfigPrompt,
ConfigIntegrationKind, ServerAppConfig, AppUserConfigCustomLinkOptions } from "../models/AppConfig";
import { WindowEnvironmentKind } from "../models/WindowEnvironmentKind";
import { SdkInitError, SdkInitErrorKind } from "../errors/SdkInitError";
import SdkEnvironment from "../managers/SdkEnvironment";
import OneSignalUtils from "../utils/OneSignalUtils";
import Utils from "../utils/Utils";
import MainHelper from './MainHelper';
export enum IntegrationConfigurationKind {
/**
* Configuration comes from the dashboard only.
*/
Dashboard,
/**
* Configuration comes from user-provided JavaScript code only.
*/
JavaScript
}
export interface IntegrationCapabilities {
configuration: IntegrationConfigurationKind;
}
export class ConfigHelper {
public static async getAppConfig(userConfig: AppUserConfig,
downloadServerAppConfig: (appId: string) => Promise<ServerAppConfig>): Promise<AppConfig> {
try {
if (!userConfig || !userConfig.appId || !OneSignalUtils.isValidUuid(userConfig.appId))
throw new SdkInitError(SdkInitErrorKind.InvalidAppId);
const serverConfig = await downloadServerAppConfig(userConfig.appId);
const appConfig = this.getMergedConfig(userConfig, serverConfig);
this.checkRestrictedOrigin(appConfig);
return appConfig;
}
catch (e) {
if (e) {
if (e.code === 1)
throw new SdkInitError(SdkInitErrorKind.InvalidAppId);
else if (e.code === 2)
throw new SdkInitError(SdkInitErrorKind.AppNotConfiguredForWebPush);
}
throw e;
}
}
public static checkRestrictedOrigin(appConfig: AppConfig) {
if (appConfig.restrictedOriginEnabled) {
if (SdkEnvironment.getWindowEnv() !== WindowEnvironmentKind.ServiceWorker) {
if (window.top === window &&
!Utils.contains(window.location.hostname, ".os.tc") &&
!Utils.contains(window.location.hostname, ".onesignal.com") &&
!this.doesCurrentOriginMatchConfigOrigin(appConfig.origin)) {
throw new SdkInitError(SdkInitErrorKind.WrongSiteUrl, {
siteUrl: appConfig.origin
});
}
}
}
}
public static doesCurrentOriginMatchConfigOrigin(configOrigin: string): boolean {
try {
return location.origin === new URL(configOrigin).origin;
} catch (e) {
return false;
}
}
public static getIntegrationCapabilities(integration: ConfigIntegrationKind): IntegrationCapabilities {
switch (integration) {
case ConfigIntegrationKind.Custom:
case ConfigIntegrationKind.WordPress:
return {configuration: IntegrationConfigurationKind.JavaScript};
default:
return {configuration: IntegrationConfigurationKind.Dashboard};
}
}
public static getMergedConfig(userConfig: AppUserConfig, serverConfig: ServerAppConfig): AppConfig {
const configIntegrationKind = this.getConfigIntegrationKind(serverConfig);
const subdomain = this.getSubdomainForConfigIntegrationKind(configIntegrationKind, userConfig, serverConfig);
const allowLocalhostAsSecureOrigin = (
serverConfig.config.setupBehavior ?
serverConfig.config.setupBehavior.allowLocalhostAsSecureOrigin :
userConfig.allowLocalhostAsSecureOrigin
);
const isUsingSubscriptionWorkaround = OneSignalUtils.internalIsUsingSubscriptionWorkaround(
subdomain,
allowLocalhostAsSecureOrigin
);
const mergedUserConfig = this.getUserConfigForConfigIntegrationKind(
configIntegrationKind, userConfig, serverConfig, isUsingSubscriptionWorkaround);
return {
appId: serverConfig.app_id,
subdomain,
origin: serverConfig.config.origin,
httpUseOneSignalCom: serverConfig.config.http_use_onesignal_com,
cookieSyncEnabled: serverConfig.features.cookie_sync.enable,
restrictedOriginEnabled: serverConfig.features.restrict_origin && serverConfig.features.restrict_origin.enable,
metrics: {
enable: serverConfig.features.metrics.enable,
mixpanelReportingToken: serverConfig.features.metrics.mixpanel_reporting_token
},
safariWebId: serverConfig.config.safari_web_id,
vapidPublicKey: serverConfig.config.vapid_public_key,
onesignalVapidPublicKey: serverConfig.config.onesignal_vapid_public_key,
emailAuthRequired: serverConfig.features.email && serverConfig.features.email.require_auth,
userConfig: mergedUserConfig,
enableOnSession: serverConfig.features.enable_on_session || false,
};
}
public static getConfigIntegrationKind(serverConfig: ServerAppConfig): ConfigIntegrationKind {
if (serverConfig.config.integration)
return serverConfig.config.integration.kind;
return ConfigIntegrationKind.Custom;
}
public static getCustomLinkConfig(serverConfig: ServerAppConfig): AppUserConfigCustomLinkOptions {
const initialState: AppUserConfigCustomLinkOptions = {
enabled: false,
style: "button",
size: "medium",
unsubscribeEnabled: false,
text: {
explanation: "",
subscribe: "",
unsubscribe: "",
},
color: {
button: "",
text: "",
}
};
if (!serverConfig || !serverConfig.config ||
!serverConfig.config.staticPrompts || !serverConfig.config.staticPrompts.customlink ||
!serverConfig.config.staticPrompts.customlink.enabled) {
return initialState;
}
const customlink = serverConfig.config.staticPrompts.customlink;
return {
enabled: customlink.enabled,
style: customlink.style,
size: customlink.size,
unsubscribeEnabled: customlink.unsubscribeEnabled,
text: customlink.text ? {
subscribe: customlink.text.subscribe,
unsubscribe: customlink.text.unsubscribe,
explanation: customlink.text.explanation,
} : initialState.text,
color: customlink.color ? {
button: customlink.color.button,
text: customlink.color.text,
} : initialState.color,
}
}
public static injectDefaultsIntoPromptOptions(
promptOptions: AppUserConfigPromptOptions | undefined,
defaultsFromServer: ServerAppConfigPrompt,
wholeUserConfig: AppUserConfig,
isUsingSubscriptionWorkaround: boolean = false,
): AppUserConfigPromptOptions | undefined {
let customlinkUser: AppUserConfigCustomLinkOptions = { enabled: false };
if (promptOptions && promptOptions.customlink) {
customlinkUser = promptOptions.customlink;
}
const customlinkDefaults = defaultsFromServer.customlink;
const promptOptionsConfig: AppUserConfigPromptOptions = {
...promptOptions,
customlink: {
enabled: Utils.getValueOrDefault(customlinkUser.enabled, customlinkDefaults.enabled),
style: Utils.getValueOrDefault(customlinkUser.style, customlinkDefaults.style),
size: Utils.getValueOrDefault(customlinkUser.size, customlinkDefaults.size),
unsubscribeEnabled: Utils.getValueOrDefault(customlinkUser.unsubscribeEnabled,
customlinkDefaults.unsubscribeEnabled),
text: {
subscribe: Utils.getValueOrDefault(customlinkUser.text ? customlinkUser.text.subscribe : undefined,
customlinkDefaults.text.subscribe),
unsubscribe: Utils.getValueOrDefault(customlinkUser.text ? customlinkUser.text.unsubscribe: undefined,
customlinkDefaults.text.unsubscribe),
explanation: Utils.getValueOrDefault(customlinkUser.text ? customlinkUser.text.explanation : undefined,
customlinkDefaults.text.explanation),
},
color: {
button: Utils.getValueOrDefault(customlinkUser.color ? customlinkUser.color.button : undefined,
customlinkDefaults.color.button),
text: Utils.getValueOrDefault(customlinkUser.color ? customlinkUser.color.text : undefined,
customlinkDefaults.color.text),
},
}
};
if (promptOptionsConfig.slidedown) {
promptOptionsConfig.slidedown.enabled = !!promptOptionsConfig.slidedown.enabled;
promptOptionsConfig.slidedown.autoPrompt = promptOptionsConfig.slidedown.hasOwnProperty("autoPrompt") ?
!!promptOptionsConfig.slidedown.enabled && !!promptOptionsConfig.slidedown.autoPrompt :
!!promptOptionsConfig.slidedown.enabled;
} else {
promptOptionsConfig.slidedown = MainHelper.getSlidedownPermissionMessageOptions(promptOptionsConfig);
promptOptionsConfig.slidedown.enabled = false;
promptOptionsConfig.slidedown.autoPrompt = false;
}
if (promptOptionsConfig.native) {
promptOptionsConfig.native.enabled = !!promptOptionsConfig.native.enabled;
promptOptionsConfig.native.autoPrompt = promptOptionsConfig.native.hasOwnProperty("autoPrompt") ?
!!promptOptionsConfig.native.enabled && !!promptOptionsConfig.native.autoPrompt :
!!promptOptionsConfig.native.enabled;
} else {
promptOptionsConfig.native = {
enabled: false,
autoPrompt: false,
}
}
/**
* If autoRegister is true, show native prompt for https and slidedown for http ignoring any other related
* prompt options.
*/
if (wholeUserConfig.autoRegister === true) {
if (isUsingSubscriptionWorkaround) {
// disable native prompt
promptOptionsConfig.native.enabled = false;
promptOptionsConfig.native.autoPrompt = false;
// enable slidedown & make it autoPrompt
promptOptionsConfig.slidedown.enabled = true;
promptOptionsConfig.slidedown.autoPrompt = true;
} else {
//enable native prompt & make it autoPrompt
promptOptionsConfig.native.enabled = true;
promptOptionsConfig.native.autoPrompt = true;
//leave slidedown settings without change
}
}
promptOptionsConfig.autoPrompt = promptOptionsConfig.native.autoPrompt ||
promptOptionsConfig.slidedown.autoPrompt;
return promptOptionsConfig;
}
private static getPromptOptionsForDashboardConfiguration(serverConfig: ServerAppConfig): AppUserConfigPromptOptions {
const staticPrompts = serverConfig.config.staticPrompts;
const native = staticPrompts.native ? {
enabled: staticPrompts.native.enabled,
autoPrompt: staticPrompts.native.enabled && staticPrompts.native.autoPrompt !== false,
} : {
enabled: false,
autoPrompt: false,
};
const slidedown = {
enabled: staticPrompts.slidedown.enabled,
// for backwards compatibility if not specifically false, then assume true for autoPrompt on slidedown
autoPrompt: staticPrompts.slidedown.enabled &&
staticPrompts.slidedown.autoPrompt !== false,
actionMessage: staticPrompts.slidedown.actionMessage,
acceptButtonText: staticPrompts.slidedown.acceptButton,
cancelButtonText: staticPrompts.slidedown.cancelButton,
};
return {
autoPrompt: native.autoPrompt || slidedown.autoPrompt,
native,
slidedown,
fullscreen: {
enabled: staticPrompts.fullscreen.enabled,
actionMessage: staticPrompts.fullscreen.actionMessage,
acceptButton: staticPrompts.fullscreen.acceptButton,
cancelButton: staticPrompts.fullscreen.cancelButton,
title: staticPrompts.fullscreen.title,
message: staticPrompts.fullscreen.message,
caption: staticPrompts.fullscreen.caption,
autoAcceptTitle: staticPrompts.fullscreen.autoAcceptTitle,
},
customlink: this.getCustomLinkConfig(serverConfig),
};
}
public static getUserConfigForConfigIntegrationKind(
configIntegrationKind: ConfigIntegrationKind,
userConfig: AppUserConfig,
serverConfig: ServerAppConfig,
isUsingSubscriptionWorkaround: boolean = false,
): AppUserConfig {
const integrationCapabilities = this.getIntegrationCapabilities(configIntegrationKind);
switch (integrationCapabilities.configuration) {
case IntegrationConfigurationKind.Dashboard:
/*
Ignores code-based initialization configuration and uses dashboard configuration only.
*/
return {
appId: serverConfig.app_id,
autoRegister: false,
autoResubscribe: serverConfig.config.autoResubscribe,
path: serverConfig.config.serviceWorker.path,
serviceWorkerPath: serverConfig.config.serviceWorker.workerName,
serviceWorkerUpdaterPath: serverConfig.config.serviceWorker.updaterWorkerName,
serviceWorkerParam: { scope: serverConfig.config.serviceWorker.registrationScope },
subdomainName: serverConfig.config.siteInfo.proxyOrigin,
promptOptions: this.getPromptOptionsForDashboardConfiguration(serverConfig),
welcomeNotification: {
disable: !serverConfig.config.welcomeNotification.enable,
title: serverConfig.config.welcomeNotification.title,
message: serverConfig.config.welcomeNotification.message,
url: serverConfig.config.welcomeNotification.url
},
notifyButton: {
enable: serverConfig.config.staticPrompts.bell.enabled,
displayPredicate: serverConfig.config.staticPrompts.bell.hideWhenSubscribed ?
() => {
return OneSignal.isPushNotificationsEnabled()
.then((isPushEnabled: boolean) => {
/* The user is subscribed, so we want to return "false" to hide the notify button */
return !isPushEnabled;
});
} :
null,
size: serverConfig.config.staticPrompts.bell.size,
position: serverConfig.config.staticPrompts.bell.location,
showCredit: false,
offset: {
bottom: serverConfig.config.staticPrompts.bell.offset.bottom + 'px',
left: serverConfig.config.staticPrompts.bell.offset.left + 'px',
right: serverConfig.config.staticPrompts.bell.offset.right + 'px'
},
colors: {
'circle.background': serverConfig.config.staticPrompts.bell.color.main,
'circle.foreground': serverConfig.config.staticPrompts.bell.color.accent,
'badge.background': 'black',
'badge.foreground': 'white',
'badge.bordercolor': 'black',
'pulse.color': serverConfig.config.staticPrompts.bell.color.accent,
'dialog.button.background.hovering': serverConfig.config.staticPrompts.bell.color.main,
'dialog.button.background.active': serverConfig.config.staticPrompts.bell.color.main,
'dialog.button.background': serverConfig.config.staticPrompts.bell.color.main,
'dialog.button.foreground': 'white',
},
text: {
'tip.state.unsubscribed': serverConfig.config.staticPrompts.bell.tooltip.unsubscribed,
'tip.state.subscribed': serverConfig.config.staticPrompts.bell.tooltip.subscribed,
'tip.state.blocked': serverConfig.config.staticPrompts.bell.tooltip.blocked,
'message.prenotify': serverConfig.config.staticPrompts.bell.tooltip.unsubscribed,
'message.action.subscribing': serverConfig.config.staticPrompts.bell.message.subscribing,
'message.action.subscribed': serverConfig.config.staticPrompts.bell.message.subscribing,
'message.action.resubscribed': serverConfig.config.staticPrompts.bell.message.subscribing,
'message.action.unsubscribed': serverConfig.config.staticPrompts.bell.message.unsubscribing,
'dialog.main.title': serverConfig.config.staticPrompts.bell.dialog.main.title,
'dialog.main.button.subscribe': serverConfig.config.staticPrompts.bell.dialog.main.subscribeButton,
'dialog.main.button.unsubscribe': serverConfig.config.staticPrompts.bell.dialog.main.unsubscribeButton,
'dialog.blocked.title': serverConfig.config.staticPrompts.bell.dialog.blocked.title,
'dialog.blocked.message': serverConfig.config.staticPrompts.bell.dialog.blocked.message,
},
},
persistNotification: serverConfig.config.notificationBehavior ?
serverConfig.config.notificationBehavior.display.persist : undefined,
webhooks: {
cors: serverConfig.config.webhooks.corsEnable,
'notification.displayed': serverConfig.config.webhooks.notificationDisplayedHook,
'notification.clicked': serverConfig.config.webhooks.notificationClickedHook,
'notification.dismissed': serverConfig.config.webhooks.notificationDismissedHook,
},
notificationClickHandlerMatch: serverConfig.config.notificationBehavior ?
serverConfig.config.notificationBehavior.click.match : undefined,
notificationClickHandlerAction: serverConfig.config.notificationBehavior ?
serverConfig.config.notificationBehavior.click.action : undefined,
allowLocalhostAsSecureOrigin: serverConfig.config.setupBehavior ?
serverConfig.config.setupBehavior.allowLocalhostAsSecureOrigin : undefined,
requiresUserPrivacyConsent: userConfig.requiresUserPrivacyConsent
};
case IntegrationConfigurationKind.JavaScript:
/*
Ignores dashboard configuration and uses code-based configuration only.
Except injecting some default values for prompts.
*/
const config = {
...userConfig,
promptOptions: this.injectDefaultsIntoPromptOptions(
userConfig.promptOptions,
serverConfig.config.staticPrompts,
userConfig,
isUsingSubscriptionWorkaround
),
...{
serviceWorkerParam: typeof OneSignal !== 'undefined' && !!OneSignal.SERVICE_WORKER_PARAM
? OneSignal.SERVICE_WORKER_PARAM
: { scope: '/' },
serviceWorkerPath: typeof OneSignal !== 'undefined' && !!OneSignal.SERVICE_WORKER_PATH
? OneSignal.SERVICE_WORKER_PATH
: 'OneSignalSDKWorker.js',
serviceWorkerUpdaterPath: typeof OneSignal !== 'undefined' && !!OneSignal.SERVICE_WORKER_UPDATER_PATH
? OneSignal.SERVICE_WORKER_UPDATER_PATH
: 'OneSignalSDUpdaterKWorker.js',
path: !!userConfig.path ? userConfig.path : '/'
}
};
if (userConfig.hasOwnProperty("autoResubscribe")) {
config.autoResubscribe = !!userConfig.autoResubscribe;
} else if (userConfig.hasOwnProperty("autoRegister")) {
config.autoResubscribe = !!userConfig.autoRegister;
} else {
config.autoResubscribe = !!serverConfig.config.autoResubscribe;
}
return config;
}
}
/**
* Describes how to merge a dashboard-set subdomain with a/lack of user-supplied subdomain.
*/
public static getSubdomainForConfigIntegrationKind(
configIntegrationKind: ConfigIntegrationKind,
userConfig: AppUserConfig,
serverConfig: ServerAppConfig
): string | undefined {
const integrationCapabilities = this.getIntegrationCapabilities(configIntegrationKind);
let userValue: string | undefined = userConfig.subdomainName;
let serverValue: string | undefined = '';
switch (integrationCapabilities.configuration) {
case IntegrationConfigurationKind.Dashboard:
serverValue = serverConfig.config.siteInfo.proxyOriginEnabled ?
serverConfig.config.siteInfo.proxyOrigin :
undefined;
break;
case IntegrationConfigurationKind.JavaScript:
serverValue = serverConfig.config.subdomain;
break;
}
if (serverValue && !this.shouldUseServerConfigSubdomain(userValue, integrationCapabilities)) {
return undefined;
} else {
return serverValue;
}
}
public static shouldUseServerConfigSubdomain(
userProvidedSubdomain: string | undefined,
capabilities: IntegrationCapabilities
): boolean {
switch (capabilities.configuration) {
case IntegrationConfigurationKind.Dashboard:
/*
Dashboard config using the new web config editor always takes precedence.
*/
return true;
case IntegrationConfigurationKind.JavaScript:
/*
* An HTTPS site may be using either a native push integration or a fallback
* subdomain integration. Our SDK decides the integration based on whether
* init option subdomainName appears and the site's protocol.
*
* To avoid having developers write JavaScript to customize the SDK,
* configuration properties like subdomainName are downloaded on page start.
*
* New developers setting up web push can omit subdomainName, but existing
* developers already having written code to configure OneSignal aren't
* removing their code.
*
* When an HTTPS site is configured with a subdomain on the server-side, we do
* not apply it even though we've downloaded this configuration unless the
* user also declares it manually in their initialization code.
*/
switch (location.protocol) {
case 'https:':
return !!userProvidedSubdomain;
case 'http:':
return true;
default:
return false;
}
}
}
}