rampkit-expo-dev 0.0.76 → 0.0.78

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.
@@ -45,7 +45,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
45
45
  const react_1 = __importStar(require("react"));
46
46
  const react_native_1 = require("react-native");
47
47
  const react_native_root_siblings_1 = __importDefault(require("react-native-root-siblings"));
48
- const react_native_pager_view_1 = __importDefault(require("react-native-pager-view"));
48
+ // PagerView removed - we now render all screens in a stack to ensure first paint completes
49
+ // before any navigation. This fixes the "glitch on first open" bug.
49
50
  const react_native_webview_1 = require("react-native-webview");
50
51
  const RampKitNative_1 = require("./RampKitNative");
51
52
  const OnboardingResponseStorage_1 = require("./OnboardingResponseStorage");
@@ -1004,7 +1005,6 @@ const SLIDE_FADE_DURATION = 320;
1004
1005
  function Overlay(props) {
1005
1006
  // Get explicit window dimensions to prevent flex-based layout recalculations during transitions
1006
1007
  const { width: windowWidth, height: windowHeight } = (0, react_native_1.useWindowDimensions)();
1007
- const pagerRef = (0, react_1.useRef)(null);
1008
1008
  const [index, setIndex] = (0, react_1.useState)(0);
1009
1009
  const [loadedCount, setLoadedCount] = (0, react_1.useState)(0);
1010
1010
  const [firstPageLoaded, setFirstPageLoaded] = (0, react_1.useState)(false);
@@ -1014,9 +1014,14 @@ function Overlay(props) {
1014
1014
  const [onboardingCompleted, setOnboardingCompleted] = (0, react_1.useState)(false);
1015
1015
  const overlayOpacity = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
1016
1016
  const fadeOpacity = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
1017
- // slideFade animation values - animates the PagerView container
1018
- const pagerOpacity = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
1019
- const pagerTranslateX = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
1017
+ // Per-screen animation values - each screen has its own opacity and translateX
1018
+ // This replaces PagerView and ensures ALL screens are rendered (forcing first paint)
1019
+ // First screen starts visible (opacity: 1), others start hidden (opacity: 0)
1020
+ const screenAnimsRef = (0, react_1.useRef)(props.screens.map((_, i) => ({
1021
+ opacity: new react_native_1.Animated.Value(i === 0 ? 1 : 0),
1022
+ translateX: new react_native_1.Animated.Value(0),
1023
+ })));
1024
+ const screenAnims = screenAnimsRef.current;
1020
1025
  const allLoaded = loadedCount >= props.screens.length;
1021
1026
  const hasTrackedInitialScreen = (0, react_1.useRef)(false);
1022
1027
  // shared vars across all webviews - INITIALIZE from props.variables!
@@ -1036,6 +1041,84 @@ function Overlay(props) {
1036
1041
  const screenActivationTimeRef = (0, react_1.useRef)({ 0: Date.now() });
1037
1042
  // Settling period - ignore variable updates from a screen for this long after activation
1038
1043
  const SCREEN_SETTLING_MS = 300;
1044
+ // Queue of pending actions per screen - actions are queued when screen is inactive
1045
+ // and executed when the screen becomes active (matches iOS SDK behavior)
1046
+ const pendingActionsRef = (0, react_1.useRef)({});
1047
+ // Check if a screen is currently active
1048
+ const isScreenActive = (screenIndex) => {
1049
+ return screenIndex === activeScreenIndexRef.current;
1050
+ };
1051
+ // Queue an action to be executed when screen becomes active
1052
+ const queueAction = (screenIndex, action) => {
1053
+ if (__DEV__) {
1054
+ console.log(`[Rampkit] 📥 Queuing action for screen ${screenIndex}`);
1055
+ }
1056
+ if (!pendingActionsRef.current[screenIndex]) {
1057
+ pendingActionsRef.current[screenIndex] = [];
1058
+ }
1059
+ pendingActionsRef.current[screenIndex].push(action);
1060
+ };
1061
+ // Process any pending actions for a screen
1062
+ const processPendingActions = (screenIndex) => {
1063
+ const actions = pendingActionsRef.current[screenIndex];
1064
+ if (!actions || actions.length === 0)
1065
+ return;
1066
+ if (__DEV__) {
1067
+ console.log(`[Rampkit] ⚡ Processing ${actions.length} pending action(s) for screen ${screenIndex}`);
1068
+ }
1069
+ for (const action of actions) {
1070
+ try {
1071
+ action();
1072
+ }
1073
+ catch (e) {
1074
+ console.warn('[Rampkit] Error executing pending action:', e);
1075
+ }
1076
+ }
1077
+ // Clear the queue
1078
+ pendingActionsRef.current[screenIndex] = [];
1079
+ };
1080
+ // Activate a screen (set visibility flag and dispatch event)
1081
+ const activateScreen = (screenIndex) => {
1082
+ var _a;
1083
+ const wv = webviewsRef.current[screenIndex];
1084
+ if (!wv)
1085
+ return;
1086
+ if (__DEV__) {
1087
+ console.log(`[Rampkit] 🔓 Activating screen ${screenIndex}`);
1088
+ }
1089
+ const screenId = ((_a = props.screens[screenIndex]) === null || _a === void 0 ? void 0 : _a.id) || '';
1090
+ const activateScript = `(function() {
1091
+ window.__rampkitScreenVisible = true;
1092
+ window.__rampkitScreenIndex = ${screenIndex};
1093
+ console.log('🔓 Screen ${screenIndex} ACTIVATED');
1094
+
1095
+ // Dispatch custom event that HTML can listen to
1096
+ try {
1097
+ document.dispatchEvent(new CustomEvent('rampkit:screen-visible', {
1098
+ detail: { screenIndex: ${screenIndex}, screenId: '${screenId}' }
1099
+ }));
1100
+ } catch(e) {}
1101
+ })();`;
1102
+ // @ts-ignore: injectJavaScript exists on WebView instance
1103
+ wv.injectJavaScript(activateScript);
1104
+ // Process any pending actions for this screen
1105
+ processPendingActions(screenIndex);
1106
+ };
1107
+ // Deactivate a screen (clear visibility flag)
1108
+ const deactivateScreen = (screenIndex) => {
1109
+ const wv = webviewsRef.current[screenIndex];
1110
+ if (!wv)
1111
+ return;
1112
+ if (__DEV__) {
1113
+ console.log(`[Rampkit] 🔒 Deactivating screen ${screenIndex}`);
1114
+ }
1115
+ const deactivateScript = `(function() {
1116
+ window.__rampkitScreenVisible = false;
1117
+ console.log('🔒 Screen ${screenIndex} DEACTIVATED');
1118
+ })();`;
1119
+ // @ts-ignore: injectJavaScript exists on WebView instance
1120
+ wv.injectJavaScript(deactivateScript);
1121
+ };
1039
1122
  // ============================================================================
1040
1123
  // Navigation Resolution Helpers (matches iOS SDK behavior)
1041
1124
  // ============================================================================
@@ -1203,7 +1286,26 @@ function Overlay(props) {
1203
1286
  (_a = props.onRegisterClose) === null || _a === void 0 ? void 0 : _a.call(props, null);
1204
1287
  };
1205
1288
  }, [handleRequestClose, props.onRegisterClose]);
1289
+ // Helper to complete a screen transition - updates state, activates/deactivates screens, and sends data
1290
+ const completeTransition = (nextIndex, prevIndex) => {
1291
+ // Deactivate previous screen and activate new screen (matches iOS SDK behavior)
1292
+ // This ensures actions like review/notification requests only fire on the active screen
1293
+ if (prevIndex !== nextIndex) {
1294
+ deactivateScreen(prevIndex);
1295
+ activateScreen(nextIndex);
1296
+ }
1297
+ setIndex(nextIndex);
1298
+ sendVarsToWebView(nextIndex);
1299
+ sendOnboardingStateToWebView(nextIndex);
1300
+ // Track screen change event
1301
+ if (props.onScreenChange && props.screens[nextIndex]) {
1302
+ props.onScreenChange(nextIndex, props.screens[nextIndex].id);
1303
+ }
1304
+ };
1206
1305
  // Android hardware back goes to previous page, then closes
1306
+ // NOTE: This function no longer uses PagerView. Instead, all screens are rendered
1307
+ // in a stack and we animate individual screen opacity/transform values.
1308
+ // This ensures all WebViews complete their first paint before any navigation.
1207
1309
  const navigateToIndex = (nextIndex, animation = "fade") => {
1208
1310
  if (nextIndex === index ||
1209
1311
  nextIndex < 0 ||
@@ -1211,108 +1313,103 @@ function Overlay(props) {
1211
1313
  return;
1212
1314
  if (isTransitioning)
1213
1315
  return;
1316
+ // Update active screen index and activation time FIRST
1317
+ activeScreenIndexRef.current = nextIndex;
1318
+ screenActivationTimeRef.current[nextIndex] = Date.now();
1214
1319
  // Parse animation type case-insensitively
1215
1320
  const animationType = (animation === null || animation === void 0 ? void 0 : animation.toLowerCase()) || "fade";
1216
- // Slide animation: use PagerView's built-in animated page change
1217
- // and skip the fade curtain overlay.
1321
+ const currentScreenAnim = screenAnims[index];
1322
+ const nextScreenAnim = screenAnims[nextIndex];
1323
+ const isForward = nextIndex > index;
1324
+ const direction = isForward ? 1 : -1;
1325
+ // Slide animation: animate both screens simultaneously
1218
1326
  if (animationType === "slide") {
1219
- // Update active screen index and activation time FIRST
1220
- activeScreenIndexRef.current = nextIndex;
1221
- screenActivationTimeRef.current[nextIndex] = Date.now();
1222
- // @ts-ignore: methods exist on PagerView instance
1223
- const pager = pagerRef.current;
1224
- if (!pager)
1225
- return;
1226
- if (typeof pager.setPage === "function") {
1227
- pager.setPage(nextIndex);
1228
- }
1229
- else if (typeof pager.setPageWithoutAnimation === "function") {
1230
- pager.setPageWithoutAnimation(nextIndex);
1231
- }
1232
- // Explicitly send vars to the new page after setting it
1233
- requestAnimationFrame(() => {
1234
- sendVarsToWebView(nextIndex);
1235
- sendOnboardingStateToWebView(nextIndex);
1327
+ setIsTransitioning(true);
1328
+ // Set up next screen starting position (offscreen in direction of navigation)
1329
+ nextScreenAnim.translateX.setValue(SLIDE_FADE_OFFSET * direction);
1330
+ nextScreenAnim.opacity.setValue(1);
1331
+ // Animate both screens
1332
+ react_native_1.Animated.parallel([
1333
+ // Current screen slides out
1334
+ react_native_1.Animated.timing(currentScreenAnim.translateX, {
1335
+ toValue: -SLIDE_FADE_OFFSET * direction,
1336
+ duration: SLIDE_FADE_DURATION,
1337
+ easing: react_native_1.Easing.out(react_native_1.Easing.ease),
1338
+ useNativeDriver: true,
1339
+ }),
1340
+ // Next screen slides in
1341
+ react_native_1.Animated.timing(nextScreenAnim.translateX, {
1342
+ toValue: 0,
1343
+ duration: SLIDE_FADE_DURATION,
1344
+ easing: react_native_1.Easing.out(react_native_1.Easing.ease),
1345
+ useNativeDriver: true,
1346
+ }),
1347
+ ]).start(() => {
1348
+ // Hide old screen and reset its position
1349
+ currentScreenAnim.opacity.setValue(0);
1350
+ currentScreenAnim.translateX.setValue(0);
1351
+ completeTransition(nextIndex, index);
1352
+ setIsTransitioning(false);
1236
1353
  });
1237
1354
  return;
1238
1355
  }
1239
- // slideFade animation: smooth slide + fade transition
1240
- // Animates the PagerView container out, switches page, then animates back in
1356
+ // slideFade animation: smooth slide + fade transition on both screens
1241
1357
  if (animationType === "slidefade") {
1242
1358
  setIsTransitioning(true);
1243
- // Update active screen index and activation time FIRST
1244
- activeScreenIndexRef.current = nextIndex;
1245
- screenActivationTimeRef.current[nextIndex] = Date.now();
1246
- // Determine direction: forward (nextIndex > index) or backward
1247
- const isForward = nextIndex > index;
1248
- const direction = isForward ? 1 : -1;
1249
1359
  const halfDuration = SLIDE_FADE_DURATION / 2;
1250
- const timingConfig = {
1251
- duration: halfDuration,
1252
- easing: react_native_1.Easing.out(react_native_1.Easing.ease),
1253
- useNativeDriver: true,
1254
- };
1255
- // Phase 1: Fade out and slide the current page in exit direction
1360
+ // Set up next screen starting position
1361
+ nextScreenAnim.translateX.setValue(SLIDE_FADE_OFFSET * direction * 0.5);
1362
+ nextScreenAnim.opacity.setValue(0);
1363
+ // Animate both screens simultaneously with crossfade
1256
1364
  react_native_1.Animated.parallel([
1257
- react_native_1.Animated.timing(pagerOpacity, {
1365
+ // Current screen fades out and slides away
1366
+ react_native_1.Animated.timing(currentScreenAnim.opacity, {
1258
1367
  toValue: 0,
1259
- ...timingConfig,
1368
+ duration: halfDuration,
1369
+ easing: react_native_1.Easing.out(react_native_1.Easing.ease),
1370
+ useNativeDriver: true,
1371
+ }),
1372
+ react_native_1.Animated.timing(currentScreenAnim.translateX, {
1373
+ toValue: -SLIDE_FADE_OFFSET * direction * 0.5,
1374
+ duration: halfDuration,
1375
+ easing: react_native_1.Easing.out(react_native_1.Easing.ease),
1376
+ useNativeDriver: true,
1260
1377
  }),
1261
- react_native_1.Animated.timing(pagerTranslateX, {
1262
- toValue: -SLIDE_FADE_OFFSET * direction * 0.5, // Slide out in opposite direction
1263
- ...timingConfig,
1378
+ // Next screen fades in and slides to center
1379
+ react_native_1.Animated.timing(nextScreenAnim.opacity, {
1380
+ toValue: 1,
1381
+ duration: halfDuration,
1382
+ easing: react_native_1.Easing.out(react_native_1.Easing.ease),
1383
+ useNativeDriver: true,
1384
+ }),
1385
+ react_native_1.Animated.timing(nextScreenAnim.translateX, {
1386
+ toValue: 0,
1387
+ duration: halfDuration,
1388
+ easing: react_native_1.Easing.out(react_native_1.Easing.ease),
1389
+ useNativeDriver: true,
1264
1390
  }),
1265
1391
  ]).start(() => {
1266
- var _a, _b, _c, _d;
1267
- // Switch page instantly while invisible
1268
- // @ts-ignore: method exists on PagerView instance
1269
- (_c = (_b = (_a = pagerRef.current) === null || _a === void 0 ? void 0 : _a.setPageWithoutAnimation) === null || _b === void 0 ? void 0 : _b.call(_a, nextIndex)) !== null && _c !== void 0 ? _c : (_d = pagerRef.current) === null || _d === void 0 ? void 0 : _d.setPage(nextIndex);
1270
- // Set up for incoming animation - start from the direction we're navigating from
1271
- pagerTranslateX.setValue(SLIDE_FADE_OFFSET * direction * 0.5);
1272
- // Phase 2: Fade in and slide the new page to center
1273
- react_native_1.Animated.parallel([
1274
- react_native_1.Animated.timing(pagerOpacity, {
1275
- toValue: 1,
1276
- duration: halfDuration,
1277
- easing: react_native_1.Easing.out(react_native_1.Easing.ease),
1278
- useNativeDriver: true,
1279
- }),
1280
- react_native_1.Animated.timing(pagerTranslateX, {
1281
- toValue: 0,
1282
- duration: halfDuration,
1283
- easing: react_native_1.Easing.out(react_native_1.Easing.ease),
1284
- useNativeDriver: true,
1285
- }),
1286
- ]).start(() => {
1287
- // Send vars and onboarding state to the new page
1288
- sendVarsToWebView(nextIndex);
1289
- sendOnboardingStateToWebView(nextIndex);
1290
- setIsTransitioning(false);
1291
- });
1392
+ // Reset old screen position
1393
+ currentScreenAnim.translateX.setValue(0);
1394
+ completeTransition(nextIndex, index);
1395
+ setIsTransitioning(false);
1292
1396
  });
1293
1397
  return;
1294
1398
  }
1295
1399
  // Default fade animation: uses a white curtain overlay
1296
1400
  setIsTransitioning(true);
1297
- // Update active screen index and activation time FIRST
1298
- activeScreenIndexRef.current = nextIndex;
1299
- screenActivationTimeRef.current[nextIndex] = Date.now();
1300
1401
  react_native_1.Animated.timing(fadeOpacity, {
1301
1402
  toValue: 1,
1302
1403
  duration: 160,
1303
1404
  easing: react_native_1.Easing.out(react_native_1.Easing.quad),
1304
1405
  useNativeDriver: true,
1305
1406
  }).start(() => {
1306
- var _a, _b, _c, _d;
1307
- // switch page without built-in slide animation
1308
- // @ts-ignore: method exists on PagerView instance
1309
- (_c = (_b = (_a = pagerRef.current) === null || _a === void 0 ? void 0 : _a.setPageWithoutAnimation) === null || _b === void 0 ? void 0 : _b.call(_a, nextIndex)) !== null && _c !== void 0 ? _c : (_d = pagerRef.current) === null || _d === void 0 ? void 0 : _d.setPage(nextIndex);
1407
+ // Swap screens instantly while curtain is opaque
1408
+ currentScreenAnim.opacity.setValue(0);
1409
+ nextScreenAnim.opacity.setValue(1);
1310
1410
  requestAnimationFrame(() => {
1311
- // Explicitly send vars and onboarding state to the new page after the page switch completes
1312
- // This ensures the webview receives the latest state even if onPageSelected
1313
- // timing was off during the transition
1314
- sendVarsToWebView(nextIndex);
1315
- sendOnboardingStateToWebView(nextIndex);
1411
+ completeTransition(nextIndex, index);
1412
+ // Fade curtain out to reveal new screen
1316
1413
  react_native_1.Animated.timing(fadeOpacity, {
1317
1414
  toValue: 0,
1318
1415
  duration: 160,
@@ -1447,7 +1544,7 @@ function Overlay(props) {
1447
1544
  wv.injectJavaScript(buildDirectVarsScript(varsRef.current));
1448
1545
  // NOTE: Do NOT call sendOnboardingStateToWebView here - it would cause infinite loops
1449
1546
  // because the WebView echoes back variables which triggers another sendVarsToWebView.
1450
- // Onboarding state is sent separately in onLoadEnd and onPageSelected.
1547
+ // Onboarding state is sent separately in onLoadEnd and navigateToIndex.
1451
1548
  }
1452
1549
  react_1.default.useEffect(() => {
1453
1550
  const sub = react_native_1.BackHandler.addEventListener("hardwareBackPress", () => {
@@ -1490,24 +1587,8 @@ function Overlay(props) {
1490
1587
  }, 600);
1491
1588
  return () => clearTimeout(tid);
1492
1589
  }, [docs.length, firstPageLoaded]);
1493
- const onPageSelected = (e) => {
1494
- const pos = e.nativeEvent.position;
1495
- setIndex(pos);
1496
- // Update active screen index and activation time FIRST
1497
- activeScreenIndexRef.current = pos;
1498
- screenActivationTimeRef.current[pos] = Date.now();
1499
- if (__DEV__)
1500
- console.log("[Rampkit] onPageSelected - activating screen", pos);
1501
- // Send vars and onboarding state to the newly active screen
1502
- requestAnimationFrame(() => {
1503
- sendVarsToWebView(pos);
1504
- sendOnboardingStateToWebView(pos);
1505
- });
1506
- // Track screen change event
1507
- if (props.onScreenChange && props.screens[pos]) {
1508
- props.onScreenChange(pos, props.screens[pos].id);
1509
- }
1510
- };
1590
+ // NOTE: onPageSelected callback removed - we no longer use PagerView.
1591
+ // Screen transitions are now handled directly in navigateToIndex via completeTransition().
1511
1592
  const handleAdvance = (i, animation = "fade") => {
1512
1593
  var _a;
1513
1594
  const currentScreenId = (_a = props.screens[i]) === null || _a === void 0 ? void 0 : _a.id;
@@ -1623,13 +1704,19 @@ function Overlay(props) {
1623
1704
  styles.root,
1624
1705
  !visible && styles.invisible,
1625
1706
  visible && { opacity: overlayOpacity },
1626
- ], pointerEvents: visible && !isClosing ? "auto" : "none", children: [(0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: [
1627
- react_native_1.StyleSheet.absoluteFill,
1628
- {
1629
- opacity: pagerOpacity,
1630
- transform: [{ translateX: pagerTranslateX }],
1631
- },
1632
- ], 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: { width: windowWidth, height: windowHeight }, renderToHardwareTextureAndroid: true, children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: (r) => (webviewsRef.current[i] = r), style: { width: windowWidth, height: windowHeight }, 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: () => {
1707
+ ], pointerEvents: visible && !isClosing ? "auto" : "none", children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: react_native_1.StyleSheet.absoluteFill, children: docs.map((doc, i) => {
1708
+ var _a, _b;
1709
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: [
1710
+ react_native_1.StyleSheet.absoluteFill,
1711
+ {
1712
+ opacity: (_a = screenAnims[i]) === null || _a === void 0 ? void 0 : _a.opacity,
1713
+ transform: [{ translateX: ((_b = screenAnims[i]) === null || _b === void 0 ? void 0 : _b.translateX) || 0 }],
1714
+ // Active screen renders on top
1715
+ zIndex: i === index ? 1 : 0,
1716
+ },
1717
+ ],
1718
+ // Only the active screen receives touch events
1719
+ pointerEvents: i === index ? 'auto' : 'none', children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: (r) => (webviewsRef.current[i] = r), style: { width: windowWidth, height: windowHeight }, 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: () => {
1633
1720
  // Only initialize each screen ONCE to avoid repeated processing
1634
1721
  if (initializedScreensRef.current.has(i)) {
1635
1722
  if (__DEV__)
@@ -1658,6 +1745,25 @@ function Overlay(props) {
1658
1745
  // By sending state to all screens upfront, the DOM is already in its final state
1659
1746
  // before any navigation occurs.
1660
1747
  sendOnboardingStateToWebView(i);
1748
+ // Set initial visibility flag (matches iOS SDK behavior).
1749
+ // Only screen 0 starts as active - all others start inactive.
1750
+ // This prevents actions like review requests from firing on all screens at startup.
1751
+ if (i === 0) {
1752
+ // First screen is immediately active
1753
+ activateScreen(i);
1754
+ }
1755
+ else {
1756
+ // Other screens start inactive - inject the flag but don't dispatch event
1757
+ const wv = webviewsRef.current[i];
1758
+ if (wv) {
1759
+ // @ts-ignore: injectJavaScript exists on WebView instance
1760
+ wv.injectJavaScript(`(function() {
1761
+ window.__rampkitScreenVisible = false;
1762
+ window.__rampkitScreenIndex = ${i};
1763
+ console.log('🔒 Screen ${i} loaded but INACTIVE');
1764
+ })();`);
1765
+ }
1766
+ }
1661
1767
  }, onMessage: (ev) => {
1662
1768
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1663
1769
  const raw = ev.nativeEvent.data;
@@ -1743,7 +1849,7 @@ function Overlay(props) {
1743
1849
  // 3) A page requested an in-app review prompt
1744
1850
  if ((data === null || data === void 0 ? void 0 : data.type) === "rampkit:request-review" ||
1745
1851
  (data === null || data === void 0 ? void 0 : data.type) === "rampkit:review") {
1746
- (async () => {
1852
+ const executeReview = async () => {
1747
1853
  try {
1748
1854
  const available = await RampKitNative_1.StoreReview.isAvailableAsync();
1749
1855
  if (available) {
@@ -1751,16 +1857,36 @@ function Overlay(props) {
1751
1857
  }
1752
1858
  }
1753
1859
  catch (_) { }
1754
- })();
1860
+ };
1861
+ // Only execute if screen is active, otherwise queue for later
1862
+ if (isScreenActive(i)) {
1863
+ executeReview();
1864
+ }
1865
+ else {
1866
+ if (__DEV__)
1867
+ console.log(`[Rampkit] Queuing review request from inactive screen ${i}`);
1868
+ queueAction(i, executeReview);
1869
+ }
1755
1870
  return;
1756
1871
  }
1757
1872
  // 4) A page requested notification permission
1758
1873
  if ((data === null || data === void 0 ? void 0 : data.type) === "rampkit:request-notification-permission") {
1759
- handleNotificationPermissionRequest({
1760
- ios: data === null || data === void 0 ? void 0 : data.ios,
1761
- android: data === null || data === void 0 ? void 0 : data.android,
1762
- behavior: data === null || data === void 0 ? void 0 : data.behavior,
1763
- });
1874
+ const executeNotification = () => {
1875
+ handleNotificationPermissionRequest({
1876
+ ios: data === null || data === void 0 ? void 0 : data.ios,
1877
+ android: data === null || data === void 0 ? void 0 : data.android,
1878
+ behavior: data === null || data === void 0 ? void 0 : data.behavior,
1879
+ });
1880
+ };
1881
+ // Only execute if screen is active, otherwise queue for later
1882
+ if (isScreenActive(i)) {
1883
+ executeNotification();
1884
+ }
1885
+ else {
1886
+ if (__DEV__)
1887
+ console.log(`[Rampkit] Queuing notification request from inactive screen ${i}`);
1888
+ queueAction(i, executeNotification);
1889
+ }
1764
1890
  return;
1765
1891
  }
1766
1892
  // 5) Onboarding finished event from page
@@ -1847,7 +1973,7 @@ function Overlay(props) {
1847
1973
  return;
1848
1974
  }
1849
1975
  if (raw === "rampkit:request-review" || raw === "rampkit:review") {
1850
- (async () => {
1976
+ const executeReview = async () => {
1851
1977
  try {
1852
1978
  const available = await RampKitNative_1.StoreReview.isAvailableAsync();
1853
1979
  if (available) {
@@ -1855,11 +1981,29 @@ function Overlay(props) {
1855
1981
  }
1856
1982
  }
1857
1983
  catch (_) { }
1858
- })();
1984
+ };
1985
+ // Only execute if screen is active, otherwise queue for later
1986
+ if (isScreenActive(i)) {
1987
+ executeReview();
1988
+ }
1989
+ else {
1990
+ if (__DEV__)
1991
+ console.log(`[Rampkit] Queuing review request (raw) from inactive screen ${i}`);
1992
+ queueAction(i, executeReview);
1993
+ }
1859
1994
  return;
1860
1995
  }
1861
1996
  if (raw === "rampkit:request-notification-permission") {
1862
- handleNotificationPermissionRequest(undefined);
1997
+ const executeNotification = () => handleNotificationPermissionRequest(undefined);
1998
+ // Only execute if screen is active, otherwise queue for later
1999
+ if (isScreenActive(i)) {
2000
+ executeNotification();
2001
+ }
2002
+ else {
2003
+ if (__DEV__)
2004
+ console.log(`[Rampkit] Queuing notification request (raw) from inactive screen ${i}`);
2005
+ queueAction(i, executeNotification);
2006
+ }
1863
2007
  return;
1864
2008
  }
1865
2009
  if (raw === "rampkit:onboarding-finished") {
@@ -1930,7 +2074,8 @@ function Overlay(props) {
1930
2074
  }, onError: (e) => {
1931
2075
  // You can surface an inline error UI here if you want
1932
2076
  console.warn("WebView error:", e.nativeEvent);
1933
- } }) }, props.screens[i].id))) }) }), (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { pointerEvents: isTransitioning ? "auto" : "none", style: [
2077
+ } }) }, props.screens[i].id));
2078
+ }) }), (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { pointerEvents: isTransitioning ? "auto" : "none", style: [
1934
2079
  react_native_1.StyleSheet.absoluteFillObject,
1935
2080
  styles.curtain,
1936
2081
  { opacity: fadeOpacity },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rampkit-expo-dev",
3
- "version": "0.0.76",
3
+ "version": "0.0.78",
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",