├── demos ├── build │ └── .gitkeep ├── demo-react-ts │ ├── .prettierrc.json │ ├── .eslintignore │ ├── .prettierignore │ ├── src │ │ ├── visitor-ui-component-library │ │ │ ├── button │ │ │ │ ├── constants │ │ │ │ │ ├── IconButtonShapes.ts │ │ │ │ │ ├── ButtonUses.ts │ │ │ │ │ ├── IconButtonUses.ts │ │ │ │ │ ├── IconButtonSizeToIconSize.ts │ │ │ │ │ ├── ButtonSizes.ts │ │ │ │ │ └── LoadingButtonUses.ts │ │ │ │ ├── theme │ │ │ │ │ ├── closeButtonThemeOperators.js │ │ │ │ │ ├── iconButtonThemeOperators.ts │ │ │ │ │ ├── buttonTheme.ts │ │ │ │ │ └── iconButtonTheme.ts │ │ │ │ ├── VizExFileButton.js │ │ │ │ ├── VizExIconButton.tsx │ │ │ │ ├── VizExButton.tsx │ │ │ │ ├── VizExCloseButton.js │ │ │ │ ├── VizExLoadingButton.js │ │ │ │ └── VizExButton.stories.tsx │ │ │ ├── input │ │ │ │ ├── constants │ │ │ │ │ ├── InputVariations.ts │ │ │ │ │ └── ExpandingInputVariations.ts │ │ │ │ ├── theme │ │ │ │ │ ├── checkboxThemeOperators.js │ │ │ │ │ ├── inputThemeOperators.js │ │ │ │ │ └── expandingInputThemeOperators.js │ │ │ │ ├── VizExCheckbox.js │ │ │ │ └── VizExInput.js │ │ │ ├── typography │ │ │ │ ├── constants │ │ │ │ │ └── SmallVariations.ts │ │ │ │ ├── utils │ │ │ │ │ ├── getSmallStyles.js │ │ │ │ │ ├── getBodyTypographyStyles.js │ │ │ │ │ └── getHeadingStyles.js │ │ │ │ └── VizExSmall.js │ │ │ ├── constants │ │ │ │ ├── keyCodes.ts │ │ │ │ └── sizes.ts │ │ │ ├── link │ │ │ │ ├── constants │ │ │ │ │ └── LinkVariations.ts │ │ │ │ ├── theme │ │ │ │ │ ├── linkThemeOperators.ts │ │ │ │ │ └── linkTheme.ts │ │ │ │ ├── VizExLink.tsx │ │ │ │ └── VizExExternalLink.tsx │ │ │ ├── utils │ │ │ │ ├── callIfValid.ts │ │ │ │ ├── themePropType.ts │ │ │ │ ├── pipe.ts │ │ │ │ ├── mixins.ts │ │ │ │ ├── stripHTML.js │ │ │ │ ├── get.ts │ │ │ │ ├── browserTest.js │ │ │ │ ├── hexToRgba.ts │ │ │ │ ├── curryable.ts │ │ │ │ ├── hexToRGB.ts │ │ │ │ ├── types.ts │ │ │ │ ├── isUnsafeUrl.ts │ │ │ │ ├── adjustLuminance.ts │ │ │ │ ├── SyntheticEvent.ts │ │ │ │ ├── getTextColorFromBgColor.ts │ │ │ │ ├── getContrastRatio.ts │ │ │ │ ├── mergeDeep.ts │ │ │ │ └── aria-live │ │ │ │ │ ├── AriaLiveContext.stories.tsx │ │ │ │ │ ├── AriaLiveContext.tsx │ │ │ │ │ └── AriaLiveAnnouncer.ts │ │ │ ├── ratings │ │ │ │ ├── constants │ │ │ │ │ └── RatingSizes.ts │ │ │ │ ├── theme │ │ │ │ │ └── VizExCsatRatingThemeOperator.ts │ │ │ │ └── VizExCsatRating.js │ │ │ ├── list │ │ │ │ ├── theme │ │ │ │ │ ├── listTheme.ts │ │ │ │ │ └── listItemButtonTheme.ts │ │ │ │ ├── VizExList.tsx │ │ │ │ ├── VizExListItemButton.tsx │ │ │ │ └── VizExList.stories.tsx │ │ │ ├── card │ │ │ │ └── VizExCard.js │ │ │ ├── tooltip │ │ │ │ ├── utils │ │ │ │ │ ├── getPlacement.js │ │ │ │ │ ├── getArrowSpacing.js │ │ │ │ │ └── getBodySpacing.js │ │ │ │ ├── theme │ │ │ │ │ └── tooltipThemeOperators.ts │ │ │ │ ├── constants │ │ │ │ │ └── PlacementConstants.ts │ │ │ │ ├── VizExTooltipArrow.js │ │ │ │ ├── VizExTooltipBody.js │ │ │ │ └── VizExTooltip.js │ │ │ └── theme │ │ │ │ ├── VizExThemeProvider.tsx │ │ │ │ ├── ColorConstants.ts │ │ │ │ ├── styled.d.ts │ │ │ │ ├── defaultTheme.ts │ │ │ │ ├── createTheme.ts │ │ │ │ ├── defaultThemeOperators.ts │ │ │ │ └── createThemeV2.ts │ │ ├── icons │ │ │ ├── statusDot.svg │ │ │ ├── caretDown.svg │ │ │ ├── recordVinyl.svg │ │ │ ├── phone.svg │ │ │ ├── microphone.svg │ │ │ ├── deleteLeft.svg │ │ │ ├── checkmark.svg │ │ │ ├── mobileRetro.svg │ │ │ ├── microphoneSlash.svg │ │ │ ├── externalLink.svg │ │ │ └── sprocket.svg │ │ ├── types │ │ │ ├── index.d.ts │ │ │ └── ScreenTypes.ts │ │ ├── hooks │ │ │ ├── useAutoFocus.ts │ │ │ └── useTimer.ts │ │ ├── index.tsx │ │ ├── constants │ │ │ └── buttonIds.ts │ │ ├── utils │ │ │ ├── millisecondsToFormattedDuration.ts │ │ │ ├── phoneNumberUtils.ts │ │ │ └── colors.ts │ │ ├── components │ │ │ ├── Alert.tsx │ │ │ ├── screens │ │ │ │ ├── DialingScreen.tsx │ │ │ │ ├── LoginScreen.tsx │ │ │ │ ├── CallEndedScreen.tsx │ │ │ │ ├── IncomingScreen.tsx │ │ │ │ └── CallingScreen.tsx │ │ │ ├── Keypad.tsx │ │ │ ├── FromNumbersDropdown.tsx │ │ │ └── Icons.tsx │ │ └── index.html │ ├── babel.config.js │ ├── test │ │ ├── support │ │ │ └── jasmine-browser.json │ │ ├── spec │ │ │ ├── run-test.ts │ │ │ └── components │ │ │ │ ├── screens │ │ │ │ ├── CallingScreen-test.tsx │ │ │ │ ├── DialingScreen-test.tsx │ │ │ │ ├── LoginScreen-test.tsx │ │ │ │ ├── IncomingScreen-test.tsx │ │ │ │ └── CallEndedScreen-test.tsx │ │ │ │ ├── FromNumbersDropdown-test.tsx │ │ │ │ └── App-test.tsx │ │ └── render.tsx │ ├── README.md │ ├── tsconfig.json │ ├── webpack-test.config.js │ ├── .eslintrc │ ├── webpack.config.js │ └── package.json ├── demo-minimal-js │ ├── README.md │ ├── webpack.config.js │ └── package.json ├── src │ └── index.html ├── package-lock.json └── package.json ├── .eslintignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── test ├── .eslintrc ├── support │ └── jasmine-browser.json └── spec │ ├── IFrameManager-test.ts │ └── CallingExtensions-test.ts ├── .npmignore ├── webpack.common.js ├── docs └── images │ ├── InitializeCallWidgetIFrame.png │ └── OutboundCallSequenceDiagram.png ├── .gitignore ├── index.ts ├── .github ├── CODEOWNERS ├── workflows │ ├── deploy.yml │ └── preview.yml ├── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug-report.md └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── webpack.config.js ├── webpack-test.config.js ├── webpack.cjs.config.js ├── webpack.esm.config.js ├── LICENSE ├── .eslintrc ├── README.md ├── SHIP_WITH_CARE.md ├── package.json └── src └── Constants.ts /demos/build/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demos/demo-react-ts/.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | demo/build 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = https://registry.npmjs.org/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-test 3 | node_modules 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { "arrowParens": "avoid", "trailingComma": "all" } 2 | -------------------------------------------------------------------------------- /demos/demo-react-ts/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | visitor-ui-component-library 3 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "env": { 4 | "jasmine": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demos/demo-react-ts/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | dist 3 | node_modules 4 | visitor-ui-component-library 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | .eslintignore 3 | .eslintrc 4 | node_modules 5 | circle.yml 6 | webpack.config.js 7 | coverage 8 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./index.ts", 3 | resolve: { 4 | extensions: [".ts", ".tsx", ".js"], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /docs/images/InitializeCallWidgetIFrame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/calling-extensions-sdk/master/docs/images/InitializeCallWidgetIFrame.png -------------------------------------------------------------------------------- /docs/images/OutboundCallSequenceDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/calling-extensions-sdk/master/docs/images/OutboundCallSequenceDiagram.png -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/constants/IconButtonShapes.ts: -------------------------------------------------------------------------------- 1 | export const CIRCLE = 'circle'; 2 | export const DEFAULT = 'default'; 3 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/input/constants/InputVariations.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT = "default"; 2 | export const ON_DARK = "on-dark"; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | bin 5 | *.pem 6 | coverage 7 | .DS_STORE 8 | lib 9 | demos/build/* 10 | !demos/build/.gitkeep 11 | dist-test 12 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/input/constants/ExpandingInputVariations.ts: -------------------------------------------------------------------------------- 1 | export const UNSTYLED = "unstyled"; 2 | export const DEFAULT = "default"; 3 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/statusDot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/typography/constants/SmallVariations.ts: -------------------------------------------------------------------------------- 1 | export const HELP = "help"; 2 | export const DEFAULT = "default"; 3 | export const ERROR = "error"; 4 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/constants/keyCodes.ts: -------------------------------------------------------------------------------- 1 | export const UP_ARROW = 38; 2 | export const DOWN_ARROW = 40; 3 | export const ENTER = 13; 4 | export const SPACE_BAR = 32; 5 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/link/constants/LinkVariations.ts: -------------------------------------------------------------------------------- 1 | export const ON_BRIGHT = 'on-bright'; 2 | export const DEFAULT = 'default'; 3 | export const ERROR = 'error'; 4 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/callIfValid.ts: -------------------------------------------------------------------------------- 1 | export const callIfValid = (func: Function, ...args: any[]) => { 2 | if (typeof func === 'function') func(...args); 3 | }; 4 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/themePropType.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const themePropType = PropTypes.object; 4 | 5 | export default themePropType; 6 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/pipe.ts: -------------------------------------------------------------------------------- 1 | export function pipe(...functions: Array<(arg: T) => T>) { 2 | return (data: T) => functions.reduce((acc, func) => func(acc), data); 3 | } 4 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | import React = require("react"); 3 | const ReactComponent: React.FC>; 4 | export default ReactComponent; 5 | } 6 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/mixins.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export const focusRing = css` 4 | outline-offset: 1px; 5 | outline: 2px solid #00a4bd; 6 | `; 7 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/constants/ButtonUses.ts: -------------------------------------------------------------------------------- 1 | export const PRIMARY = 'primary'; 2 | export const SECONDARY = 'secondary'; 3 | export const TRANSPARENT_ON_PRIMARY = 'transparent-on-primary'; 4 | -------------------------------------------------------------------------------- /demos/demo-react-ts/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["babel-plugin-styled-components"], 3 | presets: [ 4 | "@babel/preset-typescript", 5 | "@babel/preset-react", 6 | "@babel/preset-env", 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/stripHTML.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | export const stripHTML = html => { 4 | const doc = new DOMParser().parseFromString(html, 'text/html'); 5 | return doc.body.textContent || ''; 6 | }; 7 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/get.ts: -------------------------------------------------------------------------------- 1 | import { curryable } from './curryable'; 2 | 3 | type dataObject = { 4 | [key: string]: string; 5 | }; 6 | export const get = curryable((key: string, data: dataObject) => data[key]); 7 | -------------------------------------------------------------------------------- /demos/demo-react-ts/test/support/jasmine-browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "srcDir": "src", 3 | "srcFiles": [], 4 | "specDir": "dist-test", 5 | "specFiles": ["test.js"], 6 | "helpers": [], 7 | "browser": { 8 | "name": "chrome" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/constants/IconButtonUses.ts: -------------------------------------------------------------------------------- 1 | export const PRIMARY = 'primary'; 2 | export const TRANSPARENT_ON_BACKGROUND = 'transparent-on-background'; 3 | export const TRANSPARENT_ON_PRIMARY = 'transparent-on-primary'; 4 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/browserTest.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | const lowercasedUserAgent = window.navigator 4 | ? navigator.userAgent.toLowerCase() 5 | : ''; 6 | 7 | export const isIE11 = () => lowercasedUserAgent.includes('trident/'); 8 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/constants/sizes.ts: -------------------------------------------------------------------------------- 1 | export const MEDIUM = "md"; 2 | export const EXTRA_SMALL = "xs"; 3 | export const SMALL = "sm"; 4 | export const EXTRA_EXTRA_SMALL = "xxs"; 5 | export const LARGE = "lg"; 6 | export const EXTRA_LARGE = "xl"; 7 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/hexToRgba.ts: -------------------------------------------------------------------------------- 1 | import { hexToRGB } from './hexToRGB'; 2 | export const hexToRgba = (hexColor: string, opacity = 1) => { 3 | const { r, g, b } = hexToRGB(hexColor); 4 | return `rgba(${r}, ${g}, ${b}, ${opacity})`; 5 | }; 6 | -------------------------------------------------------------------------------- /test/support/jasmine-browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "srcDir": "src", 3 | "srcFiles": [], 4 | "specDir": "dist-test", 5 | "specFiles": ["test.js"], 6 | "helpers": [], 7 | "env": { 8 | "random": true 9 | }, 10 | "browser": { 11 | "name": "chrome" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import CallingExtensions from "./src/CallingExtensions"; 2 | import * as Constants from "./src/Constants"; 3 | import IFrameManager from "./src/IFrameManager"; 4 | 5 | export default CallingExtensions; 6 | export { Constants, IFrameManager }; 7 | export * from "./src/types"; 8 | -------------------------------------------------------------------------------- /demos/demo-minimal-js/README.md: -------------------------------------------------------------------------------- 1 | # Demo Widget - Minimal JS 2 | 3 | Features a minimal implementation of the Calling Extensions SDK using JavaScript, HTML, and CSS. 4 | 5 | Please note: this demo app isn't a fully functional calling app and uses mock data to provide a more realistic experience. 6 | -------------------------------------------------------------------------------- /demos/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Index Page 4 | 5 | 6 | 7 | Demo App Minimal JS 8 |
9 | Demo App React TS 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. 2 | # Visit the following for more information: 3 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 4 | * @HubSpot/calling-extensions-fe 5 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/curryable.ts: -------------------------------------------------------------------------------- 1 | export const curryable = (func: Function) => { 2 | const curry: Function = (...args: any[]) => { 3 | return args.length >= func.length 4 | ? func.apply(null, args) 5 | : curry.bind(null, ...args); 6 | }; 7 | return curry; 8 | }; 9 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/ratings/constants/RatingSizes.ts: -------------------------------------------------------------------------------- 1 | import { SMALL, MEDIUM, LARGE, EXTRA_LARGE } from "../../constants/sizes"; 2 | export const BORDER_RATIO_RATING = 0.09; 3 | export const RATING_ICON_SIZES = { 4 | [SMALL]: 32, 5 | [MEDIUM]: 48, 6 | [LARGE]: 72, 7 | [EXTRA_LARGE]: 108, 8 | }; 9 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/hooks/useAutoFocus.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | 3 | export const useAutoFocus = () => { 4 | const inputRef = useRef(null); 5 | useEffect(() => { 6 | if (inputRef.current) { 7 | inputRef.current.focus(); 8 | } 9 | }, []); 10 | return inputRef; 11 | }; 12 | -------------------------------------------------------------------------------- /demos/demo-react-ts/README.md: -------------------------------------------------------------------------------- 1 | # Demo Widget - React TypeScript 2 | 3 | Features a real-life implementation of the Calling Extensions SDK using React, TypeScript, and Styled Components to act as a blueprint for your app. 4 | 5 | Please note: this demo app isn't a fully functional calling app and uses mock data to provide a more realistic experience. 6 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/typography/utils/getSmallStyles.js: -------------------------------------------------------------------------------- 1 | "use es6"; 2 | 3 | import { css } from "styled-components"; 4 | 5 | export const getSmallStyles = css` 6 | font-size: 12px; 7 | line-height: 18px; 8 | `; 9 | 10 | export const getGlobalSmallStyles = css` 11 | small { 12 | ${getSmallStyles} 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/constants/IconButtonSizeToIconSize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EXTRA_SMALL, 3 | EXTRA_EXTRA_SMALL, 4 | SMALL, 5 | MEDIUM, 6 | } from '../../constants/sizes'; 7 | 8 | export const ICON_BUTTON_SIZE_TO_ICON_SIZE = { 9 | [EXTRA_SMALL]: EXTRA_EXTRA_SMALL, 10 | [SMALL]: EXTRA_SMALL, 11 | [MEDIUM]: SMALL, 12 | }; 13 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./components/App"; 4 | 5 | const rootNode = document.getElementById("app"); 6 | if (!rootNode) { 7 | throw new Error("The element #app wasn't found"); 8 | } 9 | const root = createRoot(rootNode); 10 | root.render(React.createElement(App)); 11 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/list/theme/listTheme.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | import { VizExListProps } from '../VizExList'; 4 | import { ThemeConfig } from '../../theme/styled'; 5 | 6 | export const listTheme = { 7 | baseStyle: css` 8 | margin: 0; 9 | padding: 0; 10 | position: relative; 11 | `, 12 | } as ThemeConfig['components']['List']; 13 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationDir": "./dist/types", 7 | "outDir": "./dist", 8 | "strict": true, 9 | "allowJs": true, 10 | "checkJs": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/caretDown.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/theme/closeButtonThemeOperators.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import { 4 | getTextColor, 5 | setThemeProperty, 6 | } from '../../theme/defaultThemeOperators'; 7 | import { get } from '../../utils/get'; 8 | 9 | export const getCloseButtonColor = theme => 10 | get('closeButton', theme) || getTextColor(theme); 11 | export const setCloseButtonColor = setThemeProperty('closeButton'); 12 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/recordVinyl.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/input/theme/checkboxThemeOperators.js: -------------------------------------------------------------------------------- 1 | "use es6"; 2 | 3 | import { 4 | getDisabledBackgroundColor, 5 | getInputBorderColor, 6 | getPrimaryColor, 7 | } from "../../theme/defaultThemeOperators"; 8 | 9 | export const getCheckboxHoverBackground = getDisabledBackgroundColor; 10 | export const getCheckboxUncheckedColor = getInputBorderColor; 11 | export const getCheckboxCheckedColor = getPrimaryColor; 12 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/typography/utils/getBodyTypographyStyles.js: -------------------------------------------------------------------------------- 1 | "use es6"; 2 | 3 | import { css } from "styled-components"; 4 | import { getTextColor } from "../../theme/defaultThemeOperators"; 5 | 6 | export const getBodyTypographyStyles = ({ theme }) => css` 7 | font-family: Helvetica, Arial, sans-serif; 8 | font-weight: 400; 9 | font-size: 14px; 10 | color: ${getTextColor(theme)}; 11 | line-height: 24px; 12 | `; 13 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/card/VizExCard.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import styled from 'styled-components'; 4 | 5 | const VizExCard = styled.div` 6 | position: relative; 7 | display: flex; 8 | flex-direction: column; 9 | margin-bottom: 16px; 10 | width: 100%; 11 | padding: 0; 12 | background-color: white; 13 | box-shadow: 0 1px 5px 0 rgba(45, 62, 80, 0.12); 14 | border-radius: 3px; 15 | `; 16 | 17 | export default VizExCard; 18 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/list/VizExList.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export interface VizExListProps { 4 | /** If true, styles with browser ul/ol default bullet points or numbers */ 5 | listStyled?: boolean; 6 | } 7 | 8 | const VizExList = styled.ul` 9 | ${({ listStyled }) => (listStyled ? '' : 'list-style: none;')} 10 | ${({ theme }) => theme.components.List.style} 11 | `; 12 | 13 | export default VizExList; 14 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "declarationDir": "./dist/types", 8 | "outDir": "./dist", 9 | "strict": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /demos/demo-react-ts/test/spec/run-test.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error module not typed 2 | import JasmineDOM from "@testing-library/jasmine-dom/dist"; 3 | import { configure } from "@testing-library/react"; 4 | 5 | async function setupTestEnv() { 6 | configure({ testIdAttribute: "data-test-id" }); 7 | 8 | beforeAll(() => { 9 | (jasmine.getEnv() as any).addMatchers(JasmineDOM); 10 | }); 11 | } 12 | 13 | setupTestEnv().catch((err) => 14 | setTimeout(() => { 15 | throw err; 16 | }) 17 | ); 18 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/hexToRGB.ts: -------------------------------------------------------------------------------- 1 | export const hexToRGB = (hexColorValue: string) => { 2 | let colorValue = hexColorValue.slice(1); 3 | 4 | if (colorValue.length === 3) { 5 | colorValue = colorValue.replace(/(.)/g, '$1$1'); 6 | } 7 | 8 | const r = parseInt(colorValue.substr(0, 2), 16); 9 | const g = parseInt(colorValue.substr(2, 2), 16); 10 | const b = parseInt(colorValue.substr(4, 2), 16); 11 | return { 12 | r, 13 | g, 14 | b, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/constants/ButtonSizes.ts: -------------------------------------------------------------------------------- 1 | import { EXTRA_SMALL, MEDIUM, SMALL } from '../../constants/sizes'; 2 | 3 | export const BUTTON_SIZES = { 4 | [EXTRA_SMALL]: 18, 5 | [SMALL]: 28, 6 | [MEDIUM]: 40, 7 | }; 8 | 9 | export const BUTTON_PADDINGS = { 10 | [EXTRA_SMALL]: '4px 8px', 11 | [SMALL]: '8px 16px', 12 | [MEDIUM]: '11px 24px', 13 | }; 14 | 15 | export const BUTTON_FONT_SIZES = { 16 | [EXTRA_SMALL]: '10px', 17 | [SMALL]: '12px', 18 | [MEDIUM]: '14px', 19 | }; 20 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/phone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/types.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export type InteractionProps = { 4 | disabled?: boolean; 5 | focused?: boolean; 6 | hovered?: boolean; 7 | pressed?: boolean; 8 | }; 9 | 10 | export const interactionPropTypes = { 11 | disabled: PropTypes.bool, 12 | focused: PropTypes.bool, 13 | hovered: PropTypes.bool, 14 | pressed: PropTypes.bool, 15 | }; 16 | 17 | export type PolymorphicRef< 18 | C extends React.ElementType 19 | > = React.ComponentPropsWithRef['ref']; 20 | -------------------------------------------------------------------------------- /demos/demo-react-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "jsx": "react-jsx", 5 | "moduleResolution": "node", 6 | "forceConsistentCasingInFileNames": true, 7 | "resolveJsonModule": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "lib": ["dom", "dom.iterable", "esnext"], 11 | "module": "esnext", 12 | "target": "esnext", 13 | "isolatedModules": true, 14 | "noErrorTruncation": true, 15 | "types": ["jasmine", "node", "@testing-library/jasmine-dom"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demos/demo-react-ts/test/render.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { render } from "@testing-library/react"; 3 | import { ThemeProvider } from "styled-components"; 4 | import { createTheme } from "../src/visitor-ui-component-library/theme/createTheme"; 5 | 6 | export function renderWithContext(component: ReactElement, options?: any) { 7 | const Wrapper = ({ children }: { children: ReactElement }) => ( 8 | {children} 9 | ); 10 | 11 | return render(component, { wrapper: Wrapper, ...options }); 12 | } 13 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/ratings/theme/VizExCsatRatingThemeOperator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setThemeProperty, 3 | getThemeProperty, 4 | } from "../../theme/defaultThemeOperators"; 5 | export const getSadColor = getThemeProperty("sadColor"); 6 | export const getNeutralColor = getThemeProperty("neutralColor"); 7 | export const getHappyColor = getThemeProperty("happyColor"); 8 | export const setSadColor = setThemeProperty("sadColor"); 9 | export const setNeutralColor = setThemeProperty("neutralColor"); 10 | export const setHappyColor = setThemeProperty("happyColor"); 11 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/tooltip/utils/getPlacement.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import { PLACEMENTS_HORIZ } from '../constants/PlacementConstants'; 4 | 5 | export function getSide(placement) { 6 | return placement.split(' ')[0]; 7 | } 8 | 9 | export function isHoriz(direction) { 10 | return PLACEMENTS_HORIZ.includes(direction); 11 | } 12 | 13 | export function getEdge(placement) { 14 | const specifiedEdge = placement.split(' ')[1]; 15 | if (specifiedEdge) { 16 | return specifiedEdge; 17 | } 18 | 19 | return isHoriz(getSide(placement)) ? 'middle' : 'center'; 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const tsLoader = { 4 | test: /\.ts$/, 5 | exclude: /(node_modules)/, 6 | use: { 7 | loader: "ts-loader", 8 | options: { 9 | configFile: "tsconfig.esm.json", 10 | }, 11 | }, 12 | }; 13 | 14 | module.exports = { 15 | entry: "./index.ts", 16 | mode: "production", 17 | output: { 18 | filename: "main.js", 19 | libraryTarget: "umd", 20 | path: path.resolve(__dirname, "dist"), 21 | clean: true, 22 | }, 23 | resolve: { 24 | extensions: [".ts", ".tsx", ".js"], 25 | }, 26 | module: { 27 | rules: [tsLoader], 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/microphone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webpack-test.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const glob = require("glob"); 3 | 4 | module.exports = { 5 | entry: glob.sync("./test/spec/**/*-test.ts"), 6 | resolve: { 7 | extensions: [".tsx", ".ts", ".js"], 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, "dist-test"), 11 | filename: "test.js", 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(ts|tsx)$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: "ts-loader", 20 | options: { 21 | configFile: "tsconfig.esm.json", 22 | }, 23 | }, 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/deleteLeft.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/input/theme/inputThemeOperators.js: -------------------------------------------------------------------------------- 1 | "use es6"; 2 | 3 | import { 4 | getDisabledBackgroundColor, 5 | getDisabledTextColor, 6 | getPlaceholderTextColor, 7 | getPrimaryColor, 8 | } from "../../theme/defaultThemeOperators"; 9 | import { WHITE } from "../../theme/ColorConstants"; 10 | 11 | export const getInputDisabledBackgroundColor = getDisabledBackgroundColor; 12 | export const getInputDisabledTextColor = getDisabledTextColor; 13 | export const getInputPlaceholderColor = getPlaceholderTextColor; 14 | export const getInputFocusColor = getPrimaryColor; 15 | 16 | export const getInputOnDarkBackgroundColor = () => WHITE; 17 | -------------------------------------------------------------------------------- /webpack.cjs.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { merge } = require("webpack-merge"); 3 | const common = require("./webpack.common"); 4 | 5 | module.exports = merge(common, { 6 | mode: "production", 7 | output: { 8 | filename: "main.js", 9 | library: { 10 | type: "umd", 11 | }, 12 | path: path.resolve(__dirname, "dist"), 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | exclude: /node_modules/, 19 | use: { 20 | loader: "ts-loader", 21 | options: { 22 | configFile: "tsconfig.cjs.json", 23 | }, 24 | }, 25 | }, 26 | ], 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/constants/buttonIds.ts: -------------------------------------------------------------------------------- 1 | export const ANSWER_CALL = "answer-call"; 2 | export const COMPLETE_CALL = "complete-call"; 3 | export const END_CALL = "end-call"; 4 | export const TOGGLE_INCOMING_CALL_OPTION = "toggle-incoming-call-option"; 5 | export const INCOMING_CALL = "incoming-call"; 6 | export const LOG_IN = "log-in"; 7 | export const LOG_OUT = "log-out"; 8 | export const OUTGOING_CALL = "outgoing-call"; 9 | export const RESIZE_WIDGET = "resize-widget"; 10 | export const TOGGLE_AVAILABILITY = "toggle-availability"; 11 | export const USER_AVAILABLE = "user-available"; 12 | export const USER_UNAVAILABLE = "user-unavailable"; 13 | export const FINALIZE_ENGAGEMENT = "finalize-engagement"; 14 | -------------------------------------------------------------------------------- /demos/demo-minimal-js/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | module.exports = { 4 | entry: "./index.js", 5 | mode: "development", 6 | plugins: [new HtmlWebpackPlugin({ 7 | template: "./index.html", 8 | inject: false, 9 | filename: "demo-minimal-js.html", 10 | })], 11 | output: { 12 | libraryTarget: "umd", 13 | path: path.resolve(__dirname, "bin"), 14 | filename: "demo-minimal-js.bundle.js", 15 | clean: true, 16 | }, 17 | devServer: { 18 | https: true, 19 | port: 9025, 20 | static: { 21 | directory: path.resolve(__dirname), 22 | }, 23 | historyApiFallback: { 24 | index: "index.html", 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /webpack.esm.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { merge } = require("webpack-merge"); 3 | const common = require("./webpack.common"); 4 | 5 | module.exports = merge(common, { 6 | mode: "production", 7 | output: { 8 | filename: "main.esm.js", 9 | library: { 10 | type: "module", 11 | }, 12 | path: path.resolve(__dirname, "dist"), 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | exclude: /node_modules/, 19 | use: { 20 | loader: "ts-loader", 21 | options: { 22 | configFile: "tsconfig.esm.json", 23 | }, 24 | }, 25 | }, 26 | ], 27 | }, 28 | experiments: { 29 | outputModule: true, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/tooltip/theme/tooltipThemeOperators.ts: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import { 4 | DEFAULT_TOOLTIP_BACKGROUND_COLOR, 5 | DEFAULT_TOOLTIP_TEXT_COLOR, 6 | } from '../../theme/ColorConstants'; 7 | import { 8 | getThemeProperty, 9 | setThemeProperty, 10 | } from '../../theme/defaultThemeOperators'; 11 | 12 | // Override 13 | export const getTooltipBackgroundColor = 14 | getThemeProperty('tooltipBackground') || DEFAULT_TOOLTIP_BACKGROUND_COLOR; 15 | export const setTooltipBackgroundColor = setThemeProperty('tooltipBackground'); 16 | 17 | export const getTooltipTextColor = 18 | getThemeProperty('tooltipText') || DEFAULT_TOOLTIP_TEXT_COLOR; 19 | export const setTooltipTextColor = setThemeProperty('tooltipText'); 20 | -------------------------------------------------------------------------------- /demos/demo-minimal-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calling-extensions-sdk-demo-minimal-js", 3 | "version": "1.0.0", 4 | "description": "HubSpot calling extension sdk demo minimal js", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack", 8 | "build:gh": "npm ci && cross-env NODE_ENV=production npm run build", 9 | "serve": "webpack serve", 10 | "start": "webpack serve --open" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=14" 15 | }, 16 | "devDependencies": { 17 | "cross-env": "^7.0.3", 18 | "html-webpack-plugin": "^5.5.0", 19 | "webpack": "^5.94.0", 20 | "webpack-cli": "^5.0.1", 21 | "webpack-dev-server": "^5.2.1" 22 | }, 23 | "dependencies": { 24 | "uuid": "^10.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/utils/millisecondsToFormattedDuration.ts: -------------------------------------------------------------------------------- 1 | export const millisecondsToFormattedDuration = (milliseconds: number) => { 2 | const seconds = milliseconds / 1000; 3 | const minutes = Math.floor(seconds / 60); 4 | const hours = Math.floor(minutes / 60); 5 | 6 | let minutesString = `${Math.floor(minutes % 60)}`; 7 | let secondsString = `${Math.floor(seconds % 60)}`; 8 | 9 | if (secondsString.length === 1) { 10 | secondsString = 0 + secondsString; 11 | } 12 | if (hours > 0 && minutesString.length === 1) { 13 | minutesString = 0 + minutesString; 14 | } 15 | 16 | const duration = [minutesString, secondsString]; 17 | if (hours > 0) { 18 | duration.unshift(hours.toString()); 19 | } 20 | 21 | return duration.join(":"); 22 | }; 23 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demos/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calling-extensions-sdk-demos", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "calling-extensions-sdk-demos", 8 | "dependencies": { 9 | "@hubspot/calling-extensions-sdk": "^0.9.1-alpha.1" 10 | } 11 | }, 12 | "node_modules/@hubspot/calling-extensions-sdk": { 13 | "version": "0.9.1-alpha.1", 14 | "resolved": "https://registry.npmjs.org/@hubspot/calling-extensions-sdk/-/calling-extensions-sdk-0.9.1-alpha.1.tgz", 15 | "integrity": "sha512-N+39rKC+kEhoWnIZTg1WDchNcr4E0IhG0MVU3Mi184+L48ZBiC79VRRF/FvQf6GWwurfdczgwVHo4ixQdkAK5Q==", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">=14" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/tooltip/constants/PlacementConstants.ts: -------------------------------------------------------------------------------- 1 | export const PLACEMENTS_HORIZ = ['left', 'right']; 2 | export const PLACEMENTS_VERT = ['top', 'bottom']; 3 | 4 | export const PLACEMENTS_SIDES = PLACEMENTS_HORIZ.concat(PLACEMENTS_VERT); 5 | 6 | export const PLACEMENTS = PLACEMENTS_SIDES.concat([ 7 | 'left top', 8 | 'left bottom', 9 | 'right top', 10 | 'right bottom', 11 | 'top left', 12 | 'top right', 13 | 'bottom left', 14 | 'bottom right', 15 | 'top center', 16 | 'bottom center', 17 | 'left middle', 18 | 'right middle', 19 | ]); 20 | 21 | export const OPPOSITE_DIRECTIONS = { 22 | top: 'bottom', 23 | bottom: 'top', 24 | left: 'right', 25 | right: 'left', 26 | middle: 'middle', 27 | center: 'center', 28 | }; 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy demo apps to GH Pages 2 | concurrency: ci-${{ github.ref }} 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths: 8 | - 'demos/**' 9 | permissions: 10 | contents: write 11 | defaults: 12 | run: 13 | working-directory: demos 14 | jobs: 15 | build-and-deploy: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 🛎️ 19 | uses: actions/checkout@v3 20 | - name: Install and build 🔧 21 | run: npm run build:gh 22 | - name: Run tests ✅ 23 | run: npm run test 24 | - name: Deploy 🚀 25 | uses: JamesIves/github-pages-deploy-action@v4 26 | with: 27 | folder: demos/build 28 | branch: gh-pages 29 | clean-exclude: pr-preview 30 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/mobileRetro.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/microphoneSlash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/link/theme/linkThemeOperators.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components'; 2 | import { 3 | getPrimaryColor, 4 | getTextColor, 5 | setThemeProperty, 6 | } from '../../theme/defaultThemeOperators'; 7 | import { get } from '../../utils/get'; 8 | import { 9 | DEFAULT_HELP_TEXT_COLOR, 10 | DEFAULT_ERROR_TEXT_COLOR, 11 | } from '../../theme/ColorConstants'; 12 | 13 | export const getLinkTextColor = (theme: DefaultTheme) => 14 | get('linkText', theme) || getPrimaryColor(theme); 15 | export const setLinkTextColor = setThemeProperty('linkText'); 16 | export const getExternalLinkIconColor = () => DEFAULT_HELP_TEXT_COLOR; 17 | export const getOnBrightLinkTextColor = getTextColor; 18 | export const getErrorTextColor = () => DEFAULT_ERROR_TEXT_COLOR; 19 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/components/Alert.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, MouseEventHandler } from "react"; 2 | import { BATTLESHIP, OLAF } from "../utils/colors"; 3 | import { LinkButton } from "./Components"; 4 | 5 | function Alert({ 6 | title, 7 | onConfirm, 8 | }: { 9 | title: string | ReactElement; 10 | onConfirm: MouseEventHandler; 11 | }) { 12 | return ( 13 |
22 | {title} 23 | 24 | × 25 | 26 |
27 | ); 28 | } 29 | 30 | export default Alert; 31 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/input/theme/expandingInputThemeOperators.js: -------------------------------------------------------------------------------- 1 | "use es6"; 2 | 3 | import { 4 | getErrorTextColor, 5 | getDisabledBackgroundColor, 6 | getDisabledTextColor, 7 | getPlaceholderTextColor, 8 | getInputBorderColor, 9 | getInputBackgroundColor, 10 | } from "../../theme/defaultThemeOperators"; 11 | 12 | export const getExpandingInputBorderColor = getInputBorderColor; 13 | export const getExpandingInputBackgroundColor = getInputBackgroundColor; 14 | 15 | export const getExpandingInputErrorBorderColor = getErrorTextColor; 16 | export const getExpandingInputDisabledBackgroundColor = 17 | getDisabledBackgroundColor; 18 | export const getExpandingInputDisabledTextColor = getDisabledTextColor; 19 | export const getExpandingInputPlaceholderColor = getPlaceholderTextColor; 20 | -------------------------------------------------------------------------------- /demos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calling-extensions-sdk-demos", 3 | "homepage": "https://github.hubspot.com/calling-extensions-sdk", 4 | "description": "Build files for deployment to GitHub Pages", 5 | "scripts": { 6 | "start:js": "cd demo-minimal-js && npm run start", 7 | "start:react": "cd demo-react-ts && npm run start", 8 | "build:js": "cd demo-minimal-js && npm run build:gh", 9 | "build:react": "cd demo-react-ts && npm run build:gh", 10 | "build:gh": "npm run build:js && npm run build:react && cp ./src/index.html build/index.html && cp -a demo-minimal-js/bin/. build && cp -a demo-react-ts/dist/. build", 11 | "test:react": "cd demo-react-ts && npm run test", 12 | "test": "npm run test:react" 13 | }, 14 | "dependencies": { 15 | "@hubspot/calling-extensions-sdk": "^0.9.1-alpha.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/tooltip/VizExTooltipArrow.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import styled from 'styled-components'; 4 | import { getArrowSpacing } from './utils/getArrowSpacing'; 5 | import { getTooltipBackgroundColor } from './theme/tooltipThemeOperators'; 6 | 7 | const VizExTooltipArrow = styled.div` 8 | position: absolute; 9 | pointer-events: none; 10 | border: none; 11 | clip-path: polygon(100% 100%, 0 100%, 100% 0); 12 | 13 | border-top-left-radius: 100%; 14 | border-radius: 3px; 15 | border-top-color: transparent !important; 16 | border-left-color: transparent !important; 17 | border-bottom-right-radius: 3px; 18 | 19 | width: 16px; 20 | height: 16px; 21 | background-color: ${({ theme }) => getTooltipBackgroundColor(theme)}; 22 | ${getArrowSpacing} 23 | `; 24 | 25 | export default VizExTooltipArrow; 26 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/theme/VizExThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { DefaultTheme, ThemeProvider } from 'styled-components'; 3 | import VizExGlobalStyle from '../global/VizExGlobalStyle'; 4 | import themePropType from '../utils/themePropType'; 5 | import { ReactNode } from 'react'; 6 | 7 | export type VizExThemeProviderProps = { 8 | theme: DefaultTheme; 9 | children: ReactNode; 10 | }; 11 | 12 | const VizExThemeProvider = ({ theme, children }: VizExThemeProviderProps) => ( 13 | 14 | {children} 15 | 16 | 17 | ); 18 | 19 | VizExThemeProvider.displayName = 'VizExThemeProvider'; 20 | VizExThemeProvider.propTypes = { 21 | children: PropTypes.node, 22 | theme: themePropType, 23 | }; 24 | 25 | export default VizExThemeProvider; 26 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | name: Deploy PR previews for demo apps 2 | run-name: ${{ github.actor }} is deploying a PR 3 | concurrency: preview-${{ github.ref }} 4 | on: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | paths: 11 | - "demos/**" 12 | defaults: 13 | run: 14 | working-directory: demos 15 | jobs: 16 | deploy-preview: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 🛎️ 20 | uses: actions/checkout@v3 21 | - name: Install and build 🔧 22 | run: npm run build:gh 23 | - name: Run tests ✅ 24 | run: npm run test 25 | - name: Deploy preview 🚀 26 | uses: rossjrw/pr-preview-action@v1 27 | with: 28 | source-dir: demos/build 29 | preview-branch: gh-pages 30 | umbrella-dir: pr-preview 31 | action: auto 32 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/typography/VizExSmall.js: -------------------------------------------------------------------------------- 1 | "use es6"; 2 | 3 | import styled, { css } from "styled-components"; 4 | import { 5 | getErrorTextColor, 6 | getHelpTextColor, 7 | } from "../theme/defaultThemeOperators"; 8 | import { ERROR, HELP, DEFAULT } from "./constants/SmallVariations"; 9 | import { getSmallStyles } from "./utils/getSmallStyles"; 10 | 11 | const getVariationStyles = ({ use, theme }) => { 12 | switch (use) { 13 | case ERROR: 14 | return css` 15 | color: ${getErrorTextColor(theme)}; 16 | `; 17 | case HELP: 18 | return css` 19 | color: ${getHelpTextColor(theme)}; 20 | `; 21 | case DEFAULT: 22 | default: 23 | return null; 24 | } 25 | }; 26 | 27 | const VizExSmall = styled.small` 28 | display: block; 29 | ${getSmallStyles}; 30 | ${getVariationStyles}; 31 | `; 32 | 33 | export default VizExSmall; 34 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/hooks/useTimer.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | import { millisecondsToFormattedDuration } from "../utils/millisecondsToFormattedDuration"; 3 | 4 | export const useCallDurationTimer = () => { 5 | const timerId = useRef(); 6 | const [callDuration, setCallDuration] = useState(0); 7 | 8 | const startTimer = (callStartTime: number) => { 9 | const tick = () => { 10 | setCallDuration(Date.now() - callStartTime); 11 | }; 12 | timerId.current = setInterval(tick, 1000); 13 | }; 14 | 15 | const stopTimer = () => { 16 | clearInterval(timerId.current); 17 | }; 18 | 19 | const resetCallDuration = () => { 20 | setCallDuration(0); 21 | }; 22 | 23 | return { 24 | callDuration, 25 | callDurationString: millisecondsToFormattedDuration(callDuration), 26 | startTimer, 27 | stopTimer, 28 | resetCallDuration, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/theme/ColorConstants.ts: -------------------------------------------------------------------------------- 1 | export const WHITE = '#fff'; 2 | export const GREEN = '#00bda5'; 3 | export const GREY = '#cbd6e2'; 4 | 5 | export const DEFAULT_PRIMARY_COLOR = '#f15f79'; 6 | export const DEFAULT_TEXT_COLOR = '#33475b'; 7 | export const DEFAULT_ERROR_TEXT_COLOR = '#f2545b'; 8 | export const DISABLED_BACKGROUND_COLOR = '#eaf0f6'; 9 | export const DISABLED_TEXT_COLOR = '#b0c1d4'; 10 | 11 | export const DEFAULT_HELP_TEXT_COLOR = '#425b76'; 12 | export const DEFAULT_PLACEHOLDER_TEXT_COLOR = '#7b98b6'; 13 | 14 | export const DEFAULT_INPUT_BORDER_COLOR = '#cbd6e2'; 15 | export const DEFAULT_INPUT_BACKGROUND_COLOR = '#f5f8fa'; 16 | 17 | export const DEFAULT_HAPPY_COLOR = '#a2d28f'; 18 | export const DEFAULT_NEUTRAL_COLOR = '#fea58e'; 19 | export const DEFAULT_SAD_COLOR = '#ea90b1'; 20 | 21 | export const DEFAULT_TOOLTIP_BACKGROUND_COLOR = '#425b76'; 22 | export const DEFAULT_TOOLTIP_TEXT_COLOR = WHITE; 23 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/isUnsafeUrl.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-script-url 2 | const NOOP_HREF = 'javascript:void(0)'; 3 | 4 | // Match the protocol part of an absolute URL, e.g. "http:" 5 | const PROTOCOL_REGEX = /^([^:]+):/; 6 | 7 | // Whitespace and control characters are ignored, so we must strip them out 8 | // eslint-disable-next-line no-control-regex 9 | const IGNORED_PROTOCOL_CHARS_REGEX = /[\s\x00-\x1f]/g; 10 | 11 | export const isUnsafeUrl = (href: string) => { 12 | if (href && typeof href === 'string') { 13 | const protocolMatch = href.match(PROTOCOL_REGEX); 14 | if (!protocolMatch || href === NOOP_HREF) return false; 15 | if ( 16 | protocolMatch[0] 17 | .replace(IGNORED_PROTOCOL_CHARS_REGEX, '') 18 | // eslint-disable-next-line no-script-url 19 | .toLowerCase() === 'javascript:' 20 | ) { 21 | return true; 22 | } 23 | } 24 | return false; 25 | }; 26 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/constants/LoadingButtonUses.ts: -------------------------------------------------------------------------------- 1 | import * as ButtonUses from './ButtonUses'; 2 | import * as SpinnerUses from '../../loading/constants/SpinnerUses'; 3 | 4 | export const PRIMARY = 'primary'; 5 | export const SECONDARY = 'secondary'; 6 | 7 | type LoadingButtonUses = typeof PRIMARY | typeof SECONDARY; 8 | const LoadingButtonUses = { PRIMARY, SECONDARY }; 9 | 10 | const loadingButtonToButtonMap = { 11 | [PRIMARY]: ButtonUses.PRIMARY, 12 | [SECONDARY]: ButtonUses.SECONDARY, 13 | }; 14 | 15 | export const buttonUse = (loadingButtonUse: LoadingButtonUses) => 16 | loadingButtonToButtonMap[loadingButtonUse]; 17 | 18 | const loadingButtonToSpinnerMap = { 19 | [PRIMARY]: SpinnerUses.SECONDARY, 20 | [SECONDARY]: SpinnerUses.PRIMARY, 21 | }; 22 | 23 | export const spinnerUse = (loadingButtonUse: LoadingButtonUses) => 24 | loadingButtonToSpinnerMap[loadingButtonUse]; 25 | 26 | export default LoadingButtonUses; 27 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/tooltip/VizExTooltipBody.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import styled from 'styled-components'; 4 | import { getBodySpacing } from './utils/getBodySpacing'; 5 | import { 6 | getTooltipBackgroundColor, 7 | getTooltipTextColor, 8 | } from './theme/tooltipThemeOperators'; 9 | 10 | const VizExTooltipBody = styled.div` 11 | border-radius: 3px; 12 | font-size: 13px; 13 | max-width: 232px; 14 | display: block; 15 | position: absolute; 16 | visibility: visible; 17 | box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1); 18 | line-height: 1.5; 19 | padding: 10px 16px; 20 | text-decoration: none; 21 | word-wrap: break-word; 22 | ${getBodySpacing}; 23 | white-space: nowrap; 24 | background-color: ${({ theme }) => getTooltipBackgroundColor(theme)}; 25 | color: ${({ theme }) => getTooltipTextColor(theme)}; 26 | pointer-events: ${({ open }) => (open ? 'all' : 'none')}; 27 | `; 28 | 29 | export default VizExTooltipBody; 30 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/adjustLuminance.ts: -------------------------------------------------------------------------------- 1 | /* hs-eslint ignored failing-rules */ 2 | /* eslint-disable no-bitwise */ 3 | 4 | import { hexToRGB } from './hexToRGB'; 5 | 6 | export const adjustLuminance = ( 7 | colorHex: string, 8 | luminanceShiftPercentage: number 9 | ) => { 10 | const { r, g, b } = hexToRGB(colorHex); 11 | 12 | const newRedColor = 13 | 0 | ((1 << 8) + r + ((256 - r) * luminanceShiftPercentage) / 100); 14 | const redHex = `0${newRedColor.toString(16).substr(1)}`.substr(-2); 15 | 16 | const newGreenColor = 17 | 0 | ((1 << 8) + g + ((256 - g) * luminanceShiftPercentage) / 100); 18 | const greenHex = `0${newGreenColor.toString(16).substr(1)}`.substr(-2); 19 | 20 | const newBlueColor = 21 | 0 | ((1 << 8) + b + ((256 - b) * luminanceShiftPercentage) / 100); 22 | const blueHex = `0${newBlueColor.toString(16).substr(1)}`.substr(-2); 23 | 24 | return `#${redHex}${greenHex}${blueHex}`; 25 | }; 26 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/SyntheticEvent.ts: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from 'react'; 2 | 3 | type Value = { text: string; value: string }; 4 | 5 | class SyntheticEventClass { 6 | target: { value: Value }; 7 | currentTarget: { value: Value }; 8 | source: ChangeEvent; 9 | constructor(value: Value, evt: ChangeEvent) { 10 | const target = { 11 | value, 12 | }; 13 | this.target = target; 14 | this.currentTarget = target; 15 | this.source = evt; 16 | } 17 | 18 | preventDefault() { 19 | if (this.source) { 20 | this.source.preventDefault(); 21 | } 22 | } 23 | 24 | stopPropagation() { 25 | if (this.source) { 26 | this.source.stopPropagation(); 27 | } 28 | } 29 | } 30 | 31 | function SyntheticEvent(value: Value, evt: ChangeEvent) { 32 | return new SyntheticEventClass(value, evt); 33 | } 34 | 35 | SyntheticEvent.constructor = SyntheticEventClass; 36 | 37 | export default SyntheticEvent; 38 | -------------------------------------------------------------------------------- /demos/demo-react-ts/webpack-test.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const glob = require("glob"); 3 | 4 | module.exports = { 5 | entry: glob.sync("./test/spec/**/*-test.ts?(x)"), 6 | resolve: { 7 | extensions: [".tsx", ".ts", ".js"], 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, "dist-test"), 11 | filename: "test.js", 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.svg$/i, 17 | issuer: /\.[jt]sx?$/, 18 | use: ["@svgr/webpack"], 19 | }, 20 | { 21 | test: /\.(ts|tsx|js)$/, 22 | exclude: /node_modules/, 23 | use: { 24 | loader: "babel-loader", 25 | options: { 26 | customize: require.resolve( 27 | "babel-preset-react-app/webpack-overrides" 28 | ), 29 | presets: [ 30 | "@babel/preset-env", 31 | [ 32 | require.resolve("babel-preset-react-app"), 33 | { runtime: "automatic" }, 34 | ], 35 | ], 36 | }, 37 | }, 38 | }, 39 | ], 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 HubSpot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/getTextColorFromBgColor.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components'; 2 | import { getContrastRatio } from './getContrastRatio'; 3 | 4 | //This ratio should be set to 4.5 once the theme is finalized 5 | const MINIMUM_CONTRAST_RATIO = 3; 6 | 7 | // Select accessible text from available text color options 8 | export const getTextColorFromBgColor = ( 9 | backgroundColor: string, 10 | theme: DefaultTheme 11 | ): string => { 12 | const accessibleText = 13 | getContrastRatio(backgroundColor, theme.primary) >= MINIMUM_CONTRAST_RATIO 14 | ? theme.primary 15 | : theme.textOnPrimary; 16 | const currentContrastRatio = getContrastRatio( 17 | backgroundColor, 18 | accessibleText 19 | ); 20 | if (currentContrastRatio < MINIMUM_CONTRAST_RATIO) { 21 | // eslint-disable-next-line no-console 22 | console.error( 23 | `The current contrast ratio of ${currentContrastRatio}:1 does not meet the minimum contrast standards specified by the WCAG 2.0 (https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast).` 24 | ); 25 | } 26 | 27 | return accessibleText; 28 | }; 29 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "globals": { 4 | "window": true, 5 | "document": true 6 | }, 7 | "rules": { 8 | "no-multi-spaces": 0, 9 | "no-underscore-dangle": [0], 10 | "consistent-return": 0, 11 | "no-unused-expressions": [2, { "allowShortCircuit": true }], 12 | "no-param-reassign": 0, 13 | "func-names": 0, 14 | "space-before-function-paren": [2, "never"], 15 | "comma-dangle": 1, 16 | "no-shadow": 0, 17 | "guard-for-in": 0, 18 | "no-restricted-syntax": [2, "WithStatement"], 19 | "newline-per-chained-call": [2, { "ignoreChainWithDepth": 5 }], 20 | "space-in-parens": 0, 21 | "key-spacing": 0, 22 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 23 | "max-len": 1, 24 | "padded-blocks": 0, 25 | "no-console": 0, 26 | "no-continue": 0, 27 | "import/no-extraneous-dependencies": 0, 28 | "import/newline-after-import": 0, 29 | "no-mixed-operators": 0, 30 | "quotes": ["warn", "double"], 31 | "arrow-parens": ["error", "as-needed"], 32 | "import/prefer-default-export": 0, 33 | "arrow-body-style": 0, 34 | "prefer-object-spread": 0, 35 | "operator-linebreak": 0 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/getContrastRatio.ts: -------------------------------------------------------------------------------- 1 | //Derived from https://www.w3.org/TR/WCAG20-TECHS/G17.html 2 | import { hexToRGB } from './hexToRGB'; 3 | export const getBrightness = (hexColor: string): number => { 4 | const map = new Map(Object.entries(hexToRGB(hexColor))); 5 | map.forEach((value, key) => { 6 | RGBToLinear(value, key, map); 7 | }); 8 | return +( 9 | 0.2126 * (map.get('r') || 0) + 10 | 0.7152 * (map.get('g') || 0) + 11 | 0.0722 * (map.get('b') || 0) 12 | ).toFixed(2); 13 | }; 14 | 15 | function RGBToLinear(val: number, key: string, map: Map) { 16 | const normalizedVal = val / 255; 17 | if (normalizedVal <= 0.03928) { 18 | map.set(key, normalizedVal / 12.92); 19 | } else { 20 | map.set(key, Math.pow((normalizedVal + 0.055) / 1.055, 2.4)); 21 | } 22 | } 23 | 24 | export const getContrastRatio = ( 25 | textColor: string, 26 | backgroundColor: string 27 | ): number => { 28 | const luminanceA = getBrightness(textColor); 29 | const luminanceB = getBrightness(backgroundColor); 30 | const darker = luminanceA > luminanceB ? luminanceA : luminanceB; 31 | const lighter = luminanceA === darker ? luminanceB : luminanceA; 32 | return (darker + 0.05) / (lighter + 0.05); 33 | }; 34 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/utils/phoneNumberUtils.ts: -------------------------------------------------------------------------------- 1 | export const FROM_NUMBER_ONE = "+16179483986"; 2 | export const FROM_NUMBER_TWO = "+442073238299"; 3 | export const FORMATTED_FROM_NUMBER_ONE = "+1 617-948-3986"; 4 | export const FORMATTED_FROM_NUMBER_TWO = "+44 20 7323 8299"; 5 | 6 | export const FROM_NUMBERS_MAP = { 7 | [FROM_NUMBER_ONE]: FORMATTED_FROM_NUMBER_ONE, 8 | [FROM_NUMBER_TWO]: FORMATTED_FROM_NUMBER_TWO, 9 | }; 10 | 11 | export type PhoneNumber = typeof FROM_NUMBER_ONE | typeof FROM_NUMBER_TWO; 12 | 13 | export const validateKeypadInput = (value: string) => { 14 | return /^[0-9+*#]*$/.test(value); 15 | }; 16 | 17 | export const validatePhoneNumber = (value: string) => { 18 | return value.length > 2; 19 | }; 20 | 21 | export const formatPhoneNumber = (phoneNumberString: string): string => { 22 | const cleaned = `${phoneNumberString}`.replace(/\D/g, ""); 23 | const match = cleaned.match(/^(1|)?(\d{3})(\d{3})(\d{4})$/); 24 | if (match) { 25 | const intlCode = match[1] ? "+1 " : ""; 26 | return [intlCode, "(", match[2], ") ", match[3], "-", match[4]].join(""); 27 | } 28 | return phoneNumberString; 29 | }; 30 | 31 | export const getFormattedFromNumber = (phoneNumber: string) => { 32 | return FROM_NUMBERS_MAP[phoneNumber as PhoneNumber] || phoneNumber; 33 | }; 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Checklist** 13 | - [ ] I have read the [FAQs](https://github.com/HubSpot/calling-extensions-sdk#faqs). 14 | - [ ] I have checked the [issue tracker](https://github.com/HubSpot/calling-extensions-sdk/issues) for duplicate issues. 15 | 16 | **Is your feature request related to a problem? Please describe.** 17 | 18 | I'm always frustrated when [...] 19 | 20 | **Describe the solution you'd like** 21 | 22 | 23 | **Screenshots/source code/links** 24 | 25 | 26 | **Additional context** 27 | 28 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/mergeDeep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Performs a deep merge of two objects and returns new object. Does not modify 3 | * objects (immutable) and merges arrays via concatenation. 4 | * @param targetObj The object to act as the target to be merged onto. 5 | * @param sourceObj The object to source properties that override the target's. 6 | * @returns A single merged object. 7 | */ 8 | export function mergeDeep< 9 | Target = Record, 10 | Source = Record 11 | >(targetObj: Target, sourceObj: Source) { 12 | const isObject = (obj: any) => obj && typeof obj === 'object'; 13 | 14 | return ([targetObj, sourceObj] as Record[]).reduce( 15 | (target, source) => { 16 | Object.keys(source).forEach(key => { 17 | const targetVal = target[key]; 18 | const sourceVal = source[key]; 19 | 20 | if (Array.isArray(targetVal) && Array.isArray(sourceVal)) { 21 | target[key] = targetVal.concat(...sourceVal); 22 | } else if (isObject(targetVal) && isObject(sourceVal)) { 23 | target[key] = mergeDeep(targetVal, sourceVal); 24 | } else { 25 | target[key] = sourceVal; 26 | } 27 | }); 28 | 29 | return target; 30 | }, 31 | {} 32 | ) as Target & Source; 33 | } 34 | -------------------------------------------------------------------------------- /demos/demo-react-ts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@typescript-eslint/recommended", 4 | "plugin:import/errors", 5 | "plugin:import/warnings", 6 | "plugin:import/typescript", 7 | "plugin:react-hooks/recommended", 8 | "eslint:recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "plugins": ["@typescript-eslint"], 12 | "env": { "browser": true, "jasmine": true }, 13 | "globals": { 14 | "NodeJS": true 15 | }, 16 | "rules": { 17 | "import/extensions": [ 18 | "error", 19 | "ignorePackages", 20 | { 21 | "js": "never", 22 | "jsx": "never", 23 | "ts": "never", 24 | "tsx": "never" 25 | } 26 | ], 27 | "react/jsx-filename-extension": [1, { "extensions": [".tsx", ".ts"] }], 28 | "react/react-in-jsx-scope": 0, 29 | "react/jsx-one-expression-per-line": 0, 30 | "object-curly-newline": 0, 31 | "@typescript-eslint/ban-types": 1, 32 | "arrow-parens": 0, 33 | "react/jsx-props-no-spreading": 0, 34 | "space-before-function-paren": 0, 35 | "@typescript-eslint/no-empty-function": 0, 36 | "implicit-arrow-linebreak": 0, 37 | "function-paren-newline": 0, 38 | "@typescript-eslint/no-unused-vars": [ 39 | 1, 40 | { 41 | "argsIgnorePattern": "^_" 42 | } 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/theme/iconButtonThemeOperators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getPrimaryColor, 3 | getDisabledTextColor, 4 | getDisabledBackgroundColor, 5 | getTextOnPrimaryColor, 6 | setThemeProperty, 7 | } from '../../theme/defaultThemeOperators'; 8 | import { get } from '../../utils/get'; 9 | import { DefaultTheme } from 'styled-components'; 10 | 11 | export const getIconButtonBackgroundColor = getPrimaryColor; 12 | export const getIconButtonTextColor = getTextOnPrimaryColor; 13 | 14 | export const getTransparentOnPrimaryIconButtonBackgroundColor = getTextOnPrimaryColor; 15 | export const getTransparentOnPrimaryIconButtonTextColor = getTextOnPrimaryColor; 16 | 17 | export const getTransparentOnBackgroundIconButtonBackgroundColor = ( 18 | theme: DefaultTheme 19 | ) => get('transparentOnBackgroundIconButton', theme) || getPrimaryColor(theme); 20 | 21 | export const getTransparentOnBackgroundIconButtonTextColor = ( 22 | theme: DefaultTheme 23 | ) => get('transparentOnBackgroundIconButton', theme) || getPrimaryColor(theme); 24 | 25 | export const setTransparentOnBackgroundIconButton = setThemeProperty( 26 | 'transparentOnBackgroundIconButton' 27 | ); 28 | 29 | export const getDisabledIconButtonTextColor = getDisabledTextColor; 30 | export const getDisabledIconButtonBackgroundColor = getDisabledBackgroundColor; 31 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/VizExFileButton.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import { useRef, Fragment } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { callIfValid } from '../utils/callIfValid'; 6 | import styled from 'styled-components'; 7 | 8 | const FilePickerInput = styled.input` 9 | display: none; 10 | `; 11 | 12 | const VizExFileButton = props => { 13 | const { children, multiple, accept, onChange, onClick, ...rest } = props; 14 | const fileInputRef = useRef(); 15 | const handleClick = evt => { 16 | fileInputRef.current.click(); 17 | callIfValid(onClick, evt); 18 | }; 19 | return ( 20 | 21 | {children({ onClick: handleClick })} 22 | 30 | 31 | ); 32 | }; 33 | VizExFileButton.displayName = 'VizExFileButton'; 34 | VizExFileButton.defaultProps = { 35 | accept: [], 36 | }; 37 | VizExFileButton.propTypes = { 38 | accept: PropTypes.arrayOf(PropTypes.string), 39 | children: PropTypes.func.isRequired, 40 | multiple: PropTypes.bool, 41 | onChange: PropTypes.func, 42 | onClick: PropTypes.func, 43 | }; 44 | 45 | export default VizExFileButton; 46 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/list/theme/listItemButtonTheme.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | import { VizExListItemButtonProps } from '../VizExListItemButton'; 4 | import { ThemeConfig } from '../../theme/styled'; 5 | import { focusRing } from '../../utils/mixins'; 6 | 7 | export const listItemButtonTheme = { 8 | baseStyle: css` 9 | display: block; 10 | width: 100%; 11 | font-size: 14px; 12 | text-align: left; 13 | text-decoration: none; 14 | background-color: transparent; 15 | transition: background-color 150ms ease-out; 16 | border: none; 17 | min-height: 40px; 18 | color: ${({ theme }) => theme.text}; 19 | ${({ alignItems }) => alignItems && `align-items: ${alignItems};`} 20 | padding: 0; 21 | ${({ disablePadding }) => 22 | !disablePadding && `padding-left: 10px; padding-right: 10px;`} 23 | ${({ disableGutters }) => 24 | !disableGutters && `padding-top: 10px; padding-bottom: 10px;`} 25 | `, 26 | _hovered: css` 27 | background-color: rgba(0, 0, 0, 0.08); 28 | `, 29 | _focused: css` 30 | ${focusRing} 31 | outline-offset: -2px; 32 | `, 33 | _pressed: css` 34 | background-color: rgba(0, 0, 0, 0.16); 35 | `, 36 | } as ThemeConfig['components']['ListItemButton']; 37 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/link/theme/linkTheme.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { ThemeConfig } from '../../theme/styled'; 3 | import { focusRing } from '../../utils/mixins'; 4 | import { adjustLuminance } from '../../utils/adjustLuminance'; 5 | import { VizExLinkProps } from '../VizExLink'; 6 | import { ON_BRIGHT, ERROR } from '../constants/LinkVariations'; 7 | 8 | const getLinkColor = ({ use, theme }: VizExLinkProps) => { 9 | if (use === ON_BRIGHT) return theme.text; 10 | if (use === ERROR) return theme.errorText; 11 | return theme.linkText || theme.primary; 12 | }; 13 | 14 | export const linkTheme = { 15 | baseStyle: css` 16 | cursor: pointer; 17 | text-decoration: none; 18 | transition: all 0.15s ease-out; 19 | font-weight: 400; 20 | color: ${getLinkColor}; 21 | ${({ use }) => use === ON_BRIGHT && `text-decoration: underline;`} 22 | ${({ use }) => use === ERROR && `font-weight: bold;`} 23 | `, 24 | _hovered: css` 25 | color: ${({ use, theme }) => 26 | adjustLuminance(getLinkColor({ use, theme }), -30)}; 27 | text-decoration: underline; 28 | `, 29 | _focused: focusRing, 30 | _pressed: css` 31 | color: ${({ use, theme }) => 32 | adjustLuminance(getLinkColor({ use, theme }), 30)}; 33 | `, 34 | } as ThemeConfig['components']['Link']; 35 | -------------------------------------------------------------------------------- /demos/demo-react-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const isProduction = process.env.NODE_ENV === "production"; 4 | 5 | module.exports = { 6 | entry: "./src/index.tsx", 7 | mode: isProduction ? "production" : "development", 8 | devtool: isProduction ? "source-map" : "eval-source-map", 9 | plugins: [ 10 | new HtmlWebpackPlugin({ 11 | template: "./src/index.html", 12 | inject: false, 13 | filename: "demo-react-ts.html", 14 | }), 15 | ], 16 | resolve: { 17 | extensions: [".tsx", ".ts", ".js"], 18 | }, 19 | output: { 20 | path: path.resolve(__dirname, "dist"), 21 | filename: "demo-react-ts.bundle.js", 22 | clean: true, 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.svg$/i, 28 | issuer: /\.[jt]sx?$/, 29 | use: ["@svgr/webpack"], 30 | }, 31 | { 32 | test: /\.(ts|tsx|js)$/, 33 | exclude: /node_modules/, 34 | use: { 35 | loader: "babel-loader", 36 | options: { 37 | presets: ["@babel/preset-env"], 38 | }, 39 | }, 40 | }, 41 | ], 42 | }, 43 | devServer: { 44 | server: "https", 45 | port: 9025, 46 | static: { 47 | directory: path.resolve(__dirname, "src"), 48 | }, 49 | historyApiFallback: true, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/link/VizExLink.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import styled, { DefaultTheme } from 'styled-components'; 3 | import themePropType from '../utils/themePropType'; 4 | import { interactionPropTypes, InteractionProps } from '../utils/types'; 5 | import { ON_BRIGHT, DEFAULT, ERROR } from './constants/LinkVariations'; 6 | 7 | const defaultProps = { 8 | use: DEFAULT, 9 | }; 10 | 11 | export type VizExLinkProps = { 12 | children?: React.ReactNode; 13 | external?: boolean; 14 | href?: string; 15 | onClick?: React.MouseEventHandler; 16 | theme: DefaultTheme; 17 | use: typeof ON_BRIGHT | typeof DEFAULT | typeof ERROR; 18 | } & InteractionProps & 19 | typeof defaultProps & 20 | React.AnchorHTMLAttributes; 21 | 22 | const StyledATag = styled.a` 23 | ${({ theme }) => theme.components.Link.style} 24 | `; 25 | 26 | const VizExLink = (props: VizExLinkProps) => { 27 | return ; 28 | }; 29 | 30 | VizExLink.displayName = 'VizExLink'; 31 | VizExLink.propTypes = { 32 | children: PropTypes.node, 33 | external: PropTypes.bool, 34 | href: PropTypes.string, 35 | onClick: PropTypes.func, 36 | theme: themePropType, 37 | use: PropTypes.oneOf([ON_BRIGHT, DEFAULT, ERROR]), 38 | ...interactionPropTypes, 39 | }; 40 | VizExLink.defaultProps = defaultProps; 41 | 42 | export default VizExLink; 43 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/components/screens/DialingScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { EndCallButton, Row, Timer, Wrapper } from "../Components"; 3 | import { ScreenProps } from "../../types/ScreenTypes"; 4 | import { EndCallSvg } from "../Icons"; 5 | import { formatPhoneNumber } from "../../utils/phoneNumberUtils"; 6 | import { END_CALL } from "../../constants/buttonIds"; 7 | 8 | function DialingScreen({ 9 | handleNextScreen, 10 | dialNumber, 11 | callDurationString, 12 | handleCallEnded, 13 | cti, 14 | }: ScreenProps) { 15 | useEffect(() => { 16 | const timer = setTimeout(() => { 17 | cti.callAnswered(); 18 | handleNextScreen(); 19 | }, 2500); 20 | return () => clearTimeout(timer); 21 | }, [cti, handleNextScreen]); 22 | 23 | const onEndCall = () => { 24 | cti.callEnded({ 25 | callEndStatus: "COMPLETED", 26 | }); 27 | handleCallEnded(); 28 | }; 29 | 30 | return ( 31 | 32 |
33 |

Dialing {formatPhoneNumber(dialNumber)} ...

34 | {callDurationString} 35 |
36 | 37 | 42 | {EndCallSvg} 43 | 44 | 45 |
46 | ); 47 | } 48 | 49 | export default DialingScreen; 50 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/typography/utils/getHeadingStyles.js: -------------------------------------------------------------------------------- 1 | "use es6"; 2 | 3 | import { css } from "styled-components"; 4 | 5 | const getH1Styles = css` 6 | font-weight: 700; 7 | font-size: 32px; 8 | line-height: 44px; 9 | margin-top: 0; 10 | margin-bottom: 16px; 11 | `; 12 | 13 | const getH2Styles = css` 14 | font-weight: 400; 15 | font-size: 24px; 16 | line-height: 30px; 17 | margin-top: 0; 18 | margin-bottom: 16px; 19 | `; 20 | 21 | const getH3Styles = css` 22 | font-weight: 700; 23 | font-size: 22px; 24 | line-height: 30px; 25 | margin-top: 0; 26 | margin-bottom: 16px; 27 | `; 28 | 29 | const getH4Styles = css` 30 | font-weight: 700; 31 | font-size: 18px; 32 | line-height: 26px; 33 | margin-top: 0; 34 | margin-bottom: 16px; 35 | `; 36 | 37 | const getH5Styles = css` 38 | font-weight: 400; 39 | font-size: 16px; 40 | line-height: 26px; 41 | margin-top: 0; 42 | margin-bottom: 16px; 43 | `; 44 | 45 | const getH6Styles = css` 46 | font-weight: 400; 47 | font-size: 14px; 48 | line-height: 24px; 49 | margin-top: 0; 50 | margin-bottom: 16px; 51 | `; 52 | 53 | export const getGlobalHeadingStyles = css` 54 | h1 { 55 | ${getH1Styles}; 56 | } 57 | h2 { 58 | ${getH2Styles}; 59 | } 60 | h3 { 61 | ${getH3Styles}; 62 | } 63 | h4 { 64 | ${getH4Styles}; 65 | } 66 | h5 { 67 | ${getH5Styles}; 68 | } 69 | h6 { 70 | ${getH6Styles}; 71 | } 72 | `; 73 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Jira Ticket: CALL-xxxx 4 | 5 | 6 | 7 | ## Merge Checklist 8 | 9 | | Q | A 10 | | ------------------------ | -------------- 11 | | Adds Documentation? | 12 | | Any Dependency Changes? | 13 | | Patch: Bug Fix? | 14 | | Minor: New Feature? | 15 | | Major: Breaking Change? | 16 | 17 | [BRAVE Checklist](https://github.com/HubSpot/calling-extensions-sdk/blob/master/SHIP_WITH_CARE.md) 18 | 19 | - [ ] I have read the BRAVE checklist and confirmed if the following is necessary. 20 | 21 | 22 | 23 | | Q | A 24 | | ------------------------------ | -------------- 25 | | Backwards Compatible? | 26 | | Rollout/Rollback Plan? | 27 | | Automated test coverage? | 28 | | Verified that changes work? | 29 | | Expect Dependencies to Fail? | 30 | 31 | 32 | -------------------------------------------------------------------------------- /demos/demo-react-ts/test/spec/components/screens/CallingScreen-test.tsx: -------------------------------------------------------------------------------- 1 | import CallingScreen from "../../../../src/components/screens/CallingScreen"; 2 | import { ScreenProps } from "../../../../src/types/ScreenTypes"; 3 | import { renderWithContext } from "../../../render"; 4 | 5 | const noop = (..._args: any[]) => {}; 6 | 7 | const cti = { 8 | callEnded: noop, 9 | }; 10 | 11 | const props: Partial = { 12 | handleNextScreen: noop, 13 | handlePreviousScreen: noop, 14 | handleNavigateToScreen: noop, 15 | cti, 16 | phoneNumber: "", 17 | engagementId: null, 18 | dialNumber: "", 19 | setDialNumber: noop, 20 | notes: "", 21 | setNotes: noop, 22 | callDuration: 0, 23 | callDurationString: "", 24 | startTimer: noop, 25 | stopTimer: noop, 26 | handleCallEnded: noop, 27 | handleSaveCall: noop, 28 | fromNumber: "", 29 | setFromNumber: noop, 30 | callStatus: "COMPLETED", 31 | }; 32 | 33 | describe("CallingScreen", () => { 34 | beforeEach(() => { 35 | props.handleCallEnded = jasmine.createSpy("handleCallEnded"); 36 | cti.callEnded = jasmine.createSpy("callEnded"); 37 | }); 38 | 39 | it("Ends call", () => { 40 | const { getByRole } = renderWithContext(); 41 | const button = getByRole("button", { name: /end-call/ }); 42 | button.click(); 43 | expect(props.handleCallEnded).toHaveBeenCalled(); 44 | expect(cti.callEnded).toHaveBeenCalledWith({ 45 | callEndStatus: "COMPLETED", 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/types/ScreenTypes.ts: -------------------------------------------------------------------------------- 1 | export type CallStatus = "NO_ANSWER" | "CANCELED" | "COMPLETED"; 2 | export type Availability = "AVAILABLE" | "UNAVAILABLE"; 3 | export type Direction = "INBOUND" | "OUTBOUND"; 4 | 5 | export enum ScreenNames { 6 | Login = 0, 7 | Keypad = 1, 8 | Dialing = 2, // OUTBOUND 9 | Incoming = 2, // INBOUND 10 | Calling = 3, 11 | CallEnded = 4, 12 | } 13 | 14 | export interface ScreenProps { 15 | handleNextScreen: Function; 16 | handlePreviousScreen: Function; 17 | handleNavigateToScreen: Function; 18 | cti: any; 19 | engagementId: number | null; 20 | dialNumber: string; 21 | setDialNumber: Function; 22 | notes: string; 23 | setNotes: Function; 24 | isCallRecorded: boolean; 25 | setIsCallRecorded: Function; 26 | callDuration: number; 27 | callDurationString: string; 28 | startTimer: Function; 29 | stopTimer: Function; 30 | handleOutgoingCallStarted: Function; 31 | handleIncomingCall: Function; 32 | handleCallEnded: Function; 33 | handleCallCompleted: Function; 34 | fromNumber: string; 35 | setFromNumber: Function; 36 | availability: Availability; 37 | setAvailability: (availability: Availability) => void; 38 | direction: Direction; 39 | setDirection: (direction: Direction) => void; 40 | incomingContactName: string; 41 | incomingNumber: string; 42 | setIncomingNumber: (number: string) => void; 43 | callStatus: CallStatus | null; 44 | setCallStatus: (callStatus: CallStatus) => void; 45 | iframeLocation: string; 46 | } 47 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/link/VizExExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import themePropType from '../utils/themePropType'; 3 | import { DEFAULT } from './constants/LinkVariations'; 4 | import { getExternalLinkIconColor } from './theme/linkThemeOperators'; 5 | import SVGExternalLink from 'visitor-ui-component-library-icons/icons/SVGExternalLink'; 6 | // @ts-expect-error not typed yet 7 | import VizExIcon from '../icon/VizExIcon'; 8 | import { createTheme } from '../theme/createTheme'; 9 | import { setIconColor } from '../icon/theme/iconThemeOperators'; 10 | import VizExLink, { VizExLinkProps } from './VizExLink'; 11 | 12 | const VizExExternalLink = (props: VizExLinkProps) => { 13 | const { children, theme, ...rest } = props; 14 | 15 | return ( 16 | 23 | {children} 24 | } 27 | size="1em" 28 | style={{ marginLeft: '4px' }} 29 | /> 30 | 31 | ); 32 | }; 33 | 34 | VizExExternalLink.displayName = 'VizExExternalLink'; 35 | VizExExternalLink.propTypes = { 36 | children: PropTypes.node, 37 | theme: themePropType, 38 | }; 39 | VizExExternalLink.defaultProps = { 40 | use: DEFAULT, 41 | }; 42 | 43 | export default VizExExternalLink; 44 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/list/VizExListItemButton.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { forwardRef, ComponentProps } from 'react'; 3 | import styled, { DefaultTheme, StyledComponent } from 'styled-components'; 4 | import themePropType from '../utils/themePropType'; 5 | import { 6 | interactionPropTypes, 7 | InteractionProps, 8 | PolymorphicRef, 9 | } from '../utils/types'; 10 | 11 | type Props = { 12 | children?: React.ReactNode; 13 | theme: DefaultTheme; 14 | alignItems?: 'flex-start' | 'center'; 15 | disablePadding: boolean; 16 | disableGutters: boolean; 17 | } & InteractionProps; 18 | 19 | export type VizExListItemButtonProps = Omit< 20 | ComponentProps>, 21 | 'theme' 22 | >; 23 | 24 | const Container = styled.button` 25 | ${({ theme }) => theme.components.ListItemButton.style} 26 | `; 27 | 28 | const VizExListItemButton = forwardRef( 29 | ( 30 | props: VizExListItemButtonProps, 31 | ref: PolymorphicRef 32 | ) => { 33 | return ; 34 | } 35 | ); 36 | 37 | VizExListItemButton.displayName = 'VizExListItemButton'; 38 | VizExListItemButton.propTypes = { 39 | alignItems: PropTypes.oneOf(['center', 'flex-start']), 40 | autoFocus: PropTypes.bool, 41 | children: PropTypes.node, 42 | disableGutters: PropTypes.bool, 43 | disablePadding: PropTypes.bool, 44 | ordered: PropTypes.bool, 45 | theme: themePropType, 46 | ...interactionPropTypes, 47 | }; 48 | 49 | export default VizExListItemButton; 50 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/externalLink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Checklist** 13 | - [ ] I have read the [FAQs](https://github.com/HubSpot/calling-extensions-sdk#faqs). 14 | - [ ] I have checked the [issue tracker](https://github.com/HubSpot/calling-extensions-sdk/issues) for duplicate issues. 15 | 16 | **Description** 17 | 18 | 19 | 20 | **Expected behavior** 21 | 22 | 23 | **To Reproduce** 24 | 25 | 1. Go to '...' 26 | 2. Click on '....' 27 | 3. Scroll down to '....' 28 | 4. See error 29 | 30 | **Screenshots/source code** 31 | 32 | 33 | **Device information** 34 | 35 | Device: [e.g. iPhone6] 36 | OS: [e.g. iOS8.1] 37 | Browser: [e.g. stock browser, safari] 38 | Browser Version: [e.g. 22] 39 | 40 | **Additional context** 41 | 42 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/list/VizExList.stories.tsx: -------------------------------------------------------------------------------- 1 | import VizExList, { VizExListProps } from './VizExList'; 2 | // TODO: update when platform makes these available from '@storybook/react' 3 | import { StoryFn, Meta } from '../../storybook/types'; 4 | import VizExListItemButton from './VizExListItemButton'; 5 | 6 | export default { 7 | title: 'VizExList', 8 | component: VizExList, 9 | } as Meta; 10 | 11 | export const DefaultWithControls: StoryFn = args => ( 12 | 13 |
  • {'first item'}
  • 14 |
  • {'second item'}
  • 15 |
  • {'third item'}
  • 16 |
    17 | ); 18 | 19 | DefaultWithControls.args = { 20 | listStyled: true, 21 | }; 22 | 23 | export const StyledList: StoryFn = () => ( 24 | 25 |
  • {'first item'}
  • 26 |
  • {'second item'}
  • 27 |
  • {'third item'}
  • 28 |
    29 | ); 30 | 31 | export const OrderedList: StoryFn = () => ( 32 | 33 |
  • {'first item'}
  • 34 |
  • {'second item'}
  • 35 |
  • {'third item'}
  • 36 |
    37 | ); 38 | 39 | export const InteractiveList: StoryFn = () => ( 40 | 41 | 42 | {'as a button'} 43 | 44 | {'as an anchor link'} 45 | 46 | {'as a button'} 47 | 48 | 49 | ); 50 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/components/Keypad.tsx: -------------------------------------------------------------------------------- 1 | import { Key, Row } from "./Components"; 2 | 3 | export function Keypad({ addToDialNumber }: { addToDialNumber: Function }) { 4 | return ( 5 |
    6 | 7 | addToDialNumber("1")}>1 8 | addToDialNumber("2")}>2 9 | addToDialNumber("3")}>3 10 | 11 | 12 | addToDialNumber("4")}>4 13 | addToDialNumber("5")}>5 14 | addToDialNumber("6")}>6 15 | 16 | 17 | addToDialNumber("7")}>7 18 | addToDialNumber("8")}>8 19 | addToDialNumber("9")}>9 20 | 21 | 22 | addToDialNumber("*")}>* 23 | addToDialNumber("0")}>0 24 | addToDialNumber("#")}># 25 | 26 |
    27 | ); 28 | } 29 | 30 | export function KeypadPopover() { 31 | return ( 32 |
    33 | 34 | 1 35 | 2 36 | 3 37 | 38 | 39 | 4 40 | 5 41 | 6 42 | 43 | 44 | 7 45 | 8 46 | 9 47 | 48 | 49 | * 50 | 0 51 | # 52 | 53 |
    54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /demos/demo-react-ts/test/spec/components/FromNumbersDropdown-test.tsx: -------------------------------------------------------------------------------- 1 | import FromNumbersDropdown from "../../../src/components/FromNumbersDropdown"; 2 | import { renderWithContext } from "../../render"; 3 | 4 | const noop = (..._args: any[]) => {}; 5 | 6 | const props = { 7 | fromNumber: "", 8 | setFromNumber: noop, 9 | setToggleFromNumbers: noop, 10 | toggleFromNumbers: false, 11 | }; 12 | 13 | describe("FromNumbersDropdown", () => { 14 | beforeEach(() => { 15 | props.setToggleFromNumbers = jasmine.createSpy("setToggleFromNumbers"); 16 | }); 17 | it("Sets aria label of numbers dropdown correctly using toggleFromNumbers", () => { 18 | const { getByLabelText } = renderWithContext( 19 | 20 | ); 21 | expect(getByLabelText(/from-number-close/)).toBeInTheDocument(); 22 | }); 23 | it("Shows US and UK numbers", () => { 24 | const { getByRole } = renderWithContext( 25 | 26 | ); 27 | expect(getByRole("button", { name: "us-number" })).toBeInTheDocument(); 28 | expect(getByRole("button", { name: "uk-number" })).toBeInTheDocument(); 29 | }); 30 | it("Hides numbers dropdown when a number is clicked", async () => { 31 | const { getByLabelText, getByRole } = renderWithContext( 32 | 33 | ); 34 | expect(getByLabelText(/from-number-open/)).toBeInTheDocument(); 35 | const button = getByRole("button", { name: "uk-number" }); 36 | button.click(); 37 | expect(props.setToggleFromNumbers).toHaveBeenCalledWith(false); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/icons/sprocket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/theme/styled.d.ts: -------------------------------------------------------------------------------- 1 | // Necessary to have a d.ts file so we can override the DefaultTheme type from styled-components 2 | /* eslint-disable hubspot-dev/no-declarations */ 3 | import 'styled-components'; 4 | // eslint-disable-next-line no-duplicate-imports 5 | import { css } from 'styled-components'; 6 | 7 | type Colors = { 8 | primary: string; 9 | text: string; 10 | textOnPrimary: string; 11 | errorText: string; 12 | disabledBackground: string; 13 | disabledText: string; 14 | placeholderText: string; 15 | inputBorder: string; 16 | inputBackground: string; 17 | helpText: string; 18 | happyColor: string; 19 | neutralColor: string; 20 | sadColor: string; 21 | transparentOnBackgroundIconButton?: string; 22 | linkText?: string; 23 | }; 24 | 25 | export type CSS = ReturnType; 26 | 27 | export type ThemedStyles = { 28 | baseStyle: CSS; 29 | _disabled: CSS; 30 | _focused: CSS; 31 | _hovered: CSS; 32 | _pressed: CSS; 33 | }; 34 | 35 | type ComponentTheme = { 36 | style?: CSS; 37 | } & Partial; 38 | 39 | export type ThemedComponents = 40 | | 'Button' 41 | | 'IconButton' 42 | | 'Link' 43 | | 'List' 44 | | 'ListItemButton'; 45 | 46 | export type ThemeConfig = { 47 | colors: Partial; 48 | components: Record; 49 | }; 50 | export type ThemeFactoryConfig = { 51 | colors?: Partial; 52 | components?: Partial>; 53 | }; 54 | export type ThemeFinal = { 55 | colors: Colors; 56 | components: Record; 57 | }; 58 | 59 | declare module 'styled-components' { 60 | export interface DefaultTheme extends Colors, ThemeFinal {} 61 | } 62 | -------------------------------------------------------------------------------- /demos/demo-react-ts/test/spec/components/screens/DialingScreen-test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from "@testing-library/react"; 2 | import DialingScreen from "../../../../src/components/screens/DialingScreen"; 3 | import { renderWithContext } from "../../../render"; 4 | 5 | const noop = (..._args: any[]) => {}; 6 | 7 | const cti = { 8 | callAnswered: noop, 9 | }; 10 | 11 | const props = { 12 | handleNextScreen: noop, 13 | handlePreviousScreen: noop, 14 | handleNavigateToScreen: noop, 15 | cti, 16 | phoneNumber: "", 17 | engagementId: null, 18 | dialNumber: "", 19 | setDialNumber: noop, 20 | notes: "", 21 | setNotes: noop, 22 | callDuration: 0, 23 | callDurationString: "", 24 | startTimer: noop, 25 | stopTimer: noop, 26 | handleEndCall: noop, 27 | handleSaveCall: noop, 28 | fromNumber: "", 29 | setFromNumber: noop, 30 | }; 31 | 32 | describe("DialingScreen", () => { 33 | beforeEach(() => { 34 | jasmine.clock().install(); 35 | cti.callAnswered = jasmine.createSpy("callAnswered"); 36 | props.handleNextScreen = jasmine.createSpy("handleNextScreen"); 37 | }); 38 | 39 | afterEach(() => { 40 | jasmine.clock().uninstall(); 41 | }); 42 | it("Show dialing text", () => { 43 | renderWithContext(); 44 | expect(screen.getByText(/Dialing/)).toBeInTheDocument(); 45 | }); 46 | 47 | it("Sends callAnswered message", () => { 48 | renderWithContext(); 49 | jasmine.clock().tick(3000); 50 | expect(cti.callAnswered).toHaveBeenCalled(); 51 | }); 52 | 53 | it("Navigates to next screen", () => { 54 | renderWithContext(); 55 | jasmine.clock().tick(3000); 56 | expect(props.handleNextScreen).toHaveBeenCalled(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/theme/defaultTheme.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DEFAULT_PRIMARY_COLOR, 3 | DEFAULT_TEXT_COLOR, 4 | DEFAULT_ERROR_TEXT_COLOR, 5 | DISABLED_BACKGROUND_COLOR, 6 | DISABLED_TEXT_COLOR, 7 | WHITE, 8 | DEFAULT_PLACEHOLDER_TEXT_COLOR, 9 | DEFAULT_INPUT_BACKGROUND_COLOR, 10 | DEFAULT_INPUT_BORDER_COLOR, 11 | DEFAULT_HELP_TEXT_COLOR, 12 | DEFAULT_SAD_COLOR, 13 | DEFAULT_NEUTRAL_COLOR, 14 | DEFAULT_HAPPY_COLOR, 15 | } from './ColorConstants'; 16 | import { ThemeConfig } from './styled'; 17 | import { buttonTheme } from '../button/theme/buttonTheme'; 18 | import { iconButtonTheme } from '../button/theme/iconButtonTheme'; 19 | import { linkTheme } from '../link/theme/linkTheme'; 20 | import { listItemButtonTheme } from '../list/theme/listItemButtonTheme'; 21 | import { listTheme } from '../list/theme/listTheme'; 22 | 23 | export const colors = { 24 | primary: DEFAULT_PRIMARY_COLOR, 25 | text: DEFAULT_TEXT_COLOR, 26 | textOnPrimary: WHITE, 27 | errorText: DEFAULT_ERROR_TEXT_COLOR, 28 | disabledBackground: DISABLED_BACKGROUND_COLOR, 29 | disabledText: DISABLED_TEXT_COLOR, 30 | placeholderText: DEFAULT_PLACEHOLDER_TEXT_COLOR, 31 | inputBorder: DEFAULT_INPUT_BORDER_COLOR, 32 | inputBackground: DEFAULT_INPUT_BACKGROUND_COLOR, 33 | helpText: DEFAULT_HELP_TEXT_COLOR, 34 | happyColor: DEFAULT_HAPPY_COLOR, 35 | neutralColor: DEFAULT_NEUTRAL_COLOR, 36 | sadColor: DEFAULT_SAD_COLOR, 37 | }; 38 | 39 | export const components = { 40 | Button: buttonTheme, 41 | IconButton: iconButtonTheme, 42 | Link: linkTheme, 43 | List: listTheme, 44 | ListItemButton: listItemButtonTheme, 45 | }; 46 | 47 | export const defaultTheme = { 48 | ...colors, // will refactor out colors from here to the new colors key in the future 49 | colors, 50 | components, 51 | } as ThemeConfig; 52 | -------------------------------------------------------------------------------- /demos/demo-react-ts/test/spec/components/App-test.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | screen, 3 | waitForElementToBeRemoved, 4 | MatcherFunction, 5 | } from "@testing-library/react"; 6 | import App from "../../../src/components/App"; 7 | import { renderWithContext } from "../../render"; 8 | 9 | type Query = (f: MatcherFunction) => HTMLElement; 10 | 11 | const withMarkup = 12 | (query: Query) => 13 | (text: string): HTMLElement => 14 | query((content: string, node: HTMLElement) => { 15 | const hasText = (node: HTMLElement) => node.textContent === text; 16 | const childrenDontHaveText = Array.from(node.children).every( 17 | (child) => !hasText(child as HTMLElement) 18 | ); 19 | return hasText(node) && childrenDontHaveText; 20 | }); 21 | 22 | describe("App", () => { 23 | it("Shows login screen", () => { 24 | renderWithContext(); 25 | expect( 26 | screen.getByText(/Log into your calling account/) 27 | ).toBeInTheDocument(); 28 | }); 29 | 30 | it("Shows alert", () => { 31 | const { getByText } = renderWithContext(); 32 | const getByTextWithMarkup = withMarkup(getByText); 33 | expect( 34 | getByTextWithMarkup( 35 | "Open your console to see the incoming (beta) and outgoing messages with HubSpot." 36 | ) 37 | ).toBeInTheDocument(); 38 | }); 39 | 40 | it("Hides alert when confirm button is clicked", async () => { 41 | const { getByText } = renderWithContext(); 42 | const getByTextWithMarkup = withMarkup(getByText); 43 | const confirmAlertButton = screen.getByRole("button", { name: /×/i }); 44 | confirmAlertButton.click(); 45 | 46 | await waitForElementToBeRemoved(() => 47 | getByTextWithMarkup( 48 | "Open your console to see the incoming (beta) and outgoing messages with HubSpot." 49 | ) 50 | ); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/VizExIconButton.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import styled, { DefaultTheme } from 'styled-components'; 3 | import themePropType from '../utils/themePropType'; 4 | import { interactionPropTypes, InteractionProps } from '../utils/types'; 5 | import * as IconButtonUses from './constants/IconButtonUses'; 6 | import { CIRCLE, DEFAULT } from './constants/IconButtonShapes'; 7 | import { MEDIUM, EXTRA_SMALL, SMALL } from '../constants/sizes'; 8 | import { BUTTON_SIZES } from './constants/ButtonSizes'; 9 | 10 | const defaultProps = { 11 | use: IconButtonUses.PRIMARY, 12 | shape: DEFAULT, 13 | size: MEDIUM, 14 | }; 15 | 16 | export type VizExIconButtonProps = { 17 | children?: React.ReactNode; 18 | onClick?: React.MouseEventHandler; 19 | shape: 'circle' | 'default'; 20 | size: keyof typeof BUTTON_SIZES; 21 | theme: DefaultTheme; 22 | use: 'primary' | 'transparent-on-background' | 'transparent-on-primary'; 23 | } & InteractionProps & 24 | typeof defaultProps & 25 | React.ButtonHTMLAttributes; 26 | 27 | const AbstractVizExIconButton = styled.button` 28 | ${({ theme }) => theme.components.IconButton.style} 29 | `; 30 | 31 | const VizExIconButton = (props: VizExIconButtonProps) => { 32 | return ; 33 | }; 34 | 35 | VizExIconButton.displayName = 'VizExIconButton'; 36 | 37 | VizExIconButton.propTypes = { 38 | children: PropTypes.node, 39 | onClick: PropTypes.func, 40 | shape: PropTypes.oneOf([CIRCLE, DEFAULT]), 41 | size: PropTypes.oneOf([EXTRA_SMALL, SMALL, MEDIUM]), 42 | theme: themePropType, 43 | use: PropTypes.oneOf(Object.values(IconButtonUses)), 44 | ...interactionPropTypes, 45 | }; 46 | 47 | VizExIconButton.defaultProps = defaultProps; 48 | 49 | export default VizExIconButton; 50 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/VizExButton.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { ReactNode, MouseEventHandler, ButtonHTMLAttributes } from "react"; 3 | import styled, { DefaultTheme } from "styled-components"; 4 | import { MEDIUM } from "../constants/sizes"; 5 | import themePropType from "../utils/themePropType"; 6 | import { interactionPropTypes, InteractionProps } from "../utils/types"; 7 | import { BUTTON_SIZES } from "./constants/ButtonSizes"; 8 | import * as ButtonUses from "./constants/ButtonUses"; 9 | 10 | const defaultProps = { 11 | use: ButtonUses.SECONDARY, 12 | size: MEDIUM, 13 | }; 14 | 15 | export type VizExButtonProps = { 16 | children?: ReactNode; 17 | onClick?: MouseEventHandler; 18 | size: keyof typeof BUTTON_SIZES; 19 | theme?: DefaultTheme; 20 | use: "primary" | "secondary" | "transparent-on-primary"; 21 | } & InteractionProps & 22 | typeof defaultProps & 23 | ButtonHTMLAttributes; 24 | 25 | const AbstractVizExButton = styled.button` 26 | ${({ theme }) => theme.components.Button.style} 27 | font-family: "Lexend"; 28 | `; 29 | 30 | const NoSelect = styled.div` 31 | user-select: none; 32 | `; 33 | 34 | const VizExButton = (props: VizExButtonProps) => { 35 | const { children, ...rest } = props; 36 | 37 | return ( 38 | 39 | {children} 40 | 41 | ); 42 | }; 43 | 44 | VizExButton.displayName = "VizExButton"; 45 | 46 | VizExButton.propTypes = { 47 | children: PropTypes.node, 48 | onClick: PropTypes.func, 49 | size: PropTypes.oneOf(Object.keys(BUTTON_SIZES)), 50 | theme: themePropType, 51 | use: PropTypes.oneOf(Object.values(ButtonUses)), 52 | ...interactionPropTypes, 53 | }; 54 | 55 | VizExButton.defaultProps = defaultProps; 56 | 57 | export default VizExButton; 58 | -------------------------------------------------------------------------------- /demos/demo-react-ts/test/spec/components/screens/LoginScreen-test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent } from "@testing-library/react"; 2 | import LoginScreen from "../../../../src/components/screens/LoginScreen"; 3 | import { renderWithContext } from "../../../render"; 4 | import { ScreenProps } from "../../../../src/types/ScreenTypes"; 5 | 6 | const noop = (..._args: any[]) => {}; 7 | 8 | const cti = { 9 | userLoggedIn: noop, 10 | }; 11 | 12 | const props: Partial = { 13 | handleNextScreen: noop, 14 | handlePreviousScreen: noop, 15 | handleNavigateToScreen: noop, 16 | cti, 17 | phoneNumber: "", 18 | engagementId: null, 19 | dialNumber: "", 20 | setDialNumber: noop, 21 | notes: "", 22 | setNotes: noop, 23 | callDuration: 0, 24 | callDurationString: "", 25 | startTimer: noop, 26 | stopTimer: noop, 27 | handleCallEnded: noop, 28 | handleCallCompleted: noop, 29 | fromNumber: "", 30 | setFromNumber: noop, 31 | }; 32 | 33 | describe("LoginScreen", () => { 34 | beforeEach(() => { 35 | cti.userLoggedIn = jasmine.createSpy("userLoggedIn"); 36 | props.handleNextScreen = jasmine.createSpy("handleNextScreen"); 37 | }); 38 | 39 | it("Handles log in button click", () => { 40 | const { getByRole } = renderWithContext(); 41 | const button = getByRole("button", { 42 | name: /Log in/i, 43 | }); 44 | 45 | fireEvent.click(button); 46 | 47 | expect(cti.userLoggedIn).toHaveBeenCalled(); 48 | expect(props.handleNextScreen).toHaveBeenCalled(); 49 | }); 50 | 51 | it("Handles sign in button click", () => { 52 | const { getByRole } = renderWithContext(); 53 | const button = getByRole("button", { 54 | name: /Sign in with SSO/i, 55 | }); 56 | 57 | fireEvent.click(button); 58 | 59 | expect(cti.userLoggedIn).toHaveBeenCalled(); 60 | expect(props.handleNextScreen).toHaveBeenCalled(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/theme/buttonTheme.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { ThemeConfig } from '../../theme/styled'; 3 | import { focusRing } from '../../utils/mixins'; 4 | import { adjustLuminance } from '../../utils/adjustLuminance'; 5 | import { VizExButtonProps } from '../VizExButton'; 6 | import { BUTTON_PADDINGS, BUTTON_FONT_SIZES } from '../constants/ButtonSizes'; 7 | 8 | export const buttonTheme = { 9 | baseStyle: css` 10 | padding: ${({ size }) => BUTTON_PADDINGS[size]}; 11 | font-size: ${({ size }) => BUTTON_FONT_SIZES[size]}; 12 | flex-shrink: 0; 13 | border-radius: 3px; 14 | line-height: 16px; 15 | outline: none; 16 | transition: background-color 150ms ease-out; 17 | border-style: solid; 18 | border-width: 1px; 19 | cursor: pointer; 20 | text-align: center; 21 | word-break: normal; 22 | overflow-wrap: break-word; 23 | background-color: transparent; 24 | ${({ theme, use }) => 25 | use === 'primary' 26 | ? ` 27 | background-color: ${theme.primary}; 28 | border: none; 29 | color: ${theme.textOnPrimary}; 30 | ` 31 | : ` 32 | background-color: transparent; 33 | border-color: ${theme.primary}; 34 | color: ${theme.primary}; 35 | `} 36 | `, 37 | _disabled: css` 38 | background-color: ${({ theme }) => theme.disabledBackground}; 39 | border: none; 40 | color: ${({ theme }) => theme.disabledText}; 41 | cursor: not-allowed; 42 | user-select: none; 43 | `, 44 | _focused: focusRing, 45 | _hovered: css` 46 | ${({ theme, use }) => 47 | `background-color: ${adjustLuminance( 48 | theme.primary, 49 | use === 'primary' ? 20 : 95 50 | )};`} 51 | `, 52 | _pressed: css` 53 | ${({ theme, use }) => 54 | `background-color: ${adjustLuminance( 55 | theme.primary, 56 | use === 'primary' ? -10 : 90 57 | )};`} 58 | `, 59 | } as ThemeConfig['components']['Button']; 60 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/tooltip/utils/getArrowSpacing.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import { getSide, getEdge } from './getPlacement'; 4 | import { css } from 'styled-components'; 5 | 6 | const ARROW_SIZE = 16; 7 | const INSET = 8; 8 | 9 | const getSideStyles = ({ placement }) => { 10 | switch (getSide(placement)) { 11 | case 'top': 12 | // Arrow points down 13 | return css` 14 | transform: rotate(45deg); 15 | top: -${ARROW_SIZE + 5}px; 16 | `; 17 | case 'right': 18 | // Arrow points left 19 | return css` 20 | transform: rotate(135deg); 21 | right: -${ARROW_SIZE + 5}px; 22 | `; 23 | case 'bottom': 24 | // Arrow points up 25 | return css` 26 | transform: rotate(-135deg); 27 | bottom: -${ARROW_SIZE + 5}px; 28 | `; 29 | case 'left': 30 | // Arrow points right 31 | return css` 32 | transform: rotate(-45deg); 33 | left: -${ARROW_SIZE + 5}px; 34 | `; 35 | default: 36 | return ''; 37 | } 38 | }; 39 | 40 | const getEdgeStyles = ({ placement }) => { 41 | switch (getEdge(placement)) { 42 | case 'top': 43 | // Arrow is near the bottom of the left or right side 44 | return css` 45 | top: ${INSET}px; 46 | `; 47 | case 'middle': 48 | return css` 49 | top: calc(50% - ${ARROW_SIZE / 2}px); 50 | `; 51 | case 'bottom': 52 | // Arrow is near the top of the left or right side 53 | return css` 54 | bottom: ${INSET}px; 55 | `; 56 | case 'left': 57 | // Arrow is near the right of the top or bottom side 58 | return css` 59 | left: ${INSET}px; 60 | `; 61 | case 'center': 62 | return css` 63 | left: calc(50% - ${ARROW_SIZE / 2}px); 64 | `; 65 | case 'right': 66 | // Arrow is near the left of the top or bottom side 67 | return css` 68 | right: ${INSET}px; 69 | `; 70 | 71 | default: 72 | return ''; 73 | } 74 | }; 75 | 76 | export const getArrowSpacing = ({ placement }) => css` 77 | ${getSideStyles({ placement })}; 78 | ${getEdgeStyles({ placement })}; 79 | `; 80 | -------------------------------------------------------------------------------- /demos/demo-react-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calling-integration-sdk-demo-react-ts", 3 | "version": "1.0.0", 4 | "description": "HubSpot calling integration sdk demo react ts", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack", 8 | "build:gh": "npm ci && cross-env NODE_ENV=production npm run build", 9 | "serve": "webpack serve", 10 | "start": "webpack serve --open", 11 | "test:build": "cross-env NODE_ENV=test npx webpack --config webpack-test.config.js --mode development", 12 | "test:watch": "cross-env NODE_ENV=test npx webpack --config webpack-test.config.js --mode development --watch", 13 | "test": "npm run test:build && jasmine-browser-runner runSpecs --config=test/support/jasmine-browser.json --browser=headlessChrome", 14 | "test:serve": "npm run test:watch & jasmine-browser-runner serve --config=test/support/jasmine-browser.json" 15 | }, 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">=14" 19 | }, 20 | "dependencies": { 21 | "@hubspot/calling-extensions-sdk": "^0.9.7", 22 | "react": "^18.2.0", 23 | "react-aria": "^3.22.0", 24 | "react-dom": "^18.2.0", 25 | "styled-components": "^5.3.6", 26 | "uuid": "^10.0.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.20.12", 30 | "@babel/preset-env": "^7.20.2", 31 | "@babel/preset-react": "^7.18.6", 32 | "@babel/preset-typescript": "^7.18.6", 33 | "@svgr/webpack": "^6.5.1", 34 | "@testing-library/jasmine-dom": "^1.3.3", 35 | "@testing-library/react": "^14.0.0", 36 | "@types/jasmine": "^4.3.1", 37 | "@types/react": "^18.0.27", 38 | "@types/react-dom": "^18.0.10", 39 | "@types/styled-components": "^5.1.26", 40 | "@types/testing-library__jasmine-dom": "^1.3.0", 41 | "@types/uuid": "^10.0.0", 42 | "babel-loader": "^9.1.2", 43 | "babel-plugin-styled-components": "^2.0.7", 44 | "babel-preset-react-app": "^10.0.1", 45 | "cross-env": "^7.0.3", 46 | "html-webpack-plugin": "^5.5.0", 47 | "jasmine-browser-runner": "^1.3.0", 48 | "jasmine-core": "^4.5.0", 49 | "prettier": "2.8.4", 50 | "prop-types": "^15.8.1", 51 | "webpack": "^5.94.0", 52 | "webpack-cli": "^5.0.1", 53 | "webpack-dev-server": "^5.2.1" 54 | }, 55 | "resolutions": { 56 | "styled-components": "^5.3.6" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/theme/createTheme.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from '../utils/pipe'; 2 | import { 3 | DEFAULT_PRIMARY_COLOR, 4 | DEFAULT_TEXT_COLOR, 5 | DEFAULT_ERROR_TEXT_COLOR, 6 | DISABLED_BACKGROUND_COLOR, 7 | DISABLED_TEXT_COLOR, 8 | WHITE, 9 | DEFAULT_PLACEHOLDER_TEXT_COLOR, 10 | DEFAULT_INPUT_BACKGROUND_COLOR, 11 | DEFAULT_INPUT_BORDER_COLOR, 12 | DEFAULT_HELP_TEXT_COLOR, 13 | DEFAULT_SAD_COLOR, 14 | DEFAULT_NEUTRAL_COLOR, 15 | DEFAULT_HAPPY_COLOR, 16 | DEFAULT_TOOLTIP_BACKGROUND_COLOR, 17 | DEFAULT_TOOLTIP_TEXT_COLOR, 18 | } from './ColorConstants'; 19 | import { 20 | setPrimaryColor, 21 | setTextColor, 22 | setErrorTextColor, 23 | setDisabledBackgroundColor, 24 | setDisabledTextColor, 25 | setTextOnPrimaryColor, 26 | setPlaceholderTextColor, 27 | setInputBackgroundColor, 28 | setInputBorderColor, 29 | setHelpTextColor, 30 | } from './defaultThemeOperators'; 31 | import { 32 | setNeutralColor, 33 | setSadColor, 34 | setHappyColor, 35 | } from '../ratings/theme/VizExCsatRatingThemeOperator'; 36 | import { 37 | setTooltipBackgroundColor, 38 | setTooltipTextColor, 39 | } from '../tooltip/theme/tooltipThemeOperators'; 40 | import { DefaultTheme } from 'styled-components'; 41 | import { components as defaultComponents } from './defaultTheme'; 42 | import { computeComponentStyles } from './createThemeV2'; 43 | 44 | export const createTheme = (...themeOperatorOverrides: any[]) => 45 | pipe( 46 | setPrimaryColor(DEFAULT_PRIMARY_COLOR), 47 | setTextColor(DEFAULT_TEXT_COLOR), 48 | setErrorTextColor(DEFAULT_ERROR_TEXT_COLOR), 49 | setDisabledBackgroundColor(DISABLED_BACKGROUND_COLOR), 50 | setDisabledTextColor(DISABLED_TEXT_COLOR), 51 | setTextOnPrimaryColor(WHITE), 52 | setPlaceholderTextColor(DEFAULT_PLACEHOLDER_TEXT_COLOR), 53 | setInputBackgroundColor(DEFAULT_INPUT_BACKGROUND_COLOR), 54 | setInputBorderColor(DEFAULT_INPUT_BORDER_COLOR), 55 | setHelpTextColor(DEFAULT_HELP_TEXT_COLOR), 56 | setSadColor(DEFAULT_SAD_COLOR), 57 | setNeutralColor(DEFAULT_NEUTRAL_COLOR), 58 | setHappyColor(DEFAULT_HAPPY_COLOR), 59 | setTooltipBackgroundColor(DEFAULT_TOOLTIP_BACKGROUND_COLOR), 60 | setTooltipTextColor(DEFAULT_TOOLTIP_TEXT_COLOR), 61 | ...themeOperatorOverrides 62 | )({ 63 | components: computeComponentStyles(defaultComponents), 64 | }) as DefaultTheme; 65 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/tooltip/utils/getBodySpacing.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import { getSide, getEdge } from './getPlacement'; 4 | import { css } from 'styled-components'; 5 | 6 | const ARROW_SIZE = 16; 7 | 8 | const getEdgeStyles = ({ placement }) => { 9 | switch (getEdge(placement)) { 10 | case 'top': 11 | return css` 12 | top: 0; 13 | `; 14 | case 'bottom': 15 | return css` 16 | bottom: 0; 17 | `; 18 | case 'left': 19 | return css` 20 | left: 0; 21 | `; 22 | case 'right': 23 | return css` 24 | right: 0; 25 | `; 26 | 27 | default: 28 | return ''; 29 | } 30 | }; 31 | 32 | const getSideStyles = ({ placement }) => { 33 | switch (getSide(placement)) { 34 | case 'top': 35 | return css` 36 | transform: translateY(-100%); 37 | top: -${ARROW_SIZE - 5}px; 38 | `; 39 | case 'right': 40 | return css` 41 | transform: translateX(100%); 42 | right: -${ARROW_SIZE - 5}px; 43 | `; 44 | case 'bottom': 45 | return css` 46 | transform: translateY(100%); 47 | bottom: -${ARROW_SIZE - 5}px; 48 | `; 49 | case 'left': 50 | return css` 51 | transform: translateX(-100%); 52 | left: -${ARROW_SIZE - 5}px; 53 | `; 54 | default: 55 | return ''; 56 | } 57 | }; 58 | 59 | const getMiddleStyles = ({ placement }) => { 60 | switch (placement) { 61 | case 'top center': 62 | case 'top middle': 63 | return css` 64 | transform: translate(-50%, -100%); 65 | left: 50%; 66 | `; 67 | 68 | case 'bottom middle': 69 | case 'bottom center': 70 | return css` 71 | transform: translate(-50%, 100%); 72 | left: 50%; 73 | `; 74 | 75 | case 'left center': 76 | case 'left middle': 77 | return css` 78 | transform: translate(-100%, -50%); 79 | top: 50%; 80 | `; 81 | 82 | case 'right center': 83 | case 'right middle': 84 | return css` 85 | transform: translate(100%, -50%); 86 | top: 50%; 87 | `; 88 | 89 | default: 90 | return ''; 91 | } 92 | }; 93 | 94 | export const getBodySpacing = ({ placement }) => css` 95 | ${getSideStyles({ placement })}; 96 | ${getEdgeStyles({ placement })}; 97 | ${getMiddleStyles({ placement })} 98 | `; 99 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/components/FromNumbersDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useCallback } from "react"; 2 | 3 | import { 4 | FromNumberTooltip, 5 | FromNumberToggleButton, 6 | FromNumberButton, 7 | } from "./Components"; 8 | import { CaretDownSvg } from "./Icons"; 9 | import { 10 | FROM_NUMBER_ONE, 11 | FROM_NUMBER_TWO, 12 | getFormattedFromNumber, 13 | } from "../utils/phoneNumberUtils"; 14 | 15 | function FromNumbersDropdown({ 16 | fromNumber, 17 | setFromNumber, 18 | setToggleFromNumbers, 19 | toggleFromNumbers, 20 | }: { 21 | fromNumber: string; 22 | setFromNumber: Function; 23 | setToggleFromNumbers: Function; 24 | toggleFromNumbers: boolean; 25 | }) { 26 | const handleFromNumber = useCallback( 27 | (phoneNumber: string) => { 28 | setFromNumber(phoneNumber); 29 | setToggleFromNumbers(false); 30 | }, 31 | [setFromNumber, setToggleFromNumbers] 32 | ); 33 | 34 | const FromNumbers = useMemo(() => { 35 | return ( 36 |
    37 |
    38 | handleFromNumber(FROM_NUMBER_ONE)} 41 | > 42 | My US Number 43 | {getFormattedFromNumber(FROM_NUMBER_ONE)} 44 | 45 |
    46 |
    47 | handleFromNumber(FROM_NUMBER_TWO)} 50 | > 51 | My UK Number 52 | {getFormattedFromNumber(FROM_NUMBER_TWO)} 53 | 54 |
    55 |
    56 | ); 57 | }, [handleFromNumber]); 58 | 59 | return ( 60 | <> 61 | From number: 62 | 67 | setToggleFromNumbers(!toggleFromNumbers)} 70 | > 71 | {getFormattedFromNumber(fromNumber)} {CaretDownSvg} 72 | 73 | 74 | 75 | ); 76 | } 77 | 78 | export default FromNumbersDropdown; 79 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/VizExCloseButton.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import PropTypes from 'prop-types'; 4 | import SVGClose from 'visitor-ui-component-library-icons/icons/SVGClose'; 5 | import VizExIconButton from './VizExIconButton'; 6 | import VizExIcon from '../icon/VizExIcon'; 7 | import styled, { css, ThemeConsumer } from 'styled-components'; 8 | import themePropType from '../utils/themePropType'; 9 | import { TRANSPARENT_ON_BACKGROUND } from './constants/IconButtonUses'; 10 | import { CIRCLE } from './constants/IconButtonShapes'; 11 | import { EXTRA_SMALL, MEDIUM, SMALL } from '../constants/sizes'; 12 | import { setTransparentOnBackgroundIconButton } from './theme/iconButtonThemeOperators'; 13 | import { getCloseButtonColor } from './theme/closeButtonThemeOperators'; 14 | import { ICON_BUTTON_SIZE_TO_ICON_SIZE } from './constants/IconButtonSizeToIconSize'; 15 | 16 | const getMarginStyles = ({ size }) => { 17 | switch (size) { 18 | case EXTRA_SMALL: 19 | case SMALL: 20 | return css` 21 | margin-top: 8px; 22 | margin-right: 8px; 23 | `; 24 | case MEDIUM: 25 | default: 26 | return css` 27 | margin-top: 12px; 28 | margin-right: 12px; 29 | `; 30 | } 31 | }; 32 | export const ButtonContainer = styled(VizExIconButton)` 33 | right: 0; 34 | position: absolute; 35 | top: 0; 36 | ${getMarginStyles} 37 | `; 38 | 39 | const VizExCloseButton = props => { 40 | const { onClick, theme, size, ...rest } = props; 41 | return ( 42 | 43 | {contextTheme => ( 44 | 55 | } 57 | size={ICON_BUTTON_SIZE_TO_ICON_SIZE[size]} 58 | /> 59 | 60 | )} 61 | 62 | ); 63 | }; 64 | 65 | VizExCloseButton.displayName = 'VizExCloseButton'; 66 | 67 | VizExCloseButton.propTypes = { 68 | onClick: PropTypes.func, 69 | size: PropTypes.oneOf([EXTRA_SMALL, SMALL, MEDIUM]), 70 | theme: themePropType, 71 | }; 72 | 73 | export default VizExCloseButton; 74 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/theme/defaultThemeOperators.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components'; 2 | import { curryable } from '../utils/curryable'; 3 | 4 | export const getThemeProperty = curryable( 5 | (key: keyof DefaultTheme, theme: DefaultTheme) => { 6 | if (typeof theme !== 'object' || theme === null) { 7 | throw new Error( 8 | `Error getting '${key}': the theme for VizExComponents has not been defined. Please provide a theme through the component props or styled-components ThemeProvider.` 9 | ); 10 | } 11 | if (!theme[key]) { 12 | throw new Error( 13 | `Error getting '${key}': the property was not defined on theme.` 14 | ); 15 | } 16 | return theme[key]; 17 | } 18 | ); 19 | 20 | export const setThemeProperty = curryable( 21 | (key: keyof DefaultTheme, value: any, theme: DefaultTheme) => ({ 22 | ...theme, 23 | [key]: value, 24 | }) 25 | ); 26 | 27 | export const getPrimaryColor = getThemeProperty('primary'); 28 | export const setPrimaryColor = setThemeProperty('primary'); 29 | 30 | export const getTextColor = getThemeProperty('text'); 31 | export const setTextColor = setThemeProperty('text'); 32 | 33 | export const getTextOnPrimaryColor = getThemeProperty('textOnPrimary'); 34 | export const setTextOnPrimaryColor = setThemeProperty('textOnPrimary'); 35 | 36 | export const getErrorTextColor = getThemeProperty('errorText'); 37 | export const setErrorTextColor = setThemeProperty('errorText'); 38 | 39 | export const getDisabledBackgroundColor = 40 | getThemeProperty('disabledBackground'); 41 | export const setDisabledBackgroundColor = 42 | setThemeProperty('disabledBackground'); 43 | 44 | export const getDisabledTextColor = getThemeProperty('disabledText'); 45 | export const setDisabledTextColor = setThemeProperty('disabledText'); 46 | 47 | export const setPlaceholderTextColor = setThemeProperty('placeholderText'); 48 | export const getPlaceholderTextColor = getThemeProperty('placeholderText'); 49 | 50 | export const getInputBorderColor = getThemeProperty('inputBorder'); 51 | export const setInputBorderColor = setThemeProperty('inputBorder'); 52 | 53 | export const getInputBackgroundColor = getThemeProperty('inputBackground'); 54 | export const setInputBackgroundColor = setThemeProperty('inputBackground'); 55 | 56 | export const setHelpTextColor = setThemeProperty('helpText'); 57 | export const getHelpTextColor = getThemeProperty('helpText'); 58 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/VizExLoadingButton.js: -------------------------------------------------------------------------------- 1 | 'use es6'; 2 | 3 | import PropTypes from 'prop-types'; 4 | import styled from 'styled-components'; 5 | import VizExLoadingSpinner from '../loading/VizExLoadingSpinner'; 6 | import LoadingButtonUses, { 7 | buttonUse, 8 | spinnerUse, 9 | } from './constants/LoadingButtonUses'; 10 | import themePropType from '../utils/themePropType'; 11 | 12 | const Spinner = styled(VizExLoadingSpinner)` 13 | height: 0; 14 | position: absolute; 15 | top: 50%; 16 | right: 0; 17 | transition: opacity 0.2s; 18 | opacity: ${({ show }) => (show ? 1 : 0)}; 19 | `; 20 | 21 | const ReadyWrapper = styled.div` 22 | transition: opacity 0.2s; 23 | opacity: ${({ show }) => (show ? 1 : 0)}; 24 | `; 25 | 26 | const VizExLoadingButton = props => { 27 | const { 28 | children, 29 | Button, 30 | result, 31 | use, 32 | theme, 33 | currentState, 34 | onClick, 35 | ...rest 36 | } = props; 37 | 38 | const isReady = currentState === 'ready'; 39 | const isSubmitting = currentState === 'submitting'; 40 | const isDone = currentState === 'done'; 41 | 42 | return ( 43 | 65 | ); 66 | }; 67 | 68 | VizExLoadingButton.propTypes = { 69 | Button: PropTypes.node.isRequired, 70 | children: PropTypes.node.isRequired, 71 | currentState: PropTypes.oneOf(['ready', 'submitting', 'done']), 72 | onClick: PropTypes.func, 73 | result: PropTypes.node, 74 | theme: themePropType, 75 | use: PropTypes.oneOf(Object.values(LoadingButtonUses)), 76 | }; 77 | 78 | VizExLoadingButton.defaultProps = { 79 | 'data-test-id': 'loading-button', 80 | use: LoadingButtonUses.PRIMARY, 81 | onClick: () => {}, 82 | }; 83 | 84 | VizExLoadingButton.displayName = 'VizExLoadingButton'; 85 | 86 | export default VizExLoadingButton; 87 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/utils/aria-live/AriaLiveContext.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { 3 | AriaLiveContextProvider, 4 | AriaLiveContextProviderProps, 5 | useAriaLiveContext, 6 | Message, 7 | Assertiveness, 8 | } from './AriaLiveContext'; 9 | // TODO: update when platform makes these available from '@storybook/react' 10 | import { StoryFn, Meta } from '../../../storybook/types'; 11 | 12 | export default { 13 | title: 'AriaLiveContextProvider', 14 | component: AriaLiveContextProvider, 15 | } as Meta; 16 | 17 | const TextToAnnounce = ({ 18 | message = { id: '1', text: 'Some text' }, 19 | assertiveness, 20 | }: { 21 | message?: Message; 22 | assertiveness?: Assertiveness; 23 | }) => { 24 | const { announce } = useAriaLiveContext(); 25 | 26 | useEffect(() => announce(message, assertiveness), [ 27 | announce, 28 | assertiveness, 29 | message, 30 | ]); 31 | 32 | return

    {message.text}

    ; 33 | }; 34 | 35 | export const UsageDemo: StoryFn = () => { 36 | const { clearAnnouncedMessages } = useAriaLiveContext(); 37 | const [shouldRenderText, setShouldRenderText] = useState(false); 38 | 39 | return ( 40 | 41 | 49 |

    50 | { 51 | '(Polite text will be announced twice due to VO polite text bug where text is repeated when in an iframe)' 52 | } 53 |

    54 | {shouldRenderText && ( 55 | <> 56 | 60 | 64 | 68 | 75 | 76 | )} 77 |
    78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/button/VizExButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import VizExButton, { VizExButtonProps } from './VizExButton'; 2 | // TODO: update when platform makes these available from '@storybook/react' 3 | import { StoryFn, Meta } from '../../storybook/types'; 4 | 5 | export default { 6 | title: 'VizExButton', 7 | component: VizExButton, 8 | } as Meta; 9 | 10 | export const DefaultWithControls: StoryFn = args => ( 11 | {'Button'} 12 | ); 13 | DefaultWithControls.argTypes = { 14 | use: { 15 | options: ['primary', 'secondary', 'transparent-on-primary'], 16 | control: { type: 'radio' }, 17 | }, 18 | size: { 19 | options: ['xs', 'sm', 'md'], 20 | control: { type: 'radio' }, 21 | }, 22 | }; 23 | DefaultWithControls.args = { 24 | use: 'secondary', 25 | size: 'md', 26 | disabled: false, 27 | focused: false, 28 | hovered: false, 29 | pressed: false, 30 | }; 31 | 32 | export const Primary: StoryFn = () => ( 33 | <> 34 | {'Inactive'} 35 | 36 | {'Hovered'} 37 | 38 | 39 | {'Focused'} 40 | 41 | 42 | {'Pressed'} 43 | 44 | 45 | {'Disabled'} 46 | 47 | 48 | ); 49 | 50 | export const Secondary: StoryFn = () => ( 51 | <> 52 | {'Inactive'} 53 | 54 | {'Hovered'} 55 | 56 | 57 | {'Focused'} 58 | 59 | 60 | {'Pressed'} 61 | 62 | 63 | {'Disabled'} 64 | 65 | 66 | ); 67 | 68 | export const Sizes: StoryFn = () => ( 69 | <> 70 | 71 | {'size="md"'} 72 | 73 | 74 | {'size="sm"'} 75 | 76 | 77 | {'size="xs"'} 78 | 79 | {'size="md"'} 80 | {'size="sm"'} 81 | {'size="xs"'} 82 | 83 | ); 84 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/visitor-ui-component-library/theme/createThemeV2.ts: -------------------------------------------------------------------------------- 1 | import { ThemeConfig, ThemeFactoryConfig } from './styled'; 2 | import { 3 | colors as defaultColors, 4 | components as defaultComponents, 5 | } from './defaultTheme'; 6 | import { mergeDeep } from '../utils/mergeDeep'; 7 | import { css, DefaultTheme } from 'styled-components'; 8 | 9 | export type CSS = ReturnType; 10 | export type InteractionStyles = { 11 | _disabled: CSS; 12 | _focused: CSS; 13 | _hovered: CSS; 14 | _pressed: CSS; 15 | }; 16 | 17 | export const wrapWithSelector = (selector: string, style: CSS) => 18 | css` 19 | ${selector} { 20 | ${style} 21 | } 22 | `; 23 | export function getComponentStyles>({ 24 | baseStyle, 25 | _disabled, 26 | _focused, 27 | _hovered, 28 | _pressed, 29 | }: Partial & { baseStyle?: CSS }) { 30 | return css` 31 | ${baseStyle} 32 | ${({ disabled }) => (disabled ? _disabled : '')} 33 | ${({ focused }) => (focused ? _focused : '')} 34 | ${({ hovered }) => (hovered ? _hovered : '')} 35 | ${({ pressed }) => (pressed ? _pressed : '')} 36 | ${_disabled && wrapWithSelector('&:disabled', _disabled)} 37 | ${_focused && wrapWithSelector('&:focus-visible', _focused)} 38 | ${_hovered && 39 | wrapWithSelector(_disabled ? '&:hover:enabled' : '&:hover', _hovered)} 40 | ${_pressed && 41 | wrapWithSelector(_disabled ? '&:active:enabled' : '&:active', _pressed)} 42 | `; 43 | } 44 | export const computeComponentStyles = (components: ThemeConfig['components']) => 45 | Object.entries(components || {}).reduce((acc, [component, styleProps]) => { 46 | return { 47 | ...acc, 48 | [component]: { 49 | style: css>` 50 | ${getComponentStyles(styleProps)} 51 | `, 52 | }, 53 | }; 54 | }, {}) as DefaultTheme['components']; 55 | 56 | /** 57 | * Creates the theme object to customize the components based on the passed overrides. 58 | * 59 | * @param overrides A theme configuration object to merge/override the default values. 60 | * @returns The theme object used internally by the component library. 61 | */ 62 | export const createThemeV2 = ( 63 | overrides: ThemeFactoryConfig = {} 64 | ): DefaultTheme => { 65 | const mergedColors = overrides.colors 66 | ? mergeDeep(defaultColors, overrides.colors) 67 | : defaultColors; 68 | 69 | return { 70 | ...mergedColors, 71 | colors: mergedColors, 72 | components: computeComponentStyles( 73 | overrides.components 74 | ? mergeDeep(defaultComponents, overrides.components) 75 | : defaultComponents 76 | ), 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | [![calling-extensions-sdk on npm](https://img.shields.io/npm/v/@hubspot/calling-extensions-sdk.svg?style=flat-square)](http://npmjs.com/@hubspot/calling-extensions-sdk) 4 | 5 | The Calling Extensions SDK allows apps to provide a custom calling option to HubSpot users directly from a record in the CRM. 6 | 7 | A calling extension consists of three main components: 8 | 9 | - The Calling Extensions SDK, a JavaScript SDK that enables communication between your app and HubSpot. 10 | - The calling settings endpoints, which are used to set the calling settings for your app. Each HubSpot account that connects to your app will use these settings. 11 | - The calling iframe, which is where your app appears to HubSpot users and is configured using the calling settings endpoints. 12 | 13 | ## Getting Started 14 | 15 | 1. Create an app from your [app developer account](https://developers.hubspot.com/docs/getting-started/account-types#developer-accounts), and create a [developer test account](https://developers.hubspot.com/beta-docs/getting-started/account-types#developer-test-accounts) to test your app. 16 | 1. [Install the Calling Extensions SDK](https://developers.hubspot.com/docs/api-reference/crm-calling-extensions-v3/calling-sdk#install-the-calling-extensions-sdk-on-your-calling-app) on your calling app. Alternatively, you can [run the demos](https://developers.hubspot.com/docs/api-reference/crm-calling-extensions-v3/calling-sdk#run-the-demo-calling-app) if you'd like to see the SDK in action first. 17 | 1. Learn how to [use the Calling Extensions SDK](https://developers.hubspot.com/docs/api-reference/crm-calling-extensions-v3/calling-sdk#using-the-calling-extensions-sdk). 18 | 1. View the [available calling events](https://developers.hubspot.com/docs/api-reference/crm-calling-extensions-v3/calling-sdk#events). 19 | 1. [Test your app](https://developers.hubspot.com/docs/api-reference/crm-calling-extensions-v3/calling-sdk#test-your-app). 20 | 1. [Get your app ready for production](https://developers.hubspot.com/docs/api-reference/crm-calling-extensions-v3/calling-sdk#get-your-app-ready-for-production). 21 | 1. [Publish your app to the HubSpot marketplace](https://developers.hubspot.com/docs/api-reference/crm-calling-extensions-v3/calling-sdk#publish-your-calling-app-to-the-hubspot-marketplace). 22 | 23 | ## Getting Help 24 | 25 | 1. [Review the FAQs](https://developers.hubspot.com/docs/api-reference/crm-calling-extensions-v3/calling-sdk#calling-sdk-%7C-frequently-asked-questions). 26 | 1. Learn how to [get help with HubSpot](https://knowledge.hubspot.com/help-and-resources/get-help-with-hubspot?_gl=1*7nomik*_gcl_au*OTQ3MjgyMDk4LjE3MzA4MzkyOTE.*_ga*MjA4MzEyMjM0Mi4xNzMwODM5Mjkx*_ga_LXTM6CQ0XK*MTczMTQyOTg1Mi40LjEuMTczMTQzMDQ1My42MC4wLjA.&_ga=2.181003488.658339848.1731429852-2083122342.1730839291). 27 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/components/screens/LoginScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useState, ChangeEvent } from "react"; 2 | import { useAutoFocus } from "../../hooks/useAutoFocus"; 3 | import { 4 | Wrapper, 5 | RoundedInput, 6 | RoundedButton, 7 | LinkButton, 8 | Row, 9 | } from "../Components"; 10 | import { ScreenProps } from "../../types/ScreenTypes"; 11 | import { PANTERA } from "../../utils/colors"; 12 | import { LOG_IN } from "../../constants/buttonIds"; 13 | 14 | function LoginScreen({ cti, handleNextScreen }: ScreenProps) { 15 | const usernameInput = useAutoFocus(); 16 | const [username, setUsername] = useState(""); 17 | const [password, setPassword] = useState(""); 18 | 19 | const handleLogin = () => { 20 | cti.userLoggedIn(); 21 | handleNextScreen(); 22 | }; 23 | 24 | const handleUsername = ({ 25 | target: { value }, 26 | }: ChangeEvent) => setUsername(value); 27 | const handlePassword = ({ 28 | target: { value }, 29 | }: ChangeEvent) => setPassword(value); 30 | 31 | const handleOpenWindow = () => { 32 | const url = `${cti.hostUrl}/calling-integration-popup-ui/${cti.portalId}?usesCallingWindow=false`; 33 | window.open(url, "_blank"); 34 | }; 35 | 36 | return ( 37 | 38 |
    39 |

    Log into your calling account

    40 |
    User Name
    41 | 47 |
    Password
    48 | 54 |
    55 | 56 | 62 | Log in 63 | 64 | 65 |
    66 | 67 | 72 | Sign in with SSO 73 | 74 | 75 |
    76 | {!cti.usesCallingWindow && ( 77 | 78 | 83 | Open calling window 84 | 85 | 86 | )} 87 | 88 |
    89 | ); 90 | } 91 | 92 | export default LoginScreen; 93 | -------------------------------------------------------------------------------- /demos/demo-react-ts/src/components/screens/CallEndedScreen.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from "react"; 2 | import styled from "styled-components"; 3 | import { RoundedButton, Row, Timer, TextArea, Wrapper } from "../Components"; 4 | import { ScreenProps } from "../../types/ScreenTypes"; 5 | import { formatPhoneNumber } from "../../utils/phoneNumberUtils"; 6 | import { COMPLETE_CALL, FINALIZE_ENGAGEMENT } from "../../constants/buttonIds"; 7 | 8 | const StyledRow = styled(Row)` 9 | justify-content: flex-start; 10 | `; 11 | 12 | const RECORDING_URL = 13 | "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"; 14 | 15 | function CallEndedScreen({ 16 | cti, 17 | engagementId, 18 | handleCallCompleted, 19 | dialNumber, 20 | incomingNumber, 21 | direction, 22 | notes, 23 | setNotes, 24 | isCallRecorded, 25 | callDuration, 26 | callDurationString, 27 | callStatus, 28 | }: ScreenProps) { 29 | const handleNotes = (event: ChangeEvent) => { 30 | setNotes(event.target.value); 31 | }; 32 | 33 | const onSaveCall = () => { 34 | cti.callCompleted({ 35 | engagementId, 36 | hideWidget: false, 37 | /** 38 | * @param engagementProperties (currently not in use) 39 | * @TODO We will be releasing a feature to update engagements in the SDK without this API call 40 | * https://developers.hubspot.com/docs/api/crm/calls#update-calls 41 | */ 42 | engagementProperties: { 43 | hs_call_body: notes, 44 | hs_call_duration: callDuration.toString(), 45 | hs_call_status: callStatus || "COMPLETED", 46 | hs_call_recording_url: isCallRecorded ? RECORDING_URL : null, 47 | }, 48 | }); 49 | handleCallCompleted(); 50 | }; 51 | 52 | const onFinalizeEngagement = () => { 53 | cti.finalizeEngagement({ engagementId }); 54 | }; 55 | 56 | return ( 57 | 58 |
    59 |

    60 | Call with{" "} 61 | {formatPhoneNumber( 62 | direction === "OUTBOUND" ? dialNumber : incomingNumber 63 | )}{" "} 64 | ended 65 |

    66 | {callDurationString} 67 |
    68 | Notes 69 | 70 |