react-native-acoustic-connect-beta
Version:
BETA: React native plugin for Acoustic Connect
174 lines (167 loc) • 8.23 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _TLTRN = _interopRequireDefault(require("../TLTRN"));
var _jsxRuntime = require("react/jsx-runtime");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
/********************************************************************************************
* Copyright (C) 2025 Acoustic, L.P. All rights reserved.
*
* NOTICE: This file contains material that is confidential and proprietary to
* Acoustic, L.P. and/or other developers. No license is granted under any intellectual or
* industrial property rights of Acoustic, L.P. except as may be provided in an agreement with
* Acoustic, L.P. Any unauthorized copying or distribution of content from this file is
* prohibited.
********************************************************************************************/
// Use type-only import for LayoutChangeEvent
const Connect = ({
children,
captureKeyboardEvents,
captureDialogEvents = false,
navigationRef
}) => {
const child = children;
// React 19 moved the ref off `element.ref` onto `element.props.ref`.
// Read `props.ref` first; the legacy `child.ref` fallback is a defensive
// guard from the React 18 era — kept to avoid a behavioural change.
const childProvidedRef = child?.props?.ref ?? child?.ref;
const internalRef = (0, _react.useRef)(null);
// Resolution order (explicit → legacy child ref → auto-injected fallback).
// The chosen ref flows through the same mount-effect / touch / layout
// guards, so any path that fails to resolve a usable navigation container
// simply disables tracking rather than breaking host-app gestures.
const navigation = navigationRef ?? childProvidedRef ?? internalRef;
const shouldInjectRef = !navigationRef && !childProvidedRef && /*#__PURE__*/_react.default.isValidElement(child);
const currentRoute = (0, _react.useRef)(undefined);
const initial = (0, _react.useRef)(false);
// Tracks whether the resolved navigation ref has actually attached. See the
// readiness effect below for why a one-shot mount check is insufficient.
const [navReady, setNavReady] = (0, _react.useState)(false);
(0, _react.useEffect)(() => {
_TLTRN.default.interceptKeyboardEvents(captureKeyboardEvents);
}, [captureKeyboardEvents]);
(0, _react.useEffect)(() => {
_TLTRN.default.interceptDialogEvents(captureDialogEvents);
}, [captureDialogEvents]);
// Wait for the NavigationContainer ref to actually attach before declaring
// it usable. When <Connect> wraps the container indirectly — intermediate
// providers (AppProvider, SafeAreaProvider, …), or any provider that defers
// rendering its subtree (async init, splash gate) — the container mounts
// AFTER <Connect>'s effects run, so `navigation.current` is still null at
// first paint. A one-shot mount check would warn and permanently disable
// tracking, because the effect's only dependency is the stable ref object
// and `.current` mutations don't retrigger effects. So we poll until the
// ref is usable, then flip `navReady` to subscribe. The direct-child case
// resolves on the first synchronous check (no polling, no warning). If the
// ref never attaches within the grace window we warn once, as before.
// See CA-144314.
(0, _react.useEffect)(() => {
// Reset readiness whenever the resolved ref changes. Without this, a
// `navigationRef` prop swap leaves `navReady` stale-true, so the
// subscription effect below re-runs and attaches `addListener` to the
// old container (whose `.current` is still non-null mid-transition)
// before the new ref is validated. Resetting forces re-validation
// through `isUsable()` before tracking re-enables. See CA-144314.
setNavReady(false);
const isUsable = () => !!navigation && !!navigation.current && typeof navigation.current.addListener === "function" && typeof navigation.current.getCurrentRoute === "function";
if (isUsable()) {
setNavReady(true);
return;
}
let attempts = 0;
const POLL_INTERVAL_MS = 100;
const MAX_ATTEMPTS = 100; // ~10s grace for deferred/async provider init
let timer;
const poll = () => {
if (isUsable()) {
setNavReady(true);
return;
}
if (attempts++ >= MAX_ATTEMPTS) {
console.warn("Connect: navigation tracking disabled — no usable NavigationContainer ref resolved. " + "Pass `navigationRef` (from useNavigationContainerRef()) or ensure a NavigationContainer is rendered under <Connect>.");
return;
}
timer = setTimeout(poll, POLL_INTERVAL_MS);
};
timer = setTimeout(poll, POLL_INTERVAL_MS);
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [navigation]);
(0, _react.useEffect)(() => {
if (!navReady || !navigation?.current) {
return;
}
// Listen for the 'state' event to track navigation state changes
const unsubscribeState = navigation.current.addListener("state", () => {
currentRoute.current = extractName(navigation) || navigation.current.getCurrentRoute()?.name;
if (_reactNative.Platform.OS === "ios" && currentRoute && currentRoute.current) {
_TLTRN.default.logScreenViewPageName(currentRoute.current);
} else if (_reactNative.Platform.OS === "android") {
_TLTRN.default.logScreenViewPageName(currentRoute.current);
_TLTRN.default.logScreenLayout(currentRoute.current);
}
});
// Cleanup listeners when the component unmounts or dependencies change
return () => {
unsubscribeState();
};
}, [navReady, navigation]);
const onStartShouldSetResponderCapture = (0, _react.useCallback)(event => {
if (navigation?.current?.getCurrentRoute) {
currentRoute.current = extractName(navigation) || navigation.current.getCurrentRoute()?.name;
if (currentRoute.current) {
_TLTRN.default.logScreenViewPageName(currentRoute.current);
}
}
_TLTRN.default.logClickEvent(event);
return false; // Must remain false so events bubble to the host app's handlers
}, [navigation]);
const onLayout = (0, _react.useCallback)(_event => {
if (initial.current) {
return false;
}
initial.current = true;
if (navigation?.current?.getCurrentRoute) {
currentRoute.current = navigation.current.getCurrentRoute()?.name;
if (_reactNative.Platform.OS === "ios" && currentRoute.current) {
_TLTRN.default.logScreenViewPageName(currentRoute.current);
} else if (_reactNative.Platform.OS === "android") {
_TLTRN.default.logScreenLayout(currentRoute.current);
}
}
return true;
}, [navigation]);
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: styles.connect_main,
onLayout: onLayout,
onStartShouldSetResponderCapture: onStartShouldSetResponderCapture,
children: shouldInjectRef ? /*#__PURE__*/_react.default.cloneElement(child, {
ref: internalRef
}) : children
});
};
function extractName(navigation) {
const routeParams = navigation?.current?.getCurrentRoute?.()?.params;
if (routeParams) {
const {
name
} = routeParams;
return name ? name : navigation.current?.getCurrentRoute()?.name || "";
}
return "";
}
var _default = exports.default = Connect;
const styles = _reactNative.StyleSheet.create({
connect_main: {
flex: 1
}
});
//# sourceMappingURL=Connect.js.map