@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 +26 -8
- package/dist/FacePeMobileAvatar.js +65 -47
- package/dist/FacePeMobileProvider.js +40 -2
- package/dist/types.d.ts +22 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +61 -61
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.
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
-
"
|
|
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
|
-
| `
|
|
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 }] }
|
|
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
|
-
|
|
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
|
|
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
|
|
86
|
-
const
|
|
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,
|
|
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
|
-
|
|
144
|
-
if (conv.conversationId && transcript.length > 0 && token) {
|
|
164
|
+
if (conv.conversationId && transcript.length > 0) {
|
|
145
165
|
try {
|
|
146
|
-
const
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
if (!uId || !currentToken)
|
|
243
|
+
if (!uId)
|
|
219
244
|
return;
|
|
220
245
|
try {
|
|
221
246
|
const cleanBase = apiBaseUrl.replace(/\/+$/, '');
|
|
222
|
-
await
|
|
247
|
+
await gatewayFetch(cleanBase, `/agent-sessions/${uId}/end`, `/avatar-sessions/${uId}/end`, {
|
|
223
248
|
method: 'POST',
|
|
224
|
-
headers: {
|
|
225
|
-
|
|
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
|
-
//
|
|
260
|
-
let
|
|
261
|
-
if (!
|
|
262
|
-
// No
|
|
263
|
-
const listRes = await
|
|
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
|
|
272
|
-
if (
|
|
273
|
-
|
|
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 (!
|
|
278
|
-
throw new Error('No
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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
|
-
"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
|
+
}
|