@akhilpulse/samadhaan-session-replay 0.1.0 → 0.2.1
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/package.json +1 -1
- package/src/SessionReplayProvider.tsx +66 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akhilpulse/samadhaan-session-replay",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "React Native SDK for Samadhaan Guided Support — session recording, live remote screen-share, take-control, and guided-tour overlays.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -35,7 +35,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
|
35
35
|
import { captureRef } from "react-native-view-shot";
|
|
36
36
|
|
|
37
37
|
const STORAGE_KEY = "@akhilpulse/samadhaan-session-replay/sessionId";
|
|
38
|
-
const SDK_VERSION = "0.1
|
|
38
|
+
const SDK_VERSION = "0.2.1";
|
|
39
39
|
|
|
40
40
|
/** Default Samadhaan production endpoint. Override via `config.serverUrl` for staging. */
|
|
41
41
|
export const DEFAULT_SERVER_URL = "https://samadhaan.pulseenergy.io";
|
|
@@ -124,6 +124,8 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
124
124
|
}, [flushEvents]);
|
|
125
125
|
|
|
126
126
|
// ---- Session init / resume ----
|
|
127
|
+
// initTick is bumped whenever we recycle (sessionId went to null at runtime) so this effect re-fires.
|
|
128
|
+
const [initTick, setInitTick] = useState(0);
|
|
127
129
|
useEffect(() => {
|
|
128
130
|
let cancelled = false;
|
|
129
131
|
(async () => {
|
|
@@ -134,8 +136,17 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
134
136
|
const r = await fetch(`${serverUrl}/api/guided-support/sessions/${stored}/status`);
|
|
135
137
|
if (r.ok) {
|
|
136
138
|
const j = await r.json();
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
// If the stored session was already ended (server-side idle timeout while app
|
|
140
|
+
// was backgrounded), drop it and fall through to create a fresh one.
|
|
141
|
+
if (j.status === "ended") {
|
|
142
|
+
try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {}
|
|
143
|
+
} else {
|
|
144
|
+
if (!cancelled) { setSessionId(stored); setStatus(j.status || "active"); setIngestToken(j.ingestToken); }
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// 404 / not found — clear stale id and create fresh.
|
|
149
|
+
try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {}
|
|
139
150
|
}
|
|
140
151
|
}
|
|
141
152
|
const res = await fetch(`${serverUrl}/api/guided-support/sessions`, {
|
|
@@ -164,7 +175,7 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
164
175
|
}
|
|
165
176
|
})();
|
|
166
177
|
return () => { cancelled = true; };
|
|
167
|
-
}, [config.appId, serverUrl, config.appVersion, config.user]);
|
|
178
|
+
}, [config.appId, serverUrl, config.appVersion, config.user, initTick]);
|
|
168
179
|
|
|
169
180
|
// ---- Keyboard tracking (iOS auto-pause) ----
|
|
170
181
|
useEffect(() => {
|
|
@@ -173,7 +184,7 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
173
184
|
return () => { showSub.remove(); hideSub.remove(); };
|
|
174
185
|
}, []);
|
|
175
186
|
|
|
176
|
-
// ---- Status polling
|
|
187
|
+
// ---- Status polling: detects live transitions AND auto-end so we can recycle the session ----
|
|
177
188
|
useEffect(() => {
|
|
178
189
|
if (!sessionId) return;
|
|
179
190
|
const id = setInterval(async () => {
|
|
@@ -181,7 +192,16 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
181
192
|
const r = await fetch(`${serverUrl}/api/guided-support/sessions/${sessionId}/status`);
|
|
182
193
|
if (r.ok) {
|
|
183
194
|
const j = await r.json();
|
|
184
|
-
|
|
195
|
+
if (j.status === "ended") {
|
|
196
|
+
// Server idle-ended this session. Drop it and immediately recycle (init effect re-fires via initTick).
|
|
197
|
+
try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {}
|
|
198
|
+
setSessionId(null);
|
|
199
|
+
setIngestToken(null);
|
|
200
|
+
setStatus("idle");
|
|
201
|
+
setInitTick(t => t + 1);
|
|
202
|
+
} else {
|
|
203
|
+
setStatus(j.status);
|
|
204
|
+
}
|
|
185
205
|
}
|
|
186
206
|
} catch {}
|
|
187
207
|
}, 5000);
|
|
@@ -200,7 +220,14 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
200
220
|
try {
|
|
201
221
|
const ref = rootRef.current;
|
|
202
222
|
if (ref) {
|
|
203
|
-
|
|
223
|
+
// Cap capture width to keep payloads small. Dashboard viewport is 960px;
|
|
224
|
+
// 540px wide JPEGs render fine and load 4-6× faster than full-resolution captures.
|
|
225
|
+
const uri = await captureRef(ref as any, {
|
|
226
|
+
format: "jpg",
|
|
227
|
+
quality: liveMode ? 0.3 : 0.45,
|
|
228
|
+
result: "base64",
|
|
229
|
+
width: liveMode ? 480 : 540,
|
|
230
|
+
});
|
|
204
231
|
// upload via request-url then PUT
|
|
205
232
|
const blob = base64ToUint8Array(uri);
|
|
206
233
|
let path: string | null = null;
|
|
@@ -291,9 +318,21 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
291
318
|
case "guided_tour_request":
|
|
292
319
|
setTourState({ phase: "consent", agentName: msg.agentName });
|
|
293
320
|
break;
|
|
294
|
-
case "guided_tour_pointer":
|
|
295
|
-
|
|
321
|
+
case "guided_tour_pointer": {
|
|
322
|
+
// Prefer normalized coords (nx/ny in 0..1) and multiply by view size in points.
|
|
323
|
+
// Falls back to raw x/y for older dashboards.
|
|
324
|
+
const dim = viewSizeRef.current;
|
|
325
|
+
let px: number; let py: number;
|
|
326
|
+
if (typeof msg.nx === "number" && typeof msg.ny === "number" && dim.w > 0 && dim.h > 0) {
|
|
327
|
+
px = msg.nx * dim.w;
|
|
328
|
+
py = msg.ny * dim.h;
|
|
329
|
+
} else {
|
|
330
|
+
px = msg.x ?? 0;
|
|
331
|
+
py = msg.y ?? 0;
|
|
332
|
+
}
|
|
333
|
+
setPointer({ x: px, y: py, t: Date.now() });
|
|
296
334
|
break;
|
|
335
|
+
}
|
|
297
336
|
case "guided_tour_end":
|
|
298
337
|
setTourState({ phase: "idle" });
|
|
299
338
|
setPointer(null);
|
|
@@ -312,6 +351,8 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
312
351
|
// ---- Guided tour state ----
|
|
313
352
|
const [tourState, setTourState] = useState<{ phase: "idle" | "consent" | "active"; agentName?: string }>({ phase: "idle" });
|
|
314
353
|
const [pointer, setPointer] = useState<{ x: number; y: number; t: number } | null>(null);
|
|
354
|
+
// Captured root-View size (in points) so we can place the agent pointer correctly.
|
|
355
|
+
const viewSizeRef = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
|
|
315
356
|
|
|
316
357
|
function consentTour(accept: boolean) {
|
|
317
358
|
if (accept) {
|
|
@@ -352,6 +393,10 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
352
393
|
ref={rootRef}
|
|
353
394
|
collapsable={false}
|
|
354
395
|
style={{ flex: 1 }}
|
|
396
|
+
onLayout={(e) => {
|
|
397
|
+
const { width, height } = e.nativeEvent.layout;
|
|
398
|
+
viewSizeRef.current = { w: width, h: height };
|
|
399
|
+
}}
|
|
355
400
|
{...panResponder.panHandlers}
|
|
356
401
|
onTouchEndCapture={onTouchEndCapture}
|
|
357
402
|
>
|
|
@@ -389,7 +434,10 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
389
434
|
<Text onPress={endTourLocal} style={styles.endTourText}>End Tour</Text>
|
|
390
435
|
</View>
|
|
391
436
|
{pointer && (
|
|
392
|
-
|
|
437
|
+
<>
|
|
438
|
+
<View pointerEvents="none" style={[styles.pointerHalo, { left: pointer.x - 24, top: pointer.y - 24 }]} />
|
|
439
|
+
<View pointerEvents="none" style={[styles.pointer, { left: pointer.x - 12, top: pointer.y - 12 }]} />
|
|
440
|
+
</>
|
|
393
441
|
)}
|
|
394
442
|
</>
|
|
395
443
|
)}
|
|
@@ -441,8 +489,13 @@ const styles = StyleSheet.create({
|
|
|
441
489
|
borderRadius: 999, fontSize: 13, fontWeight: "600", overflow: "hidden",
|
|
442
490
|
},
|
|
443
491
|
pointer: {
|
|
444
|
-
position: "absolute", width:
|
|
445
|
-
backgroundColor: "rgba(239,68,68,
|
|
446
|
-
shadowColor: "#ef4444", shadowOpacity: 0.
|
|
492
|
+
position: "absolute", width: 24, height: 24, borderRadius: 12,
|
|
493
|
+
backgroundColor: "rgba(239,68,68,1)", borderWidth: 3, borderColor: "#fff",
|
|
494
|
+
shadowColor: "#ef4444", shadowOpacity: 0.9, shadowRadius: 8,
|
|
495
|
+
elevation: 12, zIndex: 10000,
|
|
496
|
+
},
|
|
497
|
+
pointerHalo: {
|
|
498
|
+
position: "absolute", width: 48, height: 48, borderRadius: 24,
|
|
499
|
+
backgroundColor: "rgba(239,68,68,0.25)", zIndex: 9999,
|
|
447
500
|
},
|
|
448
501
|
});
|