rampkit-expo-dev 0.0.55 → 0.0.56

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.
@@ -1025,6 +1025,10 @@ 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);
1028
1032
  // ============================================================================
1029
1033
  // Navigation Resolution Helpers (matches iOS SDK behavior)
1030
1034
  // ============================================================================
@@ -1205,6 +1209,8 @@ function Overlay(props) {
1205
1209
  // Slide animation: use PagerView's built-in animated page change
1206
1210
  // and skip the fade curtain overlay.
1207
1211
  if (animationType === "slide") {
1212
+ // Update active screen index FIRST
1213
+ activeScreenIndexRef.current = nextIndex;
1208
1214
  // @ts-ignore: methods exist on PagerView instance
1209
1215
  const pager = pagerRef.current;
1210
1216
  if (!pager)
@@ -1216,7 +1222,6 @@ function Overlay(props) {
1216
1222
  pager.setPageWithoutAnimation(nextIndex);
1217
1223
  }
1218
1224
  // Explicitly send vars to the new page after setting it
1219
- // This ensures the webview receives the latest state
1220
1225
  requestAnimationFrame(() => {
1221
1226
  sendVarsToWebView(nextIndex);
1222
1227
  sendOnboardingStateToWebView(nextIndex);
@@ -1227,6 +1232,8 @@ function Overlay(props) {
1227
1232
  // Animates the PagerView container out, switches page, then animates back in
1228
1233
  if (animationType === "slidefade") {
1229
1234
  setIsTransitioning(true);
1235
+ // Update active screen index FIRST
1236
+ activeScreenIndexRef.current = nextIndex;
1230
1237
  // Determine direction: forward (nextIndex > index) or backward
1231
1238
  const isForward = nextIndex > index;
1232
1239
  const direction = isForward ? 1 : -1;
@@ -1278,6 +1285,8 @@ function Overlay(props) {
1278
1285
  }
1279
1286
  // Default fade animation: uses a white curtain overlay
1280
1287
  setIsTransitioning(true);
1288
+ // Update active screen index FIRST
1289
+ activeScreenIndexRef.current = nextIndex;
1281
1290
  react_native_1.Animated.timing(fadeOpacity, {
1282
1291
  toValue: 1,
1283
1292
  duration: 160,
@@ -1415,34 +1424,6 @@ function Overlay(props) {
1415
1424
  // because the WebView echoes back variables which triggers another sendVarsToWebView.
1416
1425
  // Onboarding state is sent separately in onLoadEnd and onPageSelected.
1417
1426
  }
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
1427
  react_1.default.useEffect(() => {
1447
1428
  const sub = react_native_1.BackHandler.addEventListener("hardwareBackPress", () => {
1448
1429
  if (index > 0) {
@@ -1487,25 +1468,15 @@ function Overlay(props) {
1487
1468
  const onPageSelected = (e) => {
1488
1469
  const pos = e.nativeEvent.position;
1489
1470
  setIndex(pos);
1490
- // ensure current page is synced with latest vars when selected
1471
+ // Update active screen index FIRST (before any other processing)
1472
+ activeScreenIndexRef.current = pos;
1491
1473
  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.
1474
+ console.log("[Rampkit] onPageSelected - activating screen", pos);
1475
+ // Send vars and onboarding state to the newly active screen
1496
1476
  requestAnimationFrame(() => {
1497
1477
  sendVarsToWebView(pos);
1498
- // Send onboarding state once after vars
1499
1478
  sendOnboardingStateToWebView(pos);
1500
1479
  });
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
1480
  // Track screen change event
1510
1481
  if (props.onScreenChange && props.screens[pos]) {
1511
1482
  props.onScreenChange(pos, props.screens[pos].id);
@@ -1606,7 +1577,7 @@ function Overlay(props) {
1606
1577
  (_b = props.onNotificationPermissionResult) === null || _b === void 0 ? void 0 : _b.call(props, !!(result === null || result === void 0 ? void 0 : result.granted));
1607
1578
  }
1608
1579
  catch (_) { }
1609
- // Save to shared vars and broadcast to all pages
1580
+ // Save to shared vars and send to active screen only
1610
1581
  try {
1611
1582
  varsRef.current = {
1612
1583
  ...varsRef.current,
@@ -1617,7 +1588,8 @@ function Overlay(props) {
1617
1588
  ios: result === null || result === void 0 ? void 0 : result.ios,
1618
1589
  },
1619
1590
  };
1620
- broadcastVars();
1591
+ // Only send to active screen to avoid broadcast loops
1592
+ sendVarsToWebView(activeScreenIndexRef.current);
1621
1593
  }
1622
1594
  catch (_) { }
1623
1595
  }
@@ -1632,6 +1604,13 @@ function Overlay(props) {
1632
1604
  transform: [{ translateX: pagerTranslateX }],
1633
1605
  },
1634
1606
  ], 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: () => {
1607
+ // Only initialize each screen ONCE to avoid repeated processing
1608
+ if (initializedScreensRef.current.has(i)) {
1609
+ if (__DEV__)
1610
+ console.log(`[Rampkit] onLoadEnd skipped (already initialized): ${i}`);
1611
+ return;
1612
+ }
1613
+ initializedScreensRef.current.add(i);
1635
1614
  setLoadedCount((c) => c + 1);
1636
1615
  if (i === 0) {
1637
1616
  setFirstPageLoaded(true);
@@ -1641,12 +1620,14 @@ function Overlay(props) {
1641
1620
  props.onScreenChange(0, props.screens[0].id);
1642
1621
  }
1643
1622
  }
1644
- // Initialize this page with current vars (isInitialLoad=true to enable stale filter)
1623
+ // Initialize this page with current vars
1645
1624
  if (__DEV__)
1646
- console.log("[Rampkit] onLoadEnd init send vars", i);
1625
+ console.log("[Rampkit] onLoadEnd initializing screen", i);
1647
1626
  sendVarsToWebView(i, true);
1648
- // Send onboarding state on initial load (separate from vars to avoid loops)
1649
- sendOnboardingStateToWebView(i);
1627
+ // Only send onboarding state to the ACTIVE screen (index 0 on initial load)
1628
+ if (i === activeScreenIndexRef.current) {
1629
+ sendOnboardingStateToWebView(i);
1630
+ }
1650
1631
  }, onMessage: (ev) => {
1651
1632
  var _a, _b, _c, _d;
1652
1633
  const raw = ev.nativeEvent.data;
@@ -1655,55 +1636,32 @@ function Overlay(props) {
1655
1636
  try {
1656
1637
  // JSON path
1657
1638
  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.
1639
+ // 1) Variables from a page → update shared state
1640
+ // CRITICAL: Only accept variable updates from the ACTIVE screen
1641
+ // This prevents inactive screens from causing infinite broadcast loops
1660
1642
  if ((data === null || data === void 0 ? void 0 : data.type) === "rampkit:variables" &&
1661
1643
  (data === null || data === void 0 ? void 0 : data.vars) &&
1662
1644
  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)
1667
- const now = Date.now();
1668
- const lastSendTime = lastVarsSendTimeRef.current[i] || 0;
1669
- const timeSinceSend = now - lastSendTime;
1670
- const isWithinStaleWindow = timeSinceSend < STALE_VALUE_WINDOW_MS;
1671
- if (__DEV__) {
1672
- console.log("[Rampkit] stale check:", {
1673
- pageIndex: i,
1674
- isWithinStaleWindow,
1675
- timeSinceSend,
1676
- });
1645
+ // CRITICAL: Ignore variable updates from non-active screens
1646
+ // Only the currently visible screen should be able to update variables
1647
+ if (i !== activeScreenIndexRef.current) {
1648
+ if (__DEV__) {
1649
+ console.log(`[Rampkit] ignoring variables from inactive screen ${i} (active: ${activeScreenIndexRef.current})`);
1650
+ }
1651
+ return;
1677
1652
  }
1653
+ if (__DEV__)
1654
+ console.log("[Rampkit] received variables from ACTIVE page", i, data.vars);
1678
1655
  let changed = false;
1679
1656
  const newVars = {};
1680
1657
  for (const [key, value] of Object.entries(data.vars)) {
1681
1658
  // 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.
1659
+ // These are read-only from the WebView's perspective
1684
1660
  if (key.startsWith('onboarding.')) {
1685
- if (__DEV__) {
1686
- console.log(`[Rampkit] ignoring read-only onboarding variable: ${key}`);
1687
- }
1688
1661
  continue;
1689
1662
  }
1690
1663
  const hasHostVal = Object.prototype.hasOwnProperty.call(varsRef.current, key);
1691
1664
  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
- const hostIsNonEmpty = hostVal !== "" && hostVal !== null && hostVal !== undefined;
1699
- const incomingIsEmpty = value === "" || value === null || value === undefined;
1700
- if (hostIsNonEmpty && incomingIsEmpty) {
1701
- if (__DEV__) {
1702
- console.log(`[Rampkit] filtering stale empty value for key "${key}": keeping "${hostVal}"`);
1703
- }
1704
- continue; // Skip this key, keep host value
1705
- }
1706
- }
1707
1665
  // Accept the update if value is different
1708
1666
  if (!hasHostVal || hostVal !== value) {
1709
1667
  newVars[key] = value;
@@ -1712,12 +1670,9 @@ function Overlay(props) {
1712
1670
  }
1713
1671
  if (changed) {
1714
1672
  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);
1673
+ // NOTE: Don't broadcast to all screens - just update shared state
1674
+ // Other screens will get updated vars when they become active
1718
1675
  }
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
1676
  return;
1722
1677
  }
1723
1678
  // 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.56",
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",