@regantis-sdk/react-native-chat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # @regantis/react-native-chat
2
+
3
+ Native Regantis customer support chat for React Native Android and iOS.
4
+
5
+ ## Compatibility
6
+
7
+ - React Native 0.77+
8
+ - Android API 24+
9
+ - iOS 15.1+
10
+ - React Native New Architecture and interop-compatible apps
11
+ - No runtime npm dependencies besides `react` and `react-native`
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @regantis/react-native-chat
17
+ ```
18
+
19
+ For iOS, install pods after adding the package:
20
+
21
+ ```bash
22
+ cd ios
23
+ pod install
24
+ ```
25
+
26
+ Android is autolinked by React Native.
27
+
28
+ ## Basic usage
29
+
30
+ ```jsx
31
+ import { RegantisChat } from '@regantis/react-native-chat';
32
+
33
+ export default function SupportScreen() {
34
+ return <RegantisChat apiKey="YOUR_REACT_NATIVE_CHAT_API_KEY" />;
35
+ }
36
+ ```
37
+
38
+ The SDK downloads the Chat settings and UI texts from the CRM. Theme, colors, logo, agent display, sounds, ratings, transcript, translations and white-label settings do not need to be duplicated in the app.
39
+
40
+ ## Logged-in customer
41
+
42
+ Do not put the customer identity signing secret in the mobile app. Generate the identity timestamp and HMAC signature on your server and return them through your authenticated app API.
43
+
44
+ ```jsx
45
+ import { RegantisChat } from '@regantis/react-native-chat';
46
+
47
+ export default function SupportScreen({ user, chatIdentity }) {
48
+ return (
49
+ <RegantisChat
50
+ apiKey="YOUR_REACT_NATIVE_CHAT_API_KEY"
51
+ language="ro"
52
+ screen="support"
53
+ customer={{
54
+ id: user.id,
55
+ name: user.name,
56
+ email: user.email,
57
+ telephone: user.telephone,
58
+ identityTimestamp: chatIdentity.timestamp,
59
+ identitySignature: chatIdentity.signature,
60
+ }}
61
+ />
62
+ );
63
+ }
64
+ ```
65
+
66
+ The signing payload is identical to the web widget:
67
+
68
+ ```text
69
+ timestamp
70
+ customer_id
71
+ name
72
+ lowercase_email
73
+ telephone
74
+ ```
75
+
76
+ Use HMAC-SHA256 and a Unix timestamp in seconds. The CRM currently accepts signatures for 10 minutes.
77
+
78
+ ## Optional overrides
79
+
80
+ Server configuration remains the default. Per-app overrides are optional:
81
+
82
+ ```jsx
83
+ <RegantisChat
84
+ apiKey="YOUR_REACT_NATIVE_CHAT_API_KEY"
85
+ language="en"
86
+ translationLanguage="de"
87
+ screen="settings/support"
88
+ theme={{
89
+ theme_mode: 'dark',
90
+ primary_color: '#0B63E5',
91
+ }}
92
+ texts={{
93
+ placeholder: 'Write a message…',
94
+ }}
95
+ onMessage={message => {}}
96
+ onUnreadChange={count => {}}
97
+ onError={error => {}}
98
+ />
99
+ ```
100
+
101
+ `language` controls the SDK UI language. `translationLanguage` controls conversation translation when customer translations are enabled in Chat.
102
+
103
+ ## Included features
104
+
105
+ - persistent anonymous visitor/session identity
106
+ - optional signed logged-in customer identity
107
+ - contact form and prefilled verified customer flow
108
+ - conversation restore
109
+ - message history and older-history pagination
110
+ - send/receive messages
111
+ - chatbot replies and agent handoff
112
+ - WebSocket realtime updates with HTTP polling fallback
113
+ - typing state
114
+ - customer-side translation selector
115
+ - images and files
116
+ - current-app screenshot attachment
117
+ - native incoming-message sound
118
+ - close/reopen conversation
119
+ - rating and rating comment
120
+ - transcript email flow
121
+ - read state and unread callback
122
+ - light/dark mode
123
+ - basic/advanced color configuration
124
+ - logo, agent photo toggle and white-label behavior
125
+ - CRM-managed localized UI texts
126
+
127
+ The web-only launcher settings such as desktop/mobile corner placement and minimized bubble/bar mode are still returned in the settings object for configuration parity, but they do not change a full-page React Native component because there is no browser launcher to position.
128
+
129
+ ## Headless client
130
+
131
+ The UI is optional. The same package exports the client if an app needs its own presentation layer:
132
+
133
+ ```js
134
+ import { createRegantisChatClient } from '@regantis/react-native-chat';
135
+
136
+ const chat = createRegantisChatClient({
137
+ apiKey: 'YOUR_REACT_NATIVE_CHAT_API_KEY',
138
+ language: 'ro',
139
+ });
140
+
141
+ const unsubscribe = chat.subscribe(state => {
142
+ console.log(state.messages);
143
+ });
144
+
145
+ await chat.start();
146
+ await chat.sendMessage('Salut');
147
+
148
+ unsubscribe();
149
+ chat.stop();
150
+ ```
151
+
152
+ ## Publishing
153
+
154
+ ```bash
155
+ npm login
156
+ npm publish --access public
157
+ ```
@@ -0,0 +1,17 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "RegantisReactNativeChat"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = "https://regantis.technology"
10
+ s.license = { :type => "UNLICENSED" }
11
+ s.author = { "Regantis" => "info@regantis.technology" }
12
+ s.platforms = { :ios => "15.1" }
13
+ s.source = { :git => "https://github.com/regantis/react-native-chat.git", :tag => "#{s.version}" }
14
+ s.source_files = "ios/**/*.{h,m,mm}"
15
+ s.frameworks = "AudioToolbox", "UniformTypeIdentifiers"
16
+ s.dependency "React-Core"
17
+ end
@@ -0,0 +1,24 @@
1
+ apply plugin: "com.android.library"
2
+
3
+ def safeExtGet(prop, fallback) {
4
+ return rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
5
+ }
6
+
7
+ android {
8
+ namespace "technology.regantis.chat"
9
+ compileSdkVersion safeExtGet("compileSdkVersion", 35)
10
+
11
+ defaultConfig {
12
+ minSdkVersion safeExtGet("minSdkVersion", 24)
13
+ targetSdkVersion safeExtGet("targetSdkVersion", 35)
14
+ }
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ implementation "com.facebook.react:react-android"
24
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,244 @@
1
+ package technology.regantis.chat;
2
+
3
+ import android.app.Activity;
4
+ import android.content.Context;
5
+ import android.content.Intent;
6
+ import android.content.SharedPreferences;
7
+ import android.graphics.Bitmap;
8
+ import android.graphics.Canvas;
9
+ import android.database.Cursor;
10
+ import android.media.Ringtone;
11
+ import android.media.RingtoneManager;
12
+ import android.net.Uri;
13
+ import android.provider.OpenableColumns;
14
+ import android.view.View;
15
+ import android.webkit.MimeTypeMap;
16
+
17
+ import androidx.annotation.NonNull;
18
+ import androidx.annotation.Nullable;
19
+
20
+ import com.facebook.react.bridge.ActivityEventListener;
21
+ import com.facebook.react.bridge.Arguments;
22
+ import com.facebook.react.bridge.BaseActivityEventListener;
23
+ import com.facebook.react.bridge.Promise;
24
+ import com.facebook.react.bridge.ReactApplicationContext;
25
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
26
+ import com.facebook.react.bridge.ReactMethod;
27
+ import com.facebook.react.bridge.WritableMap;
28
+
29
+ import java.io.File;
30
+ import java.io.FileOutputStream;
31
+
32
+ public class RegantisChatNativeModule extends ReactContextBaseJavaModule {
33
+ private static final String MODULE_NAME = "RegantisChatNative";
34
+ private static final String STORAGE_NAME = "regantis_react_native_chat";
35
+ private static final int PICK_ATTACHMENT_REQUEST = 17841;
36
+
37
+ private final ReactApplicationContext reactContext;
38
+ private Promise pickerPromise;
39
+
40
+ private final ActivityEventListener activityEventListener = new BaseActivityEventListener() {
41
+ @Override
42
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, @Nullable Intent data) {
43
+ if (requestCode != PICK_ATTACHMENT_REQUEST || pickerPromise == null) return;
44
+
45
+ Promise promise = pickerPromise;
46
+ pickerPromise = null;
47
+
48
+ if (resultCode != Activity.RESULT_OK || data == null || data.getData() == null) {
49
+ promise.resolve(null);
50
+ return;
51
+ }
52
+
53
+ Uri uri = data.getData();
54
+ try {
55
+ int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
56
+ if ((flags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
57
+ reactContext.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
58
+ }
59
+ } catch (Exception ignored) {
60
+ }
61
+
62
+ WritableMap result = Arguments.createMap();
63
+ result.putString("uri", uri.toString());
64
+ result.putString("name", getDisplayName(uri));
65
+ result.putString("type", getMimeType(uri));
66
+ result.putDouble("size", getSize(uri));
67
+ promise.resolve(result);
68
+ }
69
+ };
70
+
71
+ public RegantisChatNativeModule(ReactApplicationContext reactContext) {
72
+ super(reactContext);
73
+ this.reactContext = reactContext;
74
+ reactContext.addActivityEventListener(activityEventListener);
75
+ }
76
+
77
+ @NonNull
78
+ @Override
79
+ public String getName() {
80
+ return MODULE_NAME;
81
+ }
82
+
83
+ private SharedPreferences storage() {
84
+ return reactContext.getSharedPreferences(STORAGE_NAME, Context.MODE_PRIVATE);
85
+ }
86
+
87
+ @ReactMethod
88
+ public void getItem(String key, Promise promise) {
89
+ promise.resolve(storage().getString(key, null));
90
+ }
91
+
92
+ @ReactMethod
93
+ public void setItem(String key, String value, Promise promise) {
94
+ storage().edit().putString(key, value).apply();
95
+ promise.resolve(null);
96
+ }
97
+
98
+ @ReactMethod
99
+ public void removeItem(String key, Promise promise) {
100
+ storage().edit().remove(key).apply();
101
+ promise.resolve(null);
102
+ }
103
+
104
+ @ReactMethod
105
+ public void getAppInfo(Promise promise) {
106
+ WritableMap result = Arguments.createMap();
107
+ result.putString("appId", reactContext.getPackageName());
108
+ result.putString("platform", "android");
109
+ try {
110
+ String versionName = reactContext.getPackageManager()
111
+ .getPackageInfo(reactContext.getPackageName(), 0)
112
+ .versionName;
113
+ result.putString("appVersion", versionName == null ? "" : versionName);
114
+ } catch (Exception ignored) {
115
+ result.putString("appVersion", "");
116
+ }
117
+ promise.resolve(result);
118
+ }
119
+
120
+ @ReactMethod
121
+ public void playIncomingSound() {
122
+ try {
123
+ Uri soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
124
+ Ringtone ringtone = RingtoneManager.getRingtone(reactContext, soundUri);
125
+ if (ringtone != null) ringtone.play();
126
+ } catch (Exception ignored) {
127
+ }
128
+ }
129
+
130
+ @ReactMethod
131
+ public void captureScreenshot(Promise promise) {
132
+ Activity activity = getCurrentActivity();
133
+ if (activity == null) {
134
+ promise.reject("NO_ACTIVITY", "No active Android Activity");
135
+ return;
136
+ }
137
+
138
+ activity.runOnUiThread(() -> {
139
+ try {
140
+ View root = activity.getWindow().getDecorView().getRootView();
141
+ int width = root.getWidth();
142
+ int height = root.getHeight();
143
+ if (width < 1 || height < 1) {
144
+ promise.reject("SCREENSHOT_FAILED", "Invalid app window size");
145
+ return;
146
+ }
147
+
148
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
149
+ Canvas canvas = new Canvas(bitmap);
150
+ root.draw(canvas);
151
+
152
+ File directory = new File(reactContext.getCacheDir(), "regantis-chat");
153
+ if (!directory.exists() && !directory.mkdirs()) {
154
+ promise.reject("SCREENSHOT_FAILED", "Unable to create screenshot directory");
155
+ return;
156
+ }
157
+ File file = new File(directory, "screenshot-" + System.currentTimeMillis() + ".png");
158
+ try (FileOutputStream stream = new FileOutputStream(file)) {
159
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
160
+ } finally {
161
+ bitmap.recycle();
162
+ }
163
+
164
+ WritableMap result = Arguments.createMap();
165
+ result.putString("uri", Uri.fromFile(file).toString());
166
+ result.putString("name", file.getName());
167
+ result.putString("type", "image/png");
168
+ result.putDouble("size", file.length());
169
+ promise.resolve(result);
170
+ } catch (Exception error) {
171
+ promise.reject("SCREENSHOT_FAILED", error);
172
+ }
173
+ });
174
+ }
175
+
176
+ @ReactMethod
177
+ public void pickAttachment(String kind, Promise promise) {
178
+ Activity activity = getCurrentActivity();
179
+ if (activity == null) {
180
+ promise.reject("NO_ACTIVITY", "No active Android Activity");
181
+ return;
182
+ }
183
+ if (pickerPromise != null) {
184
+ promise.reject("PICKER_BUSY", "An attachment picker is already open");
185
+ return;
186
+ }
187
+
188
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
189
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
190
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
191
+ if ("image".equals(kind)) {
192
+ intent.setType("image/*");
193
+ } else {
194
+ intent.setType("*/*");
195
+ }
196
+
197
+ pickerPromise = promise;
198
+ try {
199
+ activity.startActivityForResult(intent, PICK_ATTACHMENT_REQUEST);
200
+ } catch (Exception error) {
201
+ pickerPromise = null;
202
+ promise.reject("PICKER_FAILED", error);
203
+ }
204
+ }
205
+
206
+ private String getDisplayName(Uri uri) {
207
+ String name = null;
208
+ try (Cursor cursor = reactContext.getContentResolver().query(uri, null, null, null, null)) {
209
+ if (cursor != null && cursor.moveToFirst()) {
210
+ int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
211
+ if (index >= 0) name = cursor.getString(index);
212
+ }
213
+ } catch (Exception ignored) {
214
+ }
215
+ if (name == null || name.trim().isEmpty()) {
216
+ String segment = uri.getLastPathSegment();
217
+ name = segment == null || segment.trim().isEmpty() ? "attachment" : segment;
218
+ }
219
+ return name;
220
+ }
221
+
222
+ private double getSize(Uri uri) {
223
+ try (Cursor cursor = reactContext.getContentResolver().query(uri, null, null, null, null)) {
224
+ if (cursor != null && cursor.moveToFirst()) {
225
+ int index = cursor.getColumnIndex(OpenableColumns.SIZE);
226
+ if (index >= 0 && !cursor.isNull(index)) return cursor.getLong(index);
227
+ }
228
+ } catch (Exception ignored) {
229
+ }
230
+ return 0;
231
+ }
232
+
233
+ private String getMimeType(Uri uri) {
234
+ String type = reactContext.getContentResolver().getType(uri);
235
+ if (type != null && !type.trim().isEmpty()) return type;
236
+
237
+ String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
238
+ if (extension != null && !extension.isEmpty()) {
239
+ String guessed = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
240
+ if (guessed != null) return guessed;
241
+ }
242
+ return "application/octet-stream";
243
+ }
244
+ }
@@ -0,0 +1,28 @@
1
+ package technology.regantis.chat;
2
+
3
+ import androidx.annotation.NonNull;
4
+
5
+ import com.facebook.react.ReactPackage;
6
+ import com.facebook.react.bridge.NativeModule;
7
+ import com.facebook.react.bridge.ReactApplicationContext;
8
+ import com.facebook.react.uimanager.ViewManager;
9
+
10
+ import java.util.ArrayList;
11
+ import java.util.Collections;
12
+ import java.util.List;
13
+
14
+ public class RegantisChatPackage implements ReactPackage {
15
+ @NonNull
16
+ @Override
17
+ public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
18
+ List<NativeModule> modules = new ArrayList<>();
19
+ modules.add(new RegantisChatNativeModule(reactContext));
20
+ return modules;
21
+ }
22
+
23
+ @NonNull
24
+ @Override
25
+ public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
26
+ return Collections.emptyList();
27
+ }
28
+ }
package/index.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ import * as React from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export type RegantisChatCustomer = {
5
+ id?: string | number;
6
+ customerId?: string | number;
7
+ name?: string;
8
+ email?: string;
9
+ telephone?: string;
10
+ phone?: string;
11
+ identityTimestamp?: string | number;
12
+ identitySignature?: string;
13
+ };
14
+
15
+ export type RegantisChatTheme = Partial<{
16
+ operator_name: string;
17
+ use_profile_names: 0 | 1;
18
+ minimized_type: 'bubble' | 'bar';
19
+ theme_mode: 'light' | 'dark';
20
+ color_mode: 'theme' | 'advanced';
21
+ theme_color: string;
22
+ primary_color: string;
23
+ chat_background: string;
24
+ chat_background_opacity: number;
25
+ customer_bubble: string;
26
+ customer_text: string;
27
+ agent_bubble: string;
28
+ agent_text: string;
29
+ system_text: string;
30
+ launcher_background: string;
31
+ launcher_icon_color: string;
32
+ desktop_align: 'left' | 'right';
33
+ desktop_side_spacing: number;
34
+ desktop_bottom_spacing: number;
35
+ desktop_visibility: string;
36
+ mobile_enabled: 0 | 1;
37
+ mobile_same_as_desktop: 0 | 1;
38
+ mobile_minimized_type: 'bubble' | 'bar';
39
+ mobile_align: 'left' | 'right';
40
+ mobile_side_spacing: number;
41
+ mobile_bottom_spacing: number;
42
+ mobile_visibility: string;
43
+ show_logo: 0 | 1;
44
+ logo_url: string;
45
+ show_agent_photo: 0 | 1;
46
+ sound_enabled: 0 | 1;
47
+ rating_enabled: 0 | 1;
48
+ transcript_enabled: 0 | 1;
49
+ before_you_go_enabled: 0 | 1;
50
+ white_label: 0 | 1;
51
+ }>;
52
+
53
+ export type RegantisChatProps = {
54
+ apiKey: string;
55
+ serverUrl?: string;
56
+ language?: string;
57
+ translationLanguage?: string;
58
+ customer?: RegantisChatCustomer;
59
+ theme?: RegantisChatTheme;
60
+ texts?: Record<string, string>;
61
+ screen?: string;
62
+ style?: StyleProp<ViewStyle>;
63
+ keyboardVerticalOffset?: number;
64
+ onMessage?: (message: any) => void;
65
+ onUnreadChange?: (count: number) => void;
66
+ onError?: (error: Error) => void;
67
+ };
68
+
69
+ export class RegantisChatClient {
70
+ constructor(config: RegantisChatProps);
71
+ start(): Promise<any>;
72
+ stop(): void;
73
+ subscribe(listener: (state: any) => void): () => void;
74
+ getState(): any;
75
+ sendMessage(message: string): Promise<any>;
76
+ saveContact(name: string, email: string): Promise<any>;
77
+ uploadAttachment(file: { uri: string; name?: string; type?: string }, kind: 'image' | 'file'): Promise<any>;
78
+ pickAndUpload(kind: 'image' | 'file' | 'screenshot'): Promise<any>;
79
+ setTranslationLanguage(languageCode: string): Promise<void>;
80
+ closeChat(): Promise<any>;
81
+ reopenChat(): Promise<any>;
82
+ rate(rating: 0 | 1): Promise<any>;
83
+ rateComment(comment: string): Promise<any>;
84
+ sendTranscript(email: string): Promise<any>;
85
+ markRead(): Promise<void>;
86
+ setVisible(visible: boolean): void;
87
+ setSoundEnabled(enabled: boolean): void;
88
+ }
89
+
90
+ export const RegantisChat: React.ComponentType<RegantisChatProps>;
91
+ export function createRegantisChatClient(config: RegantisChatProps): RegantisChatClient;
package/index.js ADDED
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ const RegantisChat = require('./src/RegantisChat');
4
+ const { RegantisChatClient } = require('./src/client');
5
+
6
+ module.exports = {
7
+ RegantisChat,
8
+ RegantisChatClient,
9
+ createRegantisChatClient: config => new RegantisChatClient(config),
10
+ };
@@ -0,0 +1,5 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ @interface RegantisChatNative : NSObject <RCTBridgeModule, UIDocumentPickerDelegate>
5
+ @end