Skip to content

Commit cd280c2

Browse files
authored
feat: add loader for widget (#54)
1 parent 9c76b07 commit cd280c2

File tree

4 files changed

+209
-68
lines changed

4 files changed

+209
-68
lines changed

CLAUDE.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Chatwoot React Native Widget
2+
3+
## Project Overview
4+
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.
5+
6+
## Key Information
7+
- **Package**: @chatwoot/react-native-widget
8+
- **Supported Chatwoot version**: 2.16.0+
9+
- **Main dependencies**: react-native-webview, async-storage
10+
- **Current branch**: develop
11+
- **License**: MIT
12+
13+
## Core Component
14+
The main component is `ChatWootWidget` which accepts the following props:
15+
- `baseUrl` (String, required): Chatwoot installation URL
16+
- `websiteToken` (String, required): Website channel token
17+
- `colorScheme` (String, default: 'light'): Widget color scheme (light/dark/auto)
18+
- `locale` (String, default: 'en'): Locale setting
19+
- `isModalVisible` (Boolean, default: false): Widget visibility state
20+
- `closeModal` (Function, required): Close event handler
21+
- `user` (Object, default: {}): User information (email, name, avatar_url, identifier, identifier_hash)
22+
- `customAttributes` (Object, default: {}): Additional customer information
23+
24+
## Project Structure
25+
- `/example` folder contains usage examples
26+
- Main source code in library structure
27+
- Uses React Native WebView for chat interface
28+
29+
## Installation Requirements
30+
- React Native 60.0+
31+
- iOS: requires `cd ios && pod install`
32+
- Dependencies: react-native-webview, async-storage
33+
34+
## Usage Pattern
35+
Typically used as a modal overlay that can be toggled on/off, providing a chat interface within React Native applications.
36+
37+
## Local Development & Testing
38+
The widget source code is located in the `src/` directory. To test changes locally:
39+
40+
1. **Navigate to example project**: `cd Example`
41+
2. **Install dependencies**: `npm install` or `yarn install`
42+
3. **For iOS**: `cd ios && pod install && cd ..`
43+
4. **Run on iOS**: `npx react-native run-ios`
44+
5. **Run on Android**: `npx react-native run-android`
45+
46+
The example project uses the local source code from `src/` for testing widget changes during development.

Example/src/WebView.js

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from 'react';
2-
import { StyleSheet, Linking } from 'react-native';
1+
import React, { useState, useMemo } from 'react';
2+
import { StyleSheet, Linking, View, ActivityIndicator } from 'react-native';
33
import { WebView } from 'react-native-webview';
44
import PropTypes from 'prop-types';
55
import { isJsonString, storeHelper, generateScripts, getMessage } from './utils';
@@ -30,6 +30,7 @@ const WebViewComponent = ({
3030
closeModal,
3131
}) => {
3232
const [currentUrl, setCurrentUrl] = React.useState(null);
33+
const [loading, setLoading] = useState(true);
3334
let widgetUrl = `${baseUrl}/widget?website_token=${websiteToken}&locale=${locale}`;
3435

3536
if (cwCookie) {
@@ -59,43 +60,71 @@ const WebViewComponent = ({
5960
setCurrentUrl(newNavState.url);
6061
};
6162

63+
const opacity = useMemo(() => {
64+
if (loading) {
65+
return {
66+
opacity: 0,
67+
};
68+
}
69+
return {
70+
opacity: 1,
71+
};
72+
}, [loading]);
73+
74+
const renderLoadingComponent = () => {
75+
return (
76+
<View style={styles.loadingContainer}>
77+
<ActivityIndicator size="small" />
78+
</View>
79+
);
80+
};
81+
6282
return (
63-
<WebView
64-
source={{
65-
uri: widgetUrl,
66-
}}
67-
onMessage={(event) => {
68-
const { data } = event.nativeEvent;
69-
const message = getMessage(data);
70-
if (isJsonString(message)) {
71-
const parsedMessage = JSON.parse(message);
72-
const { event: eventType, type } = parsedMessage;
73-
if (eventType === 'loaded') {
74-
const {
75-
config: { authToken },
76-
} = parsedMessage;
77-
storeHelper.storeCookie(authToken);
78-
}
79-
if (type === 'close-widget') {
80-
closeModal();
83+
<View style={styles.container}>
84+
<WebView
85+
source={{
86+
uri: widgetUrl,
87+
}}
88+
onMessage={(event) => {
89+
const { data } = event.nativeEvent;
90+
const message = getMessage(data);
91+
if (isJsonString(message)) {
92+
const parsedMessage = JSON.parse(message);
93+
const { event: eventType, type } = parsedMessage;
94+
if (eventType === 'loaded') {
95+
const {
96+
config: { authToken },
97+
} = parsedMessage;
98+
storeHelper.storeCookie(authToken);
99+
}
100+
if (type === 'close-widget') {
101+
closeModal();
102+
}
81103
}
82-
}
83-
}}
84-
scalesPageToFit
85-
useWebKit
86-
sharedCookiesEnabled
87-
javaScriptEnabled={true}
88-
domStorageEnabled={true}
89-
style={styles.WebViewStyle}
90-
injectedJavaScript={injectedJavaScript}
91-
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
92-
onNavigationStateChange={handleWebViewNavigationStateChange}
93-
scrollEnabled
94-
/>
104+
}}
105+
scalesPageToFit
106+
useWebKit
107+
sharedCookiesEnabled
108+
javaScriptEnabled={true}
109+
domStorageEnabled={true}
110+
style={[styles.WebViewStyle, opacity]}
111+
injectedJavaScript={injectedJavaScript}
112+
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
113+
onNavigationStateChange={handleWebViewNavigationStateChange}
114+
onLoadStart={() => setLoading(true)}
115+
onLoadProgress={() => setLoading(true)}
116+
onLoadEnd={() => setLoading(false)}
117+
scrollEnabled
118+
/>
119+
{loading && renderLoadingComponent()}
120+
</View>
95121
);
96122
};
97123

98124
const styles = StyleSheet.create({
125+
container: {
126+
flex: 1,
127+
},
99128
modal: {
100129
flex: 1,
101130
borderRadius: 4,
@@ -104,6 +133,24 @@ const styles = StyleSheet.create({
104133
webViewContainer: {
105134
flex: 1,
106135
},
136+
WebViewStyle: {
137+
flex: 1,
138+
},
139+
loadingContainer: {
140+
position: 'absolute',
141+
top: 0,
142+
left: 0,
143+
right: 0,
144+
bottom: 0,
145+
justifyContent: 'center',
146+
alignItems: 'center',
147+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
148+
},
149+
loadingText: {
150+
marginTop: 10,
151+
fontSize: 16,
152+
color: '#666',
153+
},
107154
});
108155

109156
WebViewComponent.propTypes = propTypes;

LICENSE

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2017-2023 Chatwoot Inc.
3+
Copyright (c) 2017-2025 Chatwoot Inc.
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
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
1818
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21-
THE SOFTWARE.
21+
THE SOFTWARE.

src/WebView.js

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from 'react';
2-
import { StyleSheet, Linking } from 'react-native';
1+
import React, { useState, useMemo } from 'react';
2+
import { StyleSheet, Linking, View, ActivityIndicator, Text } from 'react-native';
33
import { WebView } from 'react-native-webview';
44
import PropTypes from 'prop-types';
55
import { isJsonString, storeHelper, generateScripts, getMessage } from './utils';
@@ -30,6 +30,7 @@ const WebViewComponent = ({
3030
closeModal,
3131
}) => {
3232
const [currentUrl, setCurrentUrl] = React.useState(null);
33+
const [loading, setLoading] = useState(true);
3334
let widgetUrl = `${baseUrl}/widget?website_token=${websiteToken}&locale=${locale}`;
3435

3536
if (cwCookie) {
@@ -59,43 +60,72 @@ const WebViewComponent = ({
5960
setCurrentUrl(newNavState.url);
6061
};
6162

63+
const opacity = useMemo(() => {
64+
if (loading) {
65+
return {
66+
opacity: 0,
67+
};
68+
}
69+
return {
70+
opacity: 1,
71+
};
72+
}, [loading]);
73+
74+
const renderLoadingComponent = () => {
75+
return (
76+
<View style={styles.loadingContainer}>
77+
<ActivityIndicator size="large" color="#1f93ff" />
78+
<Text style={styles.loadingText}>Loading...</Text>
79+
</View>
80+
);
81+
};
82+
6283
return (
63-
<WebView
64-
source={{
65-
uri: widgetUrl,
66-
}}
67-
onMessage={(event) => {
68-
const { data } = event.nativeEvent;
69-
const message = getMessage(data);
70-
if (isJsonString(message)) {
71-
const parsedMessage = JSON.parse(message);
72-
const { event: eventType, type } = parsedMessage;
73-
if (eventType === 'loaded') {
74-
const {
75-
config: { authToken },
76-
} = parsedMessage;
77-
storeHelper.storeCookie(authToken);
78-
}
79-
if (type === 'close-widget') {
80-
closeModal();
84+
<View style={styles.container}>
85+
<WebView
86+
source={{
87+
uri: widgetUrl,
88+
}}
89+
onMessage={(event) => {
90+
const { data } = event.nativeEvent;
91+
const message = getMessage(data);
92+
if (isJsonString(message)) {
93+
const parsedMessage = JSON.parse(message);
94+
const { event: eventType, type } = parsedMessage;
95+
if (eventType === 'loaded') {
96+
const {
97+
config: { authToken },
98+
} = parsedMessage;
99+
storeHelper.storeCookie(authToken);
100+
}
101+
if (type === 'close-widget') {
102+
closeModal();
103+
}
81104
}
82-
}
83-
}}
84-
scalesPageToFit
85-
useWebKit
86-
sharedCookiesEnabled
87-
javaScriptEnabled={true}
88-
domStorageEnabled={true}
89-
style={styles.WebViewStyle}
90-
injectedJavaScript={injectedJavaScript}
91-
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
92-
onNavigationStateChange={handleWebViewNavigationStateChange}
93-
scrollEnabled
94-
/>
105+
}}
106+
scalesPageToFit
107+
useWebKit
108+
sharedCookiesEnabled
109+
javaScriptEnabled={true}
110+
domStorageEnabled={true}
111+
style={[styles.WebViewStyle, opacity]}
112+
injectedJavaScript={injectedJavaScript}
113+
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
114+
onNavigationStateChange={handleWebViewNavigationStateChange}
115+
onLoadStart={() => setLoading(true)}
116+
onLoadProgress={() => setLoading(true)}
117+
onLoadEnd={() => setLoading(false)}
118+
scrollEnabled
119+
/>
120+
{loading && renderLoadingComponent()}
121+
</View>
95122
);
96123
};
97124

98125
const styles = StyleSheet.create({
126+
container: {
127+
flex: 1,
128+
},
99129
modal: {
100130
flex: 1,
101131
borderRadius: 4,
@@ -104,6 +134,24 @@ const styles = StyleSheet.create({
104134
webViewContainer: {
105135
flex: 1,
106136
},
137+
WebViewStyle: {
138+
flex: 1,
139+
},
140+
loadingContainer: {
141+
position: 'absolute',
142+
top: 0,
143+
left: 0,
144+
right: 0,
145+
bottom: 0,
146+
justifyContent: 'center',
147+
alignItems: 'center',
148+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
149+
},
150+
loadingText: {
151+
marginTop: 10,
152+
fontSize: 16,
153+
color: '#666',
154+
},
107155
});
108156
WebViewComponent.propTypes = propTypes;
109157
export default WebViewComponent;

0 commit comments

Comments
 (0)