@facepe/widget-mobile-app 1.4.0 → 1.5.2

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
@@ -80,7 +80,14 @@ Exchange your `accessKey` and `secretKey` on your secure backend (Node.js / Expr
80
80
 
81
81
  ```javascript
82
82
  // Express server route: GET /api/facepe-token
83
+ // A token lasts 2 hours (expiresIn, in seconds): keep one and fetch a new one a
84
+ // minute before it expires, or when the app asks with ?refresh=1 (after a 401).
85
+ let cached = null; // { token, until }
86
+
83
87
  app.get('/api/facepe-token', async (req, res) => {
88
+ if (cached && cached.until > Date.now() && req.query.refresh !== '1') {
89
+ return res.send(cached.token);
90
+ }
84
91
  const response = await fetch('https://api.facepe.ai/api/v1/sdk/token', {
85
92
  method: 'POST',
86
93
  headers: { 'Content-Type': 'application/json' },
@@ -91,10 +98,16 @@ app.get('/api/facepe-token', async (req, res) => {
91
98
  }),
92
99
  });
93
100
  const data = await response.json();
94
- res.send(data.data.token);
101
+ if (!response.ok) return res.status(response.status).json(data.error);
102
+ cached = { token: data.data.token, until: Date.now() + (data.data.expiresIn - 60) * 1000 };
103
+ res.send(cached.token);
95
104
  });
96
105
  ```
97
106
 
107
+ **Tokens last 2 hours.** The SDK renews them by itself: it calls `getToken({ forceRefresh: true })`
108
+ about 5 minutes before the token expires, and again (then retries once) if FacePe answers
109
+ `401 SESSION_EXPIRED`. Renewal is silent: a conversation in progress is not interrupted.
110
+
98
111
  ---
99
112
 
100
113
  ### 2. React Native Component
@@ -118,8 +131,9 @@ export default function App() {
118
131
 
119
132
  <FacePeMobileProvider
120
133
  apiBaseUrl="https://api.facepe.ai/api/v1/sdk"
121
- getToken={async () => {
122
- const res = await fetch('https://your-backend.com/api/facepe-token');
134
+ getToken={async ({ forceRefresh }) => {
135
+ // Tokens last 2 hours: forceRefresh asks your server for a new one.
136
+ const res = await fetch('https://your-backend.com/api/facepe-token' + (forceRefresh ? '?refresh=1' : ''));
123
137
  return await res.text();
124
138
  }}
125
139
  >
@@ -246,12 +260,14 @@ const styles = StyleSheet.create({
246
260
 
247
261
  | Prop | Type | Default | Description |
248
262
  | :--- | :--- | :--- | :--- |
249
- | `avatarKey` | `string` | *(optional)* | Specific avatar key or ID assigned to your company. |
263
+ | `agentId` | `string` | first agent assigned to the user | The agent to talk to — its id from the FacePe dashboard (SDK page) or `GET /agents`. |
264
+ | `avatarKey` | `string` | *(deprecated)* | The same as `agentId`, for apps written before agents. |
250
265
  | `language` | `string` | `'en'` | Spoken language code (`'en'`, `'hi'`, `'es'`, `'ta'`, `'te'`, etc.). |
251
266
  | `style` | `ViewStyle` | `undefined` | Style applied to avatar container. |
252
267
  | `videoStyle` | `ViewStyle` | `undefined` | Style applied to native video view. |
253
268
  | `objectFit` | `'cover' \| 'contain'` | `'cover'` | Video aspect scaling mode. |
254
269
  | `mirror` | `boolean` | `false` | Mirror video orientation. |
270
+ | `showSpeakingIndicator` | `boolean` | `true` | The "Avatar speaking" / "Listening to you…" label on the video. Set `false` to show it yourself with `onSpeakStart`/`onSpeakEnd` and `onUserSpeakStart`/`onUserSpeakEnd`. |
255
271
  | `autoStart` | `boolean` | `true` | Automatically connect and start stream on mount. |
256
272
  | `posterUrl` | `string \| null` | avatar's portrait | Picture shown until the video starts. Defaults to the avatar's portrait from FacePe; pass `null` for none. |
257
273
  | `enableMic` | `boolean` | `true` | Automatically enable microphone on connect. |
@@ -312,7 +328,7 @@ when the component unmounts.
312
328
  ```json
313
329
  {
314
330
  "conversationId": "cm_usage_123",
315
- "avatar": { "id": "cm_avatar_456", "name": "Dharun" },
331
+ "agent": { "id": "cm_agent_456", "name": "Front Desk" },
316
332
  "language": "en",
317
333
  "platform": "android",
318
334
  "sdkVersion": "1.3.0",
@@ -330,7 +346,7 @@ when the component unmounts.
330
346
  | Field | Meaning |
331
347
  |---|---|
332
348
  | `conversationId` | The conversation's FacePe id (its usage record) |
333
- | `avatar` | `{ id, name }` of the avatar |
349
+ | `agent` | `{ id, name }` of the agent that held the conversation (`avatar` is the same, kept for older apps) |
334
350
  | `language` | The language requested for the conversation |
335
351
  | `platform` | `web`, `android` or `ios` |
336
352
  | `sdkVersion` | The SDK version that recorded it |
@@ -338,7 +354,7 @@ when the component unmounts.
338
354
  | `durationSec` | Length in seconds |
339
355
  | `endReason` | `ended` (the user or your code ended it), `disconnected` (the stream dropped), `unmounted` (the component went away) |
340
356
  | `transcript` | Every line in order: `{ role: "user" \| "assistant", text, at }` |
341
- | `form` | For a form avatar: `{ title, complete, fields: [{ label, question, required, value }] }`; `null` otherwise |
357
+ | `form` | For a form avatar: `{ title, complete, fields: [{ label, question, required, value }], documents, uploadWhere }` — `documents` are what the avatar asked the customer to upload at the end; `null` for other avatars |
342
358
 
343
359
  For an avatar that **fills a form**, the record also carries the form as the
344
360
  conversation filled it: each question with the customer's answer (`value` is
@@ -354,7 +370,9 @@ For other avatars `form` is `null`.
354
370
  { "label": "Name", "question": "Could you please tell me your full name?", "required": true, "value": "Tony" },
355
371
  { "label": "Phone", "question": "What is the best phone number to reach you?", "required": true, "value": "9659390715" },
356
372
  { "label": "Email", "question": "May I have your email address?", "required": false, "value": "tony@gmail.com" }
357
- ]
373
+ ],
374
+ "documents": ["ID proof (Aadhaar / PAN)", "Address proof"],
375
+ "uploadWhere": "on our website"
358
376
  }
359
377
  ```
360
378
 
@@ -66,8 +66,30 @@ const joinNumberParts = (text) => text.replace(/\d+(?:[ \t]*[-\u2010\u2011\u2013
66
66
  const digits = m.replace(/\D/g, '');
67
67
  return digits.length >= 7 ? digits : m;
68
68
  });
69
- export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', style, videoStyle, objectFit = 'cover', mirror = false, autoStart = true, enableMic = true, onTranscript, onSpeakStart, onSpeakEnd, onUserSpeakStart, onUserSpeakEnd, onOrderUpdate, onCheckout, onStatusChange, onError, onConversationEnd, posterUrl, }, ref) => {
69
+ /**
70
+ * A gateway call with the app's token. Answered 401 (the token expired or was
71
+ * revoked), it asks the app for a fresh token once and tries again. A server
72
+ * from before agents has only /avatars…, tried when /agents… is missing.
73
+ */
74
+ async function gatewayFetch(base, agentPath, avatarPath, init, tokenFor) {
75
+ const send = async (path, force) => {
76
+ const headers = { ...(init.headers ?? {}) };
77
+ const token = await tokenFor(force);
78
+ if (token)
79
+ headers.Authorization = `Bearer ${token}`;
80
+ return fetch(`${base}${path}`, { ...init, headers });
81
+ };
82
+ const attempt = async (force) => {
83
+ const res = await send(agentPath, force);
84
+ return res.status === 404 ? send(avatarPath, force) : res;
85
+ };
86
+ const res = await attempt(false);
87
+ return res.status === 401 ? attempt(true) : res;
88
+ }
89
+ export const FacePeMobileAvatar = forwardRef(({ agentId, avatarKey, language = 'en', style, videoStyle, objectFit = 'cover', mirror = false, showSpeakingIndicator = true, autoStart = true, enableMic = true, onTranscript, onSpeakStart, onSpeakEnd, onUserSpeakStart, onUserSpeakEnd, onOrderUpdate, onCheckout, onStatusChange, onError, onConversationEnd, posterUrl, }, ref) => {
70
90
  const { token, loading: tokenLoading, error: tokenError, apiBaseUrl, refreshToken } = useFacePeMobileContext();
91
+ // The agent to talk to (`avatarKey` is its name from before agents).
92
+ const chosenAgent = agentId ?? avatarKey;
71
93
  // The avatar's portrait, shown until its video arrives. Looked up from the
72
94
  // avatars this token may use, unless the app gave one (or null for none).
73
95
  const [autoPoster, setAutoPoster] = useState(null);
@@ -77,13 +99,12 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
77
99
  let cancelled = false;
78
100
  void (async () => {
79
101
  try {
80
- const res = await fetch(`${apiBaseUrl.replace(/\/+$/, '')}/avatars`, {
81
- headers: { Authorization: `Bearer ${token}` },
82
- });
102
+ const res = await gatewayFetch(apiBaseUrl.replace(/\/+$/, ''), '/agents', '/avatars', {}, tokenFor);
83
103
  if (!res.ok)
84
104
  return;
85
- const avatars = (await res.json())?.data?.avatars ?? [];
86
- const avatar = avatarKey ? avatars.find((a) => a.id === avatarKey) : avatars[0];
105
+ const data = (await res.json())?.data;
106
+ const agents = data?.agents ?? data?.avatars ?? [];
107
+ const avatar = chosenAgent ? agents.find((a) => a.id === chosenAgent) : agents[0];
87
108
  if (!cancelled)
88
109
  setAutoPoster(avatar?.previewUrl ?? null);
89
110
  }
@@ -94,7 +115,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
94
115
  return () => {
95
116
  cancelled = true;
96
117
  };
97
- }, [token, apiBaseUrl, avatarKey, posterUrl]);
118
+ }, [token, apiBaseUrl, chosenAgent, posterUrl]);
98
119
  const poster = posterUrl !== undefined ? posterUrl : autoPoster;
99
120
  const [status, setStatus] = useState('idle');
100
121
  const [videoTrack, setVideoTrack] = useState(null);
@@ -140,14 +161,14 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
140
161
  const transcript = conv.lines.map(({ role, text, at }) => ({ role, text, at }));
141
162
  void (async () => {
142
163
  let form = null;
143
- const token = tokenRef.current;
144
- if (conv.conversationId && transcript.length > 0 && token) {
164
+ if (conv.conversationId && transcript.length > 0) {
145
165
  try {
146
- const res = await fetch(`${apiBaseUrl.replace(/\/+$/, '')}/avatar-sessions/${encodeURIComponent(conv.conversationId)}/form`, {
166
+ const id = encodeURIComponent(conv.conversationId);
167
+ const res = await gatewayFetch(apiBaseUrl.replace(/\/+$/, ''), `/agent-sessions/${id}/form`, `/avatar-sessions/${id}/form`, {
147
168
  method: 'POST',
148
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
169
+ headers: { 'Content-Type': 'application/json' },
149
170
  body: JSON.stringify({ transcript: transcript.map(({ role, text }) => ({ role, text })) }),
150
- });
171
+ }, tokenFor);
151
172
  if (res.ok)
152
173
  form = (await res.json())?.data?.form ?? null;
153
174
  }
@@ -158,7 +179,8 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
158
179
  try {
159
180
  onConversationEndRef.current?.({
160
181
  conversationId: conv.conversationId,
161
- avatar: conv.avatar,
182
+ agent: conv.agent,
183
+ avatar: conv.agent,
162
184
  language: conv.language,
163
185
  platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'web',
164
186
  sdkVersion: SDK_VERSION,
@@ -179,6 +201,10 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
179
201
  const isConnectingRef = useRef(false);
180
202
  const tokenRef = useRef(token);
181
203
  tokenRef.current = token;
204
+ // The app's token for gateway calls, or (force) a fresh one after a 401.
205
+ const refreshRef = useRef(refreshToken);
206
+ refreshRef.current = refreshToken;
207
+ const tokenFor = useCallback(async (force) => (force ? refreshRef.current(true) : tokenRef.current ?? refreshRef.current()), []);
182
208
  // Helper: Safely emit status changes
183
209
  const updateStatus = useCallback((newStatus) => {
184
210
  if (!mountedRef.current)
@@ -214,18 +240,14 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
214
240
  const endUsage = async () => {
215
241
  const uId = usageIdRef.current;
216
242
  usageIdRef.current = null;
217
- const currentToken = tokenRef.current;
218
- if (!uId || !currentToken)
243
+ if (!uId)
219
244
  return;
220
245
  try {
221
246
  const cleanBase = apiBaseUrl.replace(/\/+$/, '');
222
- await fetch(`${cleanBase}/avatar-sessions/${uId}/end`, {
247
+ await gatewayFetch(cleanBase, `/agent-sessions/${uId}/end`, `/avatar-sessions/${uId}/end`, {
223
248
  method: 'POST',
224
- headers: {
225
- 'Content-Type': 'application/json',
226
- Authorization: `Bearer ${currentToken}`,
227
- },
228
- });
249
+ headers: { 'Content-Type': 'application/json' },
250
+ }, tokenFor);
229
251
  }
230
252
  catch {
231
253
  // ignore background teardown error
@@ -256,39 +278,32 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
256
278
  throw new Error('Authentication token not available. Please verify your token provider.');
257
279
  }
258
280
  const cleanBase = apiBaseUrl.replace(/\/+$/, '');
259
- // Resolve target avatar ID
260
- let targetAvatarId = avatarKey;
261
- if (!targetAvatarId) {
262
- // No avatar given: use the first one assigned to this token's user.
263
- const listRes = await fetch(`${cleanBase}/avatars`, {
264
- method: 'GET',
265
- headers: {
266
- Authorization: `Bearer ${currentToken}`,
267
- },
268
- });
281
+ // The agent to talk to
282
+ let targetAgentId = chosenAgent;
283
+ if (!targetAgentId) {
284
+ // No agent given: use the first one assigned to this token's user.
285
+ const listRes = await gatewayFetch(cleanBase, '/agents', '/avatars', { method: 'GET' }, tokenFor);
269
286
  if (listRes.ok) {
270
- const listData = await listRes.json();
271
- const avatars = listData?.data?.avatars || [];
272
- if (avatars.length > 0) {
273
- targetAvatarId = avatars[0].id;
287
+ const listData = (await listRes.json())?.data;
288
+ const agents = listData?.agents || listData?.avatars || [];
289
+ if (agents.length > 0) {
290
+ targetAgentId = agents[0].id;
274
291
  }
275
292
  }
276
293
  }
277
- if (!targetAvatarId) {
278
- throw new Error('No Avatar configured or assigned to this token.');
294
+ if (!targetAgentId) {
295
+ throw new Error('No agent is assigned to these keys. Create one on the FacePe dashboard (Agents).');
279
296
  }
280
297
  // Request live streaming session from FacePe SDK backend
281
- const sessionResp = await fetch(`${cleanBase}/avatars/${targetAvatarId}/sessions`, {
298
+ const agentPath = encodeURIComponent(targetAgentId);
299
+ const sessionResp = await gatewayFetch(cleanBase, `/agents/${agentPath}/sessions`, `/avatars/${agentPath}/sessions`, {
282
300
  method: 'POST',
283
- headers: {
284
- 'Content-Type': 'application/json',
285
- Authorization: `Bearer ${currentToken}`,
286
- },
301
+ headers: { 'Content-Type': 'application/json' },
287
302
  body: JSON.stringify({ language }),
288
- });
303
+ }, tokenFor);
289
304
  if (!sessionResp.ok) {
290
305
  const errBody = await sessionResp.json().catch(() => ({}));
291
- const errMsg = errBody?.error?.message || errBody?.message || 'Failed to initialize avatar session';
306
+ const errMsg = errBody?.error?.message || errBody?.message || 'Failed to start the conversation';
292
307
  throw new Error(errMsg);
293
308
  }
294
309
  const sessionPayload = await sessionResp.json();
@@ -296,7 +311,10 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
296
311
  usageIdRef.current = sessionInfo?.usageId || null;
297
312
  conversationRef.current = {
298
313
  conversationId: usageIdRef.current,
299
- avatar: { id: String(sessionInfo?.avatar?.id ?? targetAvatarId), name: String(sessionInfo?.avatar?.name ?? '') },
314
+ agent: (() => {
315
+ const who = sessionInfo?.agent ?? sessionInfo?.avatar;
316
+ return { id: String(who?.id ?? targetAgentId), name: String(who?.name ?? '') };
317
+ })(),
300
318
  language: language ?? null,
301
319
  startedAt: new Date(),
302
320
  lines: [],
@@ -554,7 +572,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
554
572
  }
555
573
  }, [
556
574
  hasLiveKit,
557
- avatarKey,
575
+ chosenAgent,
558
576
  language,
559
577
  enableMic,
560
578
  apiBaseUrl,
@@ -705,7 +723,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
705
723
  // size — the video got no height and stayed invisible.
706
724
  style: StyleSheet.flatten([styles.video, videoStyle]), objectFit: objectFit, mirror: mirror })) : (_jsxs(View, { style: [styles.container, styles.center, !!poster && (status === 'idle' || status === 'disconnected') && styles.overPoster], children: [poster && _jsx(Image, { source: { uri: poster }, style: StyleSheet.absoluteFill, resizeMode: "cover" }), poster && status !== 'idle' && status !== 'disconnected' && _jsx(View, { style: [StyleSheet.absoluteFill, styles.dim] }), status === 'connecting' || status === 'reconnecting' || status === 'connected' ? (
707
725
  // connected: the stream is up and the first video frame is on its way.
708
- _jsxs(_Fragment, { children: [_jsx(ActivityIndicator, { size: "large", color: "#8B5CF6" }), _jsx(Text, { style: styles.loadingText, children: status === 'reconnecting' ? 'Reconnecting stream…' : 'Connecting to FacePe AI Avatar…' })] })) : status === 'error' ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.errorTitle, children: "Connection Failed" }), _jsx(Text, { style: styles.errorText, children: errorMessage || 'Unable to connect to stream' }), _jsx(TouchableOpacity, { style: styles.retryButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.retryText, children: "Reconnect" }) })] })) : (_jsx(TouchableOpacity, { style: styles.startButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.startText, children: "Start Avatar Session" }) }))] })), status === 'connected' && (isAvatarSpeaking || isUserSpeaking) && (_jsxs(View, { style: styles.speakerPill, children: [_jsx(View, { style: [
726
+ _jsxs(_Fragment, { children: [_jsx(ActivityIndicator, { size: "large", color: "#8B5CF6" }), _jsx(Text, { style: styles.loadingText, children: status === 'reconnecting' ? 'Reconnecting stream…' : 'Connecting to FacePe AI Avatar…' })] })) : status === 'error' ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.errorTitle, children: "Connection Failed" }), _jsx(Text, { style: styles.errorText, children: errorMessage || 'Unable to connect to stream' }), _jsx(TouchableOpacity, { style: styles.retryButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.retryText, children: "Reconnect" }) })] })) : (_jsx(TouchableOpacity, { style: styles.startButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.startText, children: "Start Avatar Session" }) }))] })), showSpeakingIndicator && status === 'connected' && (isAvatarSpeaking || isUserSpeaking) && (_jsxs(View, { style: styles.speakerPill, children: [_jsx(View, { style: [
709
727
  styles.speakerDot,
710
728
  isAvatarSpeaking ? styles.dotAvatar : styles.dotUser,
711
729
  ] }), _jsx(Text, { style: styles.speakerText, children: isAvatarSpeaking ? 'Avatar speaking' : 'Listening to you…' })] }))] }));
@@ -1,25 +1,50 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useState, useEffect, useRef, useMemo } from 'react';
3
3
  const FacePeContext = createContext(null);
4
+ /** When a token runs out (its JWT `exp`, in seconds), or null if it doesn't say. */
5
+ function expiryOf(token) {
6
+ try {
7
+ const part = token.split('.')[1] ?? '';
8
+ const b64 = part.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(part.length / 4) * 4, '=');
9
+ const exp = JSON.parse(globalThis.atob(b64))?.exp;
10
+ return typeof exp === 'number' ? exp : null;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ /** Fetch a new token this long before the current one runs out. */
17
+ const REFRESH_EARLY_MS = 5 * 60 * 1000;
4
18
  export function FacePeMobileProvider({ children, getToken, apiBaseUrl = 'https://api.facepe.ai/api/v1/sdk', config = {}, }) {
5
19
  const [token, setToken] = useState(null);
6
20
  const [loading, setLoading] = useState(true);
7
21
  const [error, setError] = useState(null);
8
22
  const tokenPromiseRef = useRef(null);
23
+ const tokenRef = useRef(null);
24
+ // A refresh with a token already in hand is silent: no "loading" (which would
25
+ // replace a live call with the sign-in screen), and on failure the current
26
+ // token stays — the next call's 401 asks again.
9
27
  const refreshToken = async (force = false) => {
10
28
  if (tokenPromiseRef.current && !force) {
11
29
  return tokenPromiseRef.current;
12
30
  }
13
- setLoading(true);
14
- setError(null);
31
+ const silent = tokenRef.current !== null;
32
+ if (!silent) {
33
+ setLoading(true);
34
+ setError(null);
35
+ }
15
36
  const promise = (async () => {
16
37
  try {
17
38
  const fetched = await getToken({ forceRefresh: force });
39
+ tokenRef.current = fetched;
18
40
  setToken(fetched);
41
+ setError(null);
19
42
  setLoading(false);
20
43
  return fetched;
21
44
  }
22
45
  catch (err) {
46
+ if (silent)
47
+ return null;
23
48
  const normalized = err instanceof Error ? err : new Error(String(err));
24
49
  setError(normalized);
25
50
  setLoading(false);
@@ -35,6 +60,19 @@ export function FacePeMobileProvider({ children, getToken, apiBaseUrl = 'https:/
35
60
  useEffect(() => {
36
61
  void refreshToken();
37
62
  }, []);
63
+ // Tokens expire (2 hours by default): fetch a new one a little before this one
64
+ // runs out, so a conversation never starts on a token about to expire. A 401
65
+ // on any call also fetches a new one.
66
+ useEffect(() => {
67
+ if (!token)
68
+ return;
69
+ const exp = expiryOf(token);
70
+ if (!exp)
71
+ return;
72
+ const wait = Math.min(Math.max(10000, exp * 1000 - Date.now() - REFRESH_EARLY_MS), 2 ** 31 - 1);
73
+ const timer = setTimeout(() => void refreshToken(true), wait);
74
+ return () => clearTimeout(timer);
75
+ }, [token]);
38
76
  const value = useMemo(() => ({
39
77
  config,
40
78
  token,
package/dist/types.d.ts CHANGED
@@ -35,10 +35,20 @@ export interface FacePeFilledForm {
35
35
  /** Every required question has an answer. */
36
36
  complete: boolean;
37
37
  fields: FacePeFormAnswer[];
38
+ /** Documents the customer was asked to upload once the form was done (may be empty). */
39
+ documents: string[];
40
+ /** Where to upload them, as the company wrote it (empty when not set). */
41
+ uploadWhere: string;
38
42
  }
39
43
  export interface FacePeConversation {
40
44
  /** The conversation's id (its FacePe usage record), or null if none was opened. */
41
45
  conversationId: string | null;
46
+ /** The agent that held the conversation. */
47
+ agent: {
48
+ id: string;
49
+ name: string;
50
+ };
51
+ /** @deprecated The same as `agent` (kept for apps from before agents). */
42
52
  avatar: {
43
53
  id: string;
44
54
  name: string;
@@ -84,7 +94,12 @@ export interface TokenProviderParams {
84
94
  }
85
95
  export type TokenProvider = (params: TokenProviderParams) => Promise<string>;
86
96
  export interface FacePeMobileAvatarProps {
87
- /** Specific avatar identifier or key override */
97
+ /**
98
+ * The agent to talk to (its id, from the FacePe dashboard's SDK page or
99
+ * GET /agents). Without one, the first agent assigned to the keys' user.
100
+ */
101
+ agentId?: string;
102
+ /** @deprecated Use `agentId` (the same id; kept for apps from before agents). */
88
103
  avatarKey?: string;
89
104
  /** Language to converse in ('en', 'es', 'hi', etc.) */
90
105
  language?: string;
@@ -96,6 +111,12 @@ export interface FacePeMobileAvatarProps {
96
111
  objectFit?: 'cover' | 'contain';
97
112
  /** Whether to mirror the video stream (default: false) */
98
113
  mirror?: boolean;
114
+ /**
115
+ * Show the "Avatar speaking" / "Listening to you…" label on the video
116
+ * (default: true). Turn it off to show who is talking yourself, with
117
+ * onSpeakStart/onSpeakEnd and onUserSpeakStart/onUserSpeakEnd.
118
+ */
119
+ showSpeakingIndicator?: boolean;
99
120
  /** Auto start streaming when component mounts (default: true) */
100
121
  autoStart?: boolean;
101
122
  /** Whether microphone should be automatically enabled on stream start (default: true) */
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** This package's version, reported in conversation records. Keep in step with package.json. */
2
- export declare const SDK_VERSION = "1.4.0";
2
+ export declare const SDK_VERSION = "1.5.2";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** This package's version, reported in conversation records. Keep in step with package.json. */
2
- export const SDK_VERSION = '1.4.0';
2
+ export const SDK_VERSION = '1.5.2';
package/package.json CHANGED
@@ -1,61 +1,61 @@
1
- {
2
- "name": "@facepe/widget-mobile-app",
3
- "version": "1.4.0",
4
- "description": "FacePe AI 3D Video Avatar Native Live Streaming SDK for React Native (iOS & Android)",
5
- "main": "./dist/index.js",
6
- "module": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "files": [
9
- "dist",
10
- "README.md"
11
- ],
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
- "keywords": [
16
- "facepe",
17
- "avatar",
18
- "ai-avatar",
19
- "livekit",
20
- "react-native",
21
- "ios",
22
- "android",
23
- "webrtc",
24
- "streaming",
25
- "digital-human"
26
- ],
27
- "author": "FacePe AI",
28
- "license": "MIT",
29
- "scripts": {
30
- "lint": "tsc --noEmit",
31
- "build": "tsc"
32
- },
33
- "peerDependencies": {
34
- "@livekit/react-native": ">=2.0.0",
35
- "@livekit/react-native-webrtc": ">=124.0.0",
36
- "livekit-client": ">=2.0.0",
37
- "react": ">=18.0.0",
38
- "react-native": ">=0.70.0",
39
- "@anam-ai/js-sdk": ">=4.27.0"
40
- },
41
- "peerDependenciesMeta": {
42
- "@livekit/react-native": {
43
- "optional": true
44
- },
45
- "@livekit/react-native-webrtc": {
46
- "optional": true
47
- },
48
- "livekit-client": {
49
- "optional": true
50
- },
51
- "@anam-ai/js-sdk": {
52
- "optional": true
53
- }
54
- },
55
- "devDependencies": {
56
- "@types/react": "^18.3.3",
57
- "react": "18.3.1",
58
- "react-native": "0.74.0",
59
- "typescript": "^5.4.5"
60
- }
61
- }
1
+ {
2
+ "name": "@facepe/widget-mobile-app",
3
+ "version": "1.5.2",
4
+ "description": "FacePe AI 3D Video Avatar Native Live Streaming SDK for React Native (iOS & Android)",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "keywords": [
16
+ "facepe",
17
+ "avatar",
18
+ "ai-avatar",
19
+ "livekit",
20
+ "react-native",
21
+ "ios",
22
+ "android",
23
+ "webrtc",
24
+ "streaming",
25
+ "digital-human"
26
+ ],
27
+ "author": "FacePe AI",
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "lint": "tsc --noEmit",
31
+ "build": "tsc"
32
+ },
33
+ "peerDependencies": {
34
+ "@livekit/react-native": ">=2.0.0",
35
+ "@livekit/react-native-webrtc": ">=124.0.0",
36
+ "livekit-client": ">=2.0.0",
37
+ "react": ">=18.0.0",
38
+ "react-native": ">=0.70.0",
39
+ "@anam-ai/js-sdk": ">=4.27.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@livekit/react-native": {
43
+ "optional": true
44
+ },
45
+ "@livekit/react-native-webrtc": {
46
+ "optional": true
47
+ },
48
+ "livekit-client": {
49
+ "optional": true
50
+ },
51
+ "@anam-ai/js-sdk": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "^18.3.3",
57
+ "react": "18.3.1",
58
+ "react-native": "0.74.0",
59
+ "typescript": "^5.4.5"
60
+ }
61
+ }