@usergist/feedback-react-native 0.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.
Files changed (242) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +231 -0
  4. package/UserGistFeedback.podspec +31 -0
  5. package/UserGistFeedbackExtension.podspec +23 -0
  6. package/android/build.gradle +92 -0
  7. package/android/consumer-rules.pro +5 -0
  8. package/android/src/main/AndroidManifest.xml +31 -0
  9. package/android/src/main/java/studio/usergist/feedback/UserGistFirebaseMessagingService.kt +153 -0
  10. package/android/src/main/java/studio/usergist/feedback/UserGistPushModule.kt +344 -0
  11. package/android/src/main/java/studio/usergist/feedback/UserGistPushPackage.kt +18 -0
  12. package/dist/NativeUserGistPush.d.ts +35 -0
  13. package/dist/NativeUserGistPush.d.ts.map +1 -0
  14. package/dist/NativeUserGistPush.js +13 -0
  15. package/dist/NativeUserGistPush.js.map +1 -0
  16. package/dist/Provider.d.ts +7 -0
  17. package/dist/Provider.d.ts.map +1 -0
  18. package/dist/Provider.js +300 -0
  19. package/dist/Provider.js.map +1 -0
  20. package/dist/Push.d.ts +103 -0
  21. package/dist/Push.d.ts.map +1 -0
  22. package/dist/Push.js +225 -0
  23. package/dist/Push.js.map +1 -0
  24. package/dist/UserGist.d.ts +210 -0
  25. package/dist/UserGist.d.ts.map +1 -0
  26. package/dist/UserGist.js +1246 -0
  27. package/dist/UserGist.js.map +1 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +4 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/internal/consent.d.ts +17 -0
  33. package/dist/internal/consent.d.ts.map +1 -0
  34. package/dist/internal/consent.js +98 -0
  35. package/dist/internal/consent.js.map +1 -0
  36. package/dist/internal/context.d.ts +12 -0
  37. package/dist/internal/context.d.ts.map +1 -0
  38. package/dist/internal/context.js +70 -0
  39. package/dist/internal/context.js.map +1 -0
  40. package/dist/internal/debug.d.ts +14 -0
  41. package/dist/internal/debug.d.ts.map +1 -0
  42. package/dist/internal/debug.js +73 -0
  43. package/dist/internal/debug.js.map +1 -0
  44. package/dist/internal/engine.d.ts +93 -0
  45. package/dist/internal/engine.d.ts.map +1 -0
  46. package/dist/internal/engine.js +904 -0
  47. package/dist/internal/engine.js.map +1 -0
  48. package/dist/internal/events.d.ts +49 -0
  49. package/dist/internal/events.d.ts.map +1 -0
  50. package/dist/internal/events.js +71 -0
  51. package/dist/internal/events.js.map +1 -0
  52. package/dist/internal/frequency-cap.d.ts +15 -0
  53. package/dist/internal/frequency-cap.d.ts.map +1 -0
  54. package/dist/internal/frequency-cap.js +84 -0
  55. package/dist/internal/frequency-cap.js.map +1 -0
  56. package/dist/internal/identity.d.ts +14 -0
  57. package/dist/internal/identity.d.ts.map +1 -0
  58. package/dist/internal/identity.js +113 -0
  59. package/dist/internal/identity.js.map +1 -0
  60. package/dist/internal/inapp-matcher.d.ts +13 -0
  61. package/dist/internal/inapp-matcher.d.ts.map +1 -0
  62. package/dist/internal/inapp-matcher.js +65 -0
  63. package/dist/internal/inapp-matcher.js.map +1 -0
  64. package/dist/internal/inapp-rules-cache.d.ts +16 -0
  65. package/dist/internal/inapp-rules-cache.d.ts.map +1 -0
  66. package/dist/internal/inapp-rules-cache.js +88 -0
  67. package/dist/internal/inapp-rules-cache.js.map +1 -0
  68. package/dist/internal/instruction-dedupe.d.ts +16 -0
  69. package/dist/internal/instruction-dedupe.d.ts.map +1 -0
  70. package/dist/internal/instruction-dedupe.js +52 -0
  71. package/dist/internal/instruction-dedupe.js.map +1 -0
  72. package/dist/internal/lifecycle.d.ts +12 -0
  73. package/dist/internal/lifecycle.d.ts.map +1 -0
  74. package/dist/internal/lifecycle.js +97 -0
  75. package/dist/internal/lifecycle.js.map +1 -0
  76. package/dist/internal/modal-coordinator.d.ts +4 -0
  77. package/dist/internal/modal-coordinator.d.ts.map +1 -0
  78. package/dist/internal/modal-coordinator.js +43 -0
  79. package/dist/internal/modal-coordinator.js.map +1 -0
  80. package/dist/internal/modal-queue.d.ts +9 -0
  81. package/dist/internal/modal-queue.d.ts.map +1 -0
  82. package/dist/internal/modal-queue.js +32 -0
  83. package/dist/internal/modal-queue.js.map +1 -0
  84. package/dist/internal/mutation-queue.d.ts +23 -0
  85. package/dist/internal/mutation-queue.d.ts.map +1 -0
  86. package/dist/internal/mutation-queue.js +66 -0
  87. package/dist/internal/mutation-queue.js.map +1 -0
  88. package/dist/internal/prompt-answers.d.ts +6 -0
  89. package/dist/internal/prompt-answers.d.ts.map +1 -0
  90. package/dist/internal/prompt-answers.js +10 -0
  91. package/dist/internal/prompt-answers.js.map +1 -0
  92. package/dist/internal/prompt-flow.d.ts +8 -0
  93. package/dist/internal/prompt-flow.d.ts.map +1 -0
  94. package/dist/internal/prompt-flow.js +26 -0
  95. package/dist/internal/prompt-flow.js.map +1 -0
  96. package/dist/internal/queue.d.ts +17 -0
  97. package/dist/internal/queue.d.ts.map +1 -0
  98. package/dist/internal/queue.js +172 -0
  99. package/dist/internal/queue.js.map +1 -0
  100. package/dist/internal/requests.d.ts +22 -0
  101. package/dist/internal/requests.d.ts.map +1 -0
  102. package/dist/internal/requests.js +146 -0
  103. package/dist/internal/requests.js.map +1 -0
  104. package/dist/internal/rules-cache.d.ts +16 -0
  105. package/dist/internal/rules-cache.d.ts.map +1 -0
  106. package/dist/internal/rules-cache.js +86 -0
  107. package/dist/internal/rules-cache.js.map +1 -0
  108. package/dist/internal/storage.d.ts +36 -0
  109. package/dist/internal/storage.d.ts.map +1 -0
  110. package/dist/internal/storage.js +213 -0
  111. package/dist/internal/storage.js.map +1 -0
  112. package/dist/internal/survey-completion.d.ts +10 -0
  113. package/dist/internal/survey-completion.d.ts.map +1 -0
  114. package/dist/internal/survey-completion.js +11 -0
  115. package/dist/internal/survey-completion.js.map +1 -0
  116. package/dist/internal/survey-matcher.d.ts +21 -0
  117. package/dist/internal/survey-matcher.d.ts.map +1 -0
  118. package/dist/internal/survey-matcher.js +139 -0
  119. package/dist/internal/survey-matcher.js.map +1 -0
  120. package/dist/internal/survey-rules-cache.d.ts +17 -0
  121. package/dist/internal/survey-rules-cache.d.ts.map +1 -0
  122. package/dist/internal/survey-rules-cache.js +91 -0
  123. package/dist/internal/survey-rules-cache.js.map +1 -0
  124. package/dist/internal/survey-store.d.ts +19 -0
  125. package/dist/internal/survey-store.d.ts.map +1 -0
  126. package/dist/internal/survey-store.js +59 -0
  127. package/dist/internal/survey-store.js.map +1 -0
  128. package/dist/internal/transport.d.ts +210 -0
  129. package/dist/internal/transport.d.ts.map +1 -0
  130. package/dist/internal/transport.js +498 -0
  131. package/dist/internal/transport.js.map +1 -0
  132. package/dist/internal/trigger-matcher.d.ts +21 -0
  133. package/dist/internal/trigger-matcher.d.ts.map +1 -0
  134. package/dist/internal/trigger-matcher.js +114 -0
  135. package/dist/internal/trigger-matcher.js.map +1 -0
  136. package/dist/internal/types.d.ts +89 -0
  137. package/dist/internal/types.d.ts.map +1 -0
  138. package/dist/internal/types.js +2 -0
  139. package/dist/internal/types.js.map +1 -0
  140. package/dist/internal/user-state.d.ts +11 -0
  141. package/dist/internal/user-state.d.ts.map +1 -0
  142. package/dist/internal/user-state.js +118 -0
  143. package/dist/internal/user-state.js.map +1 -0
  144. package/dist/native/events.d.ts +27 -0
  145. package/dist/native/events.d.ts.map +1 -0
  146. package/dist/native/events.js +12 -0
  147. package/dist/native/events.js.map +1 -0
  148. package/dist/native/push-bridge.d.ts +20 -0
  149. package/dist/native/push-bridge.d.ts.map +1 -0
  150. package/dist/native/push-bridge.js +92 -0
  151. package/dist/native/push-bridge.js.map +1 -0
  152. package/dist/preview/PromptShell.d.ts +9 -0
  153. package/dist/preview/PromptShell.d.ts.map +1 -0
  154. package/dist/preview/PromptShell.js +128 -0
  155. package/dist/preview/PromptShell.js.map +1 -0
  156. package/dist/preview/SurveyShell.d.ts +15 -0
  157. package/dist/preview/SurveyShell.d.ts.map +1 -0
  158. package/dist/preview/SurveyShell.js +197 -0
  159. package/dist/preview/SurveyShell.js.map +1 -0
  160. package/dist/preview/index.d.ts +4 -0
  161. package/dist/preview/index.d.ts.map +1 -0
  162. package/dist/preview/index.js +16 -0
  163. package/dist/preview/index.js.map +1 -0
  164. package/dist/ui/InAppMessageView.d.ts +12 -0
  165. package/dist/ui/InAppMessageView.d.ts.map +1 -0
  166. package/dist/ui/InAppMessageView.js +260 -0
  167. package/dist/ui/InAppMessageView.js.map +1 -0
  168. package/dist/ui/PromptSheet.d.ts +12 -0
  169. package/dist/ui/PromptSheet.d.ts.map +1 -0
  170. package/dist/ui/PromptSheet.js +285 -0
  171. package/dist/ui/PromptSheet.js.map +1 -0
  172. package/dist/ui/QuestionImageHeader.d.ts +12 -0
  173. package/dist/ui/QuestionImageHeader.d.ts.map +1 -0
  174. package/dist/ui/QuestionImageHeader.js +22 -0
  175. package/dist/ui/QuestionImageHeader.js.map +1 -0
  176. package/dist/ui/SurveyView.d.ts +25 -0
  177. package/dist/ui/SurveyView.d.ts.map +1 -0
  178. package/dist/ui/SurveyView.js +478 -0
  179. package/dist/ui/SurveyView.js.map +1 -0
  180. package/dist/ui/animations.d.ts +4 -0
  181. package/dist/ui/animations.d.ts.map +1 -0
  182. package/dist/ui/animations.js +24 -0
  183. package/dist/ui/animations.js.map +1 -0
  184. package/dist/ui/questions/MultipleChoiceQuestion.d.ts +12 -0
  185. package/dist/ui/questions/MultipleChoiceQuestion.d.ts.map +1 -0
  186. package/dist/ui/questions/MultipleChoiceQuestion.js +41 -0
  187. package/dist/ui/questions/MultipleChoiceQuestion.js.map +1 -0
  188. package/dist/ui/questions/NpsQuestion.d.ts +14 -0
  189. package/dist/ui/questions/NpsQuestion.d.ts.map +1 -0
  190. package/dist/ui/questions/NpsQuestion.js +73 -0
  191. package/dist/ui/questions/NpsQuestion.js.map +1 -0
  192. package/dist/ui/questions/RatingQuestion.d.ts +21 -0
  193. package/dist/ui/questions/RatingQuestion.d.ts.map +1 -0
  194. package/dist/ui/questions/RatingQuestion.js +91 -0
  195. package/dist/ui/questions/RatingQuestion.js.map +1 -0
  196. package/dist/ui/questions/ShortTextQuestion.d.ts +12 -0
  197. package/dist/ui/questions/ShortTextQuestion.d.ts.map +1 -0
  198. package/dist/ui/questions/ShortTextQuestion.js +25 -0
  199. package/dist/ui/questions/ShortTextQuestion.js.map +1 -0
  200. package/dist/ui/requests/BoardView.d.ts +9 -0
  201. package/dist/ui/requests/BoardView.d.ts.map +1 -0
  202. package/dist/ui/requests/BoardView.js +270 -0
  203. package/dist/ui/requests/BoardView.js.map +1 -0
  204. package/dist/ui/requests/CommentsSection.d.ts +7 -0
  205. package/dist/ui/requests/CommentsSection.d.ts.map +1 -0
  206. package/dist/ui/requests/CommentsSection.js +254 -0
  207. package/dist/ui/requests/CommentsSection.js.map +1 -0
  208. package/dist/ui/requests/DetailView.d.ts +8 -0
  209. package/dist/ui/requests/DetailView.d.ts.map +1 -0
  210. package/dist/ui/requests/DetailView.js +206 -0
  211. package/dist/ui/requests/DetailView.js.map +1 -0
  212. package/dist/ui/requests/RequestsHost.d.ts +12 -0
  213. package/dist/ui/requests/RequestsHost.d.ts.map +1 -0
  214. package/dist/ui/requests/RequestsHost.js +58 -0
  215. package/dist/ui/requests/RequestsHost.js.map +1 -0
  216. package/dist/ui/requests/StatusPill.d.ts +6 -0
  217. package/dist/ui/requests/StatusPill.d.ts.map +1 -0
  218. package/dist/ui/requests/StatusPill.js +25 -0
  219. package/dist/ui/requests/StatusPill.js.map +1 -0
  220. package/dist/ui/requests/SubmitView.d.ts +8 -0
  221. package/dist/ui/requests/SubmitView.d.ts.map +1 -0
  222. package/dist/ui/requests/SubmitView.js +94 -0
  223. package/dist/ui/requests/SubmitView.js.map +1 -0
  224. package/dist/ui/requests/shared.d.ts +21 -0
  225. package/dist/ui/requests/shared.d.ts.map +1 -0
  226. package/dist/ui/requests/shared.js +74 -0
  227. package/dist/ui/requests/shared.js.map +1 -0
  228. package/dist/ui/survey-questions.d.ts +20 -0
  229. package/dist/ui/survey-questions.d.ts.map +1 -0
  230. package/dist/ui/survey-questions.js +211 -0
  231. package/dist/ui/survey-questions.js.map +1 -0
  232. package/dist/ui/theme.d.ts +15 -0
  233. package/dist/ui/theme.d.ts.map +1 -0
  234. package/dist/ui/theme.js +31 -0
  235. package/dist/ui/theme.js.map +1 -0
  236. package/ios/Extension/UserGistNotificationService.swift +292 -0
  237. package/ios/UserGistPush.h +16 -0
  238. package/ios/UserGistPush.mm +158 -0
  239. package/ios/UserGistPushImpl.swift +399 -0
  240. package/ios/UserGistPushSwizzle.m +168 -0
  241. package/package.json +89 -0
  242. package/react-native.config.js +16 -0
@@ -0,0 +1,904 @@
1
+ // The engine owns all singleton state and orchestrates the modules.
2
+ // `UserGist.ts` wraps these functions in a thin, try/catch-guarded public API.
3
+ import { APP_OPEN_EVENT_NAME, APP_VERSION_CHANGED_EVENT_NAME } from '@usergist/sdk-core/mobile';
4
+ import { STORAGE_KEYS, createStorageScope, } from './storage.js';
5
+ import { createIdentityManager, generateEventId } from './identity.js';
6
+ import { createConsentManager } from './consent.js';
7
+ import { createEventQueue } from './queue.js';
8
+ import { createTransport, PermanentHttpError } from './transport.js';
9
+ import { createRulesCache } from './rules-cache.js';
10
+ import { createSurveyRulesCache, } from './survey-rules-cache.js';
11
+ import { createFrequencyCapManager } from './frequency-cap.js';
12
+ import { createUserStateStore } from './user-state.js';
13
+ import { createTriggerMatcher } from './trigger-matcher.js';
14
+ import { createSurveyMatcher } from './survey-matcher.js';
15
+ import { createInAppRulesCache } from './inapp-rules-cache.js';
16
+ import { createInAppMatcher } from './inapp-matcher.js';
17
+ import { createLifecycleManager } from './lifecycle.js';
18
+ import { createContextProvider } from './context.js';
19
+ import { createEventBus } from './events.js';
20
+ import { setDebugEnabled, reportError, debugLog } from './debug.js';
21
+ import { createMutationQueue } from './mutation-queue.js';
22
+ import { createLocalInstructionDedupe, } from './instruction-dedupe.js';
23
+ const ENVIRONMENT_API_URLS = {
24
+ production: 'https://api.usergist.com',
25
+ // `environment` describes the customer's app data, not a UserGist
26
+ // deployment tier. Production and staging apps therefore use the same
27
+ // public edge; their write keys and app ids keep data isolated.
28
+ staging: 'https://api.usergist.com',
29
+ development: 'http://localhost:28743',
30
+ };
31
+ const DEFAULTS = {
32
+ environment: 'production',
33
+ flushIntervalMs: 15_000,
34
+ flushBatchSize: 100,
35
+ maxQueueSize: 1000,
36
+ triggerSyncIntervalMs: 300_000,
37
+ debug: false,
38
+ };
39
+ export function resolveConfig(config) {
40
+ if (!config || typeof config.writeKey !== 'string' || config.writeKey.length === 0) {
41
+ throw new Error('UserGist.init requires a writeKey');
42
+ }
43
+ const environment = config.environment ?? DEFAULTS.environment;
44
+ const apiUrl = config.apiUrl ?? ENVIRONMENT_API_URLS[environment];
45
+ if (environment === 'production' && !apiUrl.startsWith('https://')) {
46
+ throw new Error('UserGist production apiUrl must use HTTPS');
47
+ }
48
+ return {
49
+ writeKey: config.writeKey,
50
+ apiUrl,
51
+ environment,
52
+ flushIntervalMs: config.flushIntervalMs ?? DEFAULTS.flushIntervalMs,
53
+ flushBatchSize: config.flushBatchSize ?? DEFAULTS.flushBatchSize,
54
+ maxQueueSize: config.maxQueueSize ?? DEFAULTS.maxQueueSize,
55
+ triggerSyncIntervalMs: config.triggerSyncIntervalMs ?? DEFAULTS.triggerSyncIntervalMs,
56
+ debug: config.debug ?? DEFAULTS.debug,
57
+ appVersion: config.appVersion?.trim() || null,
58
+ };
59
+ }
60
+ export function createEngine(config) {
61
+ const resolved = resolveConfig(config);
62
+ setDebugEnabled(resolved.debug);
63
+ const storage = createStorageScope(resolved.writeKey);
64
+ const identity = createIdentityManager(storage);
65
+ const consent = createConsentManager(storage);
66
+ const queue = createEventQueue(storage, resolved.maxQueueSize);
67
+ const mutations = createMutationQueue(storage);
68
+ const localInstructionDedupe = createLocalInstructionDedupe(storage);
69
+ const transport = createTransport({ writeKey: resolved.writeKey, apiUrl: resolved.apiUrl });
70
+ const rules = createRulesCache(storage, transport, resolved.triggerSyncIntervalMs);
71
+ const surveyRules = createSurveyRulesCache(storage, transport, resolved.triggerSyncIntervalMs);
72
+ const inAppRules = createInAppRulesCache(storage, transport, resolved.triggerSyncIntervalMs);
73
+ const caps = createFrequencyCapManager(storage);
74
+ const userState = createUserStateStore(storage);
75
+ const events = createEventBus();
76
+ const matcher = createTriggerMatcher({
77
+ rulesCache: rules,
78
+ frequencyCaps: caps,
79
+ userState,
80
+ consent,
81
+ events,
82
+ });
83
+ const surveyMatcher = createSurveyMatcher({
84
+ rulesCache: surveyRules,
85
+ frequencyCaps: caps,
86
+ userState,
87
+ consent,
88
+ events,
89
+ });
90
+ const inAppMatcher = createInAppMatcher({
91
+ rulesCache: inAppRules,
92
+ consent,
93
+ events,
94
+ });
95
+ const context = createContextProvider(resolved.appVersion);
96
+ // Ref lets us forward-reference the fully-built engine from the
97
+ // lifecycle handlers (which are only invoked asynchronously).
98
+ const ref = { value: null };
99
+ function eng() {
100
+ if (!ref.value)
101
+ throw new Error('engine not ready');
102
+ return ref.value;
103
+ }
104
+ const lifecycle = createLifecycleManager({
105
+ onForeground: () => {
106
+ const e = eng();
107
+ void ensureHydrated(e).then(async () => {
108
+ await refreshTargetingRules(e);
109
+ emitAppOpenWhenConsentReady(e);
110
+ void flushNow(e);
111
+ void pollSurveyOffers(e);
112
+ void pollInstructions(e);
113
+ });
114
+ },
115
+ onBackground: () => {
116
+ void flushNow(eng());
117
+ },
118
+ onSyncTick: () => {
119
+ const e = eng();
120
+ void refreshTargetingRules(e);
121
+ void flushMutations(e);
122
+ void pollSurveyOffers(e);
123
+ void pollInstructions(e);
124
+ },
125
+ syncIntervalMs: resolved.triggerSyncIntervalMs,
126
+ });
127
+ const engine = {
128
+ config: resolved,
129
+ storage,
130
+ identity,
131
+ consent,
132
+ queue,
133
+ mutations,
134
+ transport,
135
+ rules,
136
+ surveyRules,
137
+ inAppRules,
138
+ caps,
139
+ userState,
140
+ matcher,
141
+ surveyMatcher,
142
+ inAppMatcher,
143
+ lifecycle,
144
+ context,
145
+ events,
146
+ hydrated: false,
147
+ hydratingPromise: null,
148
+ flushTimer: null,
149
+ flushPromise: null,
150
+ mutationFlushPromise: null,
151
+ subjectToken: null,
152
+ sessionPromise: null,
153
+ instructionPollPromise: null,
154
+ resetGeneration: 0,
155
+ resetting: false,
156
+ localInstructionDedupe,
157
+ locallyHandledSurveyIds: new Set(),
158
+ themeOverride: null,
159
+ lastPushToken: null,
160
+ lastPushRegistrationKey: null,
161
+ lastPushRegistrationAt: 0,
162
+ };
163
+ ref.value = engine;
164
+ queue.onOverflow((dropped) => {
165
+ debugLog('queue overflow', { dropped, queueSize: queue.size() });
166
+ });
167
+ events.on('promptShown', (payload) => {
168
+ matcher.recordShown(payload.promptId, payload.shownAt);
169
+ });
170
+ consent.subscribe((s) => {
171
+ if (!s.analytics)
172
+ engine.queue.removePurpose('analytics');
173
+ if (!s.feedback) {
174
+ engine.queue.removePurpose('feedback');
175
+ void engine.mutations.removePurpose('feedback');
176
+ engine.matcher.resetPending();
177
+ }
178
+ if (!s.survey) {
179
+ void engine.mutations.removePurpose('survey');
180
+ engine.surveyMatcher.resetPending();
181
+ }
182
+ if (s.feedback || s.analytics)
183
+ void flushNow(engine);
184
+ });
185
+ return engine;
186
+ }
187
+ export async function ensureHydrated(engine) {
188
+ if (engine.hydrated)
189
+ return;
190
+ if (engine.hydratingPromise)
191
+ return engine.hydratingPromise;
192
+ engine.hydratingPromise = (async () => {
193
+ try {
194
+ await Promise.all([
195
+ engine.identity.hydrate(),
196
+ engine.consent.hydrate(),
197
+ engine.queue.hydrate(),
198
+ engine.mutations.hydrate(),
199
+ engine.caps.hydrate(),
200
+ engine.userState.hydrate(),
201
+ engine.rules.hydrate(),
202
+ engine.surveyRules.hydrate(),
203
+ engine.inAppRules.hydrate(),
204
+ engine.localInstructionDedupe.hydrate(),
205
+ ]);
206
+ await ensureSubjectSession(engine);
207
+ engine.hydrated = true;
208
+ }
209
+ catch (e) {
210
+ reportError('ensureHydrated failed', e);
211
+ }
212
+ finally {
213
+ // A transient session/storage failure must not permanently poison this
214
+ // engine instance. Leave successful hydration fast, but allow a later
215
+ // public call or foreground transition to retry a failed attempt.
216
+ if (!engine.hydrated)
217
+ engine.hydratingPromise = null;
218
+ }
219
+ })();
220
+ return engine.hydratingPromise;
221
+ }
222
+ export async function ensureSubjectSession(engine) {
223
+ if (engine.subjectToken)
224
+ return;
225
+ if (engine.sessionPromise)
226
+ return engine.sessionPromise;
227
+ engine.sessionPromise = (async () => {
228
+ const id = engine.identity.get();
229
+ const persisted = await engine.storage.getJson(STORAGE_KEYS.subjectToken);
230
+ try {
231
+ const session = await engine.transport.session({
232
+ anonymousId: id.anonymousId,
233
+ ...(persisted ? { currentToken: persisted } : {}),
234
+ });
235
+ engine.subjectToken = session.subjectToken;
236
+ engine.transport.setSubjectToken(session.subjectToken);
237
+ await engine.storage.setJson(STORAGE_KEYS.subjectToken, session.subjectToken);
238
+ }
239
+ catch (error) {
240
+ const mayRotate = error instanceof PermanentHttpError &&
241
+ (error.status === 401 || error.status === 403 || error.status === 409);
242
+ if (!mayRotate)
243
+ throw error;
244
+ // A lost/expired credential must never be replaced for the same known
245
+ // anonymous ID. Rotate to a fresh installation identity and establish a
246
+ // new server-bound subject instead.
247
+ if (persisted)
248
+ await engine.storage.remove(STORAGE_KEYS.subjectToken);
249
+ const rotated = await engine.identity.rotate();
250
+ const session = await engine.transport.session({ anonymousId: rotated.anonymousId });
251
+ engine.subjectToken = session.subjectToken;
252
+ engine.transport.setSubjectToken(session.subjectToken);
253
+ await engine.storage.setJson(STORAGE_KEYS.subjectToken, session.subjectToken);
254
+ }
255
+ })().finally(() => {
256
+ engine.sessionPromise = null;
257
+ });
258
+ return engine.sessionPromise;
259
+ }
260
+ export async function refreshTargetingRules(engine, force = false) {
261
+ if (engine.resetting)
262
+ return;
263
+ await ensureHydrated(engine);
264
+ if (engine.resetting)
265
+ return;
266
+ const id = engine.identity.get();
267
+ await Promise.all([
268
+ engine.rules.refresh({
269
+ anonymousId: id.anonymousId,
270
+ externalId: id.externalId,
271
+ force,
272
+ }),
273
+ engine.surveyRules.refresh({
274
+ anonymousId: id.anonymousId,
275
+ externalId: id.externalId,
276
+ force,
277
+ }),
278
+ engine.inAppRules.refresh({
279
+ anonymousId: id.anonymousId,
280
+ externalId: id.externalId,
281
+ force,
282
+ }),
283
+ ]);
284
+ }
285
+ /**
286
+ * Emit the lifecycle `$app_open` event, but only AFTER feedback consent
287
+ * is granted. The matcher blocks by consent — if we fire too early
288
+ * (before host has called setConsent), the lifecycle event is silently
289
+ * dropped and the user never sees the prompt this session.
290
+ *
291
+ * If consent is already granted: emit immediately.
292
+ * Otherwise: subscribe to consent changes; emit on the first transition
293
+ * where `feedback === true`. Idempotent: only fires once per call.
294
+ */
295
+ export function emitAppOpenWhenConsentReady(engine) {
296
+ if (engine.resetting)
297
+ return;
298
+ const pending = pendingAppOpenConsent.get(engine);
299
+ if (engine.consent.allowsFeedback()) {
300
+ pending?.();
301
+ pendingAppOpenConsent.delete(engine);
302
+ enqueueAndEvaluate(engine, APP_OPEN_EVENT_NAME, undefined, 'feedback');
303
+ return;
304
+ }
305
+ if (pending)
306
+ return;
307
+ let fired = false;
308
+ const unsubscribe = engine.consent.subscribe((s) => {
309
+ if (fired || !s.feedback)
310
+ return;
311
+ fired = true;
312
+ enqueueAndEvaluate(engine, APP_OPEN_EVENT_NAME, undefined, 'feedback');
313
+ unsubscribe();
314
+ pendingAppOpenConsent.delete(engine);
315
+ });
316
+ pendingAppOpenConsent.set(engine, unsubscribe);
317
+ }
318
+ export async function emitAppVersionChanged(engine) {
319
+ const current = engine.config.appVersion;
320
+ if (!current)
321
+ return;
322
+ const previous = await engine.storage.getJson(STORAGE_KEYS.appVersion);
323
+ await engine.storage.setJson(STORAGE_KEYS.appVersion, current);
324
+ if (!previous || previous === current || !engine.consent.allowsAnalytics())
325
+ return;
326
+ enqueueAndEvaluate(engine, APP_VERSION_CHANGED_EVENT_NAME, {
327
+ old_version: previous,
328
+ new_version: current,
329
+ });
330
+ }
331
+ const pendingAppOpenConsent = new WeakMap();
332
+ function instructionKey(type, refId, eventId) {
333
+ return `${type}:${refId}:event:${eventId}`;
334
+ }
335
+ function rememberLocalInstruction(engine, key) {
336
+ engine.localInstructionDedupe.remember(key);
337
+ }
338
+ export function scheduleFlush(engine) {
339
+ if (engine.flushTimer)
340
+ return;
341
+ engine.flushTimer = setTimeout(() => {
342
+ engine.flushTimer = null;
343
+ void flushNow(engine);
344
+ }, engine.config.flushIntervalMs);
345
+ }
346
+ export async function flushNow(engine) {
347
+ if (engine.resetting)
348
+ return;
349
+ if (engine.flushPromise)
350
+ return engine.flushPromise;
351
+ engine.flushPromise = performFlush(engine).finally(() => {
352
+ engine.flushPromise = null;
353
+ });
354
+ return engine.flushPromise;
355
+ }
356
+ async function performFlush(engine) {
357
+ try {
358
+ await ensureHydrated(engine);
359
+ while (engine.queue.size() > 0) {
360
+ const consent = engine.consent.get();
361
+ const allowed = engine.queue.snapshot().filter((event) => event.purpose === 'analytics' ? consent.analytics : consent.feedback);
362
+ const firstAllowed = allowed[0];
363
+ if (!firstAllowed)
364
+ break;
365
+ // An identify transition can leave pre-identify events queued beside
366
+ // post-identify events. The API deliberately rejects a batch whose
367
+ // event identities differ from its context, so drain one identity at a
368
+ // time while retaining FIFO order within that identity.
369
+ const batch = allowed.filter((event) => event.anonymousId === firstAllowed.anonymousId &&
370
+ event.externalId === firstAllowed.externalId).slice(0, engine.config.flushBatchSize);
371
+ if (batch.length === 0)
372
+ break;
373
+ const ingestEvents = batch.map((e) => ({
374
+ eventId: e.eventId,
375
+ name: e.name,
376
+ timestamp: e.timestamp,
377
+ anonymousId: e.anonymousId,
378
+ externalId: e.externalId,
379
+ ...(e.properties ? { properties: e.properties } : {}),
380
+ ...(e.sessionId ? { sessionId: e.sessionId } : {}),
381
+ sdkVersion: engine.context.sdkVersion(),
382
+ platform: engine.context.platform(),
383
+ }));
384
+ const payload = {
385
+ events: ingestEvents,
386
+ context: engine.context.build({
387
+ anonymousId: batch[0].anonymousId,
388
+ externalId: batch[0].externalId,
389
+ }),
390
+ };
391
+ try {
392
+ await engine.transport.ingest(payload);
393
+ engine.queue.remove(batch.map((event) => event.eventId));
394
+ }
395
+ catch (e) {
396
+ if (e instanceof PermanentHttpError) {
397
+ if (batch.length === 1) {
398
+ engine.queue.remove([batch[0].eventId]);
399
+ reportError('ingest event quarantined after permanent rejection', {
400
+ eventId: batch[0].eventId,
401
+ status: e.status,
402
+ });
403
+ continue;
404
+ }
405
+ // Retry smaller slices on the next loop so one invalid entry cannot
406
+ // permanently block every later event on this installation.
407
+ const first = batch[0];
408
+ try {
409
+ await engine.transport.ingest({
410
+ events: [ingestEvents[0]],
411
+ context: engine.context.build({
412
+ anonymousId: first.anonymousId,
413
+ externalId: first.externalId,
414
+ }),
415
+ });
416
+ engine.queue.remove([first.eventId]);
417
+ }
418
+ catch (singleError) {
419
+ if (singleError instanceof PermanentHttpError) {
420
+ engine.queue.remove([first.eventId]);
421
+ reportError('ingest event quarantined after permanent rejection', {
422
+ eventId: first.eventId,
423
+ status: singleError.status,
424
+ });
425
+ continue;
426
+ }
427
+ reportError('ingest failed', singleError);
428
+ return;
429
+ }
430
+ continue;
431
+ }
432
+ reportError('ingest failed', e);
433
+ return;
434
+ }
435
+ }
436
+ // After draining the queue, opportunistically pull pending survey offers.
437
+ await flushMutations(engine);
438
+ // Triggered surveys appear in the offer ledger after the server processes
439
+ // the event; the small delay gives NATS + the trigger engine time to
440
+ // run before we ask.
441
+ setTimeout(() => {
442
+ void pollSurveyOffers(engine);
443
+ void pollInstructions(engine);
444
+ }, 1500);
445
+ }
446
+ catch (e) {
447
+ reportError('flushNow failed', e);
448
+ }
449
+ }
450
+ export async function flushMutations(engine) {
451
+ if (engine.resetting)
452
+ return { permanentlyRejectedIds: new Set() };
453
+ if (engine.mutationFlushPromise)
454
+ return engine.mutationFlushPromise;
455
+ engine.mutationFlushPromise = performMutationFlush(engine).finally(() => {
456
+ engine.mutationFlushPromise = null;
457
+ });
458
+ return engine.mutationFlushPromise;
459
+ }
460
+ async function performMutationFlush(engine) {
461
+ const permanentlyRejectedIds = new Set();
462
+ while (engine.mutations.size() > 0) {
463
+ if (engine.resetting)
464
+ break;
465
+ const mutation = engine.mutations.peek();
466
+ if (!mutation)
467
+ break;
468
+ const deliveryGeneration = engine.resetGeneration;
469
+ const consent = engine.consent.get();
470
+ if (mutation.purpose === 'feedback' && !consent.feedback)
471
+ break;
472
+ if (mutation.purpose === 'survey' && !consent.survey)
473
+ break;
474
+ try {
475
+ if (mutation.kind === 'feedback-response') {
476
+ await engine.transport.submitResponse(mutation.payload);
477
+ }
478
+ else if (mutation.kind === 'survey-complete') {
479
+ const attemptId = mutation.payload.attemptId;
480
+ if (typeof attemptId !== 'string')
481
+ throw new Error('invalid-survey-mutation');
482
+ await engine.transport.surveyComplete(attemptId, mutation.payload.body);
483
+ }
484
+ else if (mutation.kind === 'survey-abandon') {
485
+ const attemptId = mutation.payload.attemptId;
486
+ if (typeof attemptId !== 'string')
487
+ throw new Error('invalid-survey-mutation');
488
+ await engine.transport.surveyAbandon(attemptId);
489
+ }
490
+ else {
491
+ const subjectToken = mutation.payload.subjectToken;
492
+ const anonymousId = mutation.payload.anonymousId;
493
+ const externalId = mutation.payload.externalId;
494
+ const properties = mutation.payload.properties;
495
+ if (typeof subjectToken !== 'string' ||
496
+ typeof anonymousId !== 'string' ||
497
+ typeof externalId !== 'string')
498
+ throw new Error('invalid-identify-mutation');
499
+ try {
500
+ await engine.transport.identify({
501
+ anonymousId,
502
+ externalId,
503
+ ...(properties && typeof properties === 'object'
504
+ ? { properties: properties }
505
+ : {}),
506
+ }, subjectToken);
507
+ if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
508
+ break;
509
+ await engine.storage.setJsonStrict(STORAGE_KEYS.subjectToken, subjectToken);
510
+ if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
511
+ break;
512
+ engine.subjectToken = subjectToken;
513
+ engine.transport.setSubjectToken(subjectToken);
514
+ await engine.identity.setExternalId(externalId);
515
+ if (properties && typeof properties === 'object') {
516
+ const clean = properties;
517
+ engine.userState.mergeProperties(clean);
518
+ if (engine.consent.allowsAnalytics()) {
519
+ enqueueAndEvaluate(engine, '$identify', clean);
520
+ }
521
+ }
522
+ else if (engine.consent.allowsAnalytics()) {
523
+ enqueueAndEvaluate(engine, '$identify', undefined);
524
+ }
525
+ }
526
+ catch (error) {
527
+ throw error;
528
+ }
529
+ }
530
+ if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
531
+ break;
532
+ await engine.mutations.remove(mutation.id);
533
+ }
534
+ catch (error) {
535
+ if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
536
+ break;
537
+ if (error instanceof PermanentHttpError) {
538
+ await engine.mutations.remove(mutation.id);
539
+ permanentlyRejectedIds.add(mutation.id);
540
+ reportError('mutation quarantined after permanent rejection', {
541
+ mutationId: mutation.id,
542
+ kind: mutation.kind,
543
+ status: error.status,
544
+ });
545
+ }
546
+ else {
547
+ debugLog('mutation delivery deferred', {
548
+ mutationId: mutation.id,
549
+ kind: mutation.kind,
550
+ reason: error instanceof Error ? error.message : String(error),
551
+ });
552
+ }
553
+ break;
554
+ }
555
+ }
556
+ return { permanentlyRejectedIds };
557
+ }
558
+ const SEEN_OFFERS_KEY = 'surveys:seen-offers';
559
+ const POLL_BACKOFF_MS = 5_000;
560
+ let lastPollAt = 0;
561
+ export async function pollSurveyOffers(engine) {
562
+ try {
563
+ if (engine.resetting)
564
+ return;
565
+ const now = Date.now();
566
+ if (now - lastPollAt < POLL_BACKOFF_MS)
567
+ return;
568
+ lastPollAt = now;
569
+ if (!engine.consent.get().survey)
570
+ return;
571
+ const id = engine.identity.get();
572
+ const res = await engine.transport.surveysAvailable({
573
+ anonymousId: id.anonymousId,
574
+ externalId: id.externalId ?? null,
575
+ });
576
+ const surveys = res.surveys ?? [];
577
+ if (surveys.length === 0)
578
+ return;
579
+ const seen = (await engine.storage.getJson(SEEN_OFFERS_KEY)) ?? [];
580
+ const seenSet = new Set(seen);
581
+ const localIds = surveys
582
+ .map((survey) => survey.id)
583
+ .filter((surveyId) => engine.locallyHandledSurveyIds.has(surveyId));
584
+ const fresh = surveys.filter((survey) => !seenSet.has(survey.id) &&
585
+ !engine.locallyHandledSurveyIds.has(survey.id));
586
+ if (fresh.length === 0 && localIds.length === 0)
587
+ return;
588
+ for (const s of fresh) {
589
+ engine.events.emit('surveyInvite', {
590
+ surveyId: s.id,
591
+ name: s.name,
592
+ source: s.source,
593
+ });
594
+ }
595
+ // A locally rendered survey and its later offer-ledger row represent one
596
+ // delivery. Persist both classes as seen so polling cannot reopen a modal
597
+ // minutes after the user already completed or dismissed it.
598
+ const nextSeen = [
599
+ ...new Set([...seen, ...localIds, ...fresh.map((survey) => survey.id)]),
600
+ ].slice(-200);
601
+ await engine.storage.setJson(SEEN_OFFERS_KEY, nextSeen);
602
+ }
603
+ catch (e) {
604
+ reportError('pollSurveyOffers failed', e);
605
+ }
606
+ }
607
+ export async function pollInstructions(engine) {
608
+ if (engine.resetting)
609
+ return;
610
+ if (engine.instructionPollPromise)
611
+ return engine.instructionPollPromise;
612
+ engine.instructionPollPromise = performInstructionPoll(engine).finally(() => {
613
+ engine.instructionPollPromise = null;
614
+ });
615
+ return engine.instructionPollPromise;
616
+ }
617
+ async function performInstructionPoll(engine) {
618
+ try {
619
+ await ensureHydrated(engine);
620
+ const after = (await engine.storage.getJson(STORAGE_KEYS.instructionCursor)) ?? 0;
621
+ const seen = (await engine.storage.getJson(STORAGE_KEYS.seenInstructions)) ?? [];
622
+ const seenSet = new Set(seen);
623
+ const result = await engine.transport.instructions(after);
624
+ if (result.instructions.length === 0)
625
+ return;
626
+ const handledIds = [];
627
+ for (const instruction of result.instructions) {
628
+ handledIds.push(instruction.id);
629
+ if (seenSet.has(instruction.id))
630
+ continue;
631
+ dispatchInstruction(engine, instruction.type, instruction.payload);
632
+ seenSet.add(instruction.id);
633
+ await engine.storage.setJsonStrict(STORAGE_KEYS.seenInstructions, [...seenSet].slice(-200));
634
+ }
635
+ await engine.storage.setJsonStrict(STORAGE_KEYS.instructionCursor, Math.max(after, ...handledIds));
636
+ await engine.transport.acknowledgeInstructions(handledIds);
637
+ }
638
+ catch (error) {
639
+ reportError('instruction poll failed', error);
640
+ }
641
+ }
642
+ function dispatchInstruction(engine, type, payload) {
643
+ if (type === 'prompt.show') {
644
+ if (!engine.consent.allowsFeedback())
645
+ return;
646
+ const promptId = payload.promptId;
647
+ const prompt = payload.prompt;
648
+ if (typeof promptId !== 'string' || !prompt || typeof prompt !== 'object')
649
+ return;
650
+ const triggerEventId = payload.triggerEventId;
651
+ if (typeof triggerEventId === 'string' &&
652
+ engine.localInstructionDedupe.consume(instructionKey('prompt.show', promptId, triggerEventId))) {
653
+ debugLog('prompt instruction skipped after immediate local render', {
654
+ promptId,
655
+ triggerEventId,
656
+ });
657
+ return;
658
+ }
659
+ const shownAt = Date.now();
660
+ const typedPrompt = prompt;
661
+ const emission = {
662
+ promptId,
663
+ prompt: typedPrompt,
664
+ theme: typedPrompt.theme,
665
+ shownAt,
666
+ triggerEventName: typeof payload.triggerEventName === 'string'
667
+ ? payload.triggerEventName
668
+ : 'server',
669
+ };
670
+ engine.events.emit('showPrompt', emission);
671
+ return;
672
+ }
673
+ if (type === 'survey.offer') {
674
+ if (!engine.consent.allowsSurvey())
675
+ return;
676
+ if (typeof payload.surveyId !== 'string' || typeof payload.name !== 'string')
677
+ return;
678
+ const triggerEventId = payload.triggerEventId;
679
+ if (typeof triggerEventId === 'string' &&
680
+ engine.localInstructionDedupe.consume(instructionKey('survey.offer', payload.surveyId, triggerEventId))) {
681
+ debugLog('survey instruction skipped after immediate local render', {
682
+ surveyId: payload.surveyId,
683
+ triggerEventId,
684
+ });
685
+ return;
686
+ }
687
+ engine.events.emit('surveyInvite', {
688
+ surveyId: payload.surveyId,
689
+ name: payload.name,
690
+ source: typeof payload.source === 'string' ? payload.source : 'triggered',
691
+ });
692
+ return;
693
+ }
694
+ if (type === 'inapp.show') {
695
+ if (!engine.consent.allowsFeedback())
696
+ return;
697
+ const message = payload.message;
698
+ if (!message || typeof message !== 'object')
699
+ return;
700
+ const typed = message;
701
+ const triggerEventId = payload.triggerEventId;
702
+ if (typeof triggerEventId === 'string' &&
703
+ engine.localInstructionDedupe.consume(instructionKey('inapp.show', typed.messageId, triggerEventId))) {
704
+ debugLog('in-app instruction skipped after immediate local render', {
705
+ messageId: typed.messageId,
706
+ triggerEventId,
707
+ });
708
+ return;
709
+ }
710
+ engine.events.emit('showInAppMessage', {
711
+ messageId: typed.messageId,
712
+ message: typed,
713
+ shownAt: Date.now(),
714
+ triggerEventName: typeof payload.triggerEventName === 'string'
715
+ ? payload.triggerEventName
716
+ : 'server',
717
+ });
718
+ return;
719
+ }
720
+ if (type.startsWith('request.')) {
721
+ engine.events.emit('pushEvent', {
722
+ name: `$${type.replaceAll('.', '_')}`,
723
+ props: payload,
724
+ });
725
+ }
726
+ }
727
+ export function asEventProps(properties) {
728
+ if (!properties)
729
+ return undefined;
730
+ const out = {};
731
+ const piiKey = /(?:^|[._])(email|phone|ssn|tax_id)$/i;
732
+ for (const k of Object.keys(properties).slice(0, 100)) {
733
+ if (!k || k.length > 120 || piiKey.test(k))
734
+ continue;
735
+ const v = properties[k];
736
+ if (typeof v === 'string') {
737
+ out[k] = v.slice(0, 10_000);
738
+ }
739
+ else if (typeof v === 'number' || typeof v === 'boolean' || v === null) {
740
+ out[k] = v;
741
+ }
742
+ }
743
+ return out;
744
+ }
745
+ export function enqueueAndEvaluate(engine, eventName, properties, purpose = 'analytics') {
746
+ const now = Date.now();
747
+ const id = engine.identity.get();
748
+ const base = {
749
+ eventId: generateEventId(),
750
+ purpose,
751
+ name: eventName,
752
+ timestamp: new Date(now).toISOString(),
753
+ anonymousId: id.anonymousId || 'pending',
754
+ externalId: id.externalId,
755
+ ...(properties ? { properties } : {}),
756
+ };
757
+ if (engine.hydrated) {
758
+ engine.queue.enqueue(base);
759
+ engine.userState.recordEvent(eventName, now);
760
+ evaluateLocally(engine, eventName, base.eventId);
761
+ if (engine.queue.size() >= engine.config.flushBatchSize)
762
+ void flushNow(engine);
763
+ else
764
+ scheduleFlush(engine);
765
+ return;
766
+ }
767
+ void ensureHydrated(engine).then(() => {
768
+ const fresh = engine.identity.get();
769
+ const withId = {
770
+ ...base,
771
+ anonymousId: fresh.anonymousId,
772
+ externalId: fresh.externalId,
773
+ };
774
+ engine.queue.enqueue(withId);
775
+ engine.userState.recordEvent(eventName, now);
776
+ evaluateLocally(engine, eventName, withId.eventId);
777
+ if (engine.queue.size() >= engine.config.flushBatchSize)
778
+ void flushNow(engine);
779
+ else
780
+ scheduleFlush(engine);
781
+ });
782
+ }
783
+ /** Record an event for immediate on-device matching when the server mutation
784
+ * has already persisted the canonical event transactionally. */
785
+ export function recordServerBackedEventLocally(engine, eventName) {
786
+ const now = Date.now();
787
+ engine.userState.recordEvent(eventName, now);
788
+ evaluateLocally(engine, eventName, generateEventId());
789
+ }
790
+ function evaluateLocally(engine, eventName, eventId) {
791
+ const promptId = engine.matcher.evaluate(eventName);
792
+ if (promptId) {
793
+ rememberLocalInstruction(engine, instructionKey('prompt.show', promptId, eventId));
794
+ }
795
+ const surveyId = engine.surveyMatcher.evaluate(eventName);
796
+ if (surveyId) {
797
+ engine.locallyHandledSurveyIds.add(surveyId);
798
+ rememberLocalInstruction(engine, instructionKey('survey.offer', surveyId, eventId));
799
+ }
800
+ const messageId = engine.inAppMatcher.evaluate(eventName);
801
+ if (messageId) {
802
+ rememberLocalInstruction(engine, instructionKey('inapp.show', messageId, eventId));
803
+ }
804
+ }
805
+ export async function submitResponse(engine, payload, triggerEventName, trackInternal) {
806
+ const id = engine.identity.get();
807
+ const submitPayload = {
808
+ idempotencyKey: generateEventId(),
809
+ promptId: payload.promptId,
810
+ anonymousId: id.anonymousId,
811
+ externalId: id.externalId,
812
+ answers: payload.answers.map((a) => ({ questionId: a.questionId, value: a.value })),
813
+ dismissed: payload.dismissed,
814
+ latencyMs: payload.latencyMs,
815
+ };
816
+ engine.events.emit('response', payload);
817
+ trackInternal('$feedback_response', {
818
+ promptId: payload.promptId,
819
+ dismissed: payload.dismissed,
820
+ latencyMs: payload.latencyMs,
821
+ triggerEventName,
822
+ }, 'feedback');
823
+ debugLog('[usergist:analyze] response-submit', {
824
+ promptId: payload.promptId,
825
+ answersCount: payload.answers.length,
826
+ dismissed: payload.dismissed,
827
+ });
828
+ if (!engine.consent.allowsFeedback())
829
+ return;
830
+ await engine.mutations.enqueue('feedback-response', 'feedback', submitPayload);
831
+ void flushMutations(engine);
832
+ }
833
+ export async function clearAllState(engine) {
834
+ pendingAppOpenConsent.get(engine)?.();
835
+ pendingAppOpenConsent.delete(engine);
836
+ engine.events.emit('resetSurfaces', undefined);
837
+ engine.transport.cancelAll();
838
+ await Promise.allSettled([
839
+ engine.mutationFlushPromise ?? Promise.resolve(),
840
+ engine.flushPromise ?? Promise.resolve(),
841
+ engine.sessionPromise ?? Promise.resolve(),
842
+ engine.instructionPollPromise ?? Promise.resolve(),
843
+ ]);
844
+ // Reset is a local privacy operation and must not wait on network retries.
845
+ // Revoke the old credential through an isolated transport so failures cannot
846
+ // open the live engine's circuit or delay the new anonymous session.
847
+ const subjectToken = engine.subjectToken;
848
+ if (subjectToken) {
849
+ const revocationTransport = createTransport({
850
+ writeKey: engine.config.writeKey,
851
+ apiUrl: engine.config.apiUrl,
852
+ });
853
+ revocationTransport.setSubjectToken(subjectToken);
854
+ void revocationTransport.revokeSession().catch((error) => {
855
+ debugLog('subject session revoke deferred', {
856
+ reason: error instanceof Error ? error.message : String(error),
857
+ });
858
+ });
859
+ }
860
+ if (engine.flushTimer) {
861
+ clearTimeout(engine.flushTimer);
862
+ engine.flushTimer = null;
863
+ }
864
+ await ensureHydrated(engine);
865
+ await Promise.all([
866
+ engine.queue.clear(),
867
+ engine.mutations.clear(),
868
+ engine.identity.clear(),
869
+ engine.caps.clear(),
870
+ engine.rules.clear(),
871
+ engine.surveyRules.clear(),
872
+ engine.inAppRules.clear(),
873
+ engine.userState.clear(),
874
+ engine.consent.clear(),
875
+ engine.localInstructionDedupe.clear(),
876
+ ]);
877
+ await engine.storage.clearAll([
878
+ STORAGE_KEYS.identity,
879
+ STORAGE_KEYS.consent,
880
+ STORAGE_KEYS.queue,
881
+ STORAGE_KEYS.rulesCache,
882
+ STORAGE_KEYS.surveyRulesCache,
883
+ STORAGE_KEYS.inAppRulesCache,
884
+ STORAGE_KEYS.frequencyCaps,
885
+ STORAGE_KEYS.userProperties,
886
+ STORAGE_KEYS.eventHistory,
887
+ STORAGE_KEYS.subjectToken,
888
+ STORAGE_KEYS.mutationQueue,
889
+ STORAGE_KEYS.instructionCursor,
890
+ STORAGE_KEYS.seenInstructions,
891
+ STORAGE_KEYS.localInstructionDedupe,
892
+ SEEN_OFFERS_KEY,
893
+ ]);
894
+ engine.subjectToken = null;
895
+ engine.transport.setSubjectToken(null);
896
+ engine.locallyHandledSurveyIds.clear();
897
+ engine.matcher.resetPending();
898
+ engine.surveyMatcher.resetPending();
899
+ engine.lastPushToken = null;
900
+ lastPollAt = 0;
901
+ await engine.identity.hydrate();
902
+ await ensureSubjectSession(engine);
903
+ }
904
+ //# sourceMappingURL=engine.js.map