rampkit-expo-dev 0.0.55 → 0.0.57

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.
@@ -1016,8 +1016,8 @@ function Overlay(props) {
1016
1016
  const pagerTranslateX = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
1017
1017
  const allLoaded = loadedCount >= props.screens.length;
1018
1018
  const hasTrackedInitialScreen = (0, react_1.useRef)(false);
1019
- // shared vars across all webviews
1020
- const varsRef = (0, react_1.useRef)({});
1019
+ // shared vars across all webviews - INITIALIZE from props.variables!
1020
+ const varsRef = (0, react_1.useRef)(props.variables || {});
1021
1021
  // hold refs for injection
1022
1022
  const webviewsRef = (0, react_1.useRef)([]);
1023
1023
  // Track when we last SENT vars to each page (for stale value filtering)
@@ -1025,6 +1025,14 @@ function Overlay(props) {
1025
1025
  const lastVarsSendTimeRef = (0, react_1.useRef)([]);
1026
1026
  // Stale value window in milliseconds - matches iOS SDK (600ms)
1027
1027
  const STALE_VALUE_WINDOW_MS = 600;
1028
+ // Track which screens have completed initial setup (to avoid repeated onLoadEnd processing)
1029
+ const initializedScreensRef = (0, react_1.useRef)(new Set());
1030
+ // Track the currently active screen index (matches iOS SDK's activeScreenIndex)
1031
+ const activeScreenIndexRef = (0, react_1.useRef)(0);
1032
+ // Track when a screen was activated (to filter out stale echoes during settling period)
1033
+ const screenActivationTimeRef = (0, react_1.useRef)({ 0: Date.now() });
1034
+ // Settling period - ignore variable updates from a screen for this long after activation
1035
+ const SCREEN_SETTLING_MS = 300;
1028
1036
  // ============================================================================
1029
1037
  // Navigation Resolution Helpers (matches iOS SDK behavior)
1030
1038
  // ============================================================================
@@ -1205,6 +1213,9 @@ function Overlay(props) {
1205
1213
  // Slide animation: use PagerView's built-in animated page change
1206
1214
  // and skip the fade curtain overlay.
1207
1215
  if (animationType === "slide") {
1216
+ // Update active screen index and activation time FIRST
1217
+ activeScreenIndexRef.current = nextIndex;
1218
+ screenActivationTimeRef.current[nextIndex] = Date.now();
1208
1219
  // @ts-ignore: methods exist on PagerView instance
1209
1220
  const pager = pagerRef.current;
1210
1221
  if (!pager)
@@ -1216,7 +1227,6 @@ function Overlay(props) {
1216
1227
  pager.setPageWithoutAnimation(nextIndex);
1217
1228
  }
1218
1229
  // Explicitly send vars to the new page after setting it
1219
- // This ensures the webview receives the latest state
1220
1230
  requestAnimationFrame(() => {
1221
1231
  sendVarsToWebView(nextIndex);
1222
1232
  sendOnboardingStateToWebView(nextIndex);
@@ -1227,6 +1237,9 @@ function Overlay(props) {
1227
1237
  // Animates the PagerView container out, switches page, then animates back in
1228
1238
  if (animationType === "slidefade") {
1229
1239
  setIsTransitioning(true);
1240
+ // Update active screen index and activation time FIRST
1241
+ activeScreenIndexRef.current = nextIndex;
1242
+ screenActivationTimeRef.current[nextIndex] = Date.now();
1230
1243
  // Determine direction: forward (nextIndex > index) or backward
1231
1244
  const isForward = nextIndex > index;
1232
1245
  const direction = isForward ? 1 : -1;
@@ -1278,6 +1291,9 @@ function Overlay(props) {
1278
1291
  }
1279
1292
  // Default fade animation: uses a white curtain overlay
1280
1293
  setIsTransitioning(true);
1294
+ // Update active screen index and activation time FIRST
1295
+ activeScreenIndexRef.current = nextIndex;
1296
+ screenActivationTimeRef.current[nextIndex] = Date.now();
1281
1297
  react_native_1.Animated.timing(fadeOpacity, {
1282
1298
  toValue: 1,
1283
1299
  duration: 160,
@@ -1320,30 +1336,45 @@ function Overlay(props) {
1320
1336
  try {
1321
1337
  var payload = ${json};
1322
1338
  var newVars = payload.vars;
1323
- // Directly update the global variables object
1324
- window.__rampkitVariables = newVars;
1339
+
1340
+ // Update ALL variable storage locations for consistency
1341
+ // This ensures dynamic tap handlers can find the latest values
1342
+ window.__rampkitVariables = Object.assign({}, window.__rampkitVariables || {}, newVars);
1343
+
1344
+ // Also update window.__rampkitVars (used by template resolver and dynamic tap)
1345
+ if (window.__rampkitVars) {
1346
+ Object.keys(newVars).forEach(function(k) {
1347
+ window.__rampkitVars[k] = newVars[k];
1348
+ });
1349
+ } else {
1350
+ window.__rampkitVars = Object.assign({}, newVars);
1351
+ }
1352
+
1353
+ // Also update RK_VARS if it exists (fallback storage)
1354
+ if (window.RK_VARS) {
1355
+ Object.keys(newVars).forEach(function(k) {
1356
+ window.RK_VARS[k] = newVars[k];
1357
+ });
1358
+ }
1359
+
1325
1360
  // Call the handler if available
1326
1361
  if (typeof window.__rkHandleVarsUpdate === 'function') {
1327
1362
  window.__rkHandleVarsUpdate(newVars);
1328
1363
  }
1329
- // Dispatch MessageEvent (matches iOS SDK format) - this is what the page's JS listens for
1364
+
1365
+ // Dispatch MessageEvent to trigger template resolver
1330
1366
  try {
1331
1367
  document.dispatchEvent(new MessageEvent('message', {data: payload}));
1332
1368
  } catch(e) {}
1333
- // Also dispatch on window for compatibility
1334
- try {
1335
- window.dispatchEvent(new MessageEvent('message', {data: payload}));
1336
- } catch(e) {}
1369
+
1337
1370
  // Also dispatch custom event for any listeners
1338
1371
  try {
1339
1372
  document.dispatchEvent(new CustomEvent('rampkit:vars-updated', {detail: newVars}));
1340
1373
  } catch(e) {}
1341
- // Call global callback if defined
1342
- if (typeof window.onRampkitVarsUpdate === 'function') {
1343
- window.onRampkitVarsUpdate(newVars);
1344
- }
1374
+
1375
+ console.log('[RampKit] Variables updated:', Object.keys(newVars).length, 'keys');
1345
1376
  } catch(e) {
1346
- console.log('[Rampkit] buildDirectVarsScript error:', e);
1377
+ console.log('[RampKit] buildDirectVarsScript error:', e);
1347
1378
  }
1348
1379
  })();`;
1349
1380
  }
@@ -1415,34 +1446,6 @@ function Overlay(props) {
1415
1446
  // because the WebView echoes back variables which triggers another sendVarsToWebView.
1416
1447
  // Onboarding state is sent separately in onLoadEnd and onPageSelected.
1417
1448
  }
1418
- /**
1419
- * Broadcast variables to all WebViews, optionally excluding one.
1420
- * This mirrors the iOS SDK's broadcastVariables(excluding:) pattern.
1421
- * @param excludeIndex - Optional index of WebView to skip (typically the source of the update)
1422
- */
1423
- function broadcastVars(excludeIndex) {
1424
- if (__DEV__)
1425
- console.log("[Rampkit] broadcastVars", {
1426
- recipients: webviewsRef.current.length,
1427
- excludeIndex,
1428
- vars: varsRef.current,
1429
- });
1430
- const script = buildDirectVarsScript(varsRef.current);
1431
- const now = Date.now();
1432
- for (let i = 0; i < webviewsRef.current.length; i++) {
1433
- // Skip the source WebView to prevent echo loops
1434
- if (excludeIndex !== undefined && i === excludeIndex) {
1435
- continue;
1436
- }
1437
- const wv = webviewsRef.current[i];
1438
- if (wv) {
1439
- // Track send time for stale value filtering
1440
- lastVarsSendTimeRef.current[i] = now;
1441
- // @ts-ignore: injectJavaScript exists on WebView instance
1442
- wv.injectJavaScript(script);
1443
- }
1444
- }
1445
- }
1446
1449
  react_1.default.useEffect(() => {
1447
1450
  const sub = react_native_1.BackHandler.addEventListener("hardwareBackPress", () => {
1448
1451
  if (index > 0) {
@@ -1487,25 +1490,16 @@ function Overlay(props) {
1487
1490
  const onPageSelected = (e) => {
1488
1491
  const pos = e.nativeEvent.position;
1489
1492
  setIndex(pos);
1490
- // ensure current page is synced with latest vars when selected
1493
+ // Update active screen index and activation time FIRST
1494
+ activeScreenIndexRef.current = pos;
1495
+ screenActivationTimeRef.current[pos] = Date.now();
1491
1496
  if (__DEV__)
1492
- console.log("[Rampkit] onPageSelected", pos);
1493
- // Send vars multiple times with increasing delays to ensure the webview
1494
- // receives them. The first send might fail if the webview isn't fully ready,
1495
- // so we retry a few times.
1497
+ console.log("[Rampkit] onPageSelected - activating screen", pos);
1498
+ // Send vars and onboarding state to the newly active screen
1496
1499
  requestAnimationFrame(() => {
1497
1500
  sendVarsToWebView(pos);
1498
- // Send onboarding state once after vars
1499
1501
  sendOnboardingStateToWebView(pos);
1500
1502
  });
1501
- // Retry after a short delay in case the first send didn't work
1502
- setTimeout(() => {
1503
- sendVarsToWebView(pos);
1504
- }, 50);
1505
- // Final retry to catch any edge cases
1506
- setTimeout(() => {
1507
- sendVarsToWebView(pos);
1508
- }, 150);
1509
1503
  // Track screen change event
1510
1504
  if (props.onScreenChange && props.screens[pos]) {
1511
1505
  props.onScreenChange(pos, props.screens[pos].id);
@@ -1606,7 +1600,7 @@ function Overlay(props) {
1606
1600
  (_b = props.onNotificationPermissionResult) === null || _b === void 0 ? void 0 : _b.call(props, !!(result === null || result === void 0 ? void 0 : result.granted));
1607
1601
  }
1608
1602
  catch (_) { }
1609
- // Save to shared vars and broadcast to all pages
1603
+ // Save to shared vars and send to active screen only
1610
1604
  try {
1611
1605
  varsRef.current = {
1612
1606
  ...varsRef.current,
@@ -1617,7 +1611,8 @@ function Overlay(props) {
1617
1611
  ios: result === null || result === void 0 ? void 0 : result.ios,
1618
1612
  },
1619
1613
  };
1620
- broadcastVars();
1614
+ // Only send to active screen to avoid broadcast loops
1615
+ sendVarsToWebView(activeScreenIndexRef.current);
1621
1616
  }
1622
1617
  catch (_) { }
1623
1618
  }
@@ -1632,6 +1627,13 @@ function Overlay(props) {
1632
1627
  transform: [{ translateX: pagerTranslateX }],
1633
1628
  },
1634
1629
  ], 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 + exports.injectedDynamicTapHandler + exports.injectedButtonAnimations, injectedJavaScript: exports.injectedNoSelect + exports.injectedVarsHandler + exports.injectedButtonAnimations, 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: () => {
1630
+ // Only initialize each screen ONCE to avoid repeated processing
1631
+ if (initializedScreensRef.current.has(i)) {
1632
+ if (__DEV__)
1633
+ console.log(`[Rampkit] onLoadEnd skipped (already initialized): ${i}`);
1634
+ return;
1635
+ }
1636
+ initializedScreensRef.current.add(i);
1635
1637
  setLoadedCount((c) => c + 1);
1636
1638
  if (i === 0) {
1637
1639
  setFirstPageLoaded(true);
@@ -1641,12 +1643,14 @@ function Overlay(props) {
1641
1643
  props.onScreenChange(0, props.screens[0].id);
1642
1644
  }
1643
1645
  }
1644
- // Initialize this page with current vars (isInitialLoad=true to enable stale filter)
1646
+ // Initialize this page with current vars
1645
1647
  if (__DEV__)
1646
- console.log("[Rampkit] onLoadEnd init send vars", i);
1648
+ console.log("[Rampkit] onLoadEnd initializing screen", i);
1647
1649
  sendVarsToWebView(i, true);
1648
- // Send onboarding state on initial load (separate from vars to avoid loops)
1649
- sendOnboardingStateToWebView(i);
1650
+ // Only send onboarding state to the ACTIVE screen (index 0 on initial load)
1651
+ if (i === activeScreenIndexRef.current) {
1652
+ sendOnboardingStateToWebView(i);
1653
+ }
1650
1654
  }, onMessage: (ev) => {
1651
1655
  var _a, _b, _c, _d;
1652
1656
  const raw = ev.nativeEvent.data;
@@ -1655,53 +1659,52 @@ function Overlay(props) {
1655
1659
  try {
1656
1660
  // JSON path
1657
1661
  const data = JSON.parse(raw);
1658
- // 1) Variables from a page → update shared + broadcast to OTHER pages
1659
- // This mirrors the iOS SDK pattern with stale value filtering.
1662
+ // 1) Variables from a page → update shared state
1663
+ // CRITICAL: Only accept variable updates from the ACTIVE screen
1664
+ // This prevents inactive screens from causing infinite broadcast loops
1660
1665
  if ((data === null || data === void 0 ? void 0 : data.type) === "rampkit:variables" &&
1661
1666
  (data === null || data === void 0 ? void 0 : data.vars) &&
1662
1667
  typeof data.vars === "object") {
1663
- if (__DEV__)
1664
- console.log("[Rampkit] received variables from page", i, data.vars);
1665
- // Check if this page is within the stale value window
1666
- // (we recently sent vars to it and it may be echoing back defaults)
1668
+ // CRITICAL: Ignore variable updates from non-active screens
1669
+ // Only the currently visible screen should be able to update variables
1670
+ if (i !== activeScreenIndexRef.current) {
1671
+ if (__DEV__) {
1672
+ console.log(`[Rampkit] ignoring variables from inactive screen ${i} (active: ${activeScreenIndexRef.current})`);
1673
+ }
1674
+ return;
1675
+ }
1667
1676
  const now = Date.now();
1677
+ // Check if this screen is still in the settling period after activation
1678
+ // During settling, we filter more aggressively to prevent stale echoes
1679
+ const activationTime = screenActivationTimeRef.current[i] || 0;
1680
+ const timeSinceActivation = now - activationTime;
1681
+ const isSettling = timeSinceActivation < SCREEN_SETTLING_MS;
1682
+ // Check if we recently sent vars to this page
1668
1683
  const lastSendTime = lastVarsSendTimeRef.current[i] || 0;
1669
1684
  const timeSinceSend = now - lastSendTime;
1670
1685
  const isWithinStaleWindow = timeSinceSend < STALE_VALUE_WINDOW_MS;
1671
- if (__DEV__) {
1672
- console.log("[Rampkit] stale check:", {
1673
- pageIndex: i,
1674
- isWithinStaleWindow,
1675
- timeSinceSend,
1676
- });
1677
- }
1686
+ if (__DEV__)
1687
+ console.log("[Rampkit] received variables from ACTIVE page", i, { isSettling, timeSinceActivation, isWithinStaleWindow, timeSinceSend });
1678
1688
  let changed = false;
1679
1689
  const newVars = {};
1680
1690
  for (const [key, value] of Object.entries(data.vars)) {
1681
1691
  // CRITICAL: Filter out onboarding.* variables
1682
- // These are read-only from the WebView's perspective and should only be
1683
- // controlled by the SDK. Accepting them back creates infinite loops.
1692
+ // These are read-only from the WebView's perspective
1684
1693
  if (key.startsWith('onboarding.')) {
1685
- if (__DEV__) {
1686
- console.log(`[Rampkit] ignoring read-only onboarding variable: ${key}`);
1687
- }
1688
1694
  continue;
1689
1695
  }
1690
1696
  const hasHostVal = Object.prototype.hasOwnProperty.call(varsRef.current, key);
1691
1697
  const hostVal = varsRef.current[key];
1692
- // Stale value filtering (matches iOS SDK behavior):
1693
- // If we're within the stale window, don't let empty/default values
1694
- // overwrite existing non-empty host values.
1695
- // This prevents pages from clobbering user input with cached defaults
1696
- // when they first become active/visible.
1697
- if (isWithinStaleWindow && hasHostVal) {
1698
+ // During settling period OR stale window: protect non-empty values
1699
+ // This prevents the screen from clobbering user input with defaults
1700
+ if ((isSettling || isWithinStaleWindow) && hasHostVal) {
1698
1701
  const hostIsNonEmpty = hostVal !== "" && hostVal !== null && hostVal !== undefined;
1699
1702
  const incomingIsEmpty = value === "" || value === null || value === undefined;
1700
1703
  if (hostIsNonEmpty && incomingIsEmpty) {
1701
1704
  if (__DEV__) {
1702
- console.log(`[Rampkit] filtering stale empty value for key "${key}": keeping "${hostVal}"`);
1705
+ console.log(`[Rampkit] protecting value for "${key}": "${hostVal}" (settling: ${isSettling})`);
1703
1706
  }
1704
- continue; // Skip this key, keep host value
1707
+ continue; // Skip - keep existing non-empty value
1705
1708
  }
1706
1709
  }
1707
1710
  // Accept the update if value is different
@@ -1712,12 +1715,15 @@ function Overlay(props) {
1712
1715
  }
1713
1716
  if (changed) {
1714
1717
  varsRef.current = { ...varsRef.current, ...newVars };
1715
- // Broadcast to all WebViews EXCEPT the source (index i)
1716
- // This prevents echo loops and matches iOS SDK behavior
1717
- broadcastVars(i);
1718
+ if (__DEV__) {
1719
+ console.log("[Rampkit] variables updated:", newVars);
1720
+ }
1721
+ // CRITICAL: Send merged vars back to the active screen
1722
+ // This ensures window.__rampkitVariables has the complete state
1723
+ // which is needed for dynamic tap conditions to evaluate correctly
1724
+ // Only send if there were actual changes to prevent echo loops
1725
+ sendVarsToWebView(i);
1718
1726
  }
1719
- // NOTE: Do NOT send vars back to source page - it already has them
1720
- // and would just echo them back again, creating a ping-pong loop
1721
1727
  return;
1722
1728
  }
1723
1729
  // 2) A page asked for current vars → send only to that page
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rampkit-expo-dev",
3
- "version": "0.0.55",
3
+ "version": "0.0.57",
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",