@getecho-ai/react-native-sdk 1.0.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.
@@ -0,0 +1,473 @@
1
+ "use strict";
2
+ /**
3
+ * EchoChatModal - Chat interface using WebView
4
+ *
5
+ * Embeds Echo web chat via WebView with postMessage bridge
6
+ * Handles all communication between WebView and React Native
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.EchoChatModal = void 0;
13
+ const react_1 = require("react");
14
+ const react_native_1 = require("react-native");
15
+ const react_native_webview_1 = require("react-native-webview");
16
+ const CallbackManager_1 = __importDefault(require("../bridge/CallbackManager"));
17
+ const WebViewBridge_1 = __importDefault(require("../bridge/WebViewBridge"));
18
+ const resolveApiUrl_1 = require("../utils/resolveApiUrl");
19
+ const EchoProvider_1 = require("./EchoProvider");
20
+ const ECHO_CHAT_URL = "https://get-echo.ai/embed";
21
+ const TRAILING_SLASHES_REGEX = /\/+$/;
22
+ function ensureHttpScheme(url) {
23
+ // WebView needs an absolute URL with a scheme.
24
+ if (url.startsWith("http://") || url.startsWith("https://")) {
25
+ return url;
26
+ }
27
+ return `http://${url}`;
28
+ }
29
+ function resolveEmbedUrl(url) {
30
+ const withScheme = ensureHttpScheme(url).trim();
31
+ // If caller already passes /embed, don't second-guess it.
32
+ if (withScheme.includes("/embed")) {
33
+ return withScheme;
34
+ }
35
+ // Treat it as a base (e.g. http://localhost:3000) and append /embed.
36
+ return `${withScheme.replace(TRAILING_SLASHES_REGEX, "")}/embed`;
37
+ }
38
+ const EchoChatModal = ({ visible, onClose, }) => {
39
+ const { config, userId, chatId, isReady, updateChatId, updateUserId, handleAddToCart, handleGetCart, handleNavigateToProduct, } = (0, EchoProvider_1.useEcho)();
40
+ const webViewRef = (0, react_1.useRef)(null);
41
+ const wasVisibleRef = (0, react_1.useRef)(false);
42
+ const [isLoading, setIsLoading] = (0, react_1.useState)(true);
43
+ const [loadTimeout, setLoadTimeout] = (0, react_1.useState)(false);
44
+ const [activeResumeChatId, setActiveResumeChatId] = (0, react_1.useState)(null);
45
+ const [isResumeReady, setIsResumeReady] = (0, react_1.useState)(false);
46
+ // Set WebView ref for bridge - update whenever ref or isResumeReady changes
47
+ (0, react_1.useEffect)(() => {
48
+ if (webViewRef.current && isResumeReady) {
49
+ WebViewBridge_1.default.setWebViewRef(webViewRef.current);
50
+ console.log("[EchoChatModal] WebView ref set, ready for messaging");
51
+ }
52
+ }, [isResumeReady, webViewRef.current]);
53
+ // Handle messages from WebView
54
+ const handleMessage = (0, react_1.useCallback)(async (message) => {
55
+ console.log("[EchoChatModal] handleMessage", message);
56
+ if (message.type === "echo:request_actions") {
57
+ const actions = ["addToCart", "getCart"];
58
+ if (config.callbacks.onNavigateToCheckout) {
59
+ actions.push("redirectToCheckout");
60
+ }
61
+ WebViewBridge_1.default.sendRawToWebView({
62
+ source: "echo-widget",
63
+ type: "echo:available_actions",
64
+ actions,
65
+ schemas: {},
66
+ customerId: null,
67
+ });
68
+ return;
69
+ }
70
+ if (message.type === "requestWidgetState") {
71
+ WebViewBridge_1.default.sendRawToWebView({
72
+ source: "echo-parent",
73
+ type: "widgetState",
74
+ isExpanded: false, // Always false in RN - sidebar controlled via showSidebar URL param
75
+ isOpen: visible,
76
+ });
77
+ return;
78
+ }
79
+ if (message.type === "close") {
80
+ onClose();
81
+ return;
82
+ }
83
+ if (message.type === "chatId") {
84
+ const payload = message.payload;
85
+ if (payload?.chatId) {
86
+ await updateChatId(payload.chatId);
87
+ }
88
+ return;
89
+ }
90
+ if (message.type === "userId") {
91
+ const payload = message.payload;
92
+ if (payload?.userId) {
93
+ await updateUserId(payload.userId);
94
+ }
95
+ return;
96
+ }
97
+ if (message.type === "navigate") {
98
+ const payload = message.payload;
99
+ const navigateUrl = payload?.url;
100
+ const navigateProductId = payload?.productId;
101
+ if (navigateProductId !== undefined && navigateProductId !== null) {
102
+ handleNavigateToProduct(String(navigateProductId));
103
+ return;
104
+ }
105
+ if (navigateUrl) {
106
+ if (config.callbacks.onNavigateToUrl) {
107
+ config.callbacks.onNavigateToUrl(navigateUrl);
108
+ }
109
+ else {
110
+ try {
111
+ await react_native_1.Linking.openURL(navigateUrl);
112
+ }
113
+ catch (error) {
114
+ console.error("[EchoChatModal] Failed to open URL:", error);
115
+ }
116
+ }
117
+ }
118
+ return;
119
+ }
120
+ if (message.type === "addToCart") {
121
+ const payload = message.payload ?? {};
122
+ const productPayload = payload.product ||
123
+ payload;
124
+ const callbackId = message.callbackId ||
125
+ payload.callbackId ||
126
+ "";
127
+ const productId = productPayload.productId ??
128
+ productPayload.id ??
129
+ "";
130
+ const quantity = productPayload.quantity ?? undefined;
131
+ const product = {
132
+ id: String(productId),
133
+ title: productPayload
134
+ .productName ||
135
+ productPayload.title ||
136
+ String(productId),
137
+ priceAmount: productPayload
138
+ .productPrice ?? productPayload.price,
139
+ primaryImage: productPayload
140
+ .productImage ?? productPayload.image,
141
+ url: productPayload
142
+ .productUrl ?? productPayload.url,
143
+ };
144
+ const result = await handleAddToCart(product, quantity);
145
+ WebViewBridge_1.default.sendRawToWebView({
146
+ source: "echo-parent",
147
+ type: "addToCartResponse",
148
+ callbackId,
149
+ success: result.success,
150
+ error: result.error,
151
+ cartItemCount: result.cartItemCount,
152
+ });
153
+ return;
154
+ }
155
+ if (message.type === "getCart") {
156
+ const payload = message.payload ?? {};
157
+ const callbackId = message.callbackId ||
158
+ payload.callbackId ||
159
+ "";
160
+ const result = await handleGetCart();
161
+ WebViewBridge_1.default.sendRawToWebView({
162
+ source: "echo-parent",
163
+ type: "getCartResponse",
164
+ callbackId,
165
+ success: result.success,
166
+ cart: result.cart,
167
+ error: result.error,
168
+ });
169
+ return;
170
+ }
171
+ if (message.type === "redirectToCheckout") {
172
+ const payload = message.payload ?? {};
173
+ const callbackId = message.callbackId ||
174
+ payload.callbackId ||
175
+ "";
176
+ if (!config.callbacks.onNavigateToCheckout) {
177
+ WebViewBridge_1.default.sendRawToWebView({
178
+ source: "echo-parent",
179
+ type: "redirectToCheckoutResponse",
180
+ callbackId,
181
+ success: false,
182
+ error: "CALLBACK_NOT_REGISTERED",
183
+ });
184
+ return;
185
+ }
186
+ try {
187
+ config.callbacks.onNavigateToCheckout();
188
+ WebViewBridge_1.default.sendRawToWebView({
189
+ source: "echo-parent",
190
+ type: "redirectToCheckoutResponse",
191
+ callbackId,
192
+ success: true,
193
+ });
194
+ }
195
+ catch (error) {
196
+ WebViewBridge_1.default.sendRawToWebView({
197
+ source: "echo-parent",
198
+ type: "redirectToCheckoutResponse",
199
+ callbackId,
200
+ success: false,
201
+ error: error instanceof Error ? error.message : "CHECKOUT_FAILED",
202
+ });
203
+ }
204
+ return;
205
+ }
206
+ switch (message.type) {
207
+ case "action": {
208
+ const { action, callbackId, product } = message.payload || {};
209
+ if (action === "addToCart" && product) {
210
+ // Call partner's callback
211
+ const result = await handleAddToCart(product);
212
+ // Complete the action
213
+ CallbackManager_1.default.completeAction(callbackId, result);
214
+ // Send result back to WebView
215
+ WebViewBridge_1.default.completeActionInWebView(callbackId, result);
216
+ }
217
+ else if (action === "getCart") {
218
+ const result = await handleGetCart();
219
+ WebViewBridge_1.default.completeActionInWebView(callbackId, {
220
+ success: result.success,
221
+ data: result.cart,
222
+ error: result.error,
223
+ });
224
+ }
225
+ break;
226
+ }
227
+ case "chatReady": {
228
+ // Initialize WebView with config
229
+ WebViewBridge_1.default.initWebView({
230
+ apiKey: config.apiKey,
231
+ userId,
232
+ userEmail: config.userEmail,
233
+ theme: config.theme,
234
+ });
235
+ break;
236
+ }
237
+ default:
238
+ break;
239
+ }
240
+ }, [
241
+ config,
242
+ userId,
243
+ visible,
244
+ onClose,
245
+ updateChatId,
246
+ updateUserId,
247
+ handleAddToCart,
248
+ handleGetCart,
249
+ handleNavigateToProduct,
250
+ ]);
251
+ // Subscribe to bridge messages
252
+ (0, react_1.useEffect)(() => {
253
+ console.log("[EchoChatModal] useEffect");
254
+ const unsubscribe = WebViewBridge_1.default.onMessage((message) => {
255
+ void handleMessage(message).catch((error) => {
256
+ console.error("[EchoChatModal] handleMessage error", error);
257
+ });
258
+ });
259
+ return () => {
260
+ unsubscribe();
261
+ };
262
+ }, [handleMessage]);
263
+ // Cancel all pending actions when modal closes
264
+ (0, react_1.useEffect)(() => {
265
+ if (!visible) {
266
+ CallbackManager_1.default.cancelAll("MODAL_CLOSED");
267
+ }
268
+ }, [visible]);
269
+ // Capture resume chat id only when opening the modal
270
+ (0, react_1.useEffect)(() => {
271
+ if (!isReady) {
272
+ return;
273
+ }
274
+ if (visible && !wasVisibleRef.current) {
275
+ setActiveResumeChatId(chatId || null);
276
+ setIsResumeReady(true);
277
+ setIsLoading(true);
278
+ setLoadTimeout(false);
279
+ }
280
+ if (!visible && wasVisibleRef.current) {
281
+ setIsResumeReady(false);
282
+ }
283
+ wasVisibleRef.current = visible;
284
+ }, [visible, chatId, isReady]);
285
+ // WebView loading timeout (15 seconds)
286
+ (0, react_1.useEffect)(() => {
287
+ if (visible && isResumeReady && isLoading) {
288
+ const timeoutId = setTimeout(() => {
289
+ setLoadTimeout(true);
290
+ setIsLoading(false);
291
+ }, 15000);
292
+ return () => clearTimeout(timeoutId);
293
+ }
294
+ }, [visible, isResumeReady, isLoading]);
295
+ // Build WebView URL with params.
296
+ // Source: EchoProvider config only (no EchoChat override), then default.
297
+ // resolveApiUrl handles Android emulator localhost -> 10.0.2.2 conversion
298
+ const resolvedApiUrl = (0, resolveApiUrl_1.resolveApiUrl)(config.apiUrl) || ECHO_CHAT_URL;
299
+ const embedUrl = resolveEmbedUrl(resolvedApiUrl);
300
+ const separator = embedUrl.includes("?") ? "&" : "?";
301
+ const urlParams = new URLSearchParams();
302
+ urlParams.set("apiKey", config.apiKey);
303
+ urlParams.set("userId", userId);
304
+ urlParams.set("embed", "true");
305
+ urlParams.set("skipIdentify", "true"); // Skip web embed's auto-identify (SDK handles it)
306
+ if (config.userEmail) {
307
+ urlParams.set("userEmail", config.userEmail);
308
+ }
309
+ if (activeResumeChatId) {
310
+ urlParams.set("resumeChatId", activeResumeChatId);
311
+ }
312
+ // Add UI settings to URL params
313
+ if (config.uiSettings?.showSidebar !== undefined) {
314
+ urlParams.set("showSidebar", String(config.uiSettings.showSidebar));
315
+ }
316
+ if (config.uiSettings?.showExpandButton !== undefined) {
317
+ urlParams.set("showExpandButton", String(config.uiSettings.showExpandButton));
318
+ }
319
+ if (config.uiSettings?.showCartButton !== undefined) {
320
+ urlParams.set("showCartButton", String(config.uiSettings.showCartButton));
321
+ }
322
+ if (config.uiSettings?.showHistoryButton !== undefined) {
323
+ urlParams.set("showHistoryButton", String(config.uiSettings.showHistoryButton));
324
+ }
325
+ if (config.uiSettings?.showCloseButton !== undefined) {
326
+ urlParams.set("showCloseButton", String(config.uiSettings.showCloseButton));
327
+ }
328
+ const chatUrl = `${embedUrl}${separator}${urlParams.toString()}`;
329
+ return (<react_native_1.Modal animationType="slide" onRequestClose={onClose} transparent={true} visible={visible}>
330
+ <react_native_1.SafeAreaView style={styles.container}>
331
+ <react_native_1.View style={styles.header}>
332
+ <react_native_1.Text style={styles.title}>AI Asistan</react_native_1.Text>
333
+ <react_native_1.TouchableOpacity onPress={onClose} style={styles.closeButton}>
334
+ <react_native_1.Text style={styles.closeText}>✕</react_native_1.Text>
335
+ </react_native_1.TouchableOpacity>
336
+ </react_native_1.View>
337
+
338
+ <react_native_1.View style={styles.webviewContainer}>
339
+ {isLoading && (<react_native_1.View style={styles.loadingContainer}>
340
+ <react_native_1.ActivityIndicator color="#007AFF" size="large"/>
341
+ <react_native_1.Text style={styles.loadingText}>Yükleniyor...</react_native_1.Text>
342
+ </react_native_1.View>)}
343
+
344
+ {loadTimeout && (<react_native_1.View style={styles.errorContainer}>
345
+ <react_native_1.Text style={styles.errorTitle}>⏱️ Yükleme Zaman Aşımı</react_native_1.Text>
346
+ <react_native_1.Text style={styles.errorText}>
347
+ İnternet bağlantınızı kontrol edin
348
+ </react_native_1.Text>
349
+ <react_native_1.TouchableOpacity style={styles.retryButton} onPress={() => {
350
+ setLoadTimeout(false);
351
+ setIsLoading(true);
352
+ setIsResumeReady(false);
353
+ setTimeout(() => setIsResumeReady(true), 100);
354
+ }}>
355
+ <react_native_1.Text style={styles.retryButtonText}>🔄 Tekrar Dene</react_native_1.Text>
356
+ </react_native_1.TouchableOpacity>
357
+ </react_native_1.View>)}
358
+
359
+ {isResumeReady && (<react_native_webview_1.WebView domStorageEnabled={true} javaScriptEnabled={true} onLoad={() => {
360
+ setIsLoading(false);
361
+ // Broadcast available actions after WebView is loaded
362
+ // This ensures the web client receives actions even if it missed the initial request
363
+ const actions = ["addToCart", "getCart"];
364
+ if (config.callbacks.onNavigateToCheckout) {
365
+ actions.push("redirectToCheckout");
366
+ }
367
+ // Small delay to ensure WebView's message listeners are set up
368
+ setTimeout(() => {
369
+ WebViewBridge_1.default.sendRawToWebView({
370
+ source: "echo-widget",
371
+ type: "echo:available_actions",
372
+ actions,
373
+ schemas: {},
374
+ customerId: null,
375
+ });
376
+ }, 100);
377
+ }} onMessage={(event) => {
378
+ // console.log("[EchoChatModal] onMessage", event);
379
+ WebViewBridge_1.default.handleMessage(event);
380
+ }} ref={webViewRef} scalesPageToFit={true} source={{ uri: chatUrl }} startInLoadingState={true} style={styles.webview}/>)}
381
+ </react_native_1.View>
382
+ </react_native_1.SafeAreaView>
383
+ </react_native_1.Modal>);
384
+ };
385
+ exports.EchoChatModal = EchoChatModal;
386
+ const styles = react_native_1.StyleSheet.create({
387
+ container: {
388
+ flex: 1,
389
+ backgroundColor: "#fff",
390
+ },
391
+ header: {
392
+ flexDirection: "row",
393
+ alignItems: "center",
394
+ justifyContent: "space-between",
395
+ paddingHorizontal: 16,
396
+ paddingVertical: 12,
397
+ borderBottomWidth: 1,
398
+ borderBottomColor: "#E5E5E5",
399
+ backgroundColor: "#fff",
400
+ },
401
+ title: {
402
+ fontSize: 18,
403
+ fontWeight: "600",
404
+ color: "#000",
405
+ },
406
+ closeButton: {
407
+ padding: 8,
408
+ },
409
+ closeText: {
410
+ fontSize: 20,
411
+ color: "#666",
412
+ },
413
+ webviewContainer: {
414
+ flex: 1,
415
+ position: "relative",
416
+ },
417
+ webview: {
418
+ flex: 1,
419
+ },
420
+ loadingContainer: {
421
+ position: "absolute",
422
+ top: 0,
423
+ left: 0,
424
+ right: 0,
425
+ bottom: 0,
426
+ justifyContent: "center",
427
+ alignItems: "center",
428
+ backgroundColor: "#fff",
429
+ zIndex: 1,
430
+ },
431
+ loadingText: {
432
+ marginTop: 12,
433
+ fontSize: 14,
434
+ color: "#666",
435
+ },
436
+ errorContainer: {
437
+ position: "absolute",
438
+ top: 0,
439
+ left: 0,
440
+ right: 0,
441
+ bottom: 0,
442
+ justifyContent: "center",
443
+ alignItems: "center",
444
+ backgroundColor: "#fff",
445
+ zIndex: 2,
446
+ padding: 20,
447
+ },
448
+ errorTitle: {
449
+ fontSize: 18,
450
+ fontWeight: "600",
451
+ color: "#000",
452
+ marginBottom: 8,
453
+ textAlign: "center",
454
+ },
455
+ errorText: {
456
+ fontSize: 14,
457
+ color: "#666",
458
+ marginBottom: 20,
459
+ textAlign: "center",
460
+ },
461
+ retryButton: {
462
+ backgroundColor: "#007AFF",
463
+ paddingHorizontal: 24,
464
+ paddingVertical: 12,
465
+ borderRadius: 8,
466
+ },
467
+ retryButtonText: {
468
+ color: "#fff",
469
+ fontSize: 16,
470
+ fontWeight: "600",
471
+ },
472
+ });
473
+ exports.default = exports.EchoChatModal;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * EchoProvider - Main context provider for Echo SDK
3
+ *
4
+ * Provides:
5
+ * - SDK configuration (apiKey, callbacks)
6
+ * - User session management
7
+ * - Bridge initialization
8
+ */
9
+ import type React from "react";
10
+ import type { AddToCartResult, EchoConfig, GetCartResult, Product } from "../types";
11
+ type EchoContextType = {
12
+ config: EchoConfig;
13
+ userId: string;
14
+ chatId: string;
15
+ isReady: boolean;
16
+ updateUserId: (newUserId: string) => Promise<void>;
17
+ updateChatId: (newChatId: string) => Promise<void>;
18
+ handleAddToCart: (product: Product, quantity?: number) => Promise<AddToCartResult>;
19
+ handleGetCart: () => Promise<GetCartResult>;
20
+ handleNavigateToProduct: (productId: string) => void;
21
+ handleNavigateToCheckout: () => void;
22
+ handleAuthRequired: () => void;
23
+ };
24
+ export declare const useEcho: () => EchoContextType;
25
+ type EchoProviderProps = {
26
+ config: EchoConfig;
27
+ children: React.ReactNode;
28
+ };
29
+ export declare const EchoProvider: React.FC<EchoProviderProps>;
30
+ export default EchoProvider;