diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..4e3328b
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,46 @@
+# Chatwoot React Native Widget
+
+## Project Overview
+This is a React Native widget library for integrating Chatwoot chat functionality into React Native applications. It provides a WebView-based chat interface that connects to Chatwoot server installations.
+
+## Key Information
+- **Package**: @chatwoot/react-native-widget
+- **Supported Chatwoot version**: 2.16.0+
+- **Main dependencies**: react-native-webview, async-storage
+- **Current branch**: develop
+- **License**: MIT
+
+## Core Component
+The main component is `ChatWootWidget` which accepts the following props:
+- `baseUrl` (String, required): Chatwoot installation URL
+- `websiteToken` (String, required): Website channel token
+- `colorScheme` (String, default: 'light'): Widget color scheme (light/dark/auto)
+- `locale` (String, default: 'en'): Locale setting
+- `isModalVisible` (Boolean, default: false): Widget visibility state
+- `closeModal` (Function, required): Close event handler
+- `user` (Object, default: {}): User information (email, name, avatar_url, identifier, identifier_hash)
+- `customAttributes` (Object, default: {}): Additional customer information
+
+## Project Structure
+- `/example` folder contains usage examples
+- Main source code in library structure
+- Uses React Native WebView for chat interface
+
+## Installation Requirements
+- React Native 60.0+
+- iOS: requires `cd ios && pod install`
+- Dependencies: react-native-webview, async-storage
+
+## Usage Pattern
+Typically used as a modal overlay that can be toggled on/off, providing a chat interface within React Native applications.
+
+## Local Development & Testing
+The widget source code is located in the `src/` directory. To test changes locally:
+
+1. **Navigate to example project**: `cd Example`
+2. **Install dependencies**: `npm install` or `yarn install`
+3. **For iOS**: `cd ios && pod install && cd ..`
+4. **Run on iOS**: `npx react-native run-ios`
+5. **Run on Android**: `npx react-native run-android`
+
+The example project uses the local source code from `src/` for testing widget changes during development.
\ No newline at end of file
diff --git a/Example/src/WebView.js b/Example/src/WebView.js
index 4a2c6d4..ab0c2de 100644
--- a/Example/src/WebView.js
+++ b/Example/src/WebView.js
@@ -1,5 +1,5 @@
-import React from 'react';
-import { StyleSheet, Linking } from 'react-native';
+import React, { useState, useMemo } from 'react';
+import { StyleSheet, Linking, View, ActivityIndicator } from 'react-native';
import { WebView } from 'react-native-webview';
import PropTypes from 'prop-types';
import { isJsonString, storeHelper, generateScripts, getMessage } from './utils';
@@ -30,6 +30,7 @@ const WebViewComponent = ({
closeModal,
}) => {
const [currentUrl, setCurrentUrl] = React.useState(null);
+ const [loading, setLoading] = useState(true);
let widgetUrl = `${baseUrl}/widget?website_token=${websiteToken}&locale=${locale}`;
if (cwCookie) {
@@ -59,43 +60,71 @@ const WebViewComponent = ({
setCurrentUrl(newNavState.url);
};
+ const opacity = useMemo(() => {
+ if (loading) {
+ return {
+ opacity: 0,
+ };
+ }
+ return {
+ opacity: 1,
+ };
+ }, [loading]);
+
+ const renderLoadingComponent = () => {
+ return (
+
+
+
+ );
+ };
+
return (
- {
- const { data } = event.nativeEvent;
- const message = getMessage(data);
- if (isJsonString(message)) {
- const parsedMessage = JSON.parse(message);
- const { event: eventType, type } = parsedMessage;
- if (eventType === 'loaded') {
- const {
- config: { authToken },
- } = parsedMessage;
- storeHelper.storeCookie(authToken);
- }
- if (type === 'close-widget') {
- closeModal();
+
+ {
+ const { data } = event.nativeEvent;
+ const message = getMessage(data);
+ if (isJsonString(message)) {
+ const parsedMessage = JSON.parse(message);
+ const { event: eventType, type } = parsedMessage;
+ if (eventType === 'loaded') {
+ const {
+ config: { authToken },
+ } = parsedMessage;
+ storeHelper.storeCookie(authToken);
+ }
+ if (type === 'close-widget') {
+ closeModal();
+ }
}
- }
- }}
- scalesPageToFit
- useWebKit
- sharedCookiesEnabled
- javaScriptEnabled={true}
- domStorageEnabled={true}
- style={styles.WebViewStyle}
- injectedJavaScript={injectedJavaScript}
- onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
- onNavigationStateChange={handleWebViewNavigationStateChange}
- scrollEnabled
- />
+ }}
+ scalesPageToFit
+ useWebKit
+ sharedCookiesEnabled
+ javaScriptEnabled={true}
+ domStorageEnabled={true}
+ style={[styles.WebViewStyle, opacity]}
+ injectedJavaScript={injectedJavaScript}
+ onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
+ onNavigationStateChange={handleWebViewNavigationStateChange}
+ onLoadStart={() => setLoading(true)}
+ onLoadProgress={() => setLoading(true)}
+ onLoadEnd={() => setLoading(false)}
+ scrollEnabled
+ />
+ {loading && renderLoadingComponent()}
+
);
};
const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
modal: {
flex: 1,
borderRadius: 4,
@@ -104,6 +133,24 @@ const styles = StyleSheet.create({
webViewContainer: {
flex: 1,
},
+ WebViewStyle: {
+ flex: 1,
+ },
+ loadingContainer: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
+ },
+ loadingText: {
+ marginTop: 10,
+ fontSize: 16,
+ color: '#666',
+ },
});
WebViewComponent.propTypes = propTypes;
diff --git a/LICENSE b/LICENSE
index 7755df4..0458a05 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2017-2023 Chatwoot Inc.
+Copyright (c) 2017-2025 Chatwoot Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+THE SOFTWARE.
\ No newline at end of file
diff --git a/src/WebView.js b/src/WebView.js
index 2f9de2d..6d5a13e 100644
--- a/src/WebView.js
+++ b/src/WebView.js
@@ -1,5 +1,5 @@
-import React from 'react';
-import { StyleSheet, Linking } from 'react-native';
+import React, { useState, useMemo } from 'react';
+import { StyleSheet, Linking, View, ActivityIndicator, Text } from 'react-native';
import { WebView } from 'react-native-webview';
import PropTypes from 'prop-types';
import { isJsonString, storeHelper, generateScripts, getMessage } from './utils';
@@ -30,6 +30,7 @@ const WebViewComponent = ({
closeModal,
}) => {
const [currentUrl, setCurrentUrl] = React.useState(null);
+ const [loading, setLoading] = useState(true);
let widgetUrl = `${baseUrl}/widget?website_token=${websiteToken}&locale=${locale}`;
if (cwCookie) {
@@ -59,43 +60,72 @@ const WebViewComponent = ({
setCurrentUrl(newNavState.url);
};
+ const opacity = useMemo(() => {
+ if (loading) {
+ return {
+ opacity: 0,
+ };
+ }
+ return {
+ opacity: 1,
+ };
+ }, [loading]);
+
+ const renderLoadingComponent = () => {
+ return (
+
+
+ Loading...
+
+ );
+ };
+
return (
- {
- const { data } = event.nativeEvent;
- const message = getMessage(data);
- if (isJsonString(message)) {
- const parsedMessage = JSON.parse(message);
- const { event: eventType, type } = parsedMessage;
- if (eventType === 'loaded') {
- const {
- config: { authToken },
- } = parsedMessage;
- storeHelper.storeCookie(authToken);
- }
- if (type === 'close-widget') {
- closeModal();
+
+ {
+ const { data } = event.nativeEvent;
+ const message = getMessage(data);
+ if (isJsonString(message)) {
+ const parsedMessage = JSON.parse(message);
+ const { event: eventType, type } = parsedMessage;
+ if (eventType === 'loaded') {
+ const {
+ config: { authToken },
+ } = parsedMessage;
+ storeHelper.storeCookie(authToken);
+ }
+ if (type === 'close-widget') {
+ closeModal();
+ }
}
- }
- }}
- scalesPageToFit
- useWebKit
- sharedCookiesEnabled
- javaScriptEnabled={true}
- domStorageEnabled={true}
- style={styles.WebViewStyle}
- injectedJavaScript={injectedJavaScript}
- onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
- onNavigationStateChange={handleWebViewNavigationStateChange}
- scrollEnabled
- />
+ }}
+ scalesPageToFit
+ useWebKit
+ sharedCookiesEnabled
+ javaScriptEnabled={true}
+ domStorageEnabled={true}
+ style={[styles.WebViewStyle, opacity]}
+ injectedJavaScript={injectedJavaScript}
+ onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
+ onNavigationStateChange={handleWebViewNavigationStateChange}
+ onLoadStart={() => setLoading(true)}
+ onLoadProgress={() => setLoading(true)}
+ onLoadEnd={() => setLoading(false)}
+ scrollEnabled
+ />
+ {loading && renderLoadingComponent()}
+
);
};
const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
modal: {
flex: 1,
borderRadius: 4,
@@ -104,6 +134,24 @@ const styles = StyleSheet.create({
webViewContainer: {
flex: 1,
},
+ WebViewStyle: {
+ flex: 1,
+ },
+ loadingContainer: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
+ },
+ loadingText: {
+ marginTop: 10,
+ fontSize: 16,
+ color: '#666',
+ },
});
WebViewComponent.propTypes = propTypes;
export default WebViewComponent;