diff --git a/CHANGELOG.md b/CHANGELOG.md index d532046..461de5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Release Notes for "Intercom Messenger" plugin +## 2.1.0 - 2024-06-16 + +### Added + +- Added new configuration settings. + +### Changed + +- Updated plugin for Craft 5. +- Removed broken links. + ## 2.0.0 - 2022-06-22 ### Changed diff --git a/README.md b/README.md index eb3e5a5..cdef692 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Intercom Messenger plugin for Craft CMS 4.x +# Intercom Messenger plugin for Craft CMS 4.x|5.x This is Intercom: the Business Messenger you and your customers will love @@ -13,7 +13,7 @@ For more information visit: [Intercom.com](https://www.intercom.com/). ## Requirements -This plugin requires Craft CMS 4.0.0 or later. +This plugin requires Craft CMS 4.0.0|5.0.0 or later. You will need an Intercom [trial](https://www.intercom.com/pricing) or [subscription](https://www.intercom.com/pricing) in order to use this plugin. Or you can create a free [developer account](https://app.intercom.com/a/developer-signup) to build and test Intercom Messenger in development environment before signing up for a subcription. diff --git a/composer.json b/composer.json index e2d318f..209b06f 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "jimstrike/craft-intercom-messenger", "description": "Intercom Messenger", "type": "craft-plugin", - "version": "2.0.0", + "version": "2.1.0", "keywords": [ "craft", "cms", @@ -33,7 +33,7 @@ ], "require": { "php": "^8.0.2", - "craftcms/cms": "^4.0.0" + "craftcms/cms": "^4.0.0|^5.0.0" }, "autoload": { "psr-4": { @@ -48,4 +48,4 @@ "changelogUrl": "https://github.com/jimstrike/craft-intercom-messenger/blob/main/CHANGELOG.md", "documentationUrl": "https://github.com/jimstrike/craft-intercom-messenger/blob/main/README.md" } -} \ No newline at end of file +} diff --git a/src/Plugin.php b/src/Plugin.php index 7295052..4818c32 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -1,6 +1,6 @@ 20, + /** + * Action color + * + * Used in button links and more to highlight and emphasise. Default color is: #8f00b3 + * + * @var string + */ + 'actionColor' => '#8f00b3', + + /** + * Background color + * + * Used behind your team profile and other attributes. Default color is: #8f00b3 + * + * @var string + */ + 'backgroundColor' => '#8f00b3', + + /** + * Use your own theme color? + * + * Allows you to overwrite Intercom's color settings and set your own "Action color" and "Background color" above. + * + * @var bool + */ + 'useOwnThemeColor' => false, + /** * Show standard launcher only when a user scrolls to the bottom of the page? * @@ -209,6 +236,20 @@ * @var bool */ 'hideDefaultLauncher' => false, + + /** + * API regional location + * + * If you are using a data center hosted in one of the regional locations listed below, you will need to choose the associated API base. + * Select the default value if in doubt or preview and test before enabling the plugin. + * + * Available options are: 'default', 'us', 'eu', 'au'. + * + * A null or empty value will trigger 'default' option. + * + * @var string + */ + 'apiRegionalLocation' => 'default' ]; //** Multi-site settings */ @@ -240,4 +281,4 @@ (2) => [ 'enabled' => false, ], -]; */ \ No newline at end of file +]; */ diff --git a/src/controllers/Controller.php b/src/controllers/Controller.php index 3a1f0cf..88ad1bf 100644 --- a/src/controllers/Controller.php +++ b/src/controllers/Controller.php @@ -1,6 +1,6 @@ 'https://www.intercom.com/', 'help_center' => 'https://www.intercom.com/help', 'messenger' => 'https://www.intercom.com/help/en/collections/2094767-the-intercom-messenger', - 'customize_basics' => 'https://www.intercom.com/help/en/articles/178-customize-the-intercom-messenger-basics', - 'find_app_id' => 'https://www.intercom.com/help/en/articles/3539-where-can-i-find-my-workspace-id-app-id', + 'customize_basics' => 'https://www.intercom.com/help/en/articles/6612589-set-up-and-customize-the-messenger', + // 'find_app_id' => 'https://www.intercom.com/help/en/articles/3539-where-can-i-find-my-workspace-id-app-id', 'trusted_domains' => 'https://www.intercom.com/help/en/articles/4418-list-trusted-domains-you-use-with-intercom', 'identity_verification' => 'https://www.intercom.com/help/en/articles/183-enable-identity-verification-for-web-and-mobile', 'disable_standard_launcher' => 'https://www.intercom.com/help/en/articles/189-turn-off-show-or-hide-the-intercom-messenger', 'signup' => 'https://www.intercom.com/pricing', 'developer_signup' => 'https://app.intercom.com/a/developer-signup', 'developers' => 'https://developers.intercom.com/', -]; \ No newline at end of file + 'logged_in_url_pattern' => 'https://app.intercom.com/a/apps/{YOUR-WORKSPACE-ID}/home', +]; diff --git a/src/helpers/PluginHelper.php b/src/helpers/PluginHelper.php index d33045f..541a368 100644 --- a/src/helpers/PluginHelper.php +++ b/src/helpers/PluginHelper.php @@ -1,6 +1,6 @@ getEdition() != Craft::Pro) { + if (Craft::$app->getEdition() != CmsEdition::Pro) { return []; } @@ -97,7 +103,7 @@ public function getSetupLoggedInUser(int $siteId = null): array */ public function getIdentitySecret(int $siteId = null): string { - if (Craft::$app->getEdition() != Craft::Pro) { + if (Craft::$app->getEdition() != CmsEdition::Pro) { return ''; } @@ -114,7 +120,7 @@ public function getIdentitySecret(int $siteId = null): string */ public function getSetupUserGroups(int $siteId = null): array { - if (Craft::$app->getEdition() !== Craft::Pro) { + if (Craft::$app->getEdition() !== CmsEdition::Pro) { return []; } @@ -204,344 +210,97 @@ public function getVerticalPadding(int $siteId = null): int } /** - * Get show default launcher scroll bottom page only - * * @param int|null $siteId default - * @return bool - */ - public function getShowDefaultLauncherScrollBottomPageOnly(int $siteId = null): bool - { - $setting = $this->_getSetting('showDefaultLauncherScrollBottomPageOnly', $siteId); - - return (bool)$setting ?: false; - } - - /** - * Get enable custom launcher * - * @param int|null $siteId default - * @return bool + * @return string */ - public function getEnableCustomLauncher(int $siteId = null): bool + public function getActionColor(int $siteId = null): string { - $setting = $this->_getSetting('enableCustomLauncher', $siteId); - - return (bool)$setting ?: false; - } + $setting = $this->_getSetting('actionColor', $siteId); + $color = $setting ?: $this->getDefaultThemeColor(); - /** - * Get hide default launcher - * - * @param int|null $siteId default - * @return bool - */ - public function getHideDefaultLauncher(int $siteId = null): bool - { - $setting = $this->_getSetting('hideDefaultLauncher', $siteId); - - return (bool)$setting ?: false; + return PluginHelper::color($color); } - // Helper set methods - // ========================================================================= - /** - * Set array value for property - * - * @param string $field - * @param mixed $value - * @param int $siteId default + * @param int|null $siteId default * - * @return array - * $this->appId = [ - * ['siteId' => 'value'], - * ['siteId' => 'value'] - * ] + * @return string */ - public function makeValue(string $field, $value, int $siteId = 1): array + public function getBackgroundColor(int $siteId = null): string { - // Sanitize value - $value = $this->_sanitizeValue($field, $value, $siteId); - - $base = \is_array($this->$field) ? $this->$field : []; - - $replace = [($siteId) => (\is_string($value) ? \trim($value) : ($value ?? ''))]; + $setting = $this->_getSetting('backgroundColor', $siteId); + $color = $setting ?: $this->getDefaultThemeColor(); - $a = \array_replace($base, $replace) ?? (array)$this->$field; - - \ksort($a); - - return $a; + return PluginHelper::color($color); } - // Misc helpers - // ========================================================================= - /** - * Check is enabled by sections + * Use your own theme color * * @param int|null $siteId default * @return bool */ - public function isEnabledBySections(int $siteId = null): bool + public function getUseOwnThemeColor(int $siteId = null): bool { - $sections = \array_keys(\array_filter((array)$this->getSections($siteId))); - - if (!$sections) { - return true; - } - - $request = Craft::$app->getRequest(); - - $uri = \implode('/', (array)$request->getSegments()) ?: '__home__'; - $entry = \craft\elements\Entry::find()->uri($uri)->one(); - - if ($entry instanceof \craft\elements\Entry) { - if (\in_array(($entry->sectionId ?? null), $sections)) { - return true; - } - } - - return false; + $setting = $this->_getSetting('useOwnThemeColor', $siteId); + + return (bool)$setting ?: false; } /** - * Check is enabled by URL paths + * Get show default launcher scroll bottom page only * * @param int|null $siteId default * @return bool */ - public function isEnabledByUrlPaths(int $siteId = null): bool + public function getShowDefaultLauncherScrollBottomPageOnly(int $siteId = null): bool { - $urlPaths = $this->getUrlPaths($siteId); + $setting = $this->_getSetting('showDefaultLauncherScrollBottomPageOnly', $siteId); - if (!$urlPaths) { - return true; - } - - $hasActiveUrlPaths = $this->_hasActiveUrlPaths($siteId, $urlPaths); - - if (!$hasActiveUrlPaths) { - return true; - } - - $route = '/' . trim(Craft::$app->getRequest()->getFullPath(), '/'); - - foreach ($urlPaths as $urlPath) { - $path = $urlPath[0] ?? '/'; - $active = (bool)$urlPath[1] ?? false; - - if ($active && $path == $route) { - return true; break; - } - } - - return false; + return (bool)$setting ?: false; } /** - * Check is enabled by multiple fields + * Get enable custom launcher * * @param int|null $siteId default * @return bool */ - public function isEnabled(int $siteId = null): bool - { - $hasActiveUrlPaths = $this->_hasActiveUrlPaths($siteId); - - if ($hasActiveUrlPaths) { - return $this->getEnabled($siteId) - && $this->isEnabledByUrlPaths($siteId) - ; - } - - $isEnabledBySections = $this->isEnabledBySections($siteId); - - return $this->getEnabled($siteId) - && $this->isEnabledBySections($siteId) - ; - } - - /** - * User field map - * - * @return array - */ - public function setupLoggedInUserFieldMap(): array - { - return [ - 'name' => Plugin::t('settings.setup_logged_in_user.name.label'), - 'email' => Plugin::t('settings.setup_logged_in_user.email.label'), - 'dateCreated' => Plugin::t('settings.setup_logged_in_user.date_created.label'), - 'userId' => Plugin::t('settings.setup_logged_in_user.user_id.label'), - 'userHash' => Plugin::t('settings.setup_logged_in_user.user_hash.label'), - ]; - } - - /** - * Get alignment options - * - * @return array - */ - public function alignmentOptions(): array + public function getEnableCustomLauncher(int $siteId = null): bool { - return [ - [ - 'value' => 'right', - 'label' => Plugin::t('settings.alignment.option.label.right'), - ], - [ - 'value' => 'left', - 'label' => Plugin::t('settings.alignment.option.label.left'), - ] - ]; + $setting = $this->_getSetting('enableCustomLauncher', $siteId); + + return (bool)$setting ?: false; } - // Private methods - // ========================================================================= - /** - * Check if any active URL paths + * Get hide default launcher * * @param int|null $siteId default - * @param array|null $urlPaths default * @return bool */ - private function _hasActiveUrlPaths(int $siteId = null, array $urlPaths = null): bool - { - if (!$urlPaths) { - $urlPaths = $this->getUrlPaths($siteId); - } - - if (!$urlPaths) { - return false; - } - - foreach ($urlPaths as $urlPath) { - $path = $urlPath[0] ?? '/'; - $active = (bool)$urlPath[1] ?? false; - - if ($path && $active) { - return true; break; - } - } - - return false; - } - - /** - * Sanitize value - * - * @param string $field - * @param mixed $value - * @param int $siteId - * @return mixed - */ - private function _sanitizeValue(string $field, $value, int $siteId = 1) + public function getHideDefaultLauncher(int $siteId = null): bool { - // URL paths - if ($field == 'urlPaths') { - if (!empty($value) && is_array($value)) { - $value = call_user_func(function() use ($value) { - foreach ($value as $key => $row) { - if (!isset($row[0])) { - continue; - } - - if (empty($row[0])) { - unset($value[$key]); - continue; - } + $setting = $this->_getSetting('hideDefaultLauncher', $siteId); - $parsed = parse_url($row[0]); - $path = $parsed['path'] ?? '/'; - $path = '/' . trim(trim($path), '/'); - - $value[$key][0] = $path; - } - - $col1 = array_column($value, 0); - $col2 = array_column($value, 1); - - $col1 = array_unique($col1); - - $a = []; - - foreach ($col1 as $key => $col) { - $a[$key][0] = $col; - $a[$key][1] = $col2[$key]; - } - - return $a; - }); - } - } - - // Identity secret - if ($field == 'identitySecret') { - $value = call_user_func(function() use ($value, $siteId) { - if (empty($value)) { - return ''; - } - - $mask = PluginHelper::mask($value); - - if ($value == $mask) { - $value = $this->getIdentitySecret($siteId); - } - - return $value; - }); - } - - // Horizontal / Vertical padding - if ($field == 'horizontalPadding' || $field == 'verticalPadding') { - $value = (int)$value; - } - - // Hide default launcher - if ($field == 'hideDefaultLauncher') { - if (!$this->getEnableCustomLauncher($siteId)) { - $value = false; - } - - if (!$this->getEnableCustomLauncher($siteId) && !$this->getShowDefaultLauncherScrollBottomPageOnly($siteId)) { - $value = false; - } - - if ($this->getShowDefaultLauncherScrollBottomPageOnly($siteId)) { - $value = true; - } - } - - // -- - - return $value; + return (bool)$setting ?: false; } /** - * Get setting + * Get API reginal location * - * @param string $setting * @param int|null $siteId default - * @return mixed + * @return string */ - private function _getSetting(string $setting, int $siteId = null) + public function getApiRegionalLocation(int $siteId = null): string { - if (empty($siteId)) { - $siteId = Craft::$app->getSites()->getCurrentSite()->id ?? null; - } - - $configs = Craft::$app->getConfig()->getConfigFromFile(Plugin::$plugin->handle); - - if (isset($configs[$siteId][$setting])) { - return $configs[$siteId][$setting]; - } + $setting = $this->_getSetting('apiRegionalLocation', $siteId); - if (isset($configs[$setting])) { - return $configs[$setting]; + if (!in_array($setting, Plugin::$plugin->messenger->getApiBaseRegionsKeys())) { + $setting = Plugin::$plugin->messenger->getApiDefaultBaseRegionKey(); } - - return $this->$setting[$siteId] ?? ''; + + return $setting; } -} \ No newline at end of file +} diff --git a/src/models/SettingsTrait.php b/src/models/SettingsTrait.php new file mode 100644 index 0000000..4b9311a --- /dev/null +++ b/src/models/SettingsTrait.php @@ -0,0 +1,352 @@ +appId = [ + * ['siteId' => 'value'], + * ['siteId' => 'value'] + * ] + */ + public function makeValue(string $field, $value, int $siteId = 1): array + { + // Sanitize value + $value = $this->_sanitizeValue($field, $value, $siteId); + + $base = \is_array($this->$field) ? $this->$field : []; + + $replace = [($siteId) => (\is_string($value) ? \trim($value) : ($value ?? ''))]; + + $a = \array_replace($base, $replace) ?? (array)$this->$field; + + \ksort($a); + + return $a; + } + + // Misc helpers + // ========================================================================= + + /** + * Check is enabled by sections + * + * @param int|null $siteId default + * @return bool + */ + public function isEnabledBySections(int $siteId = null): bool + { + $sections = \array_keys(\array_filter((array)$this->getSections($siteId))); + + if (!$sections) { + return true; + } + + $request = Craft::$app->getRequest(); + + $uri = \implode('/', (array)$request->getSegments()) ?: '__home__'; + $entry = \craft\elements\Entry::find()->uri($uri)->one(); + + if ($entry instanceof \craft\elements\Entry) { + if (\in_array(($entry->sectionId ?? null), $sections)) { + return true; + } + } + + return false; + } + + /** + * Check is enabled by URL paths + * + * @param int|null $siteId default + * @return bool + */ + public function isEnabledByUrlPaths(int $siteId = null): bool + { + $urlPaths = $this->getUrlPaths($siteId); + + if (!$urlPaths) { + return true; + } + + $hasActiveUrlPaths = $this->_hasActiveUrlPaths($siteId, $urlPaths); + + if (!$hasActiveUrlPaths) { + return true; + } + + $route = '/' . trim(Craft::$app->getRequest()->getFullPath(), '/'); + + foreach ($urlPaths as $urlPath) { + $path = $urlPath[0] ?? '/'; + $active = (bool)$urlPath[1] ?? false; + + if ($active && $path == $route) { + return true; + } + } + + return false; + } + + /** + * Check is enabled by multiple fields + * + * @param int|null $siteId default + * @return bool + */ + public function isEnabled(int $siteId = null): bool + { + $hasActiveUrlPaths = $this->_hasActiveUrlPaths($siteId); + + if ($hasActiveUrlPaths) { + return $this->getEnabled($siteId) + && $this->isEnabledByUrlPaths($siteId) + ; + } + + $isEnabledBySections = $this->isEnabledBySections($siteId); + + return $this->getEnabled($siteId) + && $this->isEnabledBySections($siteId) + ; + } + + /** + * User field map + * + * @return array + */ + public function setupLoggedInUserFieldMap(): array + { + return [ + 'name' => Plugin::t('settings.setup_logged_in_user.name.label'), + 'email' => Plugin::t('settings.setup_logged_in_user.email.label'), + 'dateCreated' => Plugin::t('settings.setup_logged_in_user.date_created.label'), + 'userId' => Plugin::t('settings.setup_logged_in_user.user_id.label'), + 'userHash' => Plugin::t('settings.setup_logged_in_user.user_hash.label'), + ]; + } + + /** + * Get alignment options + * + * @return array + */ + public function alignmentOptions(): array + { + return [ + [ + 'value' => 'right', + 'label' => Plugin::t('settings.alignment.option.label.right'), + ], + [ + 'value' => 'left', + 'label' => Plugin::t('settings.alignment.option.label.left'), + ] + ]; + } + + /** + * Get API regional location options + * + * @return array + */ + public function apiRegionalLocationOptions(): array + { + $a = []; + + $regions = Plugin::$plugin->messenger->getApiBaseRegions(); + + foreach ($regions as $key => $region) { + $a[] = [ + 'value' => $key, + 'label' => Plugin::t($region['name']), + ]; + } + + return $a; + } + + /** + * Get default theme color + * + * @return string + */ + public function getDefaultThemeColor(): string + { + return Plugin::$plugin->messenger->getDefaultThemeColor(); + } + + // Private methods + // ========================================================================= + + /** + * Check if any active URL paths + * + * @param int|null $siteId default + * @param array|null $urlPaths default + * @return bool + */ + private function _hasActiveUrlPaths(int $siteId = null, array $urlPaths = null): bool + { + if (!$urlPaths) { + $urlPaths = $this->getUrlPaths($siteId); + } + + if (!$urlPaths) { + return false; + } + + foreach ($urlPaths as $urlPath) { + $path = $urlPath[0] ?? '/'; + $active = (bool)$urlPath[1] ?? false; + + if ($path && $active) { + return true; + } + } + + return false; + } + + /** + * Sanitize value + * + * @param string $field + * @param mixed $value + * @param int $siteId + * @return mixed + */ + private function _sanitizeValue(string $field, $value, int $siteId = 1) + { + // URL paths + if ($field == 'urlPaths') { + if (!empty($value) && is_array($value)) { + $value = call_user_func(function() use ($value) { + foreach ($value as $key => $row) { + if (!isset($row[0])) { + continue; + } + + if (empty($row[0])) { + unset($value[$key]); + continue; + } + + $parsed = parse_url($row[0]); + $path = $parsed['path'] ?? '/'; + $path = '/' . trim(trim($path), '/'); + + $value[$key][0] = $path; + } + + $col1 = array_column($value, 0); + $col2 = array_column($value, 1); + + $col1 = array_unique($col1); + + $a = []; + + foreach ($col1 as $key => $col) { + $a[$key][0] = $col; + $a[$key][1] = $col2[$key]; + } + + return $a; + }); + } + } + + // Identity secret + if ($field == 'identitySecret') { + $value = call_user_func(function() use ($value, $siteId) { + if (empty($value)) { + return ''; + } + + $mask = PluginHelper::mask($value); + + if ($value == $mask) { + $value = $this->getIdentitySecret($siteId); + } + + return $value; + }); + } + + // Horizontal / Vertical padding + if ($field == 'horizontalPadding' || $field == 'verticalPadding') { + $value = (int)$value; + } + + // Hide default launcher + if ($field == 'hideDefaultLauncher') { + if (!$this->getEnableCustomLauncher($siteId)) { + $value = false; + } + + if (!$this->getEnableCustomLauncher($siteId) && !$this->getShowDefaultLauncherScrollBottomPageOnly($siteId)) { + $value = false; + } + + if ($this->getShowDefaultLauncherScrollBottomPageOnly($siteId)) { + $value = true; + } + } + + // -- + + return $value; + } + + /** + * Get setting + * + * @param string $setting + * @param int|null $siteId default + * @return mixed + */ + private function _getSetting(string $setting, int $siteId = null) + { + if (empty($siteId)) { + $siteId = Craft::$app->getSites()->getCurrentSite()->id ?? null; + } + + $configs = Craft::$app->getConfig()->getConfigFromFile(Plugin::$plugin->handle); + + if (isset($configs[$siteId][$setting])) { + return $configs[$siteId][$setting]; + } + + if (isset($configs[$setting])) { + return $configs[$setting]; + } + + return $this->$setting[$siteId] ?? ''; + } +} diff --git a/src/services/Messenger.php b/src/services/Messenger.php index 6c9c4ca..8a9fc58 100644 --- a/src/services/Messenger.php +++ b/src/services/Messenger.php @@ -1,6 +1,6 @@ $settings->getHideDefaultLauncher($siteId), ]; + if ('default' !== $settings->getApiRegionalLocation($siteId)) { + $a = array_merge([ + 'api_base' => $this->getApiBaseUrl($settings->getApiRegionalLocation($siteId)), + ], $a); + } + if ($settings->getEnableCustomLauncher($siteId)) { $a = array_merge($a, [ 'custom_launcher_selector' => '[' . $this->customLauncherSelector() . ']', ]); } - if (Craft::$app->getEdition() !== Craft::Pro) { + if (true === $settings->getUseOwnThemeColor($siteId)) { + if ($settings->getActionColor($siteId)) { + $a = array_merge($a, [ + 'action_color' => $settings->getActionColor($siteId), + ]); + } + + if ($settings->getBackgroundColor($siteId)) { + $a = array_merge($a, [ + 'background_color' => $settings->getBackgroundColor($siteId), + ]); + } + } + + if (Craft::$app->getEdition() !== CmsEdition::Pro) { return $a; } @@ -160,7 +183,7 @@ private function _user(Settings $settings, int $siteId = null): array { $a = []; - if (Craft::$app->getEdition() !== Craft::Pro) { + if (Craft::$app->getEdition() !== CmsEdition::Pro) { return $a; } @@ -245,7 +268,7 @@ private function _user(Settings $settings, int $siteId = null): array */ private function _userBelongsToSetupUserGroups(int $siteId = null, Settings $settings = null, \craft\elements\User $user = null): bool { - if (Craft::$app->getEdition() !== Craft::Pro) { + if (Craft::$app->getEdition() !== CmsEdition::Pro) { return false; } @@ -270,7 +293,7 @@ private function _userBelongsToSetupUserGroups(int $siteId = null, Settings $set } if ($user->isInGroup($groupId)) { - return true; break; + return true; } } @@ -284,10 +307,12 @@ private function _userBelongsToSetupUserGroups(int $siteId = null, Settings $set */ private function _script(): string { - return "/** {COMMENT} */ " - . "window.intercomSettings = {YOUR_OBJECT};" - . "(function(){var w=window;var ic=w.Intercom;if(typeof ic==='function'){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/{YOUR_APP_ID}';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})(); - "; + $heredoc = <<