rampkit-expo-dev 0.0.27 → 0.0.29

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 // Template pattern: matches ${varName}\n var TEMPLATE_MARKER = '$' + '{';\n var TEMPLATE_REGEX = /\\x24\\x7B([A-Za-z_][A-Za-z0-9_.]*)\\x7D/g;\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(TEMPLATE_MARKER) === -1) return;\n \n var resolved = text.replace(TEMPLATE_REGEX, 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(TEMPLATE_MARKER) !== -1) {\n var resolvedAttr = attr.value.replace(TEMPLATE_REGEX, 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,132 @@ 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
+ // Template pattern: matches ${'$'}{varName}
185
+ var TEMPLATE_MARKER = '$' + '{';
186
+ var TEMPLATE_REGEX = /\\x24\\x7B([A-Za-z_][A-Za-z0-9_.]*)\\x7D/g;
187
+
188
+ // Build variable map from context
189
+ function buildVarMap() {
190
+ var vars = {};
191
+ var ctx = window.rampkitContext || { device: {}, user: {} };
192
+ var state = window.__rampkitVariables || {};
193
+
194
+ // Device vars (device.xxx)
195
+ if (ctx.device) {
196
+ Object.keys(ctx.device).forEach(function(key) {
197
+ vars['device.' + key] = ctx.device[key];
198
+ });
199
+ }
200
+
201
+ // User vars (user.xxx)
202
+ if (ctx.user) {
203
+ Object.keys(ctx.user).forEach(function(key) {
204
+ vars['user.' + key] = ctx.user[key];
205
+ });
206
+ }
207
+
208
+ // State vars (varName - no prefix)
209
+ Object.keys(state).forEach(function(key) {
210
+ vars[key] = state[key];
211
+ });
212
+
213
+ return vars;
214
+ }
215
+
216
+ // Format a value for display
217
+ function formatValue(value) {
218
+ if (value === undefined || value === null) return '';
219
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
220
+ if (typeof value === 'object') return JSON.stringify(value);
221
+ return String(value);
222
+ }
223
+
224
+ // Resolve templates in a single text node
225
+ function resolveTextNode(node, vars) {
226
+ var text = node.textContent;
227
+ if (!text || text.indexOf(TEMPLATE_MARKER) === -1) return;
228
+
229
+ var resolved = text.replace(TEMPLATE_REGEX, function(match, varName) {
230
+ if (vars.hasOwnProperty(varName)) {
231
+ return formatValue(vars[varName]);
232
+ }
233
+ return match; // Keep original if var not found
234
+ });
235
+
236
+ if (resolved !== text) {
237
+ node.textContent = resolved;
238
+ }
239
+ }
240
+
241
+ // Resolve templates in all text nodes
242
+ function resolveAllTemplates() {
243
+ var vars = buildVarMap();
244
+ var walker = document.createTreeWalker(
245
+ document.body,
246
+ NodeFilter.SHOW_TEXT,
247
+ null,
248
+ false
249
+ );
250
+
251
+ var node;
252
+ while (node = walker.nextNode()) {
253
+ resolveTextNode(node, vars);
254
+ }
255
+
256
+ // Also resolve in attribute values that might contain templates
257
+ var allElements = document.body.getElementsByTagName('*');
258
+ for (var i = 0; i < allElements.length; i++) {
259
+ var el = allElements[i];
260
+ for (var j = 0; j < el.attributes.length; j++) {
261
+ var attr = el.attributes[j];
262
+ if (attr.value && attr.value.indexOf(TEMPLATE_MARKER) !== -1) {
263
+ var resolvedAttr = attr.value.replace(TEMPLATE_REGEX, function(match, varName) {
264
+ if (vars.hasOwnProperty(varName)) {
265
+ return formatValue(vars[varName]);
266
+ }
267
+ return match;
268
+ });
269
+ if (resolvedAttr !== attr.value) {
270
+ el.setAttribute(attr.name, resolvedAttr);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+
277
+ // Run on DOMContentLoaded
278
+ if (document.readyState === 'loading') {
279
+ document.addEventListener('DOMContentLoaded', resolveAllTemplates);
280
+ } else {
281
+ // DOM already ready, run immediately
282
+ resolveAllTemplates();
283
+ }
284
+
285
+ // Also run after a short delay to catch dynamically added content
286
+ setTimeout(resolveAllTemplates, 100);
287
+
288
+ // Expose for manual re-resolution
289
+ window.rampkitResolveTemplates = resolveAllTemplates;
290
+
291
+ // Re-resolve when variables update
292
+ document.addEventListener('rampkit:vars-updated', function() {
293
+ setTimeout(resolveAllTemplates, 0);
294
+ });
295
+
296
+ } catch(e) {
297
+ console.log('[Rampkit] Template resolver error:', e);
298
+ }
299
+ true;
300
+ })();
301
+ `;
176
302
  function performRampkitHaptic(event) {
177
303
  if (!event || event.action !== "haptic") {
178
304
  // Backwards compatible default
@@ -229,7 +355,7 @@ function showRampkitOverlay(opts) {
229
355
  if (sibling)
230
356
  return; // already visible
231
357
  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: () => {
358
+ 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
359
  var _a;
234
360
  activeCloseHandler = null;
235
361
  hideRampkitOverlay();
@@ -261,7 +387,7 @@ function preloadRampkitOverlay(opts) {
261
387
  try {
262
388
  if (preloadCache.has(opts.onboardingId))
263
389
  return;
264
- const docs = opts.screens.map((s) => buildHtmlDocument(s, opts.variables, opts.requiredScripts));
390
+ const docs = opts.screens.map((s) => buildHtmlDocument(s, opts.variables, opts.requiredScripts, opts.rampkitContext));
265
391
  preloadCache.set(opts.onboardingId, docs);
266
392
  // Mount a hidden WebView to warm up the WebView process and cache
267
393
  if (preloadSibling)
@@ -273,14 +399,14 @@ function preloadRampkitOverlay(opts) {
273
399
  opacity: 0,
274
400
  top: -1000,
275
401
  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 }) }));
402
+ }, 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
403
  preloadSibling = new react_native_root_siblings_1.default((0, jsx_runtime_1.jsx)(HiddenPreloader, {}));
278
404
  }
279
405
  catch (e) {
280
406
  // best-effort preloading; ignore errors
281
407
  }
282
408
  }
283
- function buildHtmlDocument(screen, variables, requiredScripts) {
409
+ function buildHtmlDocument(screen, variables, requiredScripts, rampkitContext) {
284
410
  const css = screen.css || "";
285
411
  const html = screen.html || "";
286
412
  const js = screen.js || "";
@@ -315,6 +441,31 @@ function buildHtmlDocument(screen, variables, requiredScripts) {
315
441
  return "";
316
442
  }
317
443
  })();
444
+ // Default context if not provided
445
+ const context = rampkitContext || {
446
+ device: {
447
+ platform: "unknown",
448
+ model: "unknown",
449
+ locale: "en_US",
450
+ language: "en",
451
+ country: "US",
452
+ currencyCode: "USD",
453
+ currencySymbol: "$",
454
+ appVersion: "1.0.0",
455
+ buildNumber: "1",
456
+ bundleId: "",
457
+ interfaceStyle: "light",
458
+ timezone: 0,
459
+ daysSinceInstall: 0,
460
+ },
461
+ user: {
462
+ id: "",
463
+ isNewUser: true,
464
+ hasAppleSearchAdsAttribution: false,
465
+ sessionId: "",
466
+ installedAt: new Date().toISOString(),
467
+ },
468
+ };
318
469
  return `<!doctype html>
319
470
  <html>
320
471
  <head>
@@ -328,6 +479,9 @@ ${scripts}
328
479
  <body>
329
480
  ${html}
330
481
  <script>
482
+ // Device and user context for template resolution
483
+ window.rampkitContext = ${JSON.stringify(context)};
484
+ // State variables from onboarding
331
485
  window.__rampkitVariables = ${JSON.stringify(variables || {})};
332
486
  ${js}
333
487
  </script>
@@ -525,7 +679,7 @@ function Overlay(props) {
525
679
  return () => sub.remove();
526
680
  }, [index, handleRequestClose]);
527
681
  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]);
682
+ props.screens.map((s) => buildHtmlDocument(s, props.variables, props.requiredScripts, props.rampkitContext)), [props.prebuiltDocs, props.screens, props.variables, props.requiredScripts, props.rampkitContext]);
529
683
  react_1.default.useEffect(() => {
530
684
  try {
531
685
  console.log("[Rampkit] Overlay mounted: docs=", docs.length);
@@ -654,7 +808,7 @@ function Overlay(props) {
654
808
  styles.root,
655
809
  !visible && styles.invisible,
656
810
  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: () => {
811
+ ], 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
812
  setLoadedCount((c) => c + 1);
659
813
  if (i === 0) {
660
814
  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.29",
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",