Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"react/jsx-filename-extension": "off",
"import/no-unresolved": "off",
"import/extensions": "off",
"import/prefer-default-export": "off",
"jsx-a11y/anchor-is-valid": "off",
"react/no-unescaped-entities": "off",
"react/sort-comp": "off",
Expand All @@ -20,7 +21,9 @@
"devDependencies": ["**/*.test.tsx"]
}
],
"no-use-before-define": "off"
"no-use-before-define": "off",
"no-restricted-syntax": "off",
"no-continue": "off"
},
"env": {
"browser": true,
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ An [Intercom](http://intercom.com/) component for React that truly encapsulates
yarn add intercom-react
```

_Before you install the library make sure to read the [notes on reliability](#️-a-few-notes-on-reliability)._

## Setup

```tsx
Expand All @@ -38,6 +40,7 @@ yarn add intercom-react
* `onClose` (optional): called when intercom closes.
* `onUnreadCountChange` (optional): called when the unread count changes.
* `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).
* `launcher` (optional): whether a launcher button should be shown. Defaults to `true`.

## How is this different?

Expand All @@ -47,6 +50,14 @@ What this means is that if you would get into a situation where you would have t

I wrote this component to create an isolated Intercom component that cleans up after itself when unmounted for a "true" React experience.

## ⚠️ A few notes on reliability

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.

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 in future versions of the Intercom library**. I would therefor recommend that you only use this library if you have a 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.

Having that said I appreciate your interest in the library and look forward to hearing your experience with it 🙌 .

## 🏗 Contributing

1. Make your changes.
Expand Down
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,40 @@
"type": "git",
"url": "https://github.com/kvendrik/intercom-react.git"
},
"keywords": [
"intercom",
"react",
"live",
"chat",
"support"
],
"keywords": ["intercom", "react", "live", "chat", "support"],
"main": "build/index.js",
"module": "build/index.es.js",
"types": "build/types/index.d.ts",
"author": "Koen Vendrik <k.vendrik@gmail.com>",
"license": "MIT",
"scripts": {
"playground": "webpack-dev-server --hot --inline --config ./playground/webpack.config.js",
"playground":
"webpack-dev-server --hot --inline --config ./playground/webpack.config.js",
"lint": "yarn lint:ts && yarn lint:tslint && yarn lint:eslint",
"lint:ts": "tsc --noEmit",
"lint:eslint": "eslint src --ext .tsx --ext .ts && eslint playground --ext .tsx --ext .ts",
"lint:tslint": "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'",
"test": "jest --silent",
"lint:eslint":
"eslint src --ext .tsx --ext .ts && eslint playground --ext .tsx --ext .ts",
"lint:tslint":
"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'",
"test": "jest",
"test:bundle": "yarn build && bundlesize",
"test:debug": "jest",
"test:coverage": "yarn test --coverage",
"test:coveralls": "yarn test --coverage --coverageReporters=text-lcov | coveralls",
"test:ci": "yarn lint && yarn test && yarn test:coveralls && yarn test:bundle",
"build": "rollup -c && mv build/src build/types",
"test:coveralls":
"yarn test --coverage --coverageReporters=text-lcov | coveralls",
"test:ci":
"yarn lint && yarn test && yarn test:coveralls && yarn test:bundle",
"build": "yarn clean && rollup -c && mv build/src build/types",
"clean": "rm -rf build"
},
"bundlesize": [
{
"path": "build/index.js",
"maxSize": "3 kB"
"maxSize": "4 kB"
}
],
"devDependencies": {
"@shopify/enzyme-utilities": "^1.1.4",
"@types/enzyme": "^3.1.9",
"@types/jest": "^22.2.3",
"@types/react": "^16.3.11",
Expand Down
32 changes: 30 additions & 2 deletions playground/Playground.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
import * as React from 'react';

import {bind} from 'lodash-decorators';
import Intercom from '../src';

export default () => <Intercom appId="fyq3wodw" />;
interface State {
open: boolean;
}

export default class Playground extends React.PureComponent<{}, State> {
state = {
open: false,
};

render() {
const {open} = this.state;
return (
<>
<button onClick={this.openIntercom}>Open</button>
<Intercom appId="fyq3wodw" open={open} onClose={this.closeIntercom} />
</>
);
}

@bind()
private openIntercom() {
this.setState({open: true});
}

@bind()
private closeIntercom() {
this.setState({open: false});
}
}
5 changes: 5 additions & 0 deletions playground/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../tsconfig.json",
"include": ["./**/*"],
"exclude": []
}
9 changes: 7 additions & 2 deletions src/Intercom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
bottom: 0;
right: 0;
z-index: 999;
width: 90px;
height: 90px;
width: 0;
height: 0;
background: transparent;
border: none;
}

.IntercomHasLauncher {
width: 90px;
height: 90px;
}

.IntercomAnimating {
pointer-events: none;
}
Expand Down
99 changes: 82 additions & 17 deletions src/Intercom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import {
objectEqual,
classNames,
IntercomType,
injectCustomStyles,
} from './utilities';
import {ImportIsolatedRemote} from './components';

import {
ImportIsolatedRemote,
BorderlessFrameListener,
BorderlessFrameSizes,
} from './components';
import * as styles from './Intercom.scss';

/* eslint-disable camelcase */
Expand All @@ -22,21 +26,33 @@ export interface Props {
appId: string;
user?: User;
open?: boolean;
launcher?: boolean;
onOpen?(): void;
onClose?(): void;
onUnreadCountChange?(unreadCount: number): void;
onInitialization?(intercom: IntercomType): void;
}

interface FakeState {
open: boolean;
animating: boolean;
open?: boolean;
animating?: boolean;
borderlessFrameSizes?: BorderlessFrameSizes | null;
}

export interface State {
frame: HTMLIFrameElement | null;
}

const ANIMATION_DURATION = 300;

export default class Intercom extends React.PureComponent<Props, never> {
private frame: HTMLIFrameElement | null = null;
class Intercom extends React.PureComponent<Props, State> {
static defaultProps: Partial<Props> = {
launcher: true,
};

state: State = {
frame: null,
};

componentWillReceiveProps({open: nextOpen, user: nextUser}: Props) {
const {user} = this.props;
Expand All @@ -55,26 +71,52 @@ export default class Intercom extends React.PureComponent<Props, never> {
}

render() {
const {appId} = this.props;
const {appId, launcher} = this.props;
const {frame} = this.state;
const importUrl = `https://widget.intercom.io/widget/${appId}`;
return (
<ImportIsolatedRemote
title="intercom"
source={importUrl}
onImported={this.initializeIntercom}

const borderlessFrameListener = frame && (
<BorderlessFrameListener
frame={frame}
onSizesUpdate={this.handleBorderlessFrameSizesUpdate}
launcher={Boolean(launcher)}
/>
);

return (
<>
<ImportIsolatedRemote
title="intercom"
source={importUrl}
onImported={this.initializeIntercom}
/>
{borderlessFrameListener}
</>
);
}

private updateState({open, animating}: FakeState) {
const {frame} = this;
private updateState({
open = false,
animating = false,
borderlessFrameSizes = null,
}: FakeState) {
const {launcher} = this.props;
const {frame} = this.state;

if (!frame) {
return;
}

if (borderlessFrameSizes) {
const {width, height} = borderlessFrameSizes;
frame.setAttribute('style', `width: ${width}; height: ${height};`);
} else {
frame.removeAttribute('style');
}

const className = classNames(
styles.Intercom,
launcher && styles.IntercomHasLauncher,
open && styles.IntercomOpen,
animating && styles.IntercomAnimating,
);
Expand All @@ -95,6 +137,7 @@ export default class Intercom extends React.PureComponent<Props, never> {
} = this.props;

const intercom = getIntercomFromFrame(frame);
this.setState({frame});

intercom('boot', {
app_id: appId,
Expand All @@ -107,6 +150,7 @@ export default class Intercom extends React.PureComponent<Props, never> {
onOpen();
}
});

intercom('onHide', () => {
this.updateState({open: true, animating: true});
setTimeout(
Expand All @@ -122,24 +166,45 @@ export default class Intercom extends React.PureComponent<Props, never> {
intercom('onUnreadCountChange', onUnreadCountChange);
}

this.frame = frame;

if (open) {
intercom('show');
} else {
this.updateState({open: false, animating: false});
}

this.injectCustomLauncherStyles();

if (onInitialization) {
onInitialization(intercom);
}
}

private getIntercom() {
const {frame} = this;
const {frame} = this.state;
if (!frame) {
return () => {};
}
return getIntercomFromFrame(frame);
}

private injectCustomLauncherStyles() {
const {frame} = this.state;
injectCustomStyles(
frame!,
`
.intercom-launcher-frame-shadow {
box-shadow: none !important;
}
`,
);
}

@bind()
private handleBorderlessFrameSizesUpdate(
borderlessFrameSizes: BorderlessFrameSizes,
) {
this.updateState({borderlessFrameSizes});
}
}

export default Intercom;
Loading