react-native-netmera 2.0.0 → 2.1.0-alpha01

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.
Files changed (135) hide show
  1. package/RNNetmera.podspec +1 -1
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/com/netmera/reactnativesdk/RNNetmera.kt +5 -1
  4. package/android/src/main/java/com/netmera/reactnativesdk/RNNetmeraConfigHandler.kt +33 -0
  5. package/android/src/main/java/com/netmera/reactnativesdk/RNNetmeraModule.kt +13 -1
  6. package/ios/RNNetmera.mm +3 -0
  7. package/ios/RNNetmera.swift +12 -1
  8. package/ios/RNNetmeraConfigHandler.swift +28 -0
  9. package/ios/RNNetmeraPushObject.swift +3 -3
  10. package/ios/RNNetmeraRCTEventEmitter.swift +7 -0
  11. package/lib/commonjs/Netmera.js +277 -0
  12. package/lib/commonjs/Netmera.js.map +1 -0
  13. package/lib/commonjs/autotrack/NetmeraAutoTrackProvider.js +406 -0
  14. package/lib/commonjs/autotrack/NetmeraAutoTrackProvider.js.map +1 -0
  15. package/lib/commonjs/events/NetmeraEvent.js +13 -0
  16. package/lib/commonjs/events/NetmeraEvent.js.map +1 -0
  17. package/lib/commonjs/events/NetmeraEventBannerOpen.js +19 -0
  18. package/lib/commonjs/events/NetmeraEventBannerOpen.js.map +1 -0
  19. package/lib/commonjs/events/NetmeraEventBatteryLevel.js +16 -0
  20. package/lib/commonjs/events/NetmeraEventBatteryLevel.js.map +1 -0
  21. package/lib/commonjs/events/NetmeraEventCategoryView.js +19 -0
  22. package/lib/commonjs/events/NetmeraEventCategoryView.js.map +1 -0
  23. package/lib/commonjs/events/NetmeraEventInAppPurchase.js +34 -0
  24. package/lib/commonjs/events/NetmeraEventInAppPurchase.js.map +1 -0
  25. package/lib/commonjs/events/NetmeraEventLogin.js +16 -0
  26. package/lib/commonjs/events/NetmeraEventLogin.js.map +1 -0
  27. package/lib/commonjs/events/NetmeraEventRegister.js +16 -0
  28. package/lib/commonjs/events/NetmeraEventRegister.js.map +1 -0
  29. package/lib/commonjs/events/NetmeraEventScreenView.js +28 -0
  30. package/lib/commonjs/events/NetmeraEventScreenView.js.map +1 -0
  31. package/lib/commonjs/events/NetmeraEventSearch.js +19 -0
  32. package/lib/commonjs/events/NetmeraEventSearch.js.map +1 -0
  33. package/lib/commonjs/events/NetmeraEventShare.js +19 -0
  34. package/lib/commonjs/events/NetmeraEventShare.js.map +1 -0
  35. package/lib/commonjs/events/commerce/NetmeraEventCartAddProduct.js +19 -0
  36. package/lib/commonjs/events/commerce/NetmeraEventCartAddProduct.js.map +1 -0
  37. package/lib/commonjs/events/commerce/NetmeraEventCartRemoveProduct.js +16 -0
  38. package/lib/commonjs/events/commerce/NetmeraEventCartRemoveProduct.js.map +1 -0
  39. package/lib/commonjs/events/commerce/NetmeraEventCartView.js +19 -0
  40. package/lib/commonjs/events/commerce/NetmeraEventCartView.js.map +1 -0
  41. package/lib/commonjs/events/commerce/NetmeraEventOrderCancel.js +25 -0
  42. package/lib/commonjs/events/commerce/NetmeraEventOrderCancel.js.map +1 -0
  43. package/lib/commonjs/events/commerce/NetmeraEventProductComment.js +13 -0
  44. package/lib/commonjs/events/commerce/NetmeraEventProductComment.js.map +1 -0
  45. package/lib/commonjs/events/commerce/NetmeraEventProductRate.js +16 -0
  46. package/lib/commonjs/events/commerce/NetmeraEventProductRate.js.map +1 -0
  47. package/lib/commonjs/events/commerce/NetmeraEventProductView.js +13 -0
  48. package/lib/commonjs/events/commerce/NetmeraEventProductView.js.map +1 -0
  49. package/lib/commonjs/events/commerce/NetmeraEventPurchase.js +40 -0
  50. package/lib/commonjs/events/commerce/NetmeraEventPurchase.js.map +1 -0
  51. package/lib/commonjs/events/commerce/NetmeraEventWishList.js +13 -0
  52. package/lib/commonjs/events/commerce/NetmeraEventWishList.js.map +1 -0
  53. package/lib/commonjs/events/commerce/NetmeraLineItem.js +14 -0
  54. package/lib/commonjs/events/commerce/NetmeraLineItem.js.map +1 -0
  55. package/lib/commonjs/events/commerce/NetmeraProduct.js +51 -0
  56. package/lib/commonjs/events/commerce/NetmeraProduct.js.map +1 -0
  57. package/lib/commonjs/events/media/NetmeraEventContent.js +48 -0
  58. package/lib/commonjs/events/media/NetmeraEventContent.js.map +1 -0
  59. package/lib/commonjs/events/media/NetmeraEventContentComment.js +12 -0
  60. package/lib/commonjs/events/media/NetmeraEventContentComment.js.map +1 -0
  61. package/lib/commonjs/events/media/NetmeraEventContentRate.js +15 -0
  62. package/lib/commonjs/events/media/NetmeraEventContentRate.js.map +1 -0
  63. package/lib/commonjs/events/media/NetmeraEventContentView.js +12 -0
  64. package/lib/commonjs/events/media/NetmeraEventContentView.js.map +1 -0
  65. package/lib/commonjs/index.js +352 -0
  66. package/lib/commonjs/index.js.map +1 -0
  67. package/lib/commonjs/models/NMCategoryPreference.js +13 -0
  68. package/lib/commonjs/models/NMCategoryPreference.js.map +1 -0
  69. package/lib/commonjs/models/NMInboxStatus.js +15 -0
  70. package/lib/commonjs/models/NMInboxStatus.js.map +1 -0
  71. package/lib/commonjs/models/NMInboxStatusCountFilter.js +17 -0
  72. package/lib/commonjs/models/NMInboxStatusCountFilter.js.map +1 -0
  73. package/lib/commonjs/models/NetmeraCarouselObject.js +24 -0
  74. package/lib/commonjs/models/NetmeraCarouselObject.js.map +1 -0
  75. package/lib/commonjs/models/NetmeraCategory.js +13 -0
  76. package/lib/commonjs/models/NetmeraCategory.js.map +1 -0
  77. package/lib/commonjs/models/NetmeraCategoryFilter.js +13 -0
  78. package/lib/commonjs/models/NetmeraCategoryFilter.js.map +1 -0
  79. package/lib/commonjs/models/NetmeraCouponObject.js +13 -0
  80. package/lib/commonjs/models/NetmeraCouponObject.js.map +1 -0
  81. package/lib/commonjs/models/NetmeraInboxFilter.js +13 -0
  82. package/lib/commonjs/models/NetmeraInboxFilter.js.map +1 -0
  83. package/lib/commonjs/models/NetmeraInteractiveAction.js +24 -0
  84. package/lib/commonjs/models/NetmeraInteractiveAction.js.map +1 -0
  85. package/lib/commonjs/models/NetmeraProfileAttribute.js +80 -0
  86. package/lib/commonjs/models/NetmeraProfileAttribute.js.map +1 -0
  87. package/lib/commonjs/models/NetmeraPushAction.js +31 -0
  88. package/lib/commonjs/models/NetmeraPushAction.js.map +1 -0
  89. package/lib/commonjs/models/NetmeraPushObject.js +63 -0
  90. package/lib/commonjs/models/NetmeraPushObject.js.map +1 -0
  91. package/lib/commonjs/models/NetmeraUser.js +93 -0
  92. package/lib/commonjs/models/NetmeraUser.js.map +1 -0
  93. package/lib/commonjs/models/NetmeraUserProfile.js +63 -0
  94. package/lib/commonjs/models/NetmeraUserProfile.js.map +1 -0
  95. package/lib/commonjs/models/NotificationPermissionStatus.js +14 -0
  96. package/lib/commonjs/models/NotificationPermissionStatus.js.map +1 -0
  97. package/lib/commonjs/navigation/NetmeraNavigationState.js +31 -0
  98. package/lib/commonjs/navigation/NetmeraNavigationState.js.map +1 -0
  99. package/lib/commonjs/navigation/NetmeraNavigationTracker.js +70 -0
  100. package/lib/commonjs/navigation/NetmeraNavigationTracker.js.map +1 -0
  101. package/lib/commonjs/navigation/useNetmeraNavigation.js +83 -0
  102. package/lib/commonjs/navigation/useNetmeraNavigation.js.map +1 -0
  103. package/lib/commonjs/package.json +1 -0
  104. package/lib/commonjs/utils/DeviceUtils.js +16 -0
  105. package/lib/commonjs/utils/DeviceUtils.js.map +1 -0
  106. package/lib/commonjs/utils/Optional.js +37 -0
  107. package/lib/commonjs/utils/Optional.js.map +1 -0
  108. package/lib/commonjs/utils/RNNetmera.js +19 -0
  109. package/lib/commonjs/utils/RNNetmera.js.map +1 -0
  110. package/lib/module/NetmeraAnalyticProvider.js +68 -0
  111. package/lib/module/NetmeraAnalyticProvider.js.map +1 -0
  112. package/lib/module/autotracking/NetmeraNavigationState.js +14 -0
  113. package/lib/module/autotracking/NetmeraNavigationState.js.map +1 -0
  114. package/lib/module/autotracking/useNavigationTracking.js +103 -0
  115. package/lib/module/autotracking/useNavigationTracking.js.map +1 -0
  116. package/lib/module/autotracking/useTapTracking.js +246 -0
  117. package/lib/module/autotracking/useTapTracking.js.map +1 -0
  118. package/lib/module/index.js +4 -1
  119. package/lib/module/index.js.map +1 -1
  120. package/lib/typescript/src/NetmeraAnalyticProvider.d.ts +26 -0
  121. package/lib/typescript/src/NetmeraAnalyticProvider.d.ts.map +1 -0
  122. package/lib/typescript/src/autotracking/NetmeraNavigationState.d.ts +3 -0
  123. package/lib/typescript/src/autotracking/NetmeraNavigationState.d.ts.map +1 -0
  124. package/lib/typescript/src/autotracking/useNavigationTracking.d.ts +13 -0
  125. package/lib/typescript/src/autotracking/useNavigationTracking.d.ts.map +1 -0
  126. package/lib/typescript/src/autotracking/useTapTracking.d.ts +6 -0
  127. package/lib/typescript/src/autotracking/useTapTracking.d.ts.map +1 -0
  128. package/lib/typescript/src/index.d.ts +3 -1
  129. package/lib/typescript/src/index.d.ts.map +1 -1
  130. package/package.json +1 -1
  131. package/src/NetmeraAnalyticProvider.tsx +124 -0
  132. package/src/autotracking/NetmeraNavigationState.ts +13 -0
  133. package/src/autotracking/useNavigationTracking.ts +129 -0
  134. package/src/autotracking/useTapTracking.ts +301 -0
  135. package/src/index.ts +6 -0
@@ -0,0 +1,124 @@
1
+ /*
2
+ * Copyright (c) 2026 Netmera Research.
3
+ */
4
+
5
+ import React, { useEffect, useRef, useState } from 'react';
6
+ import {
7
+ NativeEventEmitter,
8
+ NativeModules,
9
+ StyleSheet,
10
+ View,
11
+ } from 'react-native';
12
+ import { useNavigationTracking } from './autotracking/useNavigationTracking';
13
+ import { useTapTracking } from './autotracking/useTapTracking';
14
+
15
+ export interface NetmeraAnalyticProviderProps {
16
+ children: React.ReactNode;
17
+ /**
18
+ * Required on Old Architecture (newArchEnabled=false). Pass your NavigationContainer
19
+ * ref so the provider can subscribe to screen changes.
20
+ *
21
+ * On New Architecture the provider detects the navigation context automatically
22
+ * through the fiber tree — this prop is not needed.
23
+ *
24
+ * const navRef = useRef(null);
25
+ * <NetmeraAnalyticProvider navigationRef={navRef}>
26
+ * <NavigationContainer ref={navRef}>…</NavigationContainer>
27
+ * </NetmeraAnalyticProvider>
28
+ */
29
+ navigationRef?: React.RefObject<any>;
30
+ }
31
+
32
+ type AutoTrackConfig = {
33
+ isScreenFlowEnabled: boolean;
34
+ isInputActionEnabled: boolean;
35
+ shouldCollectValues: boolean;
36
+ };
37
+
38
+ /**
39
+ * Wraps your app to enable automatic screen tracking and tap tracking.
40
+ *
41
+ * <NetmeraAnalyticProvider>
42
+ * <NavigationContainer>…</NavigationContainer>
43
+ * </NetmeraAnalyticProvider>
44
+ */
45
+ export function NetmeraAnalyticProvider({
46
+ children,
47
+ navigationRef,
48
+ }: NetmeraAnalyticProviderProps) {
49
+ const viewRef = useRef<any>(null);
50
+ const [config, setConfig] = useState<AutoTrackConfig | null>(null);
51
+ const appliedConfigRef = useRef<AutoTrackConfig | null>(null);
52
+
53
+ const applyConfig = (cfg: AutoTrackConfig) => {
54
+ const prev = appliedConfigRef.current;
55
+ if (
56
+ prev?.isScreenFlowEnabled === cfg.isScreenFlowEnabled &&
57
+ prev?.isInputActionEnabled === cfg.isInputActionEnabled &&
58
+ prev?.shouldCollectValues === cfg.shouldCollectValues
59
+ )
60
+ return;
61
+ appliedConfigRef.current = cfg;
62
+ setConfig(cfg);
63
+ console.log(
64
+ '[NMAutotrack] screen tracking ',
65
+ cfg.isScreenFlowEnabled ? 'enabled' : 'disabled'
66
+ );
67
+ console.log(
68
+ '[NMAutotrack] input action tracking ',
69
+ cfg.isInputActionEnabled ? 'enabled' : 'disabled'
70
+ );
71
+ };
72
+
73
+ useEffect(() => {
74
+ NativeModules.RNNetmera?.getAutoTrackConfig?.()
75
+ ?.then((cfg: AutoTrackConfig | null) => {
76
+ if (cfg) applyConfig(cfg);
77
+ })
78
+ ?.catch((_err: unknown) => {});
79
+ }, []);
80
+
81
+ useEffect(() => {
82
+ const emitter = new NativeEventEmitter(
83
+ NativeModules.RNNetmeraRCTEventEmitter
84
+ );
85
+ const subscription = emitter.addListener(
86
+ 'onAutoTrackConfigUpdate',
87
+ (cfg: AutoTrackConfig) => {
88
+ if (cfg) applyConfig(cfg);
89
+ }
90
+ );
91
+ return () => subscription.remove();
92
+ }, []);
93
+
94
+ const screenFlowEnabled = config?.isScreenFlowEnabled ?? false;
95
+ const inputActionEnabled = config?.isInputActionEnabled ?? false;
96
+ const shouldCollectValues = config?.shouldCollectValues ?? false;
97
+
98
+ const navigationEnabled = screenFlowEnabled || inputActionEnabled;
99
+ useNavigationTracking(
100
+ viewRef,
101
+ navigationRef,
102
+ navigationEnabled,
103
+ screenFlowEnabled
104
+ );
105
+ const { onTouchStart, onTouchEnd } = useTapTracking(
106
+ inputActionEnabled,
107
+ shouldCollectValues
108
+ );
109
+
110
+ return (
111
+ <View
112
+ ref={navigationEnabled ? viewRef : undefined}
113
+ style={styles.container}
114
+ onTouchStart={onTouchStart}
115
+ onTouchEnd={onTouchEnd}
116
+ >
117
+ {children}
118
+ </View>
119
+ );
120
+ }
121
+
122
+ const styles = StyleSheet.create({
123
+ container: { flex: 1 },
124
+ });
@@ -0,0 +1,13 @@
1
+ /*
2
+ * Copyright (c) 2026 Netmera Research.
3
+ */
4
+
5
+ let currentScreen: string | null = null;
6
+
7
+ export function getCurrentScreen(): string | null {
8
+ return currentScreen;
9
+ }
10
+
11
+ export function setCurrentScreen(name: string): void {
12
+ currentScreen = name;
13
+ }
@@ -0,0 +1,129 @@
1
+ /*
2
+ * Copyright (c) 2026 Netmera Research.
3
+ */
4
+
5
+ import React, { useEffect, useRef } from 'react';
6
+ import { setCurrentScreen } from './NetmeraNavigationState';
7
+
8
+ // ContextProvider fiber tag — stable React internal constant since React 16.
9
+ const CONTEXT_PROVIDER_TAG = 10;
10
+ const NAV_SEARCH_DEPTH = 25;
11
+
12
+ // Duck-type check: React Navigation v5/v6/v7 all expose this surface on the container ref.
13
+ function isNavContainerRef(value: any): boolean {
14
+ return (
15
+ value != null &&
16
+ typeof value.addListener === 'function' &&
17
+ typeof value.getCurrentRoute === 'function' &&
18
+ typeof value.isReady === 'function'
19
+ );
20
+ }
21
+
22
+ // DFS through child fibers to find NavigationContainerRefContext.Provider.
23
+ // Siblings walk at the same depth — they sit at the same level in the tree.
24
+ function findNavContainerRef(fiber: any, depth = 0): any {
25
+ if (!fiber || depth > NAV_SEARCH_DEPTH) return null;
26
+ if (
27
+ fiber.tag === CONTEXT_PROVIDER_TAG &&
28
+ isNavContainerRef(fiber.memoizedProps?.value)
29
+ ) {
30
+ return fiber.memoizedProps.value;
31
+ }
32
+ return (
33
+ findNavContainerRef(fiber.child, depth + 1) ??
34
+ findNavContainerRef(fiber.sibling, depth)
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Subscribes to React Navigation screen changes and keeps NetmeraNavigationState
40
+ * up to date. Resolves the navigation container ref via two paths:
41
+ *
42
+ * - New Arch (Fabric): auto-detected through the fiber tree via viewRef.
43
+ * - Old Arch (Paper): caller passes navigationRef prop explicitly.
44
+ *
45
+ * Silently no-ops if neither path yields a valid ref.
46
+ * Respects the isScreenFlowEnabled flag — screen changes are ignored when disabled.
47
+ */
48
+ export function useNavigationTracking(
49
+ viewRef: React.RefObject<any>,
50
+ navigationRef: React.RefObject<any> | undefined,
51
+ enabled: boolean,
52
+ screenLogEnabled: boolean
53
+ ): void {
54
+ // React 18: capture own fiber during render via ReactCurrentOwner.
55
+ // React 19 removed ReactCurrentOwner — this silently yields null, which is fine
56
+ // because the Fabric path (viewRef.__internalInstanceHandle) covers React 19.
57
+ const screenLogEnabledRef = useRef(screenLogEnabled);
58
+
59
+ useEffect(() => {
60
+ screenLogEnabledRef.current = screenLogEnabled;
61
+ }, [screenLogEnabled]);
62
+
63
+ const selfFiberRef = useRef<any>(null);
64
+ if (selfFiberRef.current === null) {
65
+ try {
66
+ const R = React as any;
67
+ selfFiberRef.current =
68
+ R.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner
69
+ ?.current ?? null;
70
+ } catch (_) {}
71
+ }
72
+
73
+ useEffect(() => {
74
+ if (!enabled) return;
75
+ let navRef: any = null;
76
+
77
+ // Path 1 — New Arch: ReactNativeElement exposes its fiber via __internalInstanceHandle.
78
+ // Guard with 'child' in candidate to confirm it's a fiber, not another host object.
79
+ const candidate = viewRef.current?.__internalInstanceHandle;
80
+ const viewFiber: any =
81
+ candidate != null && 'child' in candidate
82
+ ? candidate
83
+ : (selfFiberRef.current?.child ?? null);
84
+
85
+ if (viewFiber) {
86
+ navRef = findNavContainerRef(viewFiber);
87
+ }
88
+
89
+ // Path 2 — Old Arch: use the ref passed explicitly via prop.
90
+ if (!navRef && isNavContainerRef(navigationRef?.current)) {
91
+ navRef = navigationRef!.current;
92
+ }
93
+
94
+ if (!navRef) return;
95
+
96
+ const handleChange = () => {
97
+ try {
98
+ const route = navRef.getCurrentRoute();
99
+ if (route?.name) {
100
+ setCurrentScreen(route.name);
101
+ if (screenLogEnabledRef.current) {
102
+ console.log(`[NMAutotrack] screen → "${route.name}"`);
103
+ }
104
+ }
105
+ } catch (_) {}
106
+ };
107
+
108
+ const unsubState = navRef.addListener('state', handleChange);
109
+
110
+ // NavigationContainer's effects run before ours (child-first), so 'ready' has
111
+ // already fired. isReady() captures the initial screen synchronously; the 'ready'
112
+ // listener is a safety net for async startup (deferred auth, deep links, etc.).
113
+ let unsubReady: (() => void) | undefined;
114
+ if (navRef.isReady()) {
115
+ handleChange();
116
+ } else {
117
+ unsubReady = navRef.addListener('ready', () => {
118
+ handleChange();
119
+ unsubReady?.();
120
+ unsubReady = undefined;
121
+ });
122
+ }
123
+
124
+ return () => {
125
+ unsubState?.();
126
+ unsubReady?.();
127
+ };
128
+ }, [enabled]); // eslint-disable-line react-hooks/exhaustive-deps
129
+ }
@@ -0,0 +1,301 @@
1
+ /*
2
+ * Copyright (c) 2026 Netmera Research.
3
+ */
4
+
5
+ import { useCallback, useEffect, useRef } from 'react';
6
+ import type { GestureResponderEvent } from 'react-native';
7
+ import { getCurrentScreen } from './NetmeraNavigationState';
8
+
9
+ const MAX_FIBER_DEPTH = 20;
10
+ const MAX_TEXT_DEPTH = 5;
11
+ const TAP_THRESHOLD = 10; // px — above this distance the gesture is a scroll, not a tap
12
+
13
+ // ── Fiber predicates ──────────────────────────────────────────────────────────
14
+
15
+ // Skip onStartShouldSetResponder — it matches ScrollViews, which are not interactive targets.
16
+ function isInteractive(props: any): boolean {
17
+ return !!(props?.onPress || props?.onClick || props?.onValueChange);
18
+ }
19
+
20
+ // Switch has onValueChange + boolean value, but no onPress/onClick.
21
+ function isSwitch(props: any): boolean {
22
+ return (
23
+ !!props?.onValueChange &&
24
+ typeof props?.value === 'boolean' &&
25
+ !(props?.onPress || props?.onClick)
26
+ );
27
+ }
28
+
29
+ // SelectDropdown and similar pickers expose onSelect on an ancestor fiber.
30
+ // Tapping them only opens the picker UI — the real select event fires separately.
31
+ function isInsideSelectionControl(fiber: any): boolean {
32
+ let node: any = fiber?.return;
33
+ for (let d = 0; d < 5 && node; d++, node = node.return) {
34
+ if (node?.memoizedProps?.onSelect != null) return true;
35
+ }
36
+ return false;
37
+ }
38
+
39
+ // ── Identifier resolution ─────────────────────────────────────────────────────
40
+
41
+ function textFromChildren(children: any, depth = 0): string | undefined {
42
+ if (depth > MAX_TEXT_DEPTH) return undefined;
43
+ if (typeof children === 'string') {
44
+ const t = children.trim();
45
+ return t.length > 0 ? t : undefined;
46
+ }
47
+ if (typeof children === 'number') return String(children);
48
+ if (Array.isArray(children)) {
49
+ for (const child of children) {
50
+ const t = textFromChildren(child, depth + 1);
51
+ if (t) return t;
52
+ }
53
+ return undefined;
54
+ }
55
+ if (children != null && typeof children === 'object' && 'props' in children) {
56
+ return textFromChildren((children as any).props?.children, depth + 1);
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ // Priority: accessibilityLabel → testID → placeholder (TextInput) → first child text.
62
+ function resolveIdentifier(props: any): string | undefined {
63
+ if (!props) return undefined;
64
+ if (
65
+ typeof props.accessibilityLabel === 'string' &&
66
+ props.accessibilityLabel.trim()
67
+ ) {
68
+ return props.accessibilityLabel.trim();
69
+ }
70
+ if (typeof props.testID === 'string' && props.testID.trim()) {
71
+ return props.testID.trim();
72
+ }
73
+ if (typeof props.placeholder === 'string' && props.placeholder.trim()) {
74
+ return props.placeholder.trim();
75
+ }
76
+ return textFromChildren(props.children);
77
+ }
78
+
79
+ // ── List index resolution ─────────────────────────────────────────────────────
80
+
81
+ type ListIndex =
82
+ | { type: 'flatlist'; index: number }
83
+ | { type: 'sectionlist'; sectionIndex: number; itemIndex: number };
84
+
85
+ function resolveListIndex(startFiber: any): ListIndex | undefined {
86
+ let fiber: any = startFiber;
87
+ for (let d = 0; d < MAX_FIBER_DEPTH && fiber; d++, fiber = fiber.return) {
88
+ const p = fiber.memoizedProps;
89
+ if (
90
+ typeof p?.index !== 'number' ||
91
+ typeof p?.cellKey !== 'string' ||
92
+ typeof p?.onUnmount !== 'function'
93
+ )
94
+ continue;
95
+
96
+ const cellKey: string = p.cellKey;
97
+
98
+ // Prefer stateNode.props.index over memoizedProps.index: both current and alternate
99
+ // fibers share the same class instance, and React writes nextProps to instance.props
100
+ // during reconciliation — so stateNode.props is always current even when _targetInst
101
+ // resolves to the stale alternate fiber (common on iOS after a commit).
102
+ const flatIndex: number =
103
+ typeof fiber.stateNode?.props?.index === 'number'
104
+ ? fiber.stateNode.props.index
105
+ : p.index;
106
+
107
+ // `sections` Array prop is only present on SectionList; VirtualizedList uses `data`.
108
+ let sections: any[] | undefined;
109
+ let up: any = fiber.return;
110
+ for (let i = 0; i < 15 && up; i++, up = up.return) {
111
+ const upProps = up.stateNode?.props ?? up.memoizedProps;
112
+ if (Array.isArray(upProps?.sections)) {
113
+ sections = upProps.sections;
114
+ break;
115
+ }
116
+ }
117
+
118
+ if (sections) {
119
+ if (cellKey.endsWith(':header') || cellKey.endsWith(':footer'))
120
+ return undefined;
121
+
122
+ // Mirrors VirtualizedSectionList._getItem offset arithmetic:
123
+ // flatIndex 0 → section 0 header
124
+ // flatIndex 1..n → section 0 items (itemIndex = flatIndex - 1)
125
+ // flatIndex n+1 → section 0 footer
126
+ // flatIndex n+2 → section 1 header …
127
+ let idx = flatIndex - 1;
128
+ for (let si = 0; si < sections.length; si++) {
129
+ const count = sections[si].data?.length ?? 0;
130
+ if (idx === -1 || idx === count) return undefined;
131
+ if (idx < count)
132
+ return { type: 'sectionlist', sectionIndex: si, itemIndex: idx };
133
+ idx -= count + 2;
134
+ }
135
+ return undefined;
136
+ }
137
+
138
+ return { type: 'flatlist', index: flatIndex };
139
+ }
140
+ return undefined;
141
+ }
142
+
143
+ // ── Log path ──────────────────────────────────────────────────────────────────
144
+
145
+ function buildPath(
146
+ screen: string | null,
147
+ listType: string | null,
148
+ identifier: string,
149
+ indexSuffix: string,
150
+ value?: boolean
151
+ ): string {
152
+ const base = listType
153
+ ? `${screen}|${listType}|${identifier}`
154
+ : `${screen}|${identifier}`;
155
+ return value !== undefined
156
+ ? `${base}|${value}${indexSuffix}`
157
+ : `${base}${indexSuffix}`;
158
+ }
159
+
160
+ // ── Hook ──────────────────────────────────────────────────────────────────────
161
+
162
+ export function useTapTracking(enabled: boolean, shouldCollectValues: boolean) {
163
+ const enabledRef = useRef(enabled);
164
+ const shouldCollectValuesRef = useRef(shouldCollectValues);
165
+ const touchStartRef = useRef<{
166
+ x: number;
167
+ y: number;
168
+ switchValue?: boolean;
169
+ } | null>(null);
170
+ const lastScreenRef = useRef<string | null | undefined>(undefined);
171
+
172
+ // Tracks the last logged value per Switch identifier so we can compute the new
173
+ // state as !lastLogged without re-reading the fiber. memoizedProps.value is
174
+ // unreliable across taps because React's current/alternate swap can expose a
175
+ // stale alternate fiber via _targetInst after any commit.
176
+ const switchLastLoggedRef = useRef<Map<string, boolean>>(new Map());
177
+
178
+ useEffect(() => {
179
+ enabledRef.current = enabled;
180
+ if (!enabled) {
181
+ touchStartRef.current = null;
182
+ lastScreenRef.current = undefined;
183
+ switchLastLoggedRef.current.clear();
184
+ }
185
+ }, [enabled]);
186
+
187
+ useEffect(() => {
188
+ shouldCollectValuesRef.current = shouldCollectValues;
189
+ }, [shouldCollectValues]);
190
+
191
+ const onTouchStart = useCallback((e: GestureResponderEvent) => {
192
+ if (!enabledRef.current) return;
193
+ let switchValue: boolean | undefined;
194
+ try {
195
+ let fiber: any = (e as any)._targetInst;
196
+ for (let d = 0; d < MAX_FIBER_DEPTH && fiber; d++, fiber = fiber.return) {
197
+ if (isSwitch(fiber.memoizedProps)) {
198
+ // Capture the committed value before any event fires — reliable first-tap baseline.
199
+ switchValue = fiber.memoizedProps.value;
200
+ break;
201
+ }
202
+ }
203
+ } catch (_) {}
204
+ touchStartRef.current = {
205
+ x: e.nativeEvent.pageX,
206
+ y: e.nativeEvent.pageY,
207
+ switchValue,
208
+ };
209
+ }, []);
210
+
211
+ const onTouchEnd = useCallback((e: GestureResponderEvent) => {
212
+ if (!enabledRef.current) return;
213
+ const start = touchStartRef.current;
214
+ if (start) {
215
+ const dx = Math.abs(e.nativeEvent.pageX - start.x);
216
+ const dy = Math.abs(e.nativeEvent.pageY - start.y);
217
+ if (dx > TAP_THRESHOLD || dy > TAP_THRESHOLD) return;
218
+ }
219
+
220
+ try {
221
+ let node: any = (e as any)._targetInst;
222
+ if (!node) return;
223
+
224
+ // Pass 1: walk up to find the first interactive ancestor.
225
+ // e.g. tapping a <Text> inside a <Pressable> yields the Pressable.
226
+ let interactiveNode: any = null;
227
+ let depth = 0;
228
+ while (node && depth < MAX_FIBER_DEPTH) {
229
+ if (isInteractive(node.memoizedProps)) {
230
+ interactiveNode = node;
231
+ break;
232
+ }
233
+ node = node.return;
234
+ depth++;
235
+ }
236
+ if (!interactiveNode) return;
237
+
238
+ // Pass 2: resolve identifier from the interactive fiber and up to 2 ancestors.
239
+ // The look-ahead handles TouchableOpacity, where the inner Animated.View fiber
240
+ // (where Pass 1 stops) may carry the accessibilityLabel from the parent TO.
241
+ let identifierNode: any = interactiveNode;
242
+ let identifier: string | undefined;
243
+ for (let i = 0; i < 3 && identifierNode; i++) {
244
+ identifier = resolveIdentifier(identifierNode.memoizedProps);
245
+ if (identifier) break;
246
+ identifierNode = identifierNode.return;
247
+ }
248
+
249
+ if (!identifier) return;
250
+
251
+ const isSwitchNode = isSwitch(interactiveNode.memoizedProps);
252
+ if (!isSwitchNode && isInsideSelectionControl(interactiveNode)) return;
253
+
254
+ const screen = getCurrentScreen();
255
+ if (screen !== lastScreenRef.current) {
256
+ switchLastLoggedRef.current.clear();
257
+ lastScreenRef.current = screen;
258
+ }
259
+
260
+ const listIndex = shouldCollectValuesRef.current
261
+ ? resolveListIndex(interactiveNode)
262
+ : undefined;
263
+ const listType = listIndex
264
+ ? listIndex.type === 'sectionlist'
265
+ ? 'SectionList'
266
+ : 'FlatList'
267
+ : null;
268
+ const indexSuffix = listIndex
269
+ ? listIndex.type === 'sectionlist'
270
+ ? `|section:${listIndex.sectionIndex},index:${listIndex.itemIndex}`
271
+ : `|index:${listIndex.index}`
272
+ : '';
273
+
274
+ if (isSwitchNode) {
275
+ const lastLogged = switchLastLoggedRef.current.get(identifier);
276
+ let newValue: boolean;
277
+ if (lastLogged !== undefined) {
278
+ newValue = !lastLogged;
279
+ } else {
280
+ const preTapValue = touchStartRef.current?.switchValue;
281
+ newValue =
282
+ preTapValue !== undefined
283
+ ? !preTapValue
284
+ : !interactiveNode.memoizedProps.value;
285
+ }
286
+ switchLastLoggedRef.current.set(identifier, newValue);
287
+ console.log(
288
+ `[NMAutotrack] tap → "${buildPath(screen, listType, identifier, indexSuffix, newValue)}"`
289
+ );
290
+ } else {
291
+ console.log(
292
+ `[NMAutotrack] tap → "${buildPath(screen, listType, identifier, indexSuffix)}"`
293
+ );
294
+ }
295
+ } catch (_) {
296
+ // Fiber tree is a private React API — fail silently.
297
+ }
298
+ }, []);
299
+
300
+ return { onTouchStart, onTouchEnd };
301
+ }
package/src/index.ts CHANGED
@@ -45,6 +45,8 @@ import { NetmeraProduct } from './events/commerce/NetmeraProduct';
45
45
  import { NetmeraEventContentComment } from './events/media/NetmeraEventContentComment';
46
46
  import { NetmeraEventContentRate } from './events/media/NetmeraEventContentRate';
47
47
  import { NetmeraEventContentView } from './events/media/NetmeraEventContentView';
48
+ import { NetmeraAnalyticProvider } from './NetmeraAnalyticProvider';
49
+ import type { NetmeraAnalyticProviderProps } from './NetmeraAnalyticProvider';
48
50
 
49
51
  export {
50
52
  Netmera,
@@ -94,4 +96,8 @@ export {
94
96
  NetmeraEventContentComment,
95
97
  NetmeraEventContentRate,
96
98
  NetmeraEventContentView,
99
+
100
+ // AutoTracking
101
+ NetmeraAnalyticProvider,
102
+ type NetmeraAnalyticProviderProps,
97
103
  };