rampkit-expo-dev 0.0.93 → 0.0.95

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.
@@ -18,6 +18,7 @@ declare class EventManager {
18
18
  private currentTargetId;
19
19
  private currentTargetName;
20
20
  private currentBucket;
21
+ private currentVersionId;
21
22
  private onboardingStartTime;
22
23
  private currentOnboardingId;
23
24
  private onboardingCompletedForSession;
@@ -47,7 +48,7 @@ declare class EventManager {
47
48
  * Set targeting context (called after target evaluation)
48
49
  * This persists for all subsequent events
49
50
  */
50
- setTargetingContext(targetId: string, targetName: string, onboardingId: string, bucket: number): void;
51
+ setTargetingContext(targetId: string, targetName: string, onboardingId: string, bucket: number, versionId?: string | null): void;
51
52
  /**
52
53
  * Get current targeting info (for user profile updates)
53
54
  */
@@ -88,7 +89,7 @@ declare class EventManager {
88
89
  * Track target matched event
89
90
  * Called when targeting evaluation completes and a target is selected
90
91
  */
91
- trackTargetMatched(targetId: string, targetName: string, onboardingId: string, bucket: number): void;
92
+ trackTargetMatched(targetId: string, targetName: string, onboardingId: string, bucket: number, versionId?: string | null): void;
92
93
  /**
93
94
  * Track onboarding started
94
95
  */
@@ -145,6 +146,14 @@ declare class EventManager {
145
146
  * Track purchase restored
146
147
  */
147
148
  trackPurchaseRestored(properties: PurchaseRestoredProperties): void;
149
+ /**
150
+ * Track screen navigation
151
+ */
152
+ trackScreenNavigated(fromScreenId: string | null, toScreenId: string, direction: "forward" | "back", trigger?: "button"): void;
153
+ /**
154
+ * Track variable set event
155
+ */
156
+ trackVariableSet(variableName: string, previousValue: any, newValue: any): void;
148
157
  /**
149
158
  * Reset the event manager (e.g., on logout)
150
159
  */
@@ -35,6 +35,7 @@ class EventManager {
35
35
  this.currentTargetId = null;
36
36
  this.currentTargetName = null;
37
37
  this.currentBucket = null;
38
+ this.currentVersionId = null;
38
39
  // Onboarding tracking
39
40
  this.onboardingStartTime = null;
40
41
  this.currentOnboardingId = null;
@@ -104,13 +105,14 @@ class EventManager {
104
105
  * Set targeting context (called after target evaluation)
105
106
  * This persists for all subsequent events
106
107
  */
107
- setTargetingContext(targetId, targetName, onboardingId, bucket) {
108
+ setTargetingContext(targetId, targetName, onboardingId, bucket, versionId) {
108
109
  this.currentTargetId = targetId;
109
110
  this.currentTargetName = targetName;
110
111
  this.currentOnboardingId = onboardingId;
111
112
  this.currentBucket = bucket;
113
+ this.currentVersionId = versionId || null;
112
114
  this.currentFlowId = onboardingId;
113
- Logger_1.Logger.verbose("EventManager: Targeting context set", { targetId, targetName, bucket });
115
+ Logger_1.Logger.verbose("EventManager: Targeting context set", { targetId, targetName, bucket, versionId });
114
116
  }
115
117
  /**
116
118
  * Get current targeting info (for user profile updates)
@@ -245,15 +247,16 @@ class EventManager {
245
247
  * Track target matched event
246
248
  * Called when targeting evaluation completes and a target is selected
247
249
  */
248
- trackTargetMatched(targetId, targetName, onboardingId, bucket) {
250
+ trackTargetMatched(targetId, targetName, onboardingId, bucket, versionId) {
249
251
  // Set targeting context for all future events
250
- this.setTargetingContext(targetId, targetName, onboardingId, bucket);
252
+ this.setTargetingContext(targetId, targetName, onboardingId, bucket, versionId);
251
253
  // Track the target_matched event
252
254
  this.track("target_matched", {
253
255
  targetId,
254
256
  targetName,
255
257
  onboardingId,
256
258
  bucket,
259
+ versionId: versionId || null,
257
260
  });
258
261
  }
259
262
  /**
@@ -363,6 +366,39 @@ class EventManager {
363
366
  Logger_1.Logger.verbose(`purchase_restored: ${properties.productId}`);
364
367
  this.track("purchase_restored", properties);
365
368
  }
369
+ /**
370
+ * Track screen navigation
371
+ */
372
+ trackScreenNavigated(fromScreenId, toScreenId, direction, trigger = "button") {
373
+ this.track("screen_navigated", {
374
+ fromScreenId,
375
+ toScreenId,
376
+ direction,
377
+ trigger,
378
+ });
379
+ }
380
+ /**
381
+ * Track variable set event
382
+ */
383
+ trackVariableSet(variableName, previousValue, newValue) {
384
+ const valueType = (() => {
385
+ if (newValue === null || newValue === undefined)
386
+ return "unknown";
387
+ if (Array.isArray(newValue))
388
+ return "array";
389
+ if (typeof newValue === "object")
390
+ return "object";
391
+ return typeof newValue;
392
+ })();
393
+ this.track("variable_set", {
394
+ variableName,
395
+ variableType: "state",
396
+ valueType,
397
+ newValue,
398
+ previousValue,
399
+ source: "user_input",
400
+ });
401
+ }
366
402
  /**
367
403
  * Reset the event manager (e.g., on logout)
368
404
  */
@@ -380,6 +416,7 @@ class EventManager {
380
416
  this.currentTargetId = null;
381
417
  this.currentTargetName = null;
382
418
  this.currentBucket = null;
419
+ this.currentVersionId = null;
383
420
  this.onboardingStartTime = null;
384
421
  this.currentOnboardingId = null;
385
422
  this.onboardingCompletedForSession = false;
package/build/RampKit.js CHANGED
@@ -112,7 +112,7 @@ class RampKitCore {
112
112
  this.targetingResult = result;
113
113
  Logger_1.Logger.verbose("Target matched:", `"${result.targetName}" -> onboarding ${result.onboarding.id} (bucket ${result.bucket})`);
114
114
  // Track target_matched event (also sets targeting context for all future events)
115
- EventManager_1.eventManager.trackTargetMatched(result.targetId, result.targetName, result.onboarding.id, result.bucket);
115
+ EventManager_1.eventManager.trackTargetMatched(result.targetId, result.targetName, result.onboarding.id, result.bucket, result.versionId);
116
116
  // Update deviceInfo with targeting data
117
117
  if (this.deviceInfo) {
118
118
  this.deviceInfo = {
@@ -120,6 +120,7 @@ class RampKitCore {
120
120
  matchedTargetId: result.targetId,
121
121
  matchedTargetName: result.targetName,
122
122
  matchedOnboardingId: result.onboarding.id,
123
+ matchedOnboardingVersionId: result.versionId,
123
124
  abTestBucket: result.bucket,
124
125
  };
125
126
  // Sync updated targeting info to backend
@@ -1,7 +1,7 @@
1
1
  import { RampKitContext, NavigationData } from "./types";
2
2
  export declare const injectedHardening = "\n(function(){\n try {\n var meta = document.querySelector('meta[name=\"viewport\"]');\n if (!meta) { meta = document.createElement('meta'); meta.name = 'viewport'; document.head.appendChild(meta); }\n meta.setAttribute('content','width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover');\n var style = document.createElement('style');\n style.textContent='html,body{overflow-x:hidden!important;} html,body,*{-webkit-user-select:none!important;user-select:none!important;-webkit-touch-callout:none!important;-ms-user-select:none!important;touch-action: pan-y;} *{-webkit-tap-highlight-color: rgba(0,0,0,0)!important;} ::selection{background: transparent!important;} ::-moz-selection{background: transparent!important;} a,img{-webkit-user-drag:none!important;user-drag:none!important;-webkit-touch-callout:none!important} input,textarea{caret-color:transparent!important;-webkit-user-select:none!important;user-select:none!important}';\n document.head.appendChild(style);\n var prevent=function(e){e.preventDefault&&e.preventDefault();};\n document.addEventListener('gesturestart',prevent,{passive:false});\n document.addEventListener('gesturechange',prevent,{passive:false});\n document.addEventListener('gestureend',prevent,{passive:false});\n document.addEventListener('dblclick',prevent,{passive:false});\n document.addEventListener('wheel',function(e){ if(e.ctrlKey) e.preventDefault(); },{passive:false});\n document.addEventListener('touchmove',function(e){ if(e.scale && e.scale !== 1) e.preventDefault(); },{passive:false});\n document.addEventListener('selectstart',prevent,{passive:false,capture:true});\n document.addEventListener('contextmenu',prevent,{passive:false,capture:true});\n document.addEventListener('copy',prevent,{passive:false,capture:true});\n document.addEventListener('cut',prevent,{passive:false,capture:true});\n document.addEventListener('paste',prevent,{passive:false,capture:true});\n document.addEventListener('dragstart',prevent,{passive:false,capture:true});\n // Belt-and-suspenders: aggressively clear any attempted selection\n var clearSel=function(){\n try{var sel=window.getSelection&&window.getSelection(); if(sel&&sel.removeAllRanges) sel.removeAllRanges();}catch(_){} }\n document.addEventListener('selectionchange',clearSel,{passive:true,capture:true});\n document.onselectstart=function(){ clearSel(); return false; };\n try{ document.documentElement.style.webkitUserSelect='none'; document.documentElement.style.userSelect='none'; }catch(_){ }\n try{ document.body.style.webkitUserSelect='none'; document.body.style.userSelect='none'; }catch(_){ }\n var __selTimer = setInterval(clearSel, 160);\n window.addEventListener('pagehide',function(){ try{ clearInterval(__selTimer); }catch(_){} });\n // Continuously enforce no-select on all elements and new nodes\n var enforceNoSelect = function(el){\n try{\n el.style && (el.style.webkitUserSelect='none', el.style.userSelect='none', el.style.webkitTouchCallout='none');\n el.setAttribute && (el.setAttribute('unselectable','on'), el.setAttribute('contenteditable','false'));\n }catch(_){}\n }\n try{\n var all=document.getElementsByTagName('*');\n for(var i=0;i<all.length;i++){ enforceNoSelect(all[i]); }\n var obs = new MutationObserver(function(muts){\n for(var j=0;j<muts.length;j++){\n var m=muts[j];\n if(m.type==='childList'){\n m.addedNodes && m.addedNodes.forEach && m.addedNodes.forEach(function(n){ if(n && n.nodeType===1){ enforceNoSelect(n); var q=n.getElementsByTagName? n.getElementsByTagName('*'): []; for(var k=0;k<q.length;k++){ enforceNoSelect(q[k]); }}});\n } else if(m.type==='attributes'){\n enforceNoSelect(m.target);\n }\n }\n });\n obs.observe(document.documentElement,{ childList:true, subtree:true, attributes:true, attributeFilter:['contenteditable','style'] });\n }catch(_){ }\n } catch(_) {}\n})(); true;\n";
3
3
  export declare const injectedNoSelect = "\n(function(){\n try {\n if (window.__rkNoSelectApplied) return true;\n window.__rkNoSelectApplied = true;\n var style = document.getElementById('rk-no-select-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'rk-no-select-style';\n style.innerHTML = \"\n * {\n user-select: none !important;\n -webkit-user-select: none !important;\n -webkit-touch-callout: none !important;\n }\n ::selection {\n background: transparent !important;\n }\n \";\n document.head.appendChild(style);\n }\n var prevent = function(e){ if(e && e.preventDefault) e.preventDefault(); return false; };\n document.addEventListener('contextmenu', prevent, { passive: false, capture: true });\n document.addEventListener('selectstart', prevent, { passive: false, capture: true });\n } catch (_) {}\n true;\n})();\n";
4
- export declare const injectedVarsHandler = "\n(function(){\n try {\n if (window.__rkVarsHandlerApplied) return true;\n window.__rkVarsHandlerApplied = true;\n \n // Handler function that updates variables and notifies the page\n window.__rkHandleVarsUpdate = function(vars) {\n if (!vars || typeof vars !== 'object') return;\n // Update the global variables object\n window.__rampkitVariables = vars;\n // Dispatch a custom event that the page's JS can listen to for re-rendering\n try {\n document.dispatchEvent(new CustomEvent('rampkit:vars-updated', { detail: vars }));\n } catch(e) {}\n // Also try calling a global handler if the page defined one\n try {\n if (typeof window.onRampkitVarsUpdate === 'function') {\n window.onRampkitVarsUpdate(vars);\n }\n } catch(e) {}\n };\n \n // Listen for message events from React Native\n document.addEventListener('message', function(event) {\n try {\n var data = event.data;\n if (data && data.type === 'rampkit:variables' && data.vars) {\n window.__rkHandleVarsUpdate(data.vars);\n }\n } catch(e) {}\n }, false);\n \n // Also listen on window for compatibility\n window.addEventListener('message', function(event) {\n try {\n var data = event.data;\n if (data && data.type === 'rampkit:variables' && data.vars) {\n window.__rkHandleVarsUpdate(data.vars);\n }\n } catch(e) {}\n }, false);\n } catch (_) {}\n true;\n})();\n";
4
+ export declare const injectedVarsHandler = "\n(function(){\n try {\n if (window.__rkVarsHandlerApplied) return true;\n window.__rkVarsHandlerApplied = true;\n \n // Handler function that updates variables and notifies the page\n window.__rkHandleVarsUpdate = function(vars) {\n if (!vars || typeof vars !== 'object') return;\n // Update the global variables object\n window.__rampkitVariables = vars;\n // Dispatch a custom event that the page's JS can listen to for re-rendering\n try {\n document.dispatchEvent(new CustomEvent('rampkit:vars-updated', { detail: vars }));\n } catch(e) {}\n // Also try calling a global handler if the page defined one\n try {\n if (typeof window.onRampkitVarsUpdate === 'function') {\n window.onRampkitVarsUpdate(vars);\n }\n } catch(e) {}\n };\n \n // Listen for message events from React Native\n document.addEventListener('message', function(event) {\n try {\n var data = event.data;\n if (data && data.type === 'rampkit:variables' && data.vars) {\n window.__rkHandleVarsUpdate(data.vars);\n }\n } catch(e) {}\n }, false);\n \n // Also listen on window for compatibility\n window.addEventListener('message', function(event) {\n try {\n var data = event.data;\n if (data && data.type === 'rampkit:variables' && data.vars) {\n window.__rkHandleVarsUpdate(data.vars);\n }\n } catch(e) {}\n }, false);\n\n // Listen for input blur events to notify native for debounce flush\n document.addEventListener('blur', function(e) {\n var target = e.target;\n if (!target) return;\n var tagName = target.tagName ? target.tagName.toUpperCase() : '';\n if (tagName !== 'INPUT' && tagName !== 'TEXTAREA') return;\n\n // Get variable name from data-var, name, or id attribute\n var varName = target.getAttribute('data-var') || target.name || target.id;\n if (!varName) return;\n\n try {\n var message = { type: 'rampkit:input-blur', variableName: varName };\n if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {\n window.ReactNativeWebView.postMessage(JSON.stringify(message));\n }\n } catch(err) {\n // Silently ignore messaging errors\n }\n }, true); // Use capture phase to catch all blur events\n } catch (_) {}\n true;\n})();\n";
5
5
  export declare const injectedDynamicTapHandler = "\n(function() {\n if (window.__rampkitClickInterceptorInstalled) return;\n window.__rampkitClickInterceptorInstalled = true;\n\n // Decode HTML entities\n function decodeHtml(str) {\n if (!str) return str;\n return str.replace(/&quot;/g, '\"').replace(/&#34;/g, '\"').replace(/&#x22;/g, '\"')\n .replace(/&apos;/g, \"'\").replace(/&#39;/g, \"'\").replace(/&#x27;/g, \"'\")\n .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');\n }\n\n // Find dynamic tap config on element or ancestors\n function findDynamicTap(el) {\n var current = el;\n var depth = 0;\n var attrNames = ['data-tap-dynamic', 'data-tapdynamic', 'tapDynamic', 'data-dynamic-tap'];\n while (current && current !== document.body && current !== document.documentElement && depth < 20) {\n if (current.getAttribute) {\n for (var i = 0; i < attrNames.length; i++) {\n var attr = current.getAttribute(attrNames[i]);\n if (attr && attr.length > 2) {\n return { element: current, config: attr };\n }\n }\n if (current.dataset && current.dataset.tapDynamic) {\n return { element: current, config: current.dataset.tapDynamic };\n }\n }\n current = current.parentElement;\n depth++;\n }\n return null;\n }\n \n // Get variables for condition evaluation - check ALL possible sources\n function getVars() {\n var vars = {};\n if (window.__rampkitVariables) {\n Object.keys(window.__rampkitVariables).forEach(function(k) {\n vars[k] = window.__rampkitVariables[k];\n });\n }\n if (window.__rampkitVars) {\n Object.keys(window.__rampkitVars).forEach(function(k) {\n vars[k] = window.__rampkitVars[k];\n });\n }\n if (window.RK_VARS) {\n Object.keys(window.RK_VARS).forEach(function(k) {\n vars[k] = window.RK_VARS[k];\n });\n }\n return vars;\n }\n \n // Evaluate a single rule\n function evalRule(rule, vars) {\n if (!rule || !rule.key) return false;\n var left = vars[rule.key];\n var right = rule.value;\n var op = rule.op || '=';\n if (left === undefined || left === null) left = '';\n if (right === undefined || right === null) right = '';\n var leftStr = String(left);\n var rightStr = String(right);\n var result = false;\n switch (op) {\n case '=': case '==': result = leftStr === rightStr; break;\n case '!=': case '<>': result = leftStr !== rightStr; break;\n case '>': result = parseFloat(left) > parseFloat(right); break;\n case '<': result = parseFloat(left) < parseFloat(right); break;\n case '>=': result = parseFloat(left) >= parseFloat(right); break;\n case '<=': result = parseFloat(left) <= parseFloat(right); break;\n default: result = false;\n }\n return result;\n }\n \n // Evaluate all rules (AND logic)\n function evalRules(rules, vars) {\n if (!rules || !rules.length) return true;\n for (var i = 0; i < rules.length; i++) {\n if (!evalRule(rules[i], vars)) return false;\n }\n return true;\n }\n \n // Execute an action\n function execAction(action) {\n if (!action || !action.type) return;\n var msg = null;\n var actionType = action.type.toLowerCase();\n \n switch (actionType) {\n case 'navigate':\n msg = { type: 'rampkit:navigate', targetScreenId: action.targetScreenId || '__continue__', animation: action.animation || 'fade' };\n break;\n case 'continue':\n msg = { type: 'rampkit:navigate', targetScreenId: '__continue__', animation: action.animation || 'fade' };\n break;\n case 'goback':\n msg = { type: 'rampkit:goBack', animation: action.animation || 'fade' };\n break;\n case 'close':\n msg = { type: 'rampkit:close' };\n break;\n case 'haptic':\n msg = { type: 'rampkit:haptic', hapticType: action.hapticType || 'impact', impactStyle: action.impactStyle || 'Medium', notificationType: action.notificationType };\n break;\n case 'showpaywall':\n msg = { type: 'rampkit:show-paywall', payload: action.payload || { paywallId: action.paywallId } };\n break;\n case 'requestreview':\n msg = { type: 'rampkit:request-review' };\n break;\n case 'requestnotificationpermission':\n msg = { type: 'rampkit:request-notification-permission' };\n break;\n case 'onboardingfinished':\n msg = { type: 'rampkit:onboarding-finished', payload: action.payload };\n break;\n case 'setvariable':\n case 'setstate':\n case 'updatevariable':\n case 'set':\n case 'assign':\n var varKey = action.key || action.variableName || action.name || action.variable;\n var varValue = action.variableValue !== undefined ? action.variableValue :\n action.value !== undefined ? action.value :\n action.newValue !== undefined ? action.newValue : undefined;\n if (varKey && varValue !== undefined) {\n if (window.__rampkitVariables) window.__rampkitVariables[varKey] = varValue;\n if (window.__rampkitVars) window.__rampkitVars[varKey] = varValue;\n var updateVars = {};\n updateVars[varKey] = varValue;\n msg = { type: 'rampkit:variables', vars: updateVars };\n }\n break;\n }\n if (msg) {\n try {\n if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {\n window.ReactNativeWebView.postMessage(JSON.stringify(msg));\n }\n } catch(e) {}\n }\n }\n \n // Evaluate dynamic tap config\n function evalDynamicTap(config) {\n if (!config || !config.values) return false;\n var vars = getVars();\n var conditions = config.values;\n for (var i = 0; i < conditions.length; i++) {\n var cond = conditions[i];\n var condType = cond.conditionType || 'if';\n var rules = cond.rules || [];\n var actions = cond.actions || [];\n if (condType === 'else' || evalRules(rules, vars)) {\n for (var j = 0; j < actions.length; j++) {\n execAction(actions[j]);\n }\n return true;\n }\n }\n return false;\n }\n \n // Click interceptor - capture phase, runs BEFORE onclick handlers\n function interceptClick(event) {\n var result = findDynamicTap(event.target);\n if (!result) return;\n\n try {\n var configStr = decodeHtml(result.config);\n var config = JSON.parse(configStr);\n var handled = evalDynamicTap(config);\n if (handled) {\n event.stopImmediatePropagation();\n event.preventDefault();\n return false;\n }\n } catch (e) {\n // Dynamic tap error - silent\n }\n }\n\n // Install interceptor on window in capture phase\n window.addEventListener('click', interceptClick, true);\n})();\n";
6
6
  export declare const injectedButtonAnimations = "\n(function(){\n try {\n if (window.__rkButtonAnimApplied) return true;\n window.__rkButtonAnimApplied = true;\n\n var pressed = null;\n var pressedOriginalTransform = '';\n var pressedOriginalOpacity = '';\n var pressedOriginalTransition = '';\n var releaseTimer = null;\n\n // Find interactive element - looks for clickable-looking elements\n function findInteractive(el) {\n var current = el;\n for (var i = 0; i < 20 && current && current !== document.body && current !== document.documentElement; i++) {\n if (!current || !current.tagName) { current = current.parentElement; continue; }\n var tag = current.tagName.toLowerCase();\n\n // Skip tiny elements (likely icons inside buttons)\n var rect = current.getBoundingClientRect();\n if (rect.width < 20 || rect.height < 20) { current = current.parentElement; continue; }\n\n // Match standard interactive elements\n if (tag === 'button' || tag === 'a' || tag === 'input' || tag === 'select') return current;\n\n // Match elements with tap/click-related data attributes\n // Exclude lifecycle attributes like data-on-open-actions, data-on-close-actions\n var attrs = current.attributes;\n if (attrs) {\n for (var j = 0; j < attrs.length; j++) {\n var attrName = attrs[j].name.toLowerCase();\n // Skip lifecycle attributes (on-open, on-close, on-load, on-appear, etc.)\n // These are for screen lifecycle events, not user tap interactions\n var isLifecycleAttr = (attrName.indexOf('on-open') !== -1 || attrName.indexOf('on-close') !== -1 ||\n attrName.indexOf('on-load') !== -1 || attrName.indexOf('on-appear') !== -1 ||\n attrName.indexOf('onopen') !== -1 || attrName.indexOf('onclose') !== -1 ||\n attrName.indexOf('onload') !== -1 || attrName.indexOf('onappear') !== -1);\n if (isLifecycleAttr) {\n continue;\n }\n // Match tap/click interaction attributes\n if (attrName.indexOf('click') !== -1 || attrName.indexOf('tap') !== -1 ||\n attrName.indexOf('action') !== -1 || attrName.indexOf('navigate') !== -1 ||\n attrName.indexOf('press') !== -1) {\n return current;\n }\n }\n }\n\n // Match elements with onclick\n if (current.onclick || current.hasAttribute('onclick')) return current;\n\n // Match elements with role=\"button\"\n if (current.getAttribute('role') === 'button') return current;\n\n // Match any element with an ID containing button/btn/cta\n var id = current.id || '';\n if (id && (id.toLowerCase().indexOf('button') !== -1 || id.toLowerCase().indexOf('btn') !== -1 || id.toLowerCase().indexOf('cta') !== -1)) return current;\n\n // Match elements with button-like classes\n var className = current.className;\n if (className && typeof className === 'string') {\n var cls = className.toLowerCase();\n if (cls.indexOf('btn') !== -1 || cls.indexOf('button') !== -1 || cls.indexOf('cta') !== -1 ||\n cls.indexOf('clickable') !== -1 || cls.indexOf('tappable') !== -1 || cls.indexOf('pressable') !== -1) {\n return current;\n }\n }\n\n // Match elements with cursor pointer\n try {\n var computed = window.getComputedStyle(current);\n if (computed && computed.cursor === 'pointer') return current;\n } catch(e) {}\n\n current = current.parentElement;\n }\n return null;\n }\n\n function applyPressedStyle(el) {\n if (!el || !el.style) return;\n // Save original styles\n pressedOriginalTransform = el.style.transform || '';\n pressedOriginalOpacity = el.style.opacity || '';\n pressedOriginalTransition = el.style.transition || '';\n // Apply pressed style with inline styles for maximum specificity\n el.style.transition = 'transform 80ms cubic-bezier(0.25, 0.1, 0.25, 1), opacity 80ms cubic-bezier(0.25, 0.1, 0.25, 1)';\n el.style.transform = 'scale(0.97)';\n el.style.opacity = '0.8';\n }\n\n function applyReleasedStyle(el) {\n if (!el || !el.style) return;\n // Apply spring-back animation\n el.style.transition = 'transform 280ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 280ms cubic-bezier(0.34, 1.56, 0.64, 1)';\n el.style.transform = pressedOriginalTransform || 'scale(1)';\n el.style.opacity = pressedOriginalOpacity || '1';\n }\n\n function resetStyle(el) {\n if (!el || !el.style) return;\n el.style.transform = pressedOriginalTransform;\n el.style.opacity = pressedOriginalOpacity;\n el.style.transition = pressedOriginalTransition;\n }\n\n function onTouchStart(e) {\n try {\n var target = findInteractive(e.target);\n if (!target) return;\n if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null; }\n if (pressed && pressed !== target) { resetStyle(pressed); }\n applyPressedStyle(target);\n pressed = target;\n } catch(err) {}\n }\n \n function onTouchEnd(e) {\n try {\n if (!pressed) return;\n var t = pressed;\n applyReleasedStyle(t);\n releaseTimer = setTimeout(function() {\n resetStyle(t);\n releaseTimer = null;\n }, 300);\n pressed = null;\n } catch(err) {}\n }\n \n function onTouchCancel(e) {\n try {\n if (!pressed) return;\n resetStyle(pressed);\n pressed = null;\n if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null; }\n } catch(err) {}\n }\n \n // Use capture phase for immediate response before any other handlers\n document.addEventListener('touchstart', onTouchStart, { passive: true, capture: true });\n document.addEventListener('touchend', onTouchEnd, { passive: true, capture: true });\n document.addEventListener('touchcancel', onTouchCancel, { passive: true, capture: true });\n // Mouse events for testing\n document.addEventListener('mousedown', onTouchStart, { passive: true, capture: true });\n document.addEventListener('mouseup', onTouchEnd, { passive: true, capture: true });\n \n } catch (err) {}\n true;\n})();\n";
7
7
  export type ScreenPayload = {
@@ -51,6 +51,7 @@ const react_native_webview_1 = require("react-native-webview");
51
51
  const RampKitNative_1 = require("./RampKitNative");
52
52
  const OnboardingResponseStorage_1 = require("./OnboardingResponseStorage");
53
53
  const Logger_1 = require("./Logger");
54
+ const EventManager_1 = require("./EventManager");
54
55
  // Reuse your injected script from App
55
56
  exports.injectedHardening = `
56
57
  (function(){
@@ -172,6 +173,27 @@ exports.injectedVarsHandler = `
172
173
  }
173
174
  } catch(e) {}
174
175
  }, false);
176
+
177
+ // Listen for input blur events to notify native for debounce flush
178
+ document.addEventListener('blur', function(e) {
179
+ var target = e.target;
180
+ if (!target) return;
181
+ var tagName = target.tagName ? target.tagName.toUpperCase() : '';
182
+ if (tagName !== 'INPUT' && tagName !== 'TEXTAREA') return;
183
+
184
+ // Get variable name from data-var, name, or id attribute
185
+ var varName = target.getAttribute('data-var') || target.name || target.id;
186
+ if (!varName) return;
187
+
188
+ try {
189
+ var message = { type: 'rampkit:input-blur', variableName: varName };
190
+ if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
191
+ window.ReactNativeWebView.postMessage(JSON.stringify(message));
192
+ }
193
+ } catch(err) {
194
+ // Silently ignore messaging errors
195
+ }
196
+ }, true); // Use capture phase to catch all blur events
175
197
  } catch (_) {}
176
198
  true;
177
199
  })();
@@ -1037,6 +1059,46 @@ function Overlay(props) {
1037
1059
  const screenActivationTimeRef = (0, react_1.useRef)({ 0: Date.now() });
1038
1060
  // Settling period - ignore variable updates from a screen for this long after activation
1039
1061
  const SCREEN_SETTLING_MS = 300;
1062
+ const pendingVariableEventsRef = (0, react_1.useRef)(new Map());
1063
+ const VARIABLE_DEBOUNCE_MS = 1000;
1064
+ // Fire a variable_set event and clean up
1065
+ const fireVariableSetEvent = (variableName) => {
1066
+ const pending = pendingVariableEventsRef.current.get(variableName);
1067
+ if (!pending)
1068
+ return;
1069
+ // Clear timer if exists
1070
+ if (pending.timer) {
1071
+ clearTimeout(pending.timer);
1072
+ }
1073
+ // Fire the event
1074
+ EventManager_1.eventManager.trackVariableSet(variableName, pending.previousValue, pending.newValue);
1075
+ // Remove from pending
1076
+ pendingVariableEventsRef.current.delete(variableName);
1077
+ };
1078
+ // Schedule a variable_set event with debouncing
1079
+ const scheduleVariableSetEvent = (variableName, previousValue, newValue) => {
1080
+ // Clear existing timer for this variable
1081
+ const existing = pendingVariableEventsRef.current.get(variableName);
1082
+ if (existing === null || existing === void 0 ? void 0 : existing.timer) {
1083
+ clearTimeout(existing.timer);
1084
+ }
1085
+ // Schedule new timer
1086
+ const timer = setTimeout(() => {
1087
+ fireVariableSetEvent(variableName);
1088
+ }, VARIABLE_DEBOUNCE_MS);
1089
+ // Store pending event
1090
+ pendingVariableEventsRef.current.set(variableName, {
1091
+ previousValue: existing ? existing.previousValue : previousValue, // Keep original previousValue
1092
+ newValue,
1093
+ timer,
1094
+ });
1095
+ };
1096
+ // Handle input blur - immediately fire pending event for that variable
1097
+ const handleInputBlur = (variableName) => {
1098
+ if (pendingVariableEventsRef.current.has(variableName)) {
1099
+ fireVariableSetEvent(variableName);
1100
+ }
1101
+ };
1040
1102
  // Queue of pending actions per screen - actions are queued when screen is inactive
1041
1103
  // and executed when the screen becomes active (matches iOS SDK behavior)
1042
1104
  const pendingActionsRef = (0, react_1.useRef)({});
@@ -1424,6 +1486,7 @@ function Overlay(props) {
1424
1486
  // in a stack and we animate individual screen opacity/transform values.
1425
1487
  // This ensures all WebViews complete their first paint before any navigation.
1426
1488
  const navigateToIndex = (nextIndex, animation = "fade") => {
1489
+ var _a, _b;
1427
1490
  if (nextIndex === index ||
1428
1491
  nextIndex < 0 ||
1429
1492
  nextIndex >= props.screens.length)
@@ -1433,6 +1496,11 @@ function Overlay(props) {
1433
1496
  // Update active screen index and activation time FIRST
1434
1497
  activeScreenIndexRef.current = nextIndex;
1435
1498
  screenActivationTimeRef.current[nextIndex] = Date.now();
1499
+ // Track screen navigation event
1500
+ const fromScreenId = ((_a = props.screens[index]) === null || _a === void 0 ? void 0 : _a.id) || null;
1501
+ const toScreenId = ((_b = props.screens[nextIndex]) === null || _b === void 0 ? void 0 : _b.id) || `screen_${nextIndex}`;
1502
+ const navigationDirection = nextIndex > index ? "forward" : "back";
1503
+ EventManager_1.eventManager.trackScreenNavigated(fromScreenId, toScreenId, navigationDirection);
1436
1504
  // Parse animation type case-insensitively
1437
1505
  const animationType = (animation === null || animation === void 0 ? void 0 : animation.toLowerCase()) || "fade";
1438
1506
  const currentScreenAnim = screenAnims[index];
@@ -1920,6 +1988,8 @@ function Overlay(props) {
1920
1988
  }
1921
1989
  // Accept the update if value is different
1922
1990
  if (!hasHostVal || hostVal !== value) {
1991
+ // Schedule variable_set event with debouncing
1992
+ scheduleVariableSetEvent(key, hasHostVal ? hostVal : null, value);
1923
1993
  newVars[key] = value;
1924
1994
  changed = true;
1925
1995
  }
@@ -1941,6 +2011,11 @@ function Overlay(props) {
1941
2011
  sendVarsToWebView(i);
1942
2012
  return;
1943
2013
  }
2014
+ // 2.5) Input blur event - flush pending variable_set event
2015
+ if ((data === null || data === void 0 ? void 0 : data.type) === "rampkit:input-blur" && (data === null || data === void 0 ? void 0 : data.variableName)) {
2016
+ handleInputBlur(data.variableName);
2017
+ return;
2018
+ }
1944
2019
  // 3) A page requested an in-app review prompt
1945
2020
  if ((data === null || data === void 0 ? void 0 : data.type) === "rampkit:request-review" ||
1946
2021
  (data === null || data === void 0 ? void 0 : data.type) === "rampkit:review") {
@@ -11,6 +11,7 @@ export interface TargetEvaluationResult {
11
11
  targetId: string;
12
12
  targetName: string;
13
13
  bucket: number;
14
+ versionId: string | null;
14
15
  }
15
16
  /**
16
17
  * Evaluate all targets and return the selected onboarding
@@ -33,6 +33,7 @@ function evaluateTargets(targets, context, userId) {
33
33
  targetId: target.id,
34
34
  targetName: target.name,
35
35
  bucket,
36
+ versionId: onboarding.version_id || null,
36
37
  };
37
38
  }
38
39
  }
@@ -45,6 +46,7 @@ function evaluateTargets(targets, context, userId) {
45
46
  targetId: fallbackTarget.id,
46
47
  targetName: fallbackTarget.name,
47
48
  bucket,
49
+ versionId: onboarding.version_id || null,
48
50
  };
49
51
  }
50
52
  /**
package/build/types.d.ts CHANGED
@@ -45,6 +45,8 @@ export interface TargetOnboarding {
45
45
  id: string;
46
46
  allocation: number;
47
47
  url: string;
48
+ /** Version ID for this specific onboarding version */
49
+ version_id?: string | null;
48
50
  }
49
51
  /**
50
52
  * Top-level onboarding reference in manifest (metadata only)
@@ -143,6 +145,8 @@ export interface DeviceInfo {
143
145
  matchedTargetName?: string | null;
144
146
  /** The ID of the selected onboarding */
145
147
  matchedOnboardingId?: string | null;
148
+ /** The version ID of the selected onboarding */
149
+ matchedOnboardingVersionId?: string | null;
146
150
  /** The A/B test bucket (0-99) for deterministic allocation */
147
151
  abTestBucket?: number | null;
148
152
  }
@@ -268,9 +272,33 @@ export interface PurchaseRestoredProperties {
268
272
  transactionId?: string;
269
273
  originalTransactionId?: string;
270
274
  }
275
+ export interface ScreenNavigatedProperties {
276
+ /** ID of the screen navigated from (null if first screen) */
277
+ fromScreenId: string | null;
278
+ /** ID of the screen navigated to */
279
+ toScreenId: string;
280
+ /** Direction of navigation */
281
+ direction: "forward" | "back";
282
+ /** What triggered the navigation */
283
+ trigger: "button";
284
+ }
285
+ export interface VariableSetProperties {
286
+ /** Name of the variable that was set */
287
+ variableName: string;
288
+ /** Type of the variable (always "state" for now) */
289
+ variableType: "state";
290
+ /** JavaScript type of the value */
291
+ valueType: "string" | "number" | "boolean" | "object" | "array" | "unknown";
292
+ /** The new value */
293
+ newValue: any;
294
+ /** The previous value (null if not set before) */
295
+ previousValue: any | null;
296
+ /** Source of the change */
297
+ source: "user_input";
298
+ }
271
299
  export type AppLifecycleEventName = "app_session_started";
272
300
  export type OnboardingEventName = "onboarding_started" | "onboarding_completed" | "onboarding_abandoned";
273
- export type InteractionEventName = "option_selected";
301
+ export type InteractionEventName = "option_selected" | "cta_tap" | "screen_navigated" | "variable_set";
274
302
  export type PermissionEventName = "notifications_response" | "tracking_response";
275
303
  export type PaywallEventName = "paywall_shown";
276
304
  export type PurchaseEventName = "purchase_started" | "purchase_completed" | "purchase_failed" | "purchase_restored";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rampkit-expo-dev",
3
- "version": "0.0.93",
3
+ "version": "0.0.95",
4
4
  "description": "The Expo SDK for RampKit. Build, test, and personalize app onboardings with instant updates.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",