@sendoracloud/sdk-react-native 1.4.0 → 1.6.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/dist/index.cjs +86 -6
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +86 -6
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1760,15 +1760,16 @@ var import_react_native3 = require("react-native");
|
|
|
1760
1760
|
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
1761
1761
|
function resolveFlags(cfg) {
|
|
1762
1762
|
if (cfg === false) {
|
|
1763
|
-
return { appOpen: false, sessionStart: false, appBackground: false };
|
|
1763
|
+
return { appOpen: false, sessionStart: false, appBackground: false, engagement: false };
|
|
1764
1764
|
}
|
|
1765
1765
|
if (cfg === true || cfg === void 0) {
|
|
1766
|
-
return { appOpen: true, sessionStart: true, appBackground: true };
|
|
1766
|
+
return { appOpen: true, sessionStart: true, appBackground: true, engagement: true };
|
|
1767
1767
|
}
|
|
1768
1768
|
return {
|
|
1769
1769
|
appOpen: cfg.appOpen ?? true,
|
|
1770
1770
|
sessionStart: cfg.sessionStart ?? true,
|
|
1771
|
-
appBackground: cfg.appBackground ?? true
|
|
1771
|
+
appBackground: cfg.appBackground ?? true,
|
|
1772
|
+
engagement: cfg.engagement ?? true
|
|
1772
1773
|
};
|
|
1773
1774
|
}
|
|
1774
1775
|
function installAutoTrack(args) {
|
|
@@ -1799,15 +1800,21 @@ function installAutoTrack(args) {
|
|
|
1799
1800
|
if (flags.appOpen) {
|
|
1800
1801
|
args.fire("app.opened", { sessionId });
|
|
1801
1802
|
}
|
|
1802
|
-
if (flags.appBackground) {
|
|
1803
|
+
if (flags.appBackground || flags.engagement) {
|
|
1803
1804
|
const AppState = import_react_native3.AppState;
|
|
1804
1805
|
if (AppState && typeof AppState.addEventListener === "function") {
|
|
1805
1806
|
const handler = (state) => {
|
|
1806
1807
|
ensureSession(true);
|
|
1807
1808
|
if (state === "background" || state === "inactive") {
|
|
1808
|
-
|
|
1809
|
+
if (flags.engagement) args.onForeground?.(false);
|
|
1810
|
+
if (flags.appBackground) {
|
|
1811
|
+
args.fire("app.backgrounded", { sessionId: args.getSessionId() });
|
|
1812
|
+
}
|
|
1809
1813
|
} else if (state === "active") {
|
|
1810
|
-
|
|
1814
|
+
if (flags.appBackground) {
|
|
1815
|
+
args.fire("app.foregrounded", { sessionId: args.getSessionId() });
|
|
1816
|
+
}
|
|
1817
|
+
if (flags.engagement) args.onForeground?.(true);
|
|
1811
1818
|
}
|
|
1812
1819
|
};
|
|
1813
1820
|
const sub = AppState.addEventListener("change", handler);
|
|
@@ -1848,6 +1855,8 @@ var MAX_PUSH_METADATA_BYTES = 4096;
|
|
|
1848
1855
|
var MAX_USER_ID_LEN = 256;
|
|
1849
1856
|
var MAX_TRAITS_BYTES = 32 * 1024;
|
|
1850
1857
|
var MAX_PROPERTIES_BYTES = 32 * 1024;
|
|
1858
|
+
var MIN_ENGAGEMENT_MS = 250;
|
|
1859
|
+
var MAX_ENGAGEMENT_MS = 6 * 60 * 60 * 1e3;
|
|
1851
1860
|
function assertCSPRNG() {
|
|
1852
1861
|
const g = globalThis;
|
|
1853
1862
|
if (g.crypto?.getRandomValues) {
|
|
@@ -1898,6 +1907,12 @@ var SendoraSDK = class {
|
|
|
1898
1907
|
this.debug = false;
|
|
1899
1908
|
this.sessionId = "";
|
|
1900
1909
|
this.autoTrackCleanup = null;
|
|
1910
|
+
// --- Engagement time (foreground-only, Wave 75) ---
|
|
1911
|
+
this.engEnabled = false;
|
|
1912
|
+
this.engScreen = null;
|
|
1913
|
+
this.engAccumMs = 0;
|
|
1914
|
+
// >0 = accumulating since this timestamp; 0 = paused (backgrounded).
|
|
1915
|
+
this.engSegmentStart = 0;
|
|
1901
1916
|
/**
|
|
1902
1917
|
* Lazy-allocated stable session id for chatbot conversations.
|
|
1903
1918
|
* Survives only the JS lifetime — chatbot multi-turn context lives
|
|
@@ -1988,12 +2003,17 @@ var SendoraSDK = class {
|
|
|
1988
2003
|
});
|
|
1989
2004
|
this.ready = true;
|
|
1990
2005
|
if (config.autoTrack !== false) {
|
|
2006
|
+
this.engEnabled = config.autoTrack === true || config.autoTrack === void 0 || config.autoTrack.engagement !== false;
|
|
1991
2007
|
this.autoTrackCleanup = installAutoTrack({
|
|
1992
2008
|
config: this.config,
|
|
1993
2009
|
fire: (eventType, properties) => this.track(eventType, properties),
|
|
1994
2010
|
getSessionId: () => this.sessionId,
|
|
1995
2011
|
setSessionId: (id) => {
|
|
1996
2012
|
this.sessionId = id;
|
|
2013
|
+
},
|
|
2014
|
+
onForeground: (active) => {
|
|
2015
|
+
if (active) this.engResume();
|
|
2016
|
+
else this.engFlush();
|
|
1997
2017
|
}
|
|
1998
2018
|
});
|
|
1999
2019
|
}
|
|
@@ -2057,10 +2077,53 @@ var SendoraSDK = class {
|
|
|
2057
2077
|
if (typeof name !== "string" || name.length === 0 || name.length > 256) {
|
|
2058
2078
|
throw new Error("[sendoracloud] screen name must be a 1-256 char string.");
|
|
2059
2079
|
}
|
|
2080
|
+
this.engFlush();
|
|
2060
2081
|
void this.sendEvent("screen", {
|
|
2061
2082
|
eventType: "screen.viewed",
|
|
2062
2083
|
properties: { screenName: name, ...properties ?? {} }
|
|
2063
2084
|
});
|
|
2085
|
+
this.engEnter(name);
|
|
2086
|
+
}
|
|
2087
|
+
// --- Engagement timer (foreground-only, Wave 75) ---
|
|
2088
|
+
/** Bank the in-flight foreground segment into the accumulator + pause. */
|
|
2089
|
+
engBank() {
|
|
2090
|
+
if (this.engSegmentStart > 0) {
|
|
2091
|
+
this.engAccumMs += Date.now() - this.engSegmentStart;
|
|
2092
|
+
this.engSegmentStart = 0;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Emit `app.engagement` for the current screen with foreground-only
|
|
2097
|
+
* durationMs, then reset the accumulator. Sub-threshold (redirect
|
|
2098
|
+
* flicker) spans are dropped; absurd spans clamp to a 6h ceiling.
|
|
2099
|
+
* Never throws — engagement is best-effort instrumentation.
|
|
2100
|
+
*/
|
|
2101
|
+
engFlush() {
|
|
2102
|
+
if (!this.engEnabled || this.engScreen === null) return;
|
|
2103
|
+
this.engBank();
|
|
2104
|
+
let ms = this.engAccumMs;
|
|
2105
|
+
this.engAccumMs = 0;
|
|
2106
|
+
if (ms < MIN_ENGAGEMENT_MS) return;
|
|
2107
|
+
if (ms > MAX_ENGAGEMENT_MS) ms = MAX_ENGAGEMENT_MS;
|
|
2108
|
+
try {
|
|
2109
|
+
void this.sendEvent("track", {
|
|
2110
|
+
eventType: "app.engagement",
|
|
2111
|
+
properties: { durationMs: Math.round(ms), screen: this.engScreen, sessionId: this.sessionId }
|
|
2112
|
+
});
|
|
2113
|
+
} catch {
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
/** Start measuring a new screen (foreground from now). */
|
|
2117
|
+
engEnter(name) {
|
|
2118
|
+
if (!this.engEnabled) return;
|
|
2119
|
+
this.engScreen = name;
|
|
2120
|
+
this.engAccumMs = 0;
|
|
2121
|
+
this.engSegmentStart = Date.now();
|
|
2122
|
+
}
|
|
2123
|
+
/** Resume accumulation after the app returns to foreground. */
|
|
2124
|
+
engResume() {
|
|
2125
|
+
if (!this.engEnabled || this.engScreen === null) return;
|
|
2126
|
+
if (this.engSegmentStart === 0) this.engSegmentStart = Date.now();
|
|
2064
2127
|
}
|
|
2065
2128
|
/**
|
|
2066
2129
|
* Register a device push token (APNs or FCM) so server-side
|
|
@@ -2308,6 +2371,22 @@ var SendoraSDK = class {
|
|
|
2308
2371
|
const parsed = await res.json();
|
|
2309
2372
|
if (!parsed.success || !parsed.data) throw new Error("[sendoracloud] support.submitCsat: bad response");
|
|
2310
2373
|
return parsed.data;
|
|
2374
|
+
},
|
|
2375
|
+
getUnreadCount: async (widgetId) => {
|
|
2376
|
+
if (!self.config || !widgetId) return 0;
|
|
2377
|
+
const token = await self.auth.getAccessToken();
|
|
2378
|
+
if (!token) return 0;
|
|
2379
|
+
try {
|
|
2380
|
+
const res = await fetch(
|
|
2381
|
+
`${self.config.apiUrl}/api/v1/widgets/${encodeURIComponent(widgetId)}/my-tickets/unread-count`,
|
|
2382
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2383
|
+
);
|
|
2384
|
+
if (!res.ok) return 0;
|
|
2385
|
+
const parsed = await res.json();
|
|
2386
|
+
return parsed?.data?.count ?? 0;
|
|
2387
|
+
} catch {
|
|
2388
|
+
return 0;
|
|
2389
|
+
}
|
|
2311
2390
|
}
|
|
2312
2391
|
};
|
|
2313
2392
|
}
|
|
@@ -2605,6 +2684,7 @@ var SendoraSDK = class {
|
|
|
2605
2684
|
}
|
|
2606
2685
|
/** Tear down auto-track listeners. Call in test harnesses. */
|
|
2607
2686
|
destroy() {
|
|
2687
|
+
this.engFlush();
|
|
2608
2688
|
if (this.autoTrackCleanup) {
|
|
2609
2689
|
this.autoTrackCleanup();
|
|
2610
2690
|
this.autoTrackCleanup = null;
|
package/dist/index.d.cts
CHANGED
|
@@ -728,6 +728,10 @@ interface SendoraConfig {
|
|
|
728
728
|
* (idle >`sessionIdleMs` closes session). Default true.
|
|
729
729
|
* - `appBackground` — emit `app.backgrounded`/`app.foregrounded` on
|
|
730
730
|
* AppState transitions. Default true.
|
|
731
|
+
* - `engagement` — emit `app.engagement` with foreground-only `durationMs`
|
|
732
|
+
* per screen on the next `screen()` call + on app-background (GA4
|
|
733
|
+
* user_engagement parity). Default true. Only measures named screens —
|
|
734
|
+
* it has no effect until you start calling `screen()`.
|
|
731
735
|
*
|
|
732
736
|
* Note: screen view tracking is NOT auto — RN has no canonical router.
|
|
733
737
|
* Call `SendoraCloud.screen(name)` from your navigation listener.
|
|
@@ -736,6 +740,7 @@ interface SendoraConfig {
|
|
|
736
740
|
appOpen?: boolean;
|
|
737
741
|
sessionStart?: boolean;
|
|
738
742
|
appBackground?: boolean;
|
|
743
|
+
engagement?: boolean;
|
|
739
744
|
};
|
|
740
745
|
/** Idle threshold (ms) for session expiry; default 30 minutes. */
|
|
741
746
|
sessionIdleMs?: number;
|
|
@@ -827,6 +832,10 @@ declare class SendoraSDK {
|
|
|
827
832
|
private debug;
|
|
828
833
|
private sessionId;
|
|
829
834
|
private autoTrackCleanup;
|
|
835
|
+
private engEnabled;
|
|
836
|
+
private engScreen;
|
|
837
|
+
private engAccumMs;
|
|
838
|
+
private engSegmentStart;
|
|
830
839
|
/** Lazy-initialised in init() once we know the apiUrl + publicKey. */
|
|
831
840
|
auth: Auth;
|
|
832
841
|
/** Lazy-initialised in init(). Deep-link surface: create + handleUniversalLink + matchDeferred. */
|
|
@@ -847,6 +856,19 @@ declare class SendoraSDK {
|
|
|
847
856
|
track(eventType: string, properties?: TrackProperties): void;
|
|
848
857
|
/** RN equivalent of page view — call on route / screen change. */
|
|
849
858
|
screen(name: string, properties?: TrackProperties): void;
|
|
859
|
+
/** Bank the in-flight foreground segment into the accumulator + pause. */
|
|
860
|
+
private engBank;
|
|
861
|
+
/**
|
|
862
|
+
* Emit `app.engagement` for the current screen with foreground-only
|
|
863
|
+
* durationMs, then reset the accumulator. Sub-threshold (redirect
|
|
864
|
+
* flicker) spans are dropped; absurd spans clamp to a 6h ceiling.
|
|
865
|
+
* Never throws — engagement is best-effort instrumentation.
|
|
866
|
+
*/
|
|
867
|
+
private engFlush;
|
|
868
|
+
/** Start measuring a new screen (foreground from now). */
|
|
869
|
+
private engEnter;
|
|
870
|
+
/** Resume accumulation after the app returns to foreground. */
|
|
871
|
+
private engResume;
|
|
850
872
|
/**
|
|
851
873
|
* Register a device push token (APNs or FCM) so server-side
|
|
852
874
|
* workflows can target this user. The caller is responsible for
|
|
@@ -1008,6 +1030,13 @@ declare class SendoraSDK {
|
|
|
1008
1030
|
rating: number;
|
|
1009
1031
|
comment: string | null;
|
|
1010
1032
|
}>;
|
|
1033
|
+
/**
|
|
1034
|
+
* Wave 73 — unread support-ticket count for the in-app badge.
|
|
1035
|
+
* Bearer JWT auth via the SDK's current end-user session.
|
|
1036
|
+
* Returns 0 if no auth (anon or identified) is available.
|
|
1037
|
+
* Recommended cadence: AppState foreground + every 60s.
|
|
1038
|
+
*/
|
|
1039
|
+
getUnreadCount: (widgetId: string) => Promise<number>;
|
|
1011
1040
|
};
|
|
1012
1041
|
/**
|
|
1013
1042
|
* Knowledge-base search namespace. Mirrors backend `GET /kb/search`
|
package/dist/index.d.ts
CHANGED
|
@@ -728,6 +728,10 @@ interface SendoraConfig {
|
|
|
728
728
|
* (idle >`sessionIdleMs` closes session). Default true.
|
|
729
729
|
* - `appBackground` — emit `app.backgrounded`/`app.foregrounded` on
|
|
730
730
|
* AppState transitions. Default true.
|
|
731
|
+
* - `engagement` — emit `app.engagement` with foreground-only `durationMs`
|
|
732
|
+
* per screen on the next `screen()` call + on app-background (GA4
|
|
733
|
+
* user_engagement parity). Default true. Only measures named screens —
|
|
734
|
+
* it has no effect until you start calling `screen()`.
|
|
731
735
|
*
|
|
732
736
|
* Note: screen view tracking is NOT auto — RN has no canonical router.
|
|
733
737
|
* Call `SendoraCloud.screen(name)` from your navigation listener.
|
|
@@ -736,6 +740,7 @@ interface SendoraConfig {
|
|
|
736
740
|
appOpen?: boolean;
|
|
737
741
|
sessionStart?: boolean;
|
|
738
742
|
appBackground?: boolean;
|
|
743
|
+
engagement?: boolean;
|
|
739
744
|
};
|
|
740
745
|
/** Idle threshold (ms) for session expiry; default 30 minutes. */
|
|
741
746
|
sessionIdleMs?: number;
|
|
@@ -827,6 +832,10 @@ declare class SendoraSDK {
|
|
|
827
832
|
private debug;
|
|
828
833
|
private sessionId;
|
|
829
834
|
private autoTrackCleanup;
|
|
835
|
+
private engEnabled;
|
|
836
|
+
private engScreen;
|
|
837
|
+
private engAccumMs;
|
|
838
|
+
private engSegmentStart;
|
|
830
839
|
/** Lazy-initialised in init() once we know the apiUrl + publicKey. */
|
|
831
840
|
auth: Auth;
|
|
832
841
|
/** Lazy-initialised in init(). Deep-link surface: create + handleUniversalLink + matchDeferred. */
|
|
@@ -847,6 +856,19 @@ declare class SendoraSDK {
|
|
|
847
856
|
track(eventType: string, properties?: TrackProperties): void;
|
|
848
857
|
/** RN equivalent of page view — call on route / screen change. */
|
|
849
858
|
screen(name: string, properties?: TrackProperties): void;
|
|
859
|
+
/** Bank the in-flight foreground segment into the accumulator + pause. */
|
|
860
|
+
private engBank;
|
|
861
|
+
/**
|
|
862
|
+
* Emit `app.engagement` for the current screen with foreground-only
|
|
863
|
+
* durationMs, then reset the accumulator. Sub-threshold (redirect
|
|
864
|
+
* flicker) spans are dropped; absurd spans clamp to a 6h ceiling.
|
|
865
|
+
* Never throws — engagement is best-effort instrumentation.
|
|
866
|
+
*/
|
|
867
|
+
private engFlush;
|
|
868
|
+
/** Start measuring a new screen (foreground from now). */
|
|
869
|
+
private engEnter;
|
|
870
|
+
/** Resume accumulation after the app returns to foreground. */
|
|
871
|
+
private engResume;
|
|
850
872
|
/**
|
|
851
873
|
* Register a device push token (APNs or FCM) so server-side
|
|
852
874
|
* workflows can target this user. The caller is responsible for
|
|
@@ -1008,6 +1030,13 @@ declare class SendoraSDK {
|
|
|
1008
1030
|
rating: number;
|
|
1009
1031
|
comment: string | null;
|
|
1010
1032
|
}>;
|
|
1033
|
+
/**
|
|
1034
|
+
* Wave 73 — unread support-ticket count for the in-app badge.
|
|
1035
|
+
* Bearer JWT auth via the SDK's current end-user session.
|
|
1036
|
+
* Returns 0 if no auth (anon or identified) is available.
|
|
1037
|
+
* Recommended cadence: AppState foreground + every 60s.
|
|
1038
|
+
*/
|
|
1039
|
+
getUnreadCount: (widgetId: string) => Promise<number>;
|
|
1011
1040
|
};
|
|
1012
1041
|
/**
|
|
1013
1042
|
* Knowledge-base search namespace. Mirrors backend `GET /kb/search`
|
package/dist/index.js
CHANGED
|
@@ -1722,15 +1722,16 @@ import { AppState as RnAppState2 } from "react-native";
|
|
|
1722
1722
|
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
1723
1723
|
function resolveFlags(cfg) {
|
|
1724
1724
|
if (cfg === false) {
|
|
1725
|
-
return { appOpen: false, sessionStart: false, appBackground: false };
|
|
1725
|
+
return { appOpen: false, sessionStart: false, appBackground: false, engagement: false };
|
|
1726
1726
|
}
|
|
1727
1727
|
if (cfg === true || cfg === void 0) {
|
|
1728
|
-
return { appOpen: true, sessionStart: true, appBackground: true };
|
|
1728
|
+
return { appOpen: true, sessionStart: true, appBackground: true, engagement: true };
|
|
1729
1729
|
}
|
|
1730
1730
|
return {
|
|
1731
1731
|
appOpen: cfg.appOpen ?? true,
|
|
1732
1732
|
sessionStart: cfg.sessionStart ?? true,
|
|
1733
|
-
appBackground: cfg.appBackground ?? true
|
|
1733
|
+
appBackground: cfg.appBackground ?? true,
|
|
1734
|
+
engagement: cfg.engagement ?? true
|
|
1734
1735
|
};
|
|
1735
1736
|
}
|
|
1736
1737
|
function installAutoTrack(args) {
|
|
@@ -1761,15 +1762,21 @@ function installAutoTrack(args) {
|
|
|
1761
1762
|
if (flags.appOpen) {
|
|
1762
1763
|
args.fire("app.opened", { sessionId });
|
|
1763
1764
|
}
|
|
1764
|
-
if (flags.appBackground) {
|
|
1765
|
+
if (flags.appBackground || flags.engagement) {
|
|
1765
1766
|
const AppState = RnAppState2;
|
|
1766
1767
|
if (AppState && typeof AppState.addEventListener === "function") {
|
|
1767
1768
|
const handler = (state) => {
|
|
1768
1769
|
ensureSession(true);
|
|
1769
1770
|
if (state === "background" || state === "inactive") {
|
|
1770
|
-
|
|
1771
|
+
if (flags.engagement) args.onForeground?.(false);
|
|
1772
|
+
if (flags.appBackground) {
|
|
1773
|
+
args.fire("app.backgrounded", { sessionId: args.getSessionId() });
|
|
1774
|
+
}
|
|
1771
1775
|
} else if (state === "active") {
|
|
1772
|
-
|
|
1776
|
+
if (flags.appBackground) {
|
|
1777
|
+
args.fire("app.foregrounded", { sessionId: args.getSessionId() });
|
|
1778
|
+
}
|
|
1779
|
+
if (flags.engagement) args.onForeground?.(true);
|
|
1773
1780
|
}
|
|
1774
1781
|
};
|
|
1775
1782
|
const sub = AppState.addEventListener("change", handler);
|
|
@@ -1810,6 +1817,8 @@ var MAX_PUSH_METADATA_BYTES = 4096;
|
|
|
1810
1817
|
var MAX_USER_ID_LEN = 256;
|
|
1811
1818
|
var MAX_TRAITS_BYTES = 32 * 1024;
|
|
1812
1819
|
var MAX_PROPERTIES_BYTES = 32 * 1024;
|
|
1820
|
+
var MIN_ENGAGEMENT_MS = 250;
|
|
1821
|
+
var MAX_ENGAGEMENT_MS = 6 * 60 * 60 * 1e3;
|
|
1813
1822
|
function assertCSPRNG() {
|
|
1814
1823
|
const g = globalThis;
|
|
1815
1824
|
if (g.crypto?.getRandomValues) {
|
|
@@ -1860,6 +1869,12 @@ var SendoraSDK = class {
|
|
|
1860
1869
|
this.debug = false;
|
|
1861
1870
|
this.sessionId = "";
|
|
1862
1871
|
this.autoTrackCleanup = null;
|
|
1872
|
+
// --- Engagement time (foreground-only, Wave 75) ---
|
|
1873
|
+
this.engEnabled = false;
|
|
1874
|
+
this.engScreen = null;
|
|
1875
|
+
this.engAccumMs = 0;
|
|
1876
|
+
// >0 = accumulating since this timestamp; 0 = paused (backgrounded).
|
|
1877
|
+
this.engSegmentStart = 0;
|
|
1863
1878
|
/**
|
|
1864
1879
|
* Lazy-allocated stable session id for chatbot conversations.
|
|
1865
1880
|
* Survives only the JS lifetime — chatbot multi-turn context lives
|
|
@@ -1950,12 +1965,17 @@ var SendoraSDK = class {
|
|
|
1950
1965
|
});
|
|
1951
1966
|
this.ready = true;
|
|
1952
1967
|
if (config.autoTrack !== false) {
|
|
1968
|
+
this.engEnabled = config.autoTrack === true || config.autoTrack === void 0 || config.autoTrack.engagement !== false;
|
|
1953
1969
|
this.autoTrackCleanup = installAutoTrack({
|
|
1954
1970
|
config: this.config,
|
|
1955
1971
|
fire: (eventType, properties) => this.track(eventType, properties),
|
|
1956
1972
|
getSessionId: () => this.sessionId,
|
|
1957
1973
|
setSessionId: (id) => {
|
|
1958
1974
|
this.sessionId = id;
|
|
1975
|
+
},
|
|
1976
|
+
onForeground: (active) => {
|
|
1977
|
+
if (active) this.engResume();
|
|
1978
|
+
else this.engFlush();
|
|
1959
1979
|
}
|
|
1960
1980
|
});
|
|
1961
1981
|
}
|
|
@@ -2019,10 +2039,53 @@ var SendoraSDK = class {
|
|
|
2019
2039
|
if (typeof name !== "string" || name.length === 0 || name.length > 256) {
|
|
2020
2040
|
throw new Error("[sendoracloud] screen name must be a 1-256 char string.");
|
|
2021
2041
|
}
|
|
2042
|
+
this.engFlush();
|
|
2022
2043
|
void this.sendEvent("screen", {
|
|
2023
2044
|
eventType: "screen.viewed",
|
|
2024
2045
|
properties: { screenName: name, ...properties ?? {} }
|
|
2025
2046
|
});
|
|
2047
|
+
this.engEnter(name);
|
|
2048
|
+
}
|
|
2049
|
+
// --- Engagement timer (foreground-only, Wave 75) ---
|
|
2050
|
+
/** Bank the in-flight foreground segment into the accumulator + pause. */
|
|
2051
|
+
engBank() {
|
|
2052
|
+
if (this.engSegmentStart > 0) {
|
|
2053
|
+
this.engAccumMs += Date.now() - this.engSegmentStart;
|
|
2054
|
+
this.engSegmentStart = 0;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Emit `app.engagement` for the current screen with foreground-only
|
|
2059
|
+
* durationMs, then reset the accumulator. Sub-threshold (redirect
|
|
2060
|
+
* flicker) spans are dropped; absurd spans clamp to a 6h ceiling.
|
|
2061
|
+
* Never throws — engagement is best-effort instrumentation.
|
|
2062
|
+
*/
|
|
2063
|
+
engFlush() {
|
|
2064
|
+
if (!this.engEnabled || this.engScreen === null) return;
|
|
2065
|
+
this.engBank();
|
|
2066
|
+
let ms = this.engAccumMs;
|
|
2067
|
+
this.engAccumMs = 0;
|
|
2068
|
+
if (ms < MIN_ENGAGEMENT_MS) return;
|
|
2069
|
+
if (ms > MAX_ENGAGEMENT_MS) ms = MAX_ENGAGEMENT_MS;
|
|
2070
|
+
try {
|
|
2071
|
+
void this.sendEvent("track", {
|
|
2072
|
+
eventType: "app.engagement",
|
|
2073
|
+
properties: { durationMs: Math.round(ms), screen: this.engScreen, sessionId: this.sessionId }
|
|
2074
|
+
});
|
|
2075
|
+
} catch {
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/** Start measuring a new screen (foreground from now). */
|
|
2079
|
+
engEnter(name) {
|
|
2080
|
+
if (!this.engEnabled) return;
|
|
2081
|
+
this.engScreen = name;
|
|
2082
|
+
this.engAccumMs = 0;
|
|
2083
|
+
this.engSegmentStart = Date.now();
|
|
2084
|
+
}
|
|
2085
|
+
/** Resume accumulation after the app returns to foreground. */
|
|
2086
|
+
engResume() {
|
|
2087
|
+
if (!this.engEnabled || this.engScreen === null) return;
|
|
2088
|
+
if (this.engSegmentStart === 0) this.engSegmentStart = Date.now();
|
|
2026
2089
|
}
|
|
2027
2090
|
/**
|
|
2028
2091
|
* Register a device push token (APNs or FCM) so server-side
|
|
@@ -2270,6 +2333,22 @@ var SendoraSDK = class {
|
|
|
2270
2333
|
const parsed = await res.json();
|
|
2271
2334
|
if (!parsed.success || !parsed.data) throw new Error("[sendoracloud] support.submitCsat: bad response");
|
|
2272
2335
|
return parsed.data;
|
|
2336
|
+
},
|
|
2337
|
+
getUnreadCount: async (widgetId) => {
|
|
2338
|
+
if (!self.config || !widgetId) return 0;
|
|
2339
|
+
const token = await self.auth.getAccessToken();
|
|
2340
|
+
if (!token) return 0;
|
|
2341
|
+
try {
|
|
2342
|
+
const res = await fetch(
|
|
2343
|
+
`${self.config.apiUrl}/api/v1/widgets/${encodeURIComponent(widgetId)}/my-tickets/unread-count`,
|
|
2344
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2345
|
+
);
|
|
2346
|
+
if (!res.ok) return 0;
|
|
2347
|
+
const parsed = await res.json();
|
|
2348
|
+
return parsed?.data?.count ?? 0;
|
|
2349
|
+
} catch {
|
|
2350
|
+
return 0;
|
|
2351
|
+
}
|
|
2273
2352
|
}
|
|
2274
2353
|
};
|
|
2275
2354
|
}
|
|
@@ -2567,6 +2646,7 @@ var SendoraSDK = class {
|
|
|
2567
2646
|
}
|
|
2568
2647
|
/** Tear down auto-track listeners. Call in test harnesses. */
|
|
2569
2648
|
destroy() {
|
|
2649
|
+
this.engFlush();
|
|
2570
2650
|
if (this.autoTrackCleanup) {
|
|
2571
2651
|
this.autoTrackCleanup();
|
|
2572
2652
|
this.autoTrackCleanup = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth, deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push token registration requires a Dev Client (EAS Build or `npx expo prebuild`).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|