├── .babelrc ├── .circleci └── config.yml ├── .coveralls.yml ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── jest.config.js ├── package.json ├── playground ├── Playground.tsx ├── index-template.html ├── index.tsx ├── tsconfig.json └── webpack.config.js ├── postcss.config.js ├── rollup.config.js ├── src ├── Intercom.scss ├── Intercom.tsx ├── components │ ├── BorderlessFrameObserver │ │ ├── BorderlessFrameObserver.ts │ │ └── index.ts │ ├── ImportIsolatedRemote │ │ ├── ImportIsolatedRemote.scss │ │ ├── ImportIsolatedRemote.tsx │ │ ├── index.ts │ │ └── tests │ │ │ └── ImportIsolatedRemote.test.tsx │ ├── Portal │ │ ├── Portal.tsx │ │ ├── index.ts │ │ └── tests │ │ │ └── Portal.test.tsx │ └── index.ts ├── config │ └── styles.d.ts ├── index.ts ├── tests │ └── Intercom.test.tsx └── utilities │ ├── classNames.ts │ ├── getIntercomFromFrame.ts │ ├── index.ts │ ├── injectCustomStyles.ts │ ├── objectEqual.ts │ └── types.ts ├── tests └── build.test.js ├── testsSetup.js ├── tsconfig.json ├── tslint.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-decorators-legacy", "transform-object-rest-spread"] 4 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:8 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test:ci 38 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: BllFuS1QW9kgfJYLPP45nEYAlnRRJUAXN 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "plugin:prettier/recommended"], 3 | "parser": "typescript-eslint-parser", 4 | "rules": { 5 | "no-undef": "off", 6 | "max-len": "off", 7 | "no-multi-str": "off", 8 | "object-curly-spacing": ["error", "never"], 9 | "react/jsx-filename-extension": "off", 10 | "import/no-unresolved": "off", 11 | "import/extensions": "off", 12 | "import/prefer-default-export": "off", 13 | "jsx-a11y/anchor-is-valid": "off", 14 | "react/no-unescaped-entities": "off", 15 | "react/sort-comp": "off", 16 | "no-restricted-globals": "off", 17 | "no-unused-vars": "off", 18 | "import/no-extraneous-dependencies": [ 19 | "error", 20 | { 21 | "devDependencies": ["**/*.test.tsx"] 22 | } 23 | ], 24 | "no-use-before-define": "off", 25 | "no-restricted-syntax": "off", 26 | "no-continue": "off" 27 | }, 28 | "env": { 29 | "browser": true, 30 | "jest": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | yarn-error.log 4 | build 5 | coverage 6 | .rpt2_cache 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": false 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "files.insertFinalNewline": true, 4 | "files.trimTrailingWhitespace": true, 5 | "editor.formatOnSave": true, 6 | "prettier.stylelintIntegration": true, 7 | "prettier.eslintIntegration": true, 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "typescript", 12 | "typescriptreact" 13 | ], 14 | 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # intercom-react 2 | 3 | [![CircleCI](https://circleci.com/gh/kvendrik/intercom-react.svg?style=svg)](https://circleci.com/gh/kvendrik/intercom-react) 4 | [![Coverage Status](https://coveralls.io/repos/github/kvendrik/intercom-react/badge.svg?branch=master)](https://coveralls.io/github/kvendrik/intercom-react?branch=master) 5 | ![Bundle Size](https://img.shields.io/badge/gzip%20size-3.42%20kB-44cc11.svg) 6 | [![NPM Version](https://badge.fury.io/js/intercom-react.svg)](https://yarnpkg.com/en/package/intercom-react) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | An [Intercom](http://intercom.com/) component for React that truly encapsulates the library and makes it work like a "real" React component. 10 | 11 | ## Installation 12 | 13 | ``` 14 | yarn add intercom-react 15 | ``` 16 | 17 | _️️⚠️ Before you install the library make sure to read the [notes on reliability](#️-a-few-notes-on-reliability)._ 18 | 19 | ## Setup 20 | 21 | ```tsx 22 | {}} 32 | onClose={() => {}} 33 | onUnreadCountChange={unreadCount => {}} 34 | onInitialization={intercom => {}} 35 | /> 36 | ``` 37 | 38 | * `appId`: the ID of your app. 39 | * `user` (optional): all user data. If this changes during the lifecycle the component will call `intercom('update', userData)`. 40 | * `open` (optional): whether Intercom is showing or not. 41 | * `onOpen` (optional): called when intercom opens. 42 | * `onClose` (optional): called when intercom closes. 43 | * `onUnreadCountChange` (optional): called when the unread count changes. 44 | * `onInitialization` (optional): called when intercom has initialized. The component passes the `intercom` method to this callback in case you require advanced usage like [emitting events](https://developers.intercom.com/docs/intercom-javascript#section-intercomtrackevent) or [pre-populating content](https://developers.intercom.com/docs/intercom-javascript#section-intercomshownewmessage). 45 | * `launcher` (optional): whether a launcher button should be shown. Defaults to `true`. 46 | 47 | ## How is this different? 48 | 49 | Intercom is the support tool of choice for myself and the companies I work for (because its an amazing tool!). An issue I have always had with their library is that its not particularly React friendly. There is a set of React Intercom libraries out there but most of them just mount Intercom to the global scope. 50 | 51 | What this means is that if you would get into a situation where you would have to unmount Intercom the three DOM nodes the library mounts (`#intercom-container`, `#intercom-stylesheet` and `#intercom-frame`) will stay mounted, as will the four event listeners the library mounts (2x `beforeunload` and 2x `message`). In other words the existing components are not unmountable. 52 | 53 | I wrote this component to create an isolated Intercom component that cleans up after itself when unmounted for a "true" React experience. 54 | 55 | ## ⚠️ A few notes on reliability 56 | 57 | The main purpose of this component is to provide a way for you to integrate Intercom into your project without having it live in the global scope and it therefor being unmountable. 58 | 59 | Getting that to work took quite a bit of [reverse engineering](https://github.com/kvendrik/intercom-react/pull/15) and I haven't been able to find a way to include a specific version of the library yet which means that **things might stop working as new versions of the Intercom library release** and that **the library should therefor be viewed as experimental**. 60 | 61 | I would therefor recommend that you only use this library if you have a very solid reason for needing Intercom to be unmountable. If not I recommend you use a solution like [`react-intercom`](https://github.com/nhagen/react-intercom) which simply mounts Intercom to the global scope. 62 | 63 | Having that said I appreciate your interest in the library and look forward to hearing about your experience with it 🙌 . 64 | 65 | ## 🏗 Contributing 66 | 67 | 1. Make your changes. 68 | 2. Check your changes in the playground (`yarn playground`). 69 | 3. Build using `yarn build` and change your playground `Intercom` import to the build by changing the path to `../`. 70 | 4. Test you changes in/on multiple browsers and devices. 71 | 5. Add/Alter the appropriate tests. 72 | 6. Make sure all tests pass (`yarn lint && yarn test`). 73 | 7. Create a PR. 74 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleDirectories: ['node_modules'], 3 | setupTestFrameworkScriptFile: '/testsSetup.js', 4 | moduleNameMapper: { 5 | '\\.(scss|css)$': 'identity-obj-proxy', 6 | }, 7 | transform: { 8 | '^.+\\.tsx?$': 'ts-jest', 9 | }, 10 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 11 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intercom-react", 3 | "version": "1.0.0-alpha.6", 4 | "description": "An Intercom component for React.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/kvendrik/intercom-react.git" 8 | }, 9 | "keywords": ["intercom", "react", "live", "chat", "support"], 10 | "main": "build/index.js", 11 | "module": "build/index.es.js", 12 | "types": "build/types/index.d.ts", 13 | "author": "Koen Vendrik ", 14 | "license": "MIT", 15 | "scripts": { 16 | "playground": 17 | "webpack-dev-server --hot --inline --config ./playground/webpack.config.js", 18 | "lint": "yarn lint:ts && yarn lint:tslint && yarn lint:eslint", 19 | "lint:ts": "tsc --noEmit", 20 | "lint:eslint": 21 | "eslint src --ext .tsx --ext .ts && eslint playground --ext .tsx --ext .ts", 22 | "lint:tslint": 23 | "tslint -c tslint.json --project tsconfig.json 'src/**/*.ts' 'src/**/*.tsx' --exclude 'src/**/*.d.ts' && tslint -c tslint.json --project tsconfig.json 'playground/**/*.ts' 'playground/**/*.tsx' --exclude 'playground/**/*.d.ts'", 24 | "test": "jest", 25 | "test:bundle": "yarn build && bundlesize", 26 | "test:debug": "jest", 27 | "test:coverage": "yarn test --coverage", 28 | "test:coveralls": 29 | "yarn test --coverage --coverageReporters=text-lcov | coveralls", 30 | "test:ci": 31 | "yarn lint && yarn test && yarn test:coveralls && yarn test:bundle", 32 | "build": "yarn clean && rollup -c && mv build/src build/types", 33 | "clean": "rm -rf build" 34 | }, 35 | "bundlesize": [ 36 | { 37 | "path": "build/index.js", 38 | "maxSize": "4 kB" 39 | } 40 | ], 41 | "devDependencies": { 42 | "@shopify/enzyme-utilities": "^1.1.4", 43 | "@types/enzyme": "^3.1.9", 44 | "@types/jest": "^22.2.3", 45 | "@types/react": "^16.3.11", 46 | "@types/react-dom": "^16.0.5", 47 | "autoprefixer": "^8.3.0", 48 | "awesome-typescript-loader": "^5.0.0", 49 | "babel-core": "^6.26.0", 50 | "babel-loader": "^7.1.4", 51 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 52 | "babel-plugin-transform-object-assign": "^6.22.0", 53 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 54 | "babel-polyfill": "^6.26.0", 55 | "babel-preset-env": "^1.6.1", 56 | "babel-preset-es2015": "^6.24.1", 57 | "babel-preset-react": "^6.24.1", 58 | "babel-preset-stage-0": "^6.24.1", 59 | "bundlesize": "^0.17.0", 60 | "coveralls": "^3.0.1", 61 | "css-loader": "^0.28.11", 62 | "enzyme": "^3.3.0", 63 | "enzyme-adapter-react-16": "^1.1.1", 64 | "eslint": "^4.19.1", 65 | "eslint-config-airbnb": "^16.1.0", 66 | "eslint-config-prettier": "^2.9.0", 67 | "eslint-plugin-import": "^2.11.0", 68 | "eslint-plugin-jsx-a11y": "^6.0.3", 69 | "eslint-plugin-prettier": "^2.6.0", 70 | "eslint-plugin-react": "^7.7.0", 71 | "file-loader": "^1.1.11", 72 | "glob": "^7.1.3", 73 | "html-webpack-plugin": "^3.2.0", 74 | "identity-obj-proxy": "^3.0.0", 75 | "jest": "^22.4.3", 76 | "node-sass": "^4.9.4", 77 | "postcss-loader": "^2.1.4", 78 | "precss": "^3.1.2", 79 | "prettier": "^1.12.1", 80 | "rollup": "^0.66.6", 81 | "rollup-plugin-postcss": "^1.6.2", 82 | "rollup-plugin-typescript2": "^0.17.2", 83 | "sass-loader": "^7.0.1", 84 | "style-loader": "^0.20.3", 85 | "ts-jest": "^22.4.6", 86 | "tslint": "^5.9.1", 87 | "tslint-react": "^3.5.1", 88 | "typescript": "^2.8.1", 89 | "typescript-eslint-parser": "^15.0.0", 90 | "webpack": "^4.6.0", 91 | "webpack-cli": "^2.0.14", 92 | "webpack-dev-server": "^3.1.3" 93 | }, 94 | "dependencies": { 95 | "lodash-decorators": "^6.0.0", 96 | "react": "^16.3.2", 97 | "react-dom": "^16.3.2" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /playground/Playground.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {bind} from 'lodash-decorators'; 3 | import Intercom from '../src'; 4 | 5 | interface State { 6 | open: boolean; 7 | } 8 | 9 | export default class Playground extends React.PureComponent<{}, State> { 10 | state = { 11 | open: false, 12 | }; 13 | 14 | render() { 15 | const {open} = this.state; 16 | return ( 17 | <> 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | @bind() 25 | private openIntercom() { 26 | this.setState({open: true}); 27 | } 28 | 29 | @bind() 30 | private closeIntercom() { 31 | this.setState({open: false}); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /playground/index-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Playground 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /playground/index.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import 'babel-polyfill'; 3 | import * as React from 'react'; 4 | import {render} from 'react-dom'; 5 | 6 | import Playground from './Playground'; 7 | 8 | render(, document.getElementById('app')); 9 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*"], 4 | "exclude": [] 5 | } 6 | -------------------------------------------------------------------------------- /playground/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const {TsConfigPathsPlugin} = require('awesome-typescript-loader'); 4 | const path = require('path'); 5 | 6 | const PLAYGROUND_PATH = __dirname; 7 | const BASENAME = '/'; 8 | 9 | module.exports = { 10 | mode: 'development', 11 | devServer: { 12 | contentBase: PLAYGROUND_PATH, 13 | port: 9000, 14 | host: '0.0.0.0', 15 | compress: true, 16 | historyApiFallback: { 17 | rewrites: [{from: /^\/[^.]+$/, to: '/index.html'}], 18 | }, 19 | }, 20 | devtool: 'cheap-module-eval-source-map', 21 | entry: { 22 | playground: PLAYGROUND_PATH, 23 | }, 24 | resolve: { 25 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 26 | modules: [path.resolve('./node_modules')], 27 | plugins: [ 28 | new TsConfigPathsPlugin({ 29 | configFileName: path.resolve(`./tsconfig.json`), 30 | compiler: 'typescript', 31 | }), 32 | ], 33 | }, 34 | plugins: [ 35 | new HtmlWebpackPlugin({ 36 | template: `${PLAYGROUND_PATH}/index-template.html`, 37 | }), 38 | ], 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.(jpe|jpg|woff|woff2|eot|ttf|svg|otf)(\?.*$|$)/, 43 | loader: 'file-loader', 44 | options: { 45 | name: '[path][name].[ext]', 46 | publicPath: BASENAME, 47 | }, 48 | }, 49 | { 50 | test: /\.scss$/, 51 | loader: [ 52 | 'style-loader', 53 | { 54 | loader: 'css-loader', 55 | query: { 56 | namedExport: true, 57 | modules: true, 58 | importLoaders: true, 59 | localIdentName: '[name]__[local]___[hash:base64:5]', 60 | }, 61 | }, 62 | 'postcss-loader', 63 | { 64 | loader: 'sass-loader', 65 | options: { 66 | includePaths: [], 67 | }, 68 | }, 69 | ], 70 | }, 71 | { 72 | test: /\.tsx?$/, 73 | loader: 'awesome-typescript-loader', 74 | exclude: /(node_modules)/, 75 | query: { 76 | cacheDirectory: true, 77 | presets: ['react', 'es2015'], 78 | plugins: [ 79 | 'transform-decorators-legacy', 80 | 'transform-object-rest-spread', 81 | ], 82 | }, 83 | }, 84 | ], 85 | }, 86 | }; 87 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: { 4 | browsers: ['last 2 versions'], 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import rollupTypescript from 'rollup-plugin-typescript2'; 3 | import typescript from 'typescript'; 4 | import postcss from 'rollup-plugin-postcss'; 5 | import { 6 | main as mainPath, 7 | module as modulePath, 8 | dependencies, 9 | } from './package.json'; 10 | 11 | export default { 12 | input: 'src/index.ts', 13 | output: [ 14 | { 15 | file: mainPath, 16 | format: 'cjs', 17 | }, 18 | { 19 | file: modulePath, 20 | format: 'es', 21 | }, 22 | ], 23 | external: [...Object.keys(dependencies || {})], 24 | plugins: [ 25 | rollupTypescript({ 26 | tsconfig: './tsconfig.json', 27 | typescript, 28 | }), 29 | postcss({ 30 | modules: true, 31 | namedExports: true, 32 | }), 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /src/Intercom.scss: -------------------------------------------------------------------------------- 1 | .Intercom { 2 | position: fixed; 3 | bottom: 0; 4 | right: 0; 5 | z-index: 999; 6 | width: 0; 7 | height: 0; 8 | background: transparent; 9 | border: none; 10 | } 11 | 12 | .IntercomHasLauncher { 13 | width: 90px; 14 | height: 90px; 15 | } 16 | 17 | .IntercomAnimating { 18 | pointer-events: none; 19 | } 20 | 21 | .IntercomOpen { 22 | // copied over directly from the styles 23 | // intercom injects inside the iframe 24 | width: 100%; 25 | max-width: 451px; 26 | 27 | height: calc(100% - 40px); 28 | min-height: 250px; 29 | max-height: 704px; 30 | 31 | @media all and (max-width: 451px) { 32 | height: 100%; 33 | max-height: none; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Intercom.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {bind} from 'lodash-decorators'; 3 | import { 4 | getIntercomFromFrame, 5 | objectEqual, 6 | classNames, 7 | IntercomType, 8 | injectCustomStyles, 9 | } from './utilities'; 10 | import { 11 | ImportIsolatedRemote, 12 | BorderlessFrameObserver, 13 | BorderlessFrameSizes, 14 | } from './components'; 15 | import * as styles from './Intercom.scss'; 16 | 17 | /* eslint-disable camelcase */ 18 | export interface User { 19 | user_id?: string; 20 | email?: string; 21 | [key: string]: any; 22 | } 23 | /* eslint-enable camelcase */ 24 | 25 | export interface Props { 26 | appId: string; 27 | user?: User; 28 | open?: boolean; 29 | launcher?: boolean; 30 | onOpen?(): void; 31 | onClose?(): void; 32 | onUnreadCountChange?(unreadCount: number): void; 33 | onInitialization?(intercom: IntercomType): void; 34 | } 35 | 36 | interface FakeState { 37 | open?: boolean; 38 | animating?: boolean; 39 | borderlessFrameSizes?: BorderlessFrameSizes | null; 40 | } 41 | 42 | export interface State { 43 | frame: HTMLIFrameElement | null; 44 | } 45 | 46 | const ANIMATION_DURATION = 300; 47 | 48 | class Intercom extends React.PureComponent { 49 | static defaultProps: Partial = { 50 | launcher: true, 51 | }; 52 | 53 | state: State = { 54 | frame: null, 55 | }; 56 | 57 | componentWillReceiveProps({open: nextOpen, user: nextUser}: Props) { 58 | const {user} = this.props; 59 | 60 | if (nextOpen) { 61 | this.getIntercom()('show'); 62 | } 63 | 64 | if (nextUser && !objectEqual(user || {}, nextUser)) { 65 | this.getIntercom()('update', nextUser); 66 | } 67 | } 68 | 69 | componentWillUnmount() { 70 | this.getIntercom()('shutdown'); 71 | } 72 | 73 | render() { 74 | const {appId, launcher} = this.props; 75 | const {frame} = this.state; 76 | const importUrl = `https://widget.intercom.io/widget/${appId}`; 77 | 78 | const borderlessFrameObserver = frame && ( 79 | 84 | ); 85 | 86 | return ( 87 | <> 88 | 93 | {borderlessFrameObserver} 94 | 95 | ); 96 | } 97 | 98 | private updateState({ 99 | open = false, 100 | animating = false, 101 | borderlessFrameSizes = null, 102 | }: FakeState) { 103 | const {launcher} = this.props; 104 | const {frame} = this.state; 105 | 106 | if (!frame) { 107 | return; 108 | } 109 | 110 | if (borderlessFrameSizes) { 111 | const {width, height} = borderlessFrameSizes; 112 | frame.setAttribute('style', `width: ${width}; height: ${height};`); 113 | } else { 114 | frame.removeAttribute('style'); 115 | } 116 | 117 | const className = classNames( 118 | styles.Intercom, 119 | launcher && styles.IntercomHasLauncher, 120 | open && styles.IntercomOpen, 121 | animating && styles.IntercomAnimating, 122 | ); 123 | 124 | frame.setAttribute('class', className); 125 | } 126 | 127 | @bind() 128 | private initializeIntercom(frame: HTMLIFrameElement) { 129 | const { 130 | open, 131 | onOpen, 132 | onClose, 133 | appId, 134 | onUnreadCountChange, 135 | user, 136 | onInitialization, 137 | launcher, 138 | } = this.props; 139 | 140 | const intercom = getIntercomFromFrame(frame); 141 | this.setState({frame}); 142 | 143 | intercom('boot', { 144 | app_id: appId, 145 | ...user, 146 | hide_default_launcher: !launcher, 147 | }); 148 | 149 | intercom('onShow', () => { 150 | this.updateState({open: true, animating: false}); 151 | if (onOpen) { 152 | onOpen(); 153 | } 154 | }); 155 | 156 | intercom('onHide', () => { 157 | this.updateState({open: true, animating: true}); 158 | setTimeout( 159 | () => this.updateState({open: false, animating: false}), 160 | ANIMATION_DURATION, 161 | ); 162 | if (onClose) { 163 | onClose(); 164 | } 165 | }); 166 | 167 | if (onUnreadCountChange) { 168 | intercom('onUnreadCountChange', onUnreadCountChange); 169 | } 170 | 171 | if (open) { 172 | intercom('show'); 173 | } else { 174 | this.updateState({open: false, animating: false}); 175 | } 176 | 177 | this.injectCustomLauncherStyles(); 178 | 179 | if (onInitialization) { 180 | onInitialization(intercom); 181 | } 182 | } 183 | 184 | private getIntercom() { 185 | const {frame} = this.state; 186 | if (!frame) { 187 | return () => {}; 188 | } 189 | return getIntercomFromFrame(frame); 190 | } 191 | 192 | private injectCustomLauncherStyles() { 193 | const {frame} = this.state; 194 | injectCustomStyles( 195 | frame!, 196 | ` 197 | .intercom-launcher-frame-shadow { 198 | box-shadow: none !important; 199 | } 200 | `, 201 | ); 202 | } 203 | 204 | @bind() 205 | private handleBorderlessFrameSizesUpdate( 206 | borderlessFrameSizes: BorderlessFrameSizes, 207 | ) { 208 | this.updateState({borderlessFrameSizes}); 209 | } 210 | } 211 | 212 | export default Intercom; 213 | -------------------------------------------------------------------------------- /src/components/BorderlessFrameObserver/BorderlessFrameObserver.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {bind} from 'lodash-decorators'; 3 | import {injectCustomStyles} from '../../utilities'; 4 | 5 | export interface BorderlessFrameSizes { 6 | width: string; 7 | height: string; 8 | } 9 | 10 | export interface Props { 11 | frame: HTMLIFrameElement; 12 | launcher: boolean; 13 | onSizesUpdate(newSizes: BorderlessFrameSizes): void; 14 | } 15 | 16 | const LAUNCHER_SIZE_PIXELS = 60; 17 | const LAUNCHER_MARGIN_PIXELS = 20; 18 | 19 | class BorderlessFrameObserver extends React.Component { 20 | private observer: MutationObserver | null = null; 21 | 22 | componentWillMount() { 23 | const {frame} = this.props; 24 | this.observeNode( 25 | frame.contentWindow!.document.body, 26 | this.handleBodyMutation, 27 | { 28 | childList: true, 29 | }, 30 | ); 31 | this.injectCustomGradientStyles(); 32 | } 33 | 34 | componentWillUnmount() { 35 | this.cleanObserver(); 36 | } 37 | 38 | render() { 39 | return null; 40 | } 41 | 42 | private injectCustomGradientStyles() { 43 | const {frame} = this.props; 44 | injectCustomStyles( 45 | frame, 46 | ` 47 | .intercom-gradient { 48 | width: 100% !important; 49 | height: 100% !important; 50 | } 51 | `, 52 | ); 53 | } 54 | 55 | private observeNode( 56 | nodeToObserve: HTMLElement, 57 | onMutation: (mutations: MutationRecord[]) => void, 58 | options: MutationObserverInit, 59 | ) { 60 | this.cleanObserver(); 61 | 62 | const observer = new MutationObserver(onMutation); 63 | observer.observe(nodeToObserve, options); 64 | 65 | this.observer = observer; 66 | return observer; 67 | } 68 | 69 | private cleanObserver() { 70 | const {observer} = this; 71 | if (observer) { 72 | observer.disconnect(); 73 | } 74 | } 75 | 76 | @bind() 77 | private handleBodyMutation([{target: body}]: MutationRecord[]) { 78 | const intercomAppNode = (body as HTMLElement).querySelector( 79 | '.intercom-app', 80 | ); 81 | this.observeNode( 82 | intercomAppNode as HTMLElement, 83 | this.handleIntercomAppMutation, 84 | { 85 | attributes: true, 86 | subtree: true, 87 | }, 88 | ); 89 | } 90 | 91 | @bind() 92 | private handleIntercomAppMutation(mutations: MutationRecord[]) { 93 | const {launcher, onSizesUpdate} = this.props; 94 | 95 | for (const {target} of mutations) { 96 | const node = target as HTMLIFrameElement; 97 | 98 | if ( 99 | !node.classList.contains('intercom-borderless-frame') && 100 | !node.classList.contains('intercom-notifications-frame') 101 | ) { 102 | continue; 103 | } 104 | 105 | const { 106 | style: {height}, 107 | offsetWidth, 108 | } = node; 109 | 110 | const finalWidth = launcher 111 | ? offsetWidth + LAUNCHER_MARGIN_PIXELS 112 | : offsetWidth; 113 | let finalHeight = height || '0px'; 114 | 115 | if (height && launcher) { 116 | finalHeight = addMarginToPixels( 117 | height, 118 | LAUNCHER_SIZE_PIXELS + LAUNCHER_MARGIN_PIXELS * 2, 119 | ); 120 | } 121 | 122 | onSizesUpdate({ 123 | width: `${finalWidth}px`, 124 | height: finalHeight, 125 | }); 126 | 127 | return; 128 | } 129 | } 130 | } 131 | 132 | function addMarginToPixels(pixelsString: string, margin: number) { 133 | const pixels = Number(pixelsString.replace('px', '')); 134 | return `${pixels + margin}px`; 135 | } 136 | 137 | export default BorderlessFrameObserver; 138 | -------------------------------------------------------------------------------- /src/components/BorderlessFrameObserver/index.ts: -------------------------------------------------------------------------------- 1 | export {default, Props, BorderlessFrameSizes} from './BorderlessFrameObserver'; 2 | -------------------------------------------------------------------------------- /src/components/ImportIsolatedRemote/ImportIsolatedRemote.scss: -------------------------------------------------------------------------------- 1 | .ImportIsolatedRemote { 2 | width: 0; 3 | height: 0; 4 | border: none; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/ImportIsolatedRemote/ImportIsolatedRemote.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Portal from '../Portal'; 3 | import * as styles from './ImportIsolatedRemote.scss'; 4 | 5 | export interface Props { 6 | title: string; 7 | source: string; 8 | onImported(frame: HTMLIFrameElement): void; 9 | } 10 | 11 | export default class ImportIsolatedRemote extends React.PureComponent< 12 | Props, 13 | never 14 | > { 15 | private frameNode: React.RefObject = React.createRef(); 16 | private scriptNode: HTMLScriptElement | null = null; 17 | 18 | componentDidMount() { 19 | const {current: frame} = this.frameNode; 20 | const {source, onImported} = this.props; 21 | 22 | if (!frame || !frame.contentWindow) { 23 | return; 24 | } 25 | 26 | const {contentWindow} = frame; 27 | 28 | const script = document.createElement('script'); 29 | script.src = source; 30 | this.scriptNode = script; 31 | 32 | function loadScript() { 33 | contentWindow.document.body.appendChild(script); 34 | script.onload = () => onImported(frame!); 35 | } 36 | 37 | // fix for FF which refreshes the content of the iframe 38 | // when done loading and therefor doesn't support 39 | // immediately loading the script 40 | if ((frame.contentDocument as any).readyState === 'uninitialized') { 41 | frame.onload = loadScript; 42 | return; 43 | } 44 | 45 | loadScript(); 46 | } 47 | 48 | componentWillUnmount() { 49 | const { 50 | scriptNode, 51 | frameNode: {current: frame}, 52 | } = this; 53 | 54 | if (frame) { 55 | frame.onload = null; 56 | } 57 | 58 | if (scriptNode) { 59 | scriptNode.onload = null; 60 | } 61 | } 62 | 63 | render() { 64 | const {title} = this.props; 65 | return ( 66 | 67 |