rampkit-expo-dev 0.0.27 → 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>
@@ -525,7 +675,7 @@ function Overlay(props) {
525
675
  return () => sub.remove();
526
676
  }, [index, handleRequestClose]);
527
677
  const docs = (0, react_1.useMemo)(() => props.prebuiltDocs ||
528
- 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]);
529
679
  react_1.default.useEffect(() => {
530
680
  try {
531
681
  console.log("[Rampkit] Overlay mounted: docs=", docs.length);
@@ -654,7 +804,7 @@ function Overlay(props) {
654
804
  styles.root,
655
805
  !visible && styles.invisible,
656
806
  visible && { opacity: overlayOpacity },
657
- ], 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: () => {
658
808
  setLoadedCount((c) => c + 1);
659
809
  if (i === 0) {
660
810
  setFirstPageLoaded(true);
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.27",
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",