diff --git a/.github/workflows/reactnative-node-ci.yml b/.github/workflows/reactnative-node-ci.yml new file mode 100644 index 0000000..99f6f72 --- /dev/null +++ b/.github/workflows/reactnative-node-ci.yml @@ -0,0 +1,51 @@ +name: React Native Chat CI +# - Install, lint, test, and build production Expo application +# - Runs only on changes to connectReactNativeChat/ folder + +on: + workflow_dispatch: + push: + branches: [ master ] + paths: 'connectReactNativeChat/**' + pull_request: + branches: [ master ] + paths: 'connectReactNativeChat/**' + +jobs: + build: + runs-on: macos-12 + strategy: + matrix: + node-version: [16.x] # 18.x, 19.x + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + defaults: + run: + working-directory: ./connectReactNativeChat + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Expo app dependencies + run: yarn + + - run: yarn lint + + - name: Create endpoint.js config + run: | + cp endpoints.sample.js endpoints.js || echo "no endpoint.sample.js found" + + - run: yarn test + + - name: Create app.json config + run: | + rm app.json || echo "no app.json found, creating new" + cp app.prod.json app.json + + - name: Build the Expo application + run: CI=1 npx expo prebuild --platform all + diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a0f291..567b580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.0] - 2023-3-29 +### Changed +- Connect React Native Chat - initial release of cross-platform Amazon Connect Chat solution + ## [1.3.3] - 2023-2-21 ### Changed - Custom Chat Widget - Support Custom Chat Duration diff --git a/README.md b/README.md index 2bf920c..87c4246 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ At the moment, these are the solutions in this repo: 5. **[customChatWidget](https://github.com/amazon-connect/amazon-connect-chat-ui-examples/tree/master/customChatWidget)** Custom Chat Widget for Amazon Connect, with a Chat Form that can be easily plugged into a webpage. This solution helps customers to have Amazon Connect Custom Chat Widget in their website, by applying simple configuration parameters. It also makes customizing the `amazon-connect-interface.js` file easier, and can be used as an easy way to host custom widget on a webpage. +6. **[connectReactNativeChat](https://github.com/amazon-connect/amazon-connect-chat-ui-examples/tree/master/connectReactNativeChat)** + React Native demo Chat application for Amazon Connect. This cross-platform solution implements basic Chat JS functionality and is fully customizable. Follow the provided documentation to build with [`amazon-connect-chatjs@^1.5.0`](https://github.com/amazon-connect/amazon-connect-chatjs). + ## Resources Here are a few resources to help you implement chat in your contact center: diff --git a/connectReactNativeChat/.eslintrc b/connectReactNativeChat/.eslintrc new file mode 100644 index 0000000..039d64a --- /dev/null +++ b/connectReactNativeChat/.eslintrc @@ -0,0 +1,41 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "settings": { + "react": { + "version": "detect" + } + }, + "extends": [ + "plugin:react/recommended" + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "react/prop-types": "off", + "global-require": 0 + }, + "ignorePatterns": [ + "assets", + "ios", + "android" + ], + "overrides": [ + { + "files": [ + "**/*.test.js", + "**/*.test.jsx" + ], + "env": { + "jest": true + } + } + ] +} diff --git a/connectReactNativeChat/.gitignore b/connectReactNativeChat/.gitignore new file mode 100644 index 0000000..a100e3b --- /dev/null +++ b/connectReactNativeChat/.gitignore @@ -0,0 +1,23 @@ +node_modules +.expo/ +dist/ +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ + +# macOS +.DS_Store + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +endpoints.js +yarn-error.log +.dev +android +ios \ No newline at end of file diff --git a/connectReactNativeChat/.husky/.gitignore b/connectReactNativeChat/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/connectReactNativeChat/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/connectReactNativeChat/.husky/pre-commit b/connectReactNativeChat/.husky/pre-commit new file mode 100755 index 0000000..36af219 --- /dev/null +++ b/connectReactNativeChat/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/connectReactNativeChat/.prettierrc b/connectReactNativeChat/.prettierrc new file mode 100644 index 0000000..23b8710 --- /dev/null +++ b/connectReactNativeChat/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120 +} diff --git a/connectReactNativeChat/App.js b/connectReactNativeChat/App.js new file mode 100644 index 0000000..9714bb5 --- /dev/null +++ b/connectReactNativeChat/App.js @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import React from "react"; +import { View, Image } from "react-native"; +import { createStackNavigator } from "@react-navigation/stack"; +import { NavigationContainer } from "@react-navigation/native"; + +import ChatWrapper from "./src/components/ChatWrapper"; +import GiftedChatWidget from "./src/components/ChatWidget"; +import DebuggerWidget from "./src/components/DebuggerWidget"; +import ChatForm from "./src/components/ChatForm"; + +import { LogBox } from "react-native"; +import { ENABLE_REACTNATIVE_LOGBOX } from "./config"; +if (!ENABLE_REACTNATIVE_LOGBOX) { + LogBox.ignoreAllLogs(); +} + +const ChatScreen = ({ navigation }) => { + return ( + + ); +}; + +const HomeScreen = ({ navigation }) => ( + + Connect logo + + navigation.navigate("Chat")} /> + + + +); + +const Stack = createStackNavigator(); + +const App = () => { + return ( + + + + + + + ); +}; + +export default App; diff --git a/connectReactNativeChat/README.md b/connectReactNativeChat/README.md new file mode 100644 index 0000000..9886b24 --- /dev/null +++ b/connectReactNativeChat/README.md @@ -0,0 +1,376 @@ +# React Native + ChatJS Demo + +A demo [Expo](https://expo.dev/) app for building custom Amazon Connect Chat in React Native. This cross-platform (Android, iOS, macOS, Windows, & Web) solution implements basic ChatJS functionality and is fully customizable. + +> Built with `expo@~48.0.6`, `react-native-gifted-chat@^2.0.0`, and `Node.js v16`. + +> React Native (v0.71.3) apps may target iOS 12.4 and Android 5.0 (API 21) or newer. You may use Windows, macOS, or Linux as your development operating system, though building and running iOS apps is limited to macOS. + +**Reference:** + +- ChatJS Repository: https://github.com/amazon-connect/amazon-connect-chatjs +- NPM package: https://www.npmjs.com/package/amazon-connect-chatjs +- Documentation: https://docs.aws.amazon.com/connect/latest/adminguide/enable-chat-in-app.html + +https://user-images.githubusercontent.com/60903378/229218472-bf2ba819-d7ea-46c9-9437-58290e8be962.mov + +## Contents + +- [Mobile Support](#mobile-support) +- [Local Development](#local-development) +- [Production Build](#production-build) +- [ChatJS Usage](#chatjs-usage) + +## Mobile Support + +Additional configuration is required to support ChatJS in React Native applications. Use `amazon-connect-chatjs@^1.5.0` and apply the changes below: + +Install the supported ChatJS library using either Yarn: + +```sh +$ yarn add amazon-connect-chatjs@^1.5.0 +``` + +or npm: + +```sh +$ npm install amazon-connect-chatjs@^1.5.0 +``` + + +#### Override Browser Network Health Check + +If running ChatJS in a mobile React Native environment, override the default network setting online and check: + +> `amazon-connect-websocket-manager.js` depencency will use `navigator.onLine`. Legacy browsers will always return `true`, but unsupported or mobile runtime will return `null/undefined`. + +```js +/** + * `amazon-connect-websocket-manager.js` depencency will use `navigator.onLine` + * Unsupported or mobile runtime will return `null/undefined` - preventing websocket connections + * Legacy browsers will always return `true` [ref: caniuse.com/netinfo] + */ +const customNetworkStatus = () => true; + +connect.ChatSession.setGlobalConfig({ + webSocketManagerConfig: { + isNetworkOnline: customNetworkStatus, // default: () => navigator.onLine + } +}); +``` + +#### Custom Network Health Check + +Extending this, device-native network health checks can be used for React Native applications. + +1. First, install the `useNetInfo` react hook: + +```sh +$ npm install --save @react-native-community/netinfo +# source: https://github.com/react-native-netinfo/react-native-netinfo +``` + +2. Make sure to update permissions, Android requires the following line in `AndroidManifest.xml`: (for SDK version after 23) + +```xml + +``` + +3. Set up the network event listener, and pass custom function to `setGlobalConfig`: + +> Note: To configure `WebSocketManager`, `setGlobalConfig` must be invoked + +```js +import ChatSession from "./ChatSession"; +import NetInfo from "@react-native-community/netinfo"; +import "amazon-connect-chatjs"; // ^1.5.0 - imports global "connect" object + +/** + * By default, `isNetworkOnline` will be invoked every 250ms + * Should only current status, and not make `NetInfo.fetch()` call + * + * @return {boolean} returns true if currently connected to network +*/ +let isOnline = true; +const customIsNetworkOnline = () => isOnline; + +const ReactNativeChatComponent = (props) => { + /** + * Network event listener native to device + * Will update `isOnline` value asynchronously whenever network calls are made + */ + const unsubscribeNetworkEventListener = NetInfo.addEventListener(state => { + console.log('NetInfo eventListener - isConnected:', state.isConnected); + isOnline = state.isConnected; + }); + useEffect(() => { + return unsubscribeNetworkEventListener(); + }, []); + const initializeChatJS = () => { + // To configure WebSocketManager, setGlobalConfig must be invoked + connect.ChatSession.setGlobalConfig({ + // ... + webSocketManagerConfig: { + isNetworkOnline: customIsNetworkOnline, // default: () => navigator.onLine + } + }); + } + // ... +} +``` + +4. Optionally, this configuration can be dynamically set based on the `Platform` + +```js +import { Platform } from 'react-native'; +const isMobile = Platform.OS === 'ios' || Platform.OS === 'android'; +connect.ChatSession.setGlobalConfig({ + // ... + webSocketManagerConfig: { + ...(isMobile ? { isNetworkOnline: () => true } : {}), // use default behavior for browsers + } +}); +``` + +## Local Development + +> Versions: Expo@~48.0.6, react-native@0.71.3, react-native-gifted-chat@^2.0.0, react@^18.2.0 +> Supported in Node v16+ + +> Setting up Android Emulator: https://docs.expo.dev/workflow/android-studio-emulator/ +> Setting up iPhone Emulator: https://docs.expo.dev/workflow/ios-simulator/ + +1. Deploy startChatContact backend (from CFN stack): https://github.com/amazon-connect/amazon-connect-chat-ui-examples/tree/master/cloudformationTemplates/startChatContactAPI + +```sh +$ cp endpoints.sample.js endpoints.js +``` + +```js +// /endpoints.js + +export const GATEWAY = { + region: "us-west-2", + apiGWId: "asdfasdf", +}; + +const ENDPOINTS = { + contactFlowId: "asdf-5056-asdf-a672-asdf6a81ca6", + instanceId: "asdf-078b-asdf-9264-asdf98f3c28", + region: GATEWAY.region, + apiGatewayEndpoint: `https://${GATEWAY.apiGWId}.execute-api.${GATEWAY.region}.amazonaws.com/Prod`, + ccpUrl: "https://.my.connect.aws/ccp-v2", // optional - for reference +}; + +export default ENDPOINTS; +``` + +2. Customize several global settings in the `config.js` file, including the `startChatRequestInput` request body. + +```js +// /config.js + +// Enable/disable ChatJS event logs +export const ENABLE_CHATJS_LOGS = false; + +// Renders pop-up on device emulator screen from console.logs +export const ENABLE_REACTNATIVE_LOGBOX = false; + +// Enable rich messaging, CCP sends "text/markdown" by default +// doc: https://docs.aws.amazon.com/connect/latest/APIReference/API_StartChatContact.html#API_StartChatContact_RequestSyntax +export const supportMessageContentTypes = ["text/plain"]; + +export const CUSTOMER_USER = { + _id: 1, + name: "Customer", + avatar: "https://i.pravatar.cc/100?img=11", +}; + +export const AGENT_USER = { + _id: 2, + name: "Agent", + avatar: + "https://www.bcbswy.com/wp-content/uploads/2020/05/20.06.26_bcbswy_avatar_@2.0x.png", +}; + +export const startChatRequestInput = { + ...ENDPOINTS, + name: "John", + contactAttributes: JSON.stringify({ + customerName: "John", + }), + supportedMessagingContentTypes: supportMessageContentTypes.join(","), +}; +``` + +3. Run the Expo demo application on an emulator + +```sh +$ yarn +$ yarn run ios +$ yarn run web +$ yarn run android +$ npx expo run:ios -d # run on device plugged into laptop +``` + +4. Edit code, regenerate bundle files, and refresh Expo app + +### Open Debugger + +> If you need to clear cache, run `expo start -c` + +- Physical device: 👋 shake it. +- iOS simulator: Cmd-Ctrl-Z in macOS. +- Android emulator: Cmd-M in macOS or Ctrl-Min Windows. + +## Production Build + +Create a production Expo build + +``` +$ rm app.json && cp app.prod.json app.json +$ CI=1 npx expo prebuild --platform all +``` + +## ChatJS Usage + +ChatScreen uses three components: + +- `initiateChat`: startChatContact request +- `ChatSession`: low-level abstraction on top of ChatJS +- `ChatWrapper`: manages chat state at the UI level, handling loading/disconnect +- `ChatWidget`: renders chat composer and transcript + +```js +// src/api/initiateChat.js + +/** + * Initiate a chat session within Amazon Connect, proxying initial StartChatContact request + * through your API Gateway. + * + * https://docs.aws.amazon.com/connect/latest/APIReference/API_StartChatContact.html + */ +const initiateChat = (input) => { + const requestBody = { + InstanceId: "asdf-5056-asdf-a672-asdf6a81ca6", + ContactFlowId: "asdf-5056-asdf-a672-asdf6a81ca6", + ParticipantDetails: { + DisplayName: "John", + }, + SupportedMessagingContentTypes: ["text/plain", "text/markdown"], + ChatDurationInMinutes: 60, + }; + + return window + .fetch( + input.apiGatewayEndpoint, + { + headers: input.headers ? input.headers : new Headers(), + method: "post", + body: JSON.stringify(requestBody), + }, + START_CHAT_CLIENT_TIMEOUT_MS // 5000 + ) + .then((res) => res.json.data) + .catch((err) => console.error(err)); +}; +``` + +```js +// src/components/ChatSession.js + +import "amazon-connect-chatjs"; // ^1.5.0 + +// Low-level abstraction on top of Chat.JS +class ChatJSClient { + session = null; + + constructor(chatDetails, region) { + this.session = connect.ChatSession.create({ + chatDetails: { + contactId: chatDetails.startChatResult.ContactId, + participantId: chatDetails.startChatResult.ParticipantId, + participantToken: chatDetails.startChatResult.ParticipantToken, + }, + type: "CUSTOMER", + options: { region }, + }); + } + + connect() { + // Intiate the websocket connection. After the connection is established, the customer's chat request + // will be routed to an agent who can then accept the request. + return this.session.connect(); + } + + disconnect() { + return this.session.disconnectParticipant(); + } + + sendMessage(content) { + // Right now we are assuming only text messages, + // later we will have to add functionality for other types. + return this.session.sendMessage({ + message: content.data, + contentType: content.type, + }); + } +} +``` + +```js +// src/components/ChatWrapper.js + +import initiateChat from "../api/initiateChat"; +import filterIncomingMessages from "../utils/filterIncomingMessages"; + +const chatDetails = await initiateChat(); +const chatSession = new ChatJSClient(chatDetails); +await chatSession.openChatSession(); + +// Add event listeners to chat session +chatSession.onIncoming(async () => { + const latestTranscript = await chatSession.loadPreviousTranscript(); + setMessages(filterIncomingMessages(latestTranscript)); +}); +chatSession.onOutgoing(async () => { + const latestTranscript = await chatSession.loadPreviousTranscript(); + setMessages(filterIncomingMessages(latestTranscript)); +}); +``` + +```js +// src/components/ChatWidget.js + +import { GiftedChat } from "react-native-gifted-chat"; // ^2.0.0 + +const ChatWidget = ({ handleSendMessage, messages }) => { + return ( + { + Keyboard.dismiss(); + handleSendMessage(msgs[0].text); + }} + /> + ); +}; +``` + + diff --git a/connectReactNativeChat/app.json b/connectReactNativeChat/app.json new file mode 100644 index 0000000..794c6e9 --- /dev/null +++ b/connectReactNativeChat/app.json @@ -0,0 +1,30 @@ +{ + "expo": { + "name": "chat-js-mobile", + "slug": "chat-js-mobile", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + } + }, + "web": { + "favicon": "./assets/favicon.png" + } + } +} diff --git a/connectReactNativeChat/app.prod.json b/connectReactNativeChat/app.prod.json new file mode 100644 index 0000000..9d2fb93 --- /dev/null +++ b/connectReactNativeChat/app.prod.json @@ -0,0 +1,31 @@ +{ + "expo": { + "name": "chat-js-mobile", + "slug": "chat-js-mobile", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "assetBundlePatterns": ["**/*"], + "ios": { + "bundleIdentifier": "com.yourcompany.yourappname", + "supportsTablet": true + }, + "android": { + "package": "com.yourcompany.yourappname", + "versionCode": 1, + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + } + }, + "web": { + "favicon": "./assets/favicon.png" + } + } +} diff --git a/connectReactNativeChat/assets/adaptive-icon.png b/connectReactNativeChat/assets/adaptive-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/connectReactNativeChat/assets/adaptive-icon.png differ diff --git a/connectReactNativeChat/assets/connect.png b/connectReactNativeChat/assets/connect.png new file mode 100644 index 0000000..950969b Binary files /dev/null and b/connectReactNativeChat/assets/connect.png differ diff --git a/connectReactNativeChat/assets/favicon.png b/connectReactNativeChat/assets/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/connectReactNativeChat/assets/favicon.png differ diff --git a/connectReactNativeChat/assets/icon.png b/connectReactNativeChat/assets/icon.png new file mode 100644 index 0000000..a0b1526 Binary files /dev/null and b/connectReactNativeChat/assets/icon.png differ diff --git a/connectReactNativeChat/assets/splash.png b/connectReactNativeChat/assets/splash.png new file mode 100644 index 0000000..0e89705 Binary files /dev/null and b/connectReactNativeChat/assets/splash.png differ diff --git a/connectReactNativeChat/babel.config.js b/connectReactNativeChat/babel.config.js new file mode 100644 index 0000000..73ebf58 --- /dev/null +++ b/connectReactNativeChat/babel.config.js @@ -0,0 +1,6 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ["babel-preset-expo"], + }; +}; diff --git a/connectReactNativeChat/config.js b/connectReactNativeChat/config.js new file mode 100644 index 0000000..2907843 --- /dev/null +++ b/connectReactNativeChat/config.js @@ -0,0 +1,31 @@ +import ENDPOINTS from "./endpoints"; + +export const ENABLE_REACTNATIVE_LOGBOX = false; +export const ENABLE_CHATJS_LOGS = true; + +export const loggerConfig = { + useDefaultLogger: true, +}; + +export const supportMessageContentTypes = ["text/plain"]; +export const startChatRequestInput = { + ...ENDPOINTS, + name: "John", + contactAttributes: JSON.stringify({ + customerName: "John", + }), + supportedMessagingContentTypes: supportMessageContentTypes.join(","), +}; + +export const CUSTOMER_USER = { + _id: 1, + name: "Customer", + avatar: "https://i.pravatar.cc/100?img=11", +}; + +export const AGENT_USER = { + _id: 2, + name: "Agent", + avatar: + "https://www.bcbswy.com/wp-content/uploads/2020/05/20.06.26_bcbswy_avatar_@2.0x.png", +}; diff --git a/connectReactNativeChat/endpoints.sample.js b/connectReactNativeChat/endpoints.sample.js new file mode 100644 index 0000000..22b8119 --- /dev/null +++ b/connectReactNativeChat/endpoints.sample.js @@ -0,0 +1,14 @@ +export const GATEWAY = { + region: "ap-southeast-2", + apiGWId: "asdfasdf", +}; + +const ENDPOINTS = { + contactFlowId: "asdf-5056-asdf-a672-asdf6a81ca6", + instanceId: "asdf-078b-asdf-9264-asdf98f3c28", + region: GATEWAY.region, + apiGatewayEndpoint: `https://${GATEWAY.apiGWId}.execute-api.${GATEWAY.region}.amazonaws.com/Prod`, + ccpUrl: "https://.my.connect.aws/ccp-v2", // optional - for reference +}; + +export default ENDPOINTS; diff --git a/connectReactNativeChat/jest.config.js b/connectReactNativeChat/jest.config.js new file mode 100644 index 0000000..03f215b --- /dev/null +++ b/connectReactNativeChat/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + testEnvironment: "jsdom", + preset: "react-native", + transformIgnorePatterns: ["node_modules/(?!react-native-url-polyfill)/"], +}; diff --git a/connectReactNativeChat/package.json b/connectReactNativeChat/package.json new file mode 100644 index 0000000..6d44f93 --- /dev/null +++ b/connectReactNativeChat/package.json @@ -0,0 +1,53 @@ +{ + "name": "amazon-connect-react-native-chat-js", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web", + "test": "jest", + "lint": "eslint --ext .js,.ts,.tsx ./", + "lint:fix": "eslint --fix --ext .js,.jsx ./", + "prettier": "prettier src --check", + "prettier:fix": "yarn prettier -- --write", + "unused:prepare": "husky install" + }, + "dependencies": { + "@expo/webpack-config": "^18.0.1", + "@react-native-community/netinfo": "^9.3.7", + "@react-navigation/native": "^6.1.6", + "@react-navigation/stack": "^6.3.16", + "amazon-connect-chatjs": "^1.5.0", + "axios": "^1.3.4", + "expo": "~48.0.6", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-native": "0.71.3", + "react-native-elements": "^3.4.3", + "react-native-gesture-handler": "^2.9.0", + "react-native-gifted-chat": "^2.0.1", + "react-native-lightbox": "^0.8.1", + "react-native-loading-spinner-overlay": "^3.0.1", + "react-native-safe-area-context": "^4.5.0", + "react-native-web": "~0.18.10" + }, + "devDependencies": { + "@babel/core": "^7.9.0", + "@babel/preset-env": "^7.20.2", + "@testing-library/react-native": "^11.5.4", + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-react": "^7.28.0", + "husky": ">=6", + "jest": "^27", + "jest-environment-jsdom": "^27", + "jest-react-native": "^18.0.0", + "lint-staged": ">=10", + "prettier": "^2.8.4", + "react-test-renderer": "^18.2.0" + }, + "lint-staged": { + "*.{js,css,md}": "prettier --write" + } +} diff --git a/connectReactNativeChat/src/api/initiateChat.js b/connectReactNativeChat/src/api/initiateChat.js new file mode 100644 index 0000000..b89b0d7 --- /dev/null +++ b/connectReactNativeChat/src/api/initiateChat.js @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import request from '../utils/fetchRequest' + +const START_CHAT_CLIENT_TIMEOUT_MS = 5000 + +/** + * Initiate a chat session within Amazon Connect, proxying initial StartChatContact request + * through your API Gateway. + * + * https://docs.aws.amazon.com/connect/latest/APIReference/API_StartChatContact.html + * + * @param {Object} input - data to initate chat + * @param {string} input.instanceId + * @param {string} input.contactFlowId + * @param {string} input.apiGatewayEndpoint + * @param {string} input.name + * @param {string} input.initialMessage - optional initial message to start chat + * @param {string} input.region + * @param {string} input.contactAttributes + * @param {object} input.headers + * @param {string} input.supportedMessagingContentTypes + * @param {number} input.chatDurationInMinutes + * @returns {Promise} Promise object that resolves to chatDetails objects + */ +const initiateChat = (input, errorCallback) => { + const initiateChatRequest = { + InstanceId: input.instanceId, + ContactFlowId: input.contactFlowId, + ParticipantDetails: { + DisplayName: input.name, + }, + Username: input.username, + ...(input.supportedMessagingContentTypes + ? { + SupportedMessagingContentTypes: input.supportedMessagingContentTypes.split(','), + } + : {}), + ...(input.chatDurationInMinutes ? { ChatDurationInMinutes: Number(input.chatDurationInMinutes) } : {}), + } + + return request( + input.apiGatewayEndpoint, + { + headers: input.headers ? input.headers : new Headers(), + method: 'post', + body: JSON.stringify(initiateChatRequest), + }, + START_CHAT_CLIENT_TIMEOUT_MS + ) + .then((res) => res.json.data) + .catch(errorCallback) +} + +export default initiateChat diff --git a/connectReactNativeChat/src/api/initiateChat.test.js b/connectReactNativeChat/src/api/initiateChat.test.js new file mode 100644 index 0000000..9caed62 --- /dev/null +++ b/connectReactNativeChat/src/api/initiateChat.test.js @@ -0,0 +1,222 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import initiateChat from './initiateChat' +import request from '../utils/fetchRequest' + +const START_CHAT_CLIENT_TIMEOUT_MS = 5000 + +jest.mock('../utils/fetchRequest') + +const input = { + apiGatewayEndpoint: 'https://localhost:3000', + name: 'Tester', +} + +const resolvedRes = { + json: { + data: [1, 2, 3], + }, +} + +Object.freeze(input) + +beforeEach(() => { + request.mockResolvedValue(resolvedRes) +}) + +afterEach(() => { + jest.resetAllMocks() +}) + +it('should attach headers from the input, if supplied', async () => { + const inputWithHeaders = { + ...input, + headers: { + testHeader: 'testHeaderValue', + }, + } + await initiateChat(inputWithHeaders) + + expect(request).toHaveBeenCalledTimes(1) + expect(request).toHaveBeenCalledWith( + input.apiGatewayEndpoint, + { + headers: inputWithHeaders.headers, + method: 'post', + body: JSON.stringify({ + ParticipantDetails: { + DisplayName: inputWithHeaders.name, + }, + }), + }, + START_CHAT_CLIENT_TIMEOUT_MS + ) +}) + +it('should resolve with the response json.data', async () => { + const result = await initiateChat(input) + + expect(result).toEqual(resolvedRes.json.data) +}) + +it('should attach headers and the optional field supportedMessagingContentTypes from the input, if supplied', async () => { + const inputWithHeaders = { + ...input, + supportedMessagingContentTypes: 'type1,type2', + headers: { + testHeader: 'testHeaderValue', + }, + } + await initiateChat(inputWithHeaders) + + expect(request).toHaveBeenCalledTimes(1) + expect(request).toHaveBeenCalledWith( + input.apiGatewayEndpoint, + { + headers: inputWithHeaders.headers, + method: 'post', + body: JSON.stringify({ + ParticipantDetails: { + DisplayName: inputWithHeaders.name, + }, + SupportedMessagingContentTypes: ['type1', 'type2'], + }), + }, + START_CHAT_CLIENT_TIMEOUT_MS + ) +}) + +it('should attach headers if supplied and not the optional field supportedMessagingContentTypes from the input if undefined', async () => { + const inputWithHeaders = { + ...input, + supportedMessagingContentTypes: undefined, + headers: { + testHeader: 'testHeaderValue', + }, + } + await initiateChat(inputWithHeaders) + + expect(request).toHaveBeenCalledTimes(1) + expect(request).toHaveBeenCalledWith( + input.apiGatewayEndpoint, + { + headers: inputWithHeaders.headers, + method: 'post', + body: JSON.stringify({ + ParticipantDetails: { + DisplayName: inputWithHeaders.name, + }, + }), + }, + START_CHAT_CLIENT_TIMEOUT_MS + ) +}) + +it('should attach headers if supplied and the optional field supportedMessagingContentTypes with one value from the input', async () => { + const inputWithHeaders = { + ...input, + supportedMessagingContentTypes: 'type1', + headers: { + testHeader: 'testHeaderValue', + }, + } + await initiateChat(inputWithHeaders) + + expect(request).toHaveBeenCalledTimes(1) + expect(request).toHaveBeenCalledWith( + input.apiGatewayEndpoint, + { + headers: inputWithHeaders.headers, + method: 'post', + body: JSON.stringify({ + ParticipantDetails: { + DisplayName: inputWithHeaders.name, + }, + SupportedMessagingContentTypes: ['type1'], + }), + }, + START_CHAT_CLIENT_TIMEOUT_MS + ) +}) + +it('should attach headers if supplied and the optional field supportedMessagingContentTypes with two values from the input', async () => { + const inputWithHeaders = { + ...input, + supportedMessagingContentTypes: 'text/plain,text/markdown', + headers: { + testHeader: 'testHeaderValue', + }, + } + await initiateChat(inputWithHeaders) + + expect(request).toHaveBeenCalledTimes(1) + expect(request).toHaveBeenCalledWith( + input.apiGatewayEndpoint, + { + headers: inputWithHeaders.headers, + method: 'post', + body: JSON.stringify({ + ParticipantDetails: { + DisplayName: inputWithHeaders.name, + }, + SupportedMessagingContentTypes: ['text/plain', 'text/markdown'], + }), + }, + START_CHAT_CLIENT_TIMEOUT_MS + ) +}) + +it('should forward chatDurationInMinutes when optional field is set', async () => { + const inputWithHeaders = { + ...input, + chatDurationInMinutes: 1500, + headers: { + testHeader: 'testHeaderValue', + }, + } + await initiateChat(inputWithHeaders) + + expect(request).toHaveBeenCalledTimes(1) + expect(request).toHaveBeenCalledWith( + input.apiGatewayEndpoint, + { + headers: inputWithHeaders.headers, + method: 'post', + body: JSON.stringify({ + ParticipantDetails: { + DisplayName: inputWithHeaders.name, + }, + ChatDurationInMinutes: 1500, + }), + }, + START_CHAT_CLIENT_TIMEOUT_MS + ) +}) + +it('should only forward number if chatDurationInMinutes field is set', async () => { + const inputWithHeaders = { + ...input, + chatDurationInMinutes: '1500', + headers: { + testHeader: 'testHeaderValue', + }, + } + await initiateChat(inputWithHeaders) + + expect(request).toHaveBeenCalledTimes(1) + expect(request).toHaveBeenCalledWith( + input.apiGatewayEndpoint, + { + headers: inputWithHeaders.headers, + method: 'post', + body: JSON.stringify({ + ParticipantDetails: { + DisplayName: inputWithHeaders.name, + }, + ChatDurationInMinutes: 1500, + }), + }, + START_CHAT_CLIENT_TIMEOUT_MS + ) +}) diff --git a/connectReactNativeChat/src/components/ChatForm.js b/connectReactNativeChat/src/components/ChatForm.js new file mode 100644 index 0000000..31febfa --- /dev/null +++ b/connectReactNativeChat/src/components/ChatForm.js @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import React, { useState } from 'react' +import { StyleSheet } from 'react-native' +import { Input, Button } from 'react-native-elements' + +const ChatForm = ({ openChatScreen }) => { + const [name, setName] = useState('TestUser') + + return ( + <> + setName(text)} + /> +