rampkit-expo-dev 0.0.26 → 0.0.28

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.
@@ -2,7 +2,7 @@
2
2
  * RampKit Device Info Collector
3
3
  * Collects device information using native modules for the /app-users endpoint
4
4
  */
5
- import { DeviceInfo } from "./types";
5
+ import { DeviceInfo, RampKitContext } from "./types";
6
6
  /**
7
7
  * Get session start time
8
8
  */
@@ -19,3 +19,8 @@ export declare function collectDeviceInfo(): Promise<DeviceInfo>;
19
19
  * Reset session (call when app is fully restarted)
20
20
  */
21
21
  export declare function resetSession(): void;
22
+ /**
23
+ * Build RampKit context from DeviceInfo for WebView template resolution
24
+ * This creates the device/user context that gets injected as window.rampkitContext
25
+ */
26
+ export declare function buildRampKitContext(deviceInfo: DeviceInfo): RampKitContext;
@@ -11,6 +11,7 @@ exports.getSessionStartTime = getSessionStartTime;
11
11
  exports.getSessionDurationSeconds = getSessionDurationSeconds;
12
12
  exports.collectDeviceInfo = collectDeviceInfo;
13
13
  exports.resetSession = resetSession;
14
+ exports.buildRampKitContext = buildRampKitContext;
14
15
  const react_native_1 = require("react-native");
15
16
  const RampKitNative_1 = __importDefault(require("./RampKitNative"));
16
17
  const constants_1 = require("./constants");
@@ -198,3 +199,49 @@ function resetSession() {
198
199
  sessionId = null;
199
200
  sessionStartTime = null;
200
201
  }
202
+ /**
203
+ * Build RampKit context from DeviceInfo for WebView template resolution
204
+ * This creates the device/user context that gets injected as window.rampkitContext
205
+ */
206
+ function buildRampKitContext(deviceInfo) {
207
+ // Calculate days since install
208
+ const daysSinceInstall = calculateDaysSinceInstall(deviceInfo.installDate);
209
+ const device = {
210
+ platform: deviceInfo.platform,
211
+ model: deviceInfo.deviceModel,
212
+ locale: deviceInfo.deviceLocale,
213
+ language: deviceInfo.deviceLanguageCode || deviceInfo.deviceLocale.split("_")[0] || "en",
214
+ country: deviceInfo.regionCode || deviceInfo.deviceLocale.split("_")[1] || "US",
215
+ currencyCode: deviceInfo.deviceCurrencyCode || "USD",
216
+ currencySymbol: deviceInfo.deviceCurrencySymbol || "$",
217
+ appVersion: deviceInfo.appVersion || "1.0.0",
218
+ buildNumber: deviceInfo.buildNumber || "1",
219
+ bundleId: deviceInfo.bundleId || "",
220
+ interfaceStyle: deviceInfo.interfaceStyle,
221
+ timezone: deviceInfo.timezoneOffsetSeconds,
222
+ daysSinceInstall,
223
+ };
224
+ const user = {
225
+ id: deviceInfo.appUserId,
226
+ isNewUser: deviceInfo.isFirstLaunch,
227
+ hasAppleSearchAdsAttribution: deviceInfo.isAppleSearchAdsAttribution,
228
+ sessionId: deviceInfo.appSessionId,
229
+ installedAt: deviceInfo.installDate,
230
+ };
231
+ return { device, user };
232
+ }
233
+ /**
234
+ * Calculate days since install from install date string
235
+ */
236
+ function calculateDaysSinceInstall(installDateString) {
237
+ try {
238
+ const installDate = new Date(installDateString);
239
+ const now = new Date();
240
+ const diffMs = now.getTime() - installDate.getTime();
241
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
242
+ return Math.max(0, diffDays);
243
+ }
244
+ catch (_a) {
245
+ return 0;
246
+ }
247
+ }
package/build/RampKit.js CHANGED
@@ -205,6 +205,10 @@ class RampKitCore {
205
205
  const requiredScripts = Array.isArray(data.requiredScripts)
206
206
  ? data.requiredScripts
207
207
  : [];
208
+ // Build device/user context for template resolution
209
+ const rampkitContext = this.deviceInfo
210
+ ? (0, DeviceInfoCollector_1.buildRampKitContext)(this.deviceInfo)
211
+ : undefined;
208
212
  // Track onboarding started event
209
213
  const onboardingId = data.onboardingId || data.id || "unknown";
210
214
  EventManager_1.eventManager.trackOnboardingStarted(onboardingId, screens.length);
@@ -215,6 +219,7 @@ class RampKitCore {
215
219
  screens,
216
220
  variables,
217
221
  requiredScripts,
222
+ rampkitContext,
218
223
  });
219
224
  }
220
225
  catch (_) { }
@@ -223,6 +228,7 @@ class RampKitCore {
223
228
  screens,
224
229
  variables,
225
230
  requiredScripts,
231
+ rampkitContext,
226
232
  onOnboardingFinished: (payload) => {
227
233
  var _a;
228
234
  // Track onboarding completed
@@ -1,6 +1,8 @@
1
+ import { RampKitContext } from "./types";
1
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";
2
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";
3
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";
5
+ export declare const injectedTemplateResolver = "\n(function(){\n try {\n if (window.__rkTemplateResolverApplied) return true;\n window.__rkTemplateResolverApplied = true;\n \n // Build variable map from context\n function buildVarMap() {\n var vars = {};\n var ctx = window.rampkitContext || { device: {}, user: {} };\n var state = window.__rampkitVariables || {};\n \n // Device vars (device.xxx)\n if (ctx.device) {\n Object.keys(ctx.device).forEach(function(key) {\n vars['device.' + key] = ctx.device[key];\n });\n }\n \n // User vars (user.xxx)\n if (ctx.user) {\n Object.keys(ctx.user).forEach(function(key) {\n vars['user.' + key] = ctx.user[key];\n });\n }\n \n // State vars (varName - no prefix)\n Object.keys(state).forEach(function(key) {\n vars[key] = state[key];\n });\n \n return vars;\n }\n \n // Format a value for display\n function formatValue(value) {\n if (value === undefined || value === null) return '';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (typeof value === 'object') return JSON.stringify(value);\n return String(value);\n }\n \n // Resolve templates in a single text node\n function resolveTextNode(node, vars) {\n var text = node.textContent;\n if (!text || text.indexOf('${') === -1) return;\n \n var resolved = text.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_\\.]*)\\}/g, function(match, varName) {\n if (vars.hasOwnProperty(varName)) {\n return formatValue(vars[varName]);\n }\n return match; // Keep original if var not found\n });\n \n if (resolved !== text) {\n node.textContent = resolved;\n }\n }\n \n // Resolve templates in all text nodes\n function resolveAllTemplates() {\n var vars = buildVarMap();\n var walker = document.createTreeWalker(\n document.body,\n NodeFilter.SHOW_TEXT,\n null,\n false\n );\n \n var node;\n while (node = walker.nextNode()) {\n resolveTextNode(node, vars);\n }\n \n // Also resolve in attribute values that might contain templates\n var allElements = document.body.getElementsByTagName('*');\n for (var i = 0; i < allElements.length; i++) {\n var el = allElements[i];\n for (var j = 0; j < el.attributes.length; j++) {\n var attr = el.attributes[j];\n if (attr.value && attr.value.indexOf('${') !== -1) {\n var resolvedAttr = attr.value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_\\.]*)\\}/g, function(match, varName) {\n if (vars.hasOwnProperty(varName)) {\n return formatValue(vars[varName]);\n }\n return match;\n });\n if (resolvedAttr !== attr.value) {\n el.setAttribute(attr.name, resolvedAttr);\n }\n }\n }\n }\n }\n \n // Run on DOMContentLoaded\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', resolveAllTemplates);\n } else {\n // DOM already ready, run immediately\n resolveAllTemplates();\n }\n \n // Also run after a short delay to catch dynamically added content\n setTimeout(resolveAllTemplates, 100);\n \n // Expose for manual re-resolution\n window.rampkitResolveTemplates = resolveAllTemplates;\n \n // Re-resolve when variables update\n document.addEventListener('rampkit:vars-updated', function() {\n setTimeout(resolveAllTemplates, 0);\n });\n \n } catch(e) {\n console.log('[Rampkit] Template resolver error:', e);\n }\n true;\n})();\n";
4
6
  export type ScreenPayload = {
5
7
  id: string;
6
8
  html: string;
@@ -12,6 +14,7 @@ export declare function showRampkitOverlay(opts: {
12
14
  screens: ScreenPayload[];
13
15
  variables?: Record<string, any>;
14
16
  requiredScripts?: string[];
17
+ rampkitContext?: RampKitContext;
15
18
  onClose?: () => void;
16
19
  onOnboardingFinished?: (payload?: any) => void;
17
20
  onShowPaywall?: (payload?: any) => void;
@@ -27,12 +30,14 @@ export declare function preloadRampkitOverlay(opts: {
27
30
  screens: ScreenPayload[];
28
31
  variables?: Record<string, any>;
29
32
  requiredScripts?: string[];
33
+ rampkitContext?: RampKitContext;
30
34
  }): void;
31
35
  declare function Overlay(props: {
32
36
  onboardingId: string;
33
37
  screens: ScreenPayload[];
34
38
  variables?: Record<string, any>;
35
39
  requiredScripts?: string[];
40
+ rampkitContext?: RampKitContext;
36
41
  prebuiltDocs?: string[];
37
42
  onRequestClose: () => void;
38
43
  onOnboardingFinished?: (payload?: any) => void;
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.injectedVarsHandler = exports.injectedNoSelect = exports.injectedHardening = void 0;
39
+ exports.injectedTemplateResolver = exports.injectedVarsHandler = exports.injectedNoSelect = exports.injectedHardening = void 0;
40
40
  exports.showRampkitOverlay = showRampkitOverlay;
41
41
  exports.hideRampkitOverlay = hideRampkitOverlay;
42
42
  exports.closeRampkitOverlay = closeRampkitOverlay;
@@ -173,6 +173,128 @@ exports.injectedVarsHandler = `
173
173
  true;
174
174
  })();
175
175
  `;
176
+ // Template resolution script that replaces ${device.xxx} and ${user.xxx} with actual values
177
+ // This runs on DOMContentLoaded and can be re-triggered via window.rampkitResolveTemplates()
178
+ exports.injectedTemplateResolver = `
179
+ (function(){
180
+ try {
181
+ if (window.__rkTemplateResolverApplied) return true;
182
+ window.__rkTemplateResolverApplied = true;
183
+
184
+ // Build variable map from context
185
+ function buildVarMap() {
186
+ var vars = {};
187
+ var ctx = window.rampkitContext || { device: {}, user: {} };
188
+ var state = window.__rampkitVariables || {};
189
+
190
+ // Device vars (device.xxx)
191
+ if (ctx.device) {
192
+ Object.keys(ctx.device).forEach(function(key) {
193
+ vars['device.' + key] = ctx.device[key];
194
+ });
195
+ }
196
+
197
+ // User vars (user.xxx)
198
+ if (ctx.user) {
199
+ Object.keys(ctx.user).forEach(function(key) {
200
+ vars['user.' + key] = ctx.user[key];
201
+ });
202
+ }
203
+
204
+ // State vars (varName - no prefix)
205
+ Object.keys(state).forEach(function(key) {
206
+ vars[key] = state[key];
207
+ });
208
+
209
+ return vars;
210
+ }
211
+
212
+ // Format a value for display
213
+ function formatValue(value) {
214
+ if (value === undefined || value === null) return '';
215
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
216
+ if (typeof value === 'object') return JSON.stringify(value);
217
+ return String(value);
218
+ }
219
+
220
+ // Resolve templates in a single text node
221
+ function resolveTextNode(node, vars) {
222
+ var text = node.textContent;
223
+ if (!text || text.indexOf('\${') === -1) return;
224
+
225
+ var resolved = text.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_\\.]*)\\}/g, function(match, varName) {
226
+ if (vars.hasOwnProperty(varName)) {
227
+ return formatValue(vars[varName]);
228
+ }
229
+ return match; // Keep original if var not found
230
+ });
231
+
232
+ if (resolved !== text) {
233
+ node.textContent = resolved;
234
+ }
235
+ }
236
+
237
+ // Resolve templates in all text nodes
238
+ function resolveAllTemplates() {
239
+ var vars = buildVarMap();
240
+ var walker = document.createTreeWalker(
241
+ document.body,
242
+ NodeFilter.SHOW_TEXT,
243
+ null,
244
+ false
245
+ );
246
+
247
+ var node;
248
+ while (node = walker.nextNode()) {
249
+ resolveTextNode(node, vars);
250
+ }
251
+
252
+ // Also resolve in attribute values that might contain templates
253
+ var allElements = document.body.getElementsByTagName('*');
254
+ for (var i = 0; i < allElements.length; i++) {
255
+ var el = allElements[i];
256
+ for (var j = 0; j < el.attributes.length; j++) {
257
+ var attr = el.attributes[j];
258
+ if (attr.value && attr.value.indexOf('\${') !== -1) {
259
+ var resolvedAttr = attr.value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_\\.]*)\\}/g, function(match, varName) {
260
+ if (vars.hasOwnProperty(varName)) {
261
+ return formatValue(vars[varName]);
262
+ }
263
+ return match;
264
+ });
265
+ if (resolvedAttr !== attr.value) {
266
+ el.setAttribute(attr.name, resolvedAttr);
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ // Run on DOMContentLoaded
274
+ if (document.readyState === 'loading') {
275
+ document.addEventListener('DOMContentLoaded', resolveAllTemplates);
276
+ } else {
277
+ // DOM already ready, run immediately
278
+ resolveAllTemplates();
279
+ }
280
+
281
+ // Also run after a short delay to catch dynamically added content
282
+ setTimeout(resolveAllTemplates, 100);
283
+
284
+ // Expose for manual re-resolution
285
+ window.rampkitResolveTemplates = resolveAllTemplates;
286
+
287
+ // Re-resolve when variables update
288
+ document.addEventListener('rampkit:vars-updated', function() {
289
+ setTimeout(resolveAllTemplates, 0);
290
+ });
291
+
292
+ } catch(e) {
293
+ console.log('[Rampkit] Template resolver error:', e);
294
+ }
295
+ true;
296
+ })();
297
+ `;
176
298
  function performRampkitHaptic(event) {
177
299
  if (!event || event.action !== "haptic") {
178
300
  // Backwards compatible default
@@ -229,7 +351,7 @@ function showRampkitOverlay(opts) {
229
351
  if (sibling)
230
352
  return; // already visible
231
353
  const prebuiltDocs = preloadCache.get(opts.onboardingId);
232
- sibling = new react_native_root_siblings_1.default(((0, jsx_runtime_1.jsx)(Overlay, { onboardingId: opts.onboardingId, screens: opts.screens, variables: opts.variables, requiredScripts: opts.requiredScripts, prebuiltDocs: prebuiltDocs, onRequestClose: () => {
354
+ sibling = new react_native_root_siblings_1.default(((0, jsx_runtime_1.jsx)(Overlay, { onboardingId: opts.onboardingId, screens: opts.screens, variables: opts.variables, requiredScripts: opts.requiredScripts, rampkitContext: opts.rampkitContext, prebuiltDocs: prebuiltDocs, onRequestClose: () => {
233
355
  var _a;
234
356
  activeCloseHandler = null;
235
357
  hideRampkitOverlay();
@@ -261,7 +383,7 @@ function preloadRampkitOverlay(opts) {
261
383
  try {
262
384
  if (preloadCache.has(opts.onboardingId))
263
385
  return;
264
- const docs = opts.screens.map((s) => buildHtmlDocument(s, opts.variables, opts.requiredScripts));
386
+ const docs = opts.screens.map((s) => buildHtmlDocument(s, opts.variables, opts.requiredScripts, opts.rampkitContext));
265
387
  preloadCache.set(opts.onboardingId, docs);
266
388
  // Mount a hidden WebView to warm up the WebView process and cache
267
389
  if (preloadSibling)
@@ -273,14 +395,14 @@ function preloadRampkitOverlay(opts) {
273
395
  opacity: 0,
274
396
  top: -1000,
275
397
  left: -1000,
276
- }, children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { originWhitelist: ["*"], source: { html: docs[0] || "<html></html>" }, injectedJavaScriptBeforeContentLoaded: exports.injectedHardening, injectedJavaScript: exports.injectedNoSelect + exports.injectedVarsHandler, automaticallyAdjustContentInsets: false, contentInsetAdjustmentBehavior: "never", bounces: false, scrollEnabled: false, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, cacheEnabled: true, hideKeyboardAccessoryView: true }) }));
398
+ }, children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { originWhitelist: ["*"], source: { html: docs[0] || "<html></html>" }, injectedJavaScriptBeforeContentLoaded: exports.injectedHardening, injectedJavaScript: exports.injectedNoSelect + exports.injectedVarsHandler + exports.injectedTemplateResolver, automaticallyAdjustContentInsets: false, contentInsetAdjustmentBehavior: "never", bounces: false, scrollEnabled: false, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, cacheEnabled: true, hideKeyboardAccessoryView: true }) }));
277
399
  preloadSibling = new react_native_root_siblings_1.default((0, jsx_runtime_1.jsx)(HiddenPreloader, {}));
278
400
  }
279
401
  catch (e) {
280
402
  // best-effort preloading; ignore errors
281
403
  }
282
404
  }
283
- function buildHtmlDocument(screen, variables, requiredScripts) {
405
+ function buildHtmlDocument(screen, variables, requiredScripts, rampkitContext) {
284
406
  const css = screen.css || "";
285
407
  const html = screen.html || "";
286
408
  const js = screen.js || "";
@@ -315,6 +437,31 @@ function buildHtmlDocument(screen, variables, requiredScripts) {
315
437
  return "";
316
438
  }
317
439
  })();
440
+ // Default context if not provided
441
+ const context = rampkitContext || {
442
+ device: {
443
+ platform: "unknown",
444
+ model: "unknown",
445
+ locale: "en_US",
446
+ language: "en",
447
+ country: "US",
448
+ currencyCode: "USD",
449
+ currencySymbol: "$",
450
+ appVersion: "1.0.0",
451
+ buildNumber: "1",
452
+ bundleId: "",
453
+ interfaceStyle: "light",
454
+ timezone: 0,
455
+ daysSinceInstall: 0,
456
+ },
457
+ user: {
458
+ id: "",
459
+ isNewUser: true,
460
+ hasAppleSearchAdsAttribution: false,
461
+ sessionId: "",
462
+ installedAt: new Date().toISOString(),
463
+ },
464
+ };
318
465
  return `<!doctype html>
319
466
  <html>
320
467
  <head>
@@ -328,6 +475,9 @@ ${scripts}
328
475
  <body>
329
476
  ${html}
330
477
  <script>
478
+ // Device and user context for template resolution
479
+ window.rampkitContext = ${JSON.stringify(context)};
480
+ // State variables from onboarding
331
481
  window.__rampkitVariables = ${JSON.stringify(variables || {})};
332
482
  ${js}
333
483
  </script>
@@ -452,11 +602,38 @@ function Overlay(props) {
452
602
  .replace(/`/g, "\\`");
453
603
  return `(function(){try{document.dispatchEvent(new MessageEvent('message',{data:${json}}));}catch(e){}})();`;
454
604
  }
605
+ // Build a script that directly sets variables and triggers updates
606
+ // This is more reliable than dispatching events which may not be caught
607
+ function buildDirectVarsScript(vars) {
608
+ const json = JSON.stringify(vars)
609
+ .replace(/\\/g, "\\\\")
610
+ .replace(/`/g, "\\`");
611
+ return `(function(){
612
+ try {
613
+ var newVars = ${json};
614
+ // Directly update the global variables object
615
+ window.__rampkitVariables = newVars;
616
+ // Call the handler if available
617
+ if (typeof window.__rkHandleVarsUpdate === 'function') {
618
+ window.__rkHandleVarsUpdate(newVars);
619
+ }
620
+ // Dispatch custom event for any listeners
621
+ try {
622
+ document.dispatchEvent(new CustomEvent('rampkit:vars-updated', {detail: newVars}));
623
+ } catch(e) {}
624
+ // Call global callback if defined
625
+ if (typeof window.onRampkitVarsUpdate === 'function') {
626
+ window.onRampkitVarsUpdate(newVars);
627
+ }
628
+ } catch(e) {
629
+ console.log('[Rampkit] buildDirectVarsScript error:', e);
630
+ }
631
+ })();`;
632
+ }
455
633
  function sendVarsToWebView(i, isInitialLoad = false) {
456
634
  const wv = webviewsRef.current[i];
457
635
  if (!wv)
458
636
  return;
459
- const payload = { type: "rampkit:variables", vars: varsRef.current };
460
637
  if (__DEV__)
461
638
  console.log("[Rampkit] sendVarsToWebView", i, varsRef.current, { isInitialLoad });
462
639
  // Only update the stale filter timestamp during initial page load,
@@ -466,8 +643,10 @@ function Overlay(props) {
466
643
  if (isInitialLoad) {
467
644
  lastInitSendRef.current[i] = Date.now();
468
645
  }
646
+ // Use direct variable setting instead of MessageEvent dispatch
647
+ // This is more reliable as it doesn't depend on event listeners being set up
469
648
  // @ts-ignore: injectJavaScript exists on WebView instance
470
- wv.injectJavaScript(buildDispatchScript(payload));
649
+ wv.injectJavaScript(buildDirectVarsScript(varsRef.current));
471
650
  }
472
651
  function broadcastVars() {
473
652
  if (__DEV__)
@@ -475,14 +654,12 @@ function Overlay(props) {
475
654
  recipients: webviewsRef.current.length,
476
655
  vars: varsRef.current,
477
656
  });
657
+ const script = buildDirectVarsScript(varsRef.current);
478
658
  for (let i = 0; i < webviewsRef.current.length; i++) {
479
659
  const wv = webviewsRef.current[i];
480
660
  if (wv) {
481
661
  // @ts-ignore: injectJavaScript exists on WebView instance
482
- wv.injectJavaScript(buildDispatchScript({
483
- type: "rampkit:variables",
484
- vars: varsRef.current,
485
- }));
662
+ wv.injectJavaScript(script);
486
663
  }
487
664
  }
488
665
  }
@@ -498,7 +675,7 @@ function Overlay(props) {
498
675
  return () => sub.remove();
499
676
  }, [index, handleRequestClose]);
500
677
  const docs = (0, react_1.useMemo)(() => props.prebuiltDocs ||
501
- props.screens.map((s) => buildHtmlDocument(s, props.variables, props.requiredScripts)), [props.prebuiltDocs, props.screens, props.variables, props.requiredScripts]);
678
+ props.screens.map((s) => buildHtmlDocument(s, props.variables, props.requiredScripts, props.rampkitContext)), [props.prebuiltDocs, props.screens, props.variables, props.requiredScripts, props.rampkitContext]);
502
679
  react_1.default.useEffect(() => {
503
680
  try {
504
681
  console.log("[Rampkit] Overlay mounted: docs=", docs.length);
@@ -627,7 +804,7 @@ function Overlay(props) {
627
804
  styles.root,
628
805
  !visible && styles.invisible,
629
806
  visible && { opacity: overlayOpacity },
630
- ], pointerEvents: visible && !isClosing ? "auto" : "none", children: [(0, jsx_runtime_1.jsx)(react_native_pager_view_1.default, { ref: pagerRef, style: react_native_1.StyleSheet.absoluteFill, scrollEnabled: false, initialPage: 0, onPageSelected: onPageSelected, offscreenPageLimit: props.screens.length, overScrollMode: "never", children: docs.map((doc, i) => ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.page, renderToHardwareTextureAndroid: true, children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: (r) => (webviewsRef.current[i] = r), style: styles.webview, originWhitelist: ["*"], source: { html: doc }, injectedJavaScriptBeforeContentLoaded: exports.injectedHardening, injectedJavaScript: exports.injectedNoSelect + exports.injectedVarsHandler, automaticallyAdjustContentInsets: false, contentInsetAdjustmentBehavior: "never", bounces: false, scrollEnabled: false, overScrollMode: "never", scalesPageToFit: false, showsHorizontalScrollIndicator: false, dataDetectorTypes: "none", allowsLinkPreview: false, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, cacheEnabled: true, javaScriptEnabled: true, domStorageEnabled: true, hideKeyboardAccessoryView: true, onLoadEnd: () => {
807
+ ], pointerEvents: visible && !isClosing ? "auto" : "none", children: [(0, jsx_runtime_1.jsx)(react_native_pager_view_1.default, { ref: pagerRef, style: react_native_1.StyleSheet.absoluteFill, scrollEnabled: false, initialPage: 0, onPageSelected: onPageSelected, offscreenPageLimit: props.screens.length, overScrollMode: "never", children: docs.map((doc, i) => ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.page, renderToHardwareTextureAndroid: true, children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: (r) => (webviewsRef.current[i] = r), style: styles.webview, originWhitelist: ["*"], source: { html: doc }, injectedJavaScriptBeforeContentLoaded: exports.injectedHardening, injectedJavaScript: exports.injectedNoSelect + exports.injectedVarsHandler + exports.injectedTemplateResolver, automaticallyAdjustContentInsets: false, contentInsetAdjustmentBehavior: "never", bounces: false, scrollEnabled: false, overScrollMode: "never", scalesPageToFit: false, showsHorizontalScrollIndicator: false, dataDetectorTypes: "none", allowsLinkPreview: false, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, cacheEnabled: true, javaScriptEnabled: true, domStorageEnabled: true, hideKeyboardAccessoryView: true, onLoadEnd: () => {
631
808
  setLoadedCount((c) => c + 1);
632
809
  if (i === 0) {
633
810
  setFirstPageLoaded(true);
@@ -655,28 +832,22 @@ function Overlay(props) {
655
832
  typeof data.vars === "object") {
656
833
  if (__DEV__)
657
834
  console.log("[Rampkit] received variables from page", i, data.vars);
658
- const now = Date.now();
659
- const lastInit = lastInitSendRef.current[i] || 0;
660
- const filtered = {};
835
+ // Accept all variable updates from pages without filtering.
836
+ // The previous filter was too aggressive and blocked legitimate
837
+ // user interactions that happened within 600ms of page load.
838
+ // We now trust that pages send correct variable updates.
661
839
  let changed = false;
840
+ const newVars = {};
662
841
  for (const [key, value] of Object.entries(data.vars)) {
663
842
  const hasHostVal = Object.prototype.hasOwnProperty.call(varsRef.current, key);
664
843
  const hostVal = varsRef.current[key];
665
- if (now - lastInit < 600 &&
666
- hasHostVal &&
667
- hostVal !== undefined &&
668
- value !== hostVal) {
669
- if (__DEV__)
670
- console.log("[Rampkit] ignore stale var from page", i, key, value, "kept", hostVal);
671
- continue;
672
- }
673
844
  if (!hasHostVal || hostVal !== value) {
674
- filtered[key] = value;
845
+ newVars[key] = value;
675
846
  changed = true;
676
847
  }
677
848
  }
678
849
  if (changed) {
679
- varsRef.current = { ...varsRef.current, ...filtered };
850
+ varsRef.current = { ...varsRef.current, ...newVars };
680
851
  broadcastVars();
681
852
  }
682
853
  return;
package/build/index.d.ts CHANGED
@@ -6,10 +6,10 @@ import { RampKitCore } from "./RampKit";
6
6
  export declare const RampKit: RampKitCore;
7
7
  export { getRampKitUserId } from "./userId";
8
8
  export { eventManager } from "./EventManager";
9
- export { collectDeviceInfo, getSessionDurationSeconds, getSessionStartTime, } from "./DeviceInfoCollector";
9
+ export { collectDeviceInfo, getSessionDurationSeconds, getSessionStartTime, buildRampKitContext, } from "./DeviceInfoCollector";
10
10
  export { default as RampKitNative } from "./RampKitNative";
11
11
  export type { NativeDeviceInfo, NativeLaunchData } from "./RampKitNative";
12
12
  export { Haptics, StoreReview, Notifications, TransactionObserver } from "./RampKitNative";
13
13
  export type { ImpactStyle, NotificationType, NotificationOptions, NotificationPermissionResult } from "./RampKitNative";
14
- export type { DeviceInfo, RampKitEvent, EventDevice, EventContext, RampKitConfig, RampKitEventName, AppSessionStartedProperties, AppSessionEndedProperties, AppBackgroundedProperties, AppForegroundedProperties, OnboardingStartedProperties, OnboardingScreenViewedProperties, OnboardingQuestionAnsweredProperties, OnboardingCompletedProperties, OnboardingAbandonedProperties, ScreenViewProperties, CtaTapProperties, NotificationsPromptShownProperties, NotificationsResponseProperties, PaywallShownProperties, PaywallPrimaryActionTapProperties, PaywallClosedProperties, PurchaseStartedProperties, PurchaseCompletedProperties, PurchaseFailedProperties, } from "./types";
14
+ export type { DeviceInfo, RampKitEvent, EventDevice, EventContext, RampKitConfig, RampKitEventName, RampKitContext, RampKitDeviceContext, RampKitUserContext, AppSessionStartedProperties, AppSessionEndedProperties, AppBackgroundedProperties, AppForegroundedProperties, OnboardingStartedProperties, OnboardingScreenViewedProperties, OnboardingQuestionAnsweredProperties, OnboardingCompletedProperties, OnboardingAbandonedProperties, ScreenViewProperties, CtaTapProperties, NotificationsPromptShownProperties, NotificationsResponseProperties, PaywallShownProperties, PaywallPrimaryActionTapProperties, PaywallClosedProperties, PurchaseStartedProperties, PurchaseCompletedProperties, PurchaseFailedProperties, } from "./types";
15
15
  export { SDK_VERSION, CAPABILITIES } from "./constants";
package/build/index.js CHANGED
@@ -7,7 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.CAPABILITIES = exports.SDK_VERSION = exports.TransactionObserver = exports.Notifications = exports.StoreReview = exports.Haptics = exports.RampKitNative = exports.getSessionStartTime = exports.getSessionDurationSeconds = exports.collectDeviceInfo = exports.eventManager = exports.getRampKitUserId = exports.RampKit = void 0;
10
+ exports.CAPABILITIES = exports.SDK_VERSION = exports.TransactionObserver = exports.Notifications = exports.StoreReview = exports.Haptics = exports.RampKitNative = exports.buildRampKitContext = exports.getSessionStartTime = exports.getSessionDurationSeconds = exports.collectDeviceInfo = exports.eventManager = exports.getRampKitUserId = exports.RampKit = void 0;
11
11
  const RampKit_1 = require("./RampKit");
12
12
  // Main SDK singleton instance
13
13
  exports.RampKit = RampKit_1.RampKitCore.instance;
@@ -22,6 +22,7 @@ var DeviceInfoCollector_1 = require("./DeviceInfoCollector");
22
22
  Object.defineProperty(exports, "collectDeviceInfo", { enumerable: true, get: function () { return DeviceInfoCollector_1.collectDeviceInfo; } });
23
23
  Object.defineProperty(exports, "getSessionDurationSeconds", { enumerable: true, get: function () { return DeviceInfoCollector_1.getSessionDurationSeconds; } });
24
24
  Object.defineProperty(exports, "getSessionStartTime", { enumerable: true, get: function () { return DeviceInfoCollector_1.getSessionStartTime; } });
25
+ Object.defineProperty(exports, "buildRampKitContext", { enumerable: true, get: function () { return DeviceInfoCollector_1.buildRampKitContext; } });
25
26
  // Export native module for direct access
26
27
  var RampKitNative_1 = require("./RampKitNative");
27
28
  Object.defineProperty(exports, "RampKitNative", { enumerable: true, get: function () { return __importDefault(RampKitNative_1).default; } });
package/build/types.d.ts CHANGED
@@ -176,3 +176,56 @@ export interface RampKitConfig {
176
176
  onShowPaywall?: (payload?: any) => void;
177
177
  showPaywall?: (payload?: any) => void;
178
178
  }
179
+ /**
180
+ * Device context variables available in templates as ${device.xxx}
181
+ */
182
+ export interface RampKitDeviceContext {
183
+ /** Platform: "iOS", "Android", or "iPadOS" */
184
+ platform: string;
185
+ /** Device model: "iPhone 15 Pro", "Pixel 7", etc. */
186
+ model: string;
187
+ /** Full locale identifier: "en_US", "fr_FR", etc. */
188
+ locale: string;
189
+ /** Language code: "en", "fr", etc. */
190
+ language: string;
191
+ /** Country/region code: "US", "FR", etc. */
192
+ country: string;
193
+ /** Currency code: "USD", "EUR", etc. */
194
+ currencyCode: string;
195
+ /** Currency symbol: "$", "€", etc. */
196
+ currencySymbol: string;
197
+ /** App version string: "1.0.0" */
198
+ appVersion: string;
199
+ /** Build number: "123" */
200
+ buildNumber: string;
201
+ /** Bundle identifier: "com.example.app" */
202
+ bundleId: string;
203
+ /** Interface style: "light" or "dark" */
204
+ interfaceStyle: string;
205
+ /** Timezone offset in seconds from GMT */
206
+ timezone: number;
207
+ /** Days since app was first installed */
208
+ daysSinceInstall: number;
209
+ }
210
+ /**
211
+ * User context variables available in templates as ${user.xxx}
212
+ */
213
+ export interface RampKitUserContext {
214
+ /** Unique user identifier */
215
+ id: string;
216
+ /** Whether this is the user's first session */
217
+ isNewUser: boolean;
218
+ /** Whether user came from Apple Search Ads */
219
+ hasAppleSearchAdsAttribution: boolean;
220
+ /** Current session identifier */
221
+ sessionId: string;
222
+ /** ISO date string when app was first installed */
223
+ installedAt: string;
224
+ }
225
+ /**
226
+ * Full context object injected into WebView as window.rampkitContext
227
+ */
228
+ export interface RampKitContext {
229
+ device: RampKitDeviceContext;
230
+ user: RampKitUserContext;
231
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rampkit-expo-dev",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
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",