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.
- package/build/EventManager.d.ts +11 -2
- package/build/EventManager.js +41 -4
- package/build/RampKit.js +2 -1
- package/build/RampkitOverlay.d.ts +1 -1
- package/build/RampkitOverlay.js +75 -0
- package/build/TargetingEngine.d.ts +1 -0
- package/build/TargetingEngine.js +2 -0
- package/build/types.d.ts +29 -1
- package/package.json +1 -1
package/build/EventManager.d.ts
CHANGED
|
@@ -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
|
*/
|
package/build/EventManager.js
CHANGED
|
@@ -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(/"/g, '\"').replace(/"/g, '\"').replace(/"/g, '\"')\n .replace(/'/g, \"'\").replace(/'/g, \"'\").replace(/'/g, \"'\")\n .replace(/</g, '<').replace(/>/g, '>').replace(/&/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 = {
|
package/build/RampkitOverlay.js
CHANGED
|
@@ -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") {
|
package/build/TargetingEngine.js
CHANGED
|
@@ -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