@getecho-ai/react-native-sdk 1.0.0 → 1.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 CHANGED
@@ -202,6 +202,8 @@ const {
202
202
  userId, // Current user ID
203
203
  chatId, // Current chat ID
204
204
  isReady, // SDK initialized
205
+ identify, // Identify user (login)
206
+ logout, // Reset to anonymous user
205
207
  } = useEcho();
206
208
  ```
207
209
 
@@ -252,6 +254,87 @@ Control visibility of UI elements:
252
254
  >
253
255
  ```
254
256
 
257
+ ## User Authentication
258
+
259
+ Echo SDK supports three user states:
260
+
261
+ 1. **Anonymous** - Default state. A random UUID is generated and persisted automatically.
262
+ 2. **Identified** - User is linked to an email or identifier. Chat history migrates from anonymous to identified user.
263
+ 3. **Logged out** - User is reset to a new anonymous state with a fresh UUID.
264
+
265
+ ### Identifying Users
266
+
267
+ **Option 1: Via config (automatic)**
268
+
269
+ Pass `userEmail` or `userIdentifier` in the provider config. The SDK auto-identifies on mount:
270
+
271
+ ```tsx
272
+ <EchoProvider
273
+ config={{
274
+ apiKey: 'your-key',
275
+ callbacks,
276
+ userEmail: user.email,
277
+ userIdentifier: user.id,
278
+ }}
279
+ >
280
+ ```
281
+
282
+ **Option 2: Via hook (imperative)**
283
+
284
+ Call `identify()` after login for full control:
285
+
286
+ ```tsx
287
+ const { identify } = useEcho();
288
+
289
+ const result = await identify({
290
+ email: 'user@example.com',
291
+ userIdentifier: 'user-123',
292
+ firstName: 'John',
293
+ lastName: 'Doe',
294
+ phone: '+905551234567',
295
+ traits: { plan: 'premium' },
296
+ });
297
+ // result: { success: true, userId: '...', userIdChanged: true }
298
+ ```
299
+
300
+ ### Logging Out
301
+
302
+ Call `logout()` to reset to anonymous state. This generates a new anonymous UUID and clears chat history:
303
+
304
+ ```tsx
305
+ const { logout } = useEcho();
306
+
307
+ await logout();
308
+ ```
309
+
310
+ ### Login / Logout Flow Example
311
+
312
+ ```tsx
313
+ import { useEcho } from '@getecho-ai/react-native-sdk';
314
+
315
+ function ProfileScreen() {
316
+ const { identify, logout, userId } = useEcho();
317
+
318
+ const handleLogin = async (email: string) => {
319
+ // Your auth logic...
320
+ await identify({ email });
321
+ };
322
+
323
+ const handleLogout = async () => {
324
+ // Your auth logic...
325
+ await logout();
326
+ };
327
+
328
+ return (
329
+ <View>
330
+ <Text>User: {userId}</Text>
331
+ <Button title="Login" onPress={() => handleLogin('user@example.com')} />
332
+ <Button title="Logout" onPress={handleLogout} />
333
+ </View>
334
+ );
335
+ }
336
+ ```
337
+
255
338
  ## Troubleshooting
256
339
 
257
340
  ### "Network request failed" on Android emulator
@@ -7,7 +7,7 @@
7
7
  * - Bridge initialization
8
8
  */
9
9
  import type React from "react";
10
- import type { AddToCartResult, EchoConfig, GetCartResult, Product } from "../types";
10
+ import type { AddToCartResult, EchoConfig, GetCartResult, IdentifyParams, IdentifyResult, Product } from "../types";
11
11
  type EchoContextType = {
12
12
  config: EchoConfig;
13
13
  userId: string;
@@ -15,6 +15,8 @@ type EchoContextType = {
15
15
  isReady: boolean;
16
16
  updateUserId: (newUserId: string) => Promise<void>;
17
17
  updateChatId: (newChatId: string) => Promise<void>;
18
+ identify: (params: IdentifyParams) => Promise<IdentifyResult>;
19
+ logout: () => Promise<void>;
18
20
  handleAddToCart: (product: Product, quantity?: number) => Promise<AddToCartResult>;
19
21
  handleGetCart: () => Promise<GetCartResult>;
20
22
  handleNavigateToProduct: (productId: string) => void;
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.EchoProvider = exports.useEcho = void 0;
15
15
  const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
16
16
  const react_1 = require("react");
17
+ const WebViewBridge_1 = __importDefault(require("../bridge/WebViewBridge"));
17
18
  const resolveApiUrl_1 = require("../utils/resolveApiUrl");
18
19
  /**
19
20
  * Generate a UUID v4 string
@@ -45,6 +46,7 @@ const EchoProvider = ({ config, children, }) => {
45
46
  const [userId, setUserId] = (0, react_1.useState)(config.userId || "");
46
47
  const [chatId, setChatId] = (0, react_1.useState)("");
47
48
  const [isReady, setIsReady] = (0, react_1.useState)(false);
49
+ const suppressAutoIdentifyRef = (0, react_1.useRef)(false);
48
50
  // Initialize user ID from storage or generate new one
49
51
  (0, react_1.useEffect)(() => {
50
52
  const syncUserWithServer = async (id) => {
@@ -175,9 +177,89 @@ const EchoProvider = ({ config, children, }) => {
175
177
  config.userIdentifier,
176
178
  updateUserId,
177
179
  ]);
180
+ const identify = (0, react_1.useCallback)(async (params) => {
181
+ const baseUrl = (0, resolveApiUrl_1.resolveApiUrl)(config.apiUrl).replace(/\/+$/, "");
182
+ try {
183
+ const response = await fetch(`${baseUrl}/api/user/identify`, {
184
+ method: "POST",
185
+ headers: {
186
+ "Content-Type": "application/json",
187
+ "X-API-Key": config.apiKey,
188
+ },
189
+ body: JSON.stringify({
190
+ userId,
191
+ email: params.email,
192
+ userIdentifier: params.userIdentifier,
193
+ firstName: params.firstName,
194
+ lastName: params.lastName,
195
+ phone: params.phone,
196
+ traits: params.traits,
197
+ }),
198
+ });
199
+ const data = await response.json();
200
+ if (data.userIdChanged && data.userId) {
201
+ await updateUserId(data.userId);
202
+ }
203
+ suppressAutoIdentifyRef.current = false;
204
+ WebViewBridge_1.default.sendRawToWebView({
205
+ type: "echo:runtime:update_identity",
206
+ userId: data.userId || userId,
207
+ email: params.email,
208
+ userIdentifier: params.userIdentifier,
209
+ });
210
+ return {
211
+ success: true,
212
+ userId: data.userId || userId,
213
+ email: params.email,
214
+ userIdentifier: params.userIdentifier,
215
+ userIdChanged: !!data.userIdChanged,
216
+ };
217
+ }
218
+ catch (error) {
219
+ console.error("[EchoSDK] identify() failed:", error);
220
+ return {
221
+ success: false,
222
+ userId,
223
+ userIdChanged: false,
224
+ };
225
+ }
226
+ }, [userId, config.apiKey, config.apiUrl, updateUserId]);
227
+ const logout = (0, react_1.useCallback)(async () => {
228
+ const newAnonymousId = generateUUID();
229
+ setUserId(newAnonymousId);
230
+ setChatId("");
231
+ try {
232
+ await async_storage_1.default.removeItem("echo_chat_id");
233
+ await async_storage_1.default.setItem("echo_user_id", newAnonymousId);
234
+ }
235
+ catch (error) {
236
+ console.error("[EchoSDK] logout() storage cleanup failed:", error);
237
+ }
238
+ suppressAutoIdentifyRef.current = true;
239
+ // Fire-and-forget user sync
240
+ const baseUrl = (0, resolveApiUrl_1.resolveApiUrl)(config.apiUrl).replace(/\/+$/, "");
241
+ fetch(`${baseUrl}/api/user`, {
242
+ method: "POST",
243
+ headers: {
244
+ "X-API-Key": config.apiKey,
245
+ "Content-Type": "application/json",
246
+ },
247
+ body: JSON.stringify({ userId: newAnonymousId }),
248
+ }).catch((err) => {
249
+ console.warn("[EchoSDK] logout user sync failed:", err.message);
250
+ });
251
+ WebViewBridge_1.default.sendRawToWebView({
252
+ type: "echo:runtime:reset_user",
253
+ userId: newAnonymousId,
254
+ });
255
+ }, [config.apiKey, config.apiUrl]);
178
256
  // Auto-identify user when ready and email/identifier is provided
179
257
  (0, react_1.useEffect)(() => {
180
258
  if (isReady && userId && (config.userEmail || config.userIdentifier)) {
259
+ if (suppressAutoIdentifyRef.current) {
260
+ suppressAutoIdentifyRef.current = false;
261
+ return;
262
+ }
181
263
  identifyUser();
182
264
  }
183
265
  }, [isReady, userId, config.userEmail, config.userIdentifier, identifyUser]);
@@ -228,6 +310,8 @@ const EchoProvider = ({ config, children, }) => {
228
310
  isReady,
229
311
  updateUserId,
230
312
  updateChatId,
313
+ identify,
314
+ logout,
231
315
  handleAddToCart,
232
316
  handleGetCart,
233
317
  handleNavigateToProduct,
package/dist/index.d.ts CHANGED
@@ -55,5 +55,5 @@ export { EchoChatModal } from "./components/EchoChatModal";
55
55
  export { EchoProvider, useEcho } from "./components/EchoProvider";
56
56
  export type { UseSimpleCartOptions, UseSimpleCartReturn, } from "./hooks/useSimpleCart";
57
57
  export { useSimpleCart } from "./hooks/useSimpleCart";
58
- export type { ActionType, AddToCartResult, BridgeMessage, BridgeMessageType, CallbackResult, Cart, CartItem, ChatMessage, EchoCallbacks, EchoConfig, EchoTheme, GetCartResult, MessageAction, Product, UISettings, } from "./types";
58
+ export type { ActionType, AddToCartResult, BridgeMessage, BridgeMessageType, CallbackResult, Cart, CartItem, ChatMessage, EchoCallbacks, EchoConfig, EchoTheme, GetCartResult, IdentifyParams, IdentifyResult, MessageAction, Product, UISettings, } from "./types";
59
59
  export { getLocalhostUrl, resolveApiUrl } from "./utils/resolveApiUrl";
@@ -154,3 +154,18 @@ export type CallbackResult = {
154
154
  data?: any;
155
155
  error?: string;
156
156
  };
157
+ export type IdentifyParams = {
158
+ email?: string;
159
+ userIdentifier?: string;
160
+ firstName?: string;
161
+ lastName?: string;
162
+ phone?: string;
163
+ traits?: Record<string, unknown>;
164
+ };
165
+ export type IdentifyResult = {
166
+ success: boolean;
167
+ userId: string;
168
+ email?: string;
169
+ userIdentifier?: string;
170
+ userIdChanged: boolean;
171
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getecho-ai/react-native-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Echo AI Chat SDK for React Native - AI-powered e-commerce assistant",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",