@npaw/forge-react-native 9.7.0 → 9.7.2
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.
- package/NPAWForgeReactNative.podspec +1 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +10 -0
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +59 -1
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +33 -0
- package/android/src/test/java/com/npaw/forge/reactnative/ForgeInstanceRegistryTest.java +65 -0
- package/app.plugin.js +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +9 -9
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +198 -1
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +12 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ConnectionType.swift +125 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +67 -1
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +4 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +80 -3
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +44 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/LocationObserver.swift +183 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +123 -2
- package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +59 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +17 -1
- package/ios/ForgeApple/Sources/NPAWForgeCore/VideoAdapter.swift +13 -4
- package/ios/ForgeInstanceRegistry.swift +69 -0
- package/ios/ForgeModuleImpl.swift +53 -6
- package/ios/NPAWForgeTurboModule.mm +6 -1
- package/package.json +3 -1
- package/plugin/index.js +82 -0
- package/plugin/mutations.js +140 -0
- package/src/NPAWForge.ts +12 -1
- package/src/events.ts +21 -1
- package/src/index.ts +11 -1
- package/src/sections.ts +292 -0
- package/src/types.ts +23 -2
package/plugin/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) NPAW - All Rights Reserved
|
|
3
|
+
*
|
|
4
|
+
* This source code is protected under international copyright law. All rights
|
|
5
|
+
* reserved and protected by the copyright holders.
|
|
6
|
+
* This file is confidential and only available to authorized individuals with the
|
|
7
|
+
* permission of the copyright holders. If you encounter this file and do not have
|
|
8
|
+
* permission, please contact the copyright holders and delete this file.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* @author: Victor Soto <victor.soto@nicepeopleatwork.com>
|
|
12
|
+
* @description: Expo config plugin wiring the Forge host-project mutations into expo prebuild; all mutation logic lives in ./mutations.js so it is testable without Expo installed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { addForgeMavenRepository, addKotlinMetadataFlag } = require('./mutations');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} ForgePluginOptions
|
|
21
|
+
* @property {boolean} [artifactoryRepository=true] Add the NPAW Artifactory Maven repository.
|
|
22
|
+
* @property {boolean} [kotlinMetadataFlag=true] Add `-Xskip-metadata-version-check` to the app module.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Applies the native host-project changes the Forge React Native packages need.
|
|
27
|
+
*
|
|
28
|
+
* Expo Go cannot load native modules, so this is for development builds and
|
|
29
|
+
* `expo prebuild`; it does not make Forge work in Expo Go. Every mutation is
|
|
30
|
+
* idempotent because prebuild runs repeatedly.
|
|
31
|
+
*
|
|
32
|
+
* ```json
|
|
33
|
+
* { "expo": { "plugins": ["@npaw/forge-react-native"] } }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* This does NOT disable bridgeless. That is a source-level change in
|
|
37
|
+
* MainApplication and AppDelegate — see ./mutations.js for why automating it
|
|
38
|
+
* would do more harm than good — and it stays a documented manual step for
|
|
39
|
+
* applications using the video adapter package.
|
|
40
|
+
*/
|
|
41
|
+
function withForge(config, options = {}) {
|
|
42
|
+
const { withProjectBuildGradle, withAppBuildGradle } = require('@expo/config-plugins');
|
|
43
|
+
|
|
44
|
+
const settings = {
|
|
45
|
+
artifactoryRepository: true,
|
|
46
|
+
kotlinMetadataFlag: true,
|
|
47
|
+
...options,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let next = config;
|
|
51
|
+
|
|
52
|
+
if (settings.artifactoryRepository) {
|
|
53
|
+
next = withProjectBuildGradle(next, (mod) => {
|
|
54
|
+
if (mod.modResults.language !== 'groovy') {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Forge config plugin: the project build.gradle is not Groovy; apply the Maven repository manually',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
mod.modResults.contents = addForgeMavenRepository(mod.modResults.contents);
|
|
60
|
+
return mod;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (settings.kotlinMetadataFlag) {
|
|
65
|
+
next = withAppBuildGradle(next, (mod) => {
|
|
66
|
+
if (mod.modResults.language !== 'groovy') {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Forge config plugin: the app build.gradle is not Groovy; apply the Kotlin flag manually',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
mod.modResults.contents = addKotlinMetadataFlag(mod.modResults.contents);
|
|
72
|
+
return mod;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = withForge;
|
|
80
|
+
module.exports.default = withForge;
|
|
81
|
+
// Re-exported so host projects and tests can reuse the raw mutations.
|
|
82
|
+
module.exports.mutations = { addForgeMavenRepository, addKotlinMetadataFlag };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) NPAW - All Rights Reserved
|
|
3
|
+
*
|
|
4
|
+
* This source code is protected under international copyright law. All rights
|
|
5
|
+
* reserved and protected by the copyright holders.
|
|
6
|
+
* This file is confidential and only available to authorized individuals with the
|
|
7
|
+
* permission of the copyright holders. If you encounter this file and do not have
|
|
8
|
+
* permission, please contact the copyright holders and delete this file.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* @author: Victor Soto <victor.soto@nicepeopleatwork.com>
|
|
12
|
+
* @description: Pure, idempotent host-project mutations applied by the Forge Expo config plugin; kept free of Expo imports so every one is unit-testable on its own.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const TAG_OPEN = '// @npaw/forge-react-native begin';
|
|
18
|
+
const TAG_CLOSE = '// @npaw/forge-react-native end';
|
|
19
|
+
|
|
20
|
+
const FORGE_MAVEN_URL = 'https://artifact.plugin.npaw.com/artifactory/plugins/android';
|
|
21
|
+
const KOTLIN_METADATA_FLAG = '-Xskip-metadata-version-check';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Replaces a previously inserted block, or inserts a new one after the line
|
|
25
|
+
* matching `anchor`.
|
|
26
|
+
*
|
|
27
|
+
* `expo prebuild` runs repeatedly and is expected to be idempotent, so every
|
|
28
|
+
* mutation is fenced by a tag pair: a second run rewrites the block between the
|
|
29
|
+
* fences instead of appending a duplicate. Without the fences a re-run would
|
|
30
|
+
* silently stack a second `maven { ... }` entry on every prebuild.
|
|
31
|
+
*/
|
|
32
|
+
function mergeBlock(contents, { name, block, anchor }) {
|
|
33
|
+
const open = `${TAG_OPEN} ${name}`;
|
|
34
|
+
const close = `${TAG_CLOSE} ${name}`;
|
|
35
|
+
const fenced = `${open}\n${block}\n${close}`;
|
|
36
|
+
|
|
37
|
+
const openIndex = contents.indexOf(open);
|
|
38
|
+
if (openIndex !== -1) {
|
|
39
|
+
const closeIndex = contents.indexOf(close, openIndex);
|
|
40
|
+
if (closeIndex === -1) {
|
|
41
|
+
throw new Error(`Forge config plugin: unterminated "${name}" block; remove it and re-run prebuild`);
|
|
42
|
+
}
|
|
43
|
+
return contents.slice(0, openIndex) + fenced + contents.slice(closeIndex + close.length);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const match = contents.match(anchor);
|
|
47
|
+
if (!match || match.index === undefined) return null;
|
|
48
|
+
const insertAt = match.index + match[0].length;
|
|
49
|
+
return `${contents.slice(0, insertAt)}\n${fenced}${contents.slice(insertAt)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Adds the NPAW Artifactory repository to the project-level build.gradle.
|
|
54
|
+
*
|
|
55
|
+
* The packages declare this repository themselves, so it is only load-bearing
|
|
56
|
+
* when the host centralises resolution — but adding it is harmless when it is
|
|
57
|
+
* already reachable, and the failure it prevents (an unresolvable Forge AAR at
|
|
58
|
+
* the first build) is opaque enough to be worth pre-empting.
|
|
59
|
+
*/
|
|
60
|
+
function addForgeMavenRepository(contents) {
|
|
61
|
+
const block = [
|
|
62
|
+
'allprojects {',
|
|
63
|
+
' repositories {',
|
|
64
|
+
` maven { url '${FORGE_MAVEN_URL}' }`,
|
|
65
|
+
' }',
|
|
66
|
+
'}',
|
|
67
|
+
].join('\n');
|
|
68
|
+
|
|
69
|
+
if (contents.includes(FORGE_MAVEN_URL) && !contents.includes(TAG_OPEN)) {
|
|
70
|
+
// Someone added the repository by hand before adopting the plugin. Leave
|
|
71
|
+
// their edit alone rather than producing a second declaration.
|
|
72
|
+
return contents;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Anchored after the buildscript block so the insert lands at top level,
|
|
76
|
+
// never inside `buildscript { repositories { ... } }` where it would only
|
|
77
|
+
// affect plugin resolution and not the app's dependencies.
|
|
78
|
+
const merged = mergeBlock(contents, {
|
|
79
|
+
name: 'maven',
|
|
80
|
+
block,
|
|
81
|
+
anchor: /^\}\s*$/m,
|
|
82
|
+
});
|
|
83
|
+
return merged === null ? `${contents.trimEnd()}\n\n${TAG_OPEN} maven\n${block}\n${TAG_CLOSE} maven\n` : merged;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Adds the Kotlin metadata compiler flag to the application module.
|
|
88
|
+
*
|
|
89
|
+
* The published Forge Android artifacts are built with a newer Kotlin than the
|
|
90
|
+
* React Native and Expo templates use — the template in this repo pins
|
|
91
|
+
* `kotlinVersion = "1.9.24"` — so an application module compiling its own
|
|
92
|
+
* Kotlin against Forge types needs this flag. Applications already on Kotlin
|
|
93
|
+
* 2.2 or newer do not, which is why callers can turn it off.
|
|
94
|
+
*/
|
|
95
|
+
function addKotlinMetadataFlag(contents) {
|
|
96
|
+
if (new RegExp(`freeCompilerArgs[^\\n]*${KOTLIN_METADATA_FLAG}`).test(contents)) {
|
|
97
|
+
return contents;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const block = [
|
|
101
|
+
' kotlinOptions {',
|
|
102
|
+
` freeCompilerArgs += ["${KOTLIN_METADATA_FLAG}"]`,
|
|
103
|
+
' }',
|
|
104
|
+
].join('\n');
|
|
105
|
+
|
|
106
|
+
const merged = mergeBlock(contents, {
|
|
107
|
+
name: 'kotlin',
|
|
108
|
+
block,
|
|
109
|
+
anchor: /^android\s*\{\s*$/m,
|
|
110
|
+
});
|
|
111
|
+
if (merged === null) {
|
|
112
|
+
throw new Error('Forge config plugin: no android { } block found in the app build.gradle');
|
|
113
|
+
}
|
|
114
|
+
return merged;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Deliberately NOT automated here: turning bridgeless off.
|
|
118
|
+
//
|
|
119
|
+
// `react-native-video` v6 has no Fabric or bridgeless support, so applications
|
|
120
|
+
// using the video adapter package must disable bridgeless — but the documented
|
|
121
|
+
// mechanism is source-level, not a build setting: `load(bridgelessEnabled =
|
|
122
|
+
// false)` in MainApplication and a `bridgelessEnabled` override returning NO in
|
|
123
|
+
// AppDelegate. There is no gradle or Podfile property that does it.
|
|
124
|
+
//
|
|
125
|
+
// A mod writing some plausible-looking gradle property would be a silent no-op
|
|
126
|
+
// that reads as success, which is worse than leaving the step manual. Rewriting
|
|
127
|
+
// MainApplication and AppDelegate source is the alternative, and it breaks
|
|
128
|
+
// across React Native versions in ways prebuild cannot verify. The client
|
|
129
|
+
// documentation keeps this as a manual step until the v7 video adapter removes
|
|
130
|
+
// the constraint entirely.
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
FORGE_MAVEN_URL,
|
|
134
|
+
KOTLIN_METADATA_FLAG,
|
|
135
|
+
TAG_OPEN,
|
|
136
|
+
TAG_CLOSE,
|
|
137
|
+
mergeBlock,
|
|
138
|
+
addForgeMavenRepository,
|
|
139
|
+
addKotlinMetadataFlag,
|
|
140
|
+
};
|
package/src/NPAWForge.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import NativeNPAWForge from './specs/NativeNPAWForge';
|
|
16
16
|
import { ForgeVideo } from './ForgeVideo';
|
|
17
|
-
import { subscribeInstanceEvents } from './events';
|
|
17
|
+
import { subscribeInstanceEvents, subscribeInstanceLogs } from './events';
|
|
18
18
|
import type {
|
|
19
19
|
ForgeConfig,
|
|
20
20
|
ForgeEventSubscription,
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
ForgeOutcome,
|
|
23
23
|
ForgeParams,
|
|
24
24
|
ForgeRecordedEvent,
|
|
25
|
+
ForgeRenderedLog,
|
|
25
26
|
ForgeVideoOptions,
|
|
26
27
|
} from './types';
|
|
27
28
|
|
|
@@ -153,6 +154,16 @@ export class NPAWForge {
|
|
|
153
154
|
return subscribeInstanceEvents(this.instanceId, callback);
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Live feed of the kernel's rendered decision log for this instance - the
|
|
159
|
+
* same lines `logLevel: 'trace'` writes to the platform console, without
|
|
160
|
+
* needing native tooling to read them. The kernel level gates what is
|
|
161
|
+
* rendered, so raise it (`setLogLevel('trace')`) before expecting detail.
|
|
162
|
+
*/
|
|
163
|
+
onLog(callback: (log: ForgeRenderedLog) => void): ForgeEventSubscription {
|
|
164
|
+
return subscribeInstanceLogs(this.instanceId, callback);
|
|
165
|
+
}
|
|
166
|
+
|
|
156
167
|
// ---- video ----
|
|
157
168
|
|
|
158
169
|
/**
|
package/src/events.ts
CHANGED
|
@@ -14,9 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
import { NativeEventEmitter, NativeModules } from 'react-native';
|
|
16
16
|
import NativeNPAWForge from './specs/NativeNPAWForge';
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
ForgeEventSubscription,
|
|
19
|
+
ForgeRecordedEvent,
|
|
20
|
+
ForgeRenderedLog,
|
|
21
|
+
} from './types';
|
|
18
22
|
|
|
19
23
|
export const FORGE_EVENT_CHANNEL = 'NPAWForgeEvent';
|
|
24
|
+
export const FORGE_LOG_CHANNEL = 'NPAWForgeLog';
|
|
20
25
|
|
|
21
26
|
const emitter = new NativeEventEmitter(
|
|
22
27
|
// The spec module implements addListener/removeListeners; iOS needs the
|
|
@@ -38,3 +43,18 @@ export function subscribeInstanceEvents(
|
|
|
38
43
|
);
|
|
39
44
|
return { remove: () => subscription.remove() };
|
|
40
45
|
}
|
|
46
|
+
|
|
47
|
+
export function subscribeInstanceLogs(
|
|
48
|
+
instanceId: string,
|
|
49
|
+
callback: (log: ForgeRenderedLog) => void
|
|
50
|
+
): ForgeEventSubscription {
|
|
51
|
+
const subscription = emitter.addListener(
|
|
52
|
+
FORGE_LOG_CHANNEL,
|
|
53
|
+
(log: ForgeRenderedLog) => {
|
|
54
|
+
if (log.instanceId === instanceId) {
|
|
55
|
+
callback(log);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
return { remove: () => subscription.remove() };
|
|
60
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,22 +15,32 @@
|
|
|
15
15
|
export { NPAWForge, DEFAULT_VIDEO_IDENTIFIER } from './NPAWForge';
|
|
16
16
|
export { ForgeVideo } from './ForgeVideo';
|
|
17
17
|
export { ForgeAdReporter } from './ForgeAdReporter';
|
|
18
|
-
export { FORGE_EVENT_CHANNEL } from './events';
|
|
18
|
+
export { FORGE_EVENT_CHANNEL, FORGE_LOG_CHANNEL } from './events';
|
|
19
19
|
export {
|
|
20
20
|
installErrorCapture,
|
|
21
21
|
trackScreenView,
|
|
22
22
|
createNavigationObserver,
|
|
23
23
|
} from './capture';
|
|
24
24
|
export type { ErrorCaptureOptions, ScreenViewOptions } from './capture';
|
|
25
|
+
export { createSectionsCapture } from './sections';
|
|
26
|
+
export type {
|
|
27
|
+
ForgeSectionContent,
|
|
28
|
+
ForgeSectionsCaptureHandle,
|
|
29
|
+
ForgeSectionsCaptureOptions,
|
|
30
|
+
ForgeViewabilityCallbackInfo,
|
|
31
|
+
ForgeViewToken,
|
|
32
|
+
} from './sections';
|
|
25
33
|
export type {
|
|
26
34
|
ForgeConfig,
|
|
27
35
|
ForgeEventSubscription,
|
|
36
|
+
ForgeLogLevelName,
|
|
28
37
|
ForgeNetworkOutcome,
|
|
29
38
|
ForgeNetworkRequestRecord,
|
|
30
39
|
ForgeOutcome,
|
|
31
40
|
ForgeOutcomeCode,
|
|
32
41
|
ForgeParams,
|
|
33
42
|
ForgeRecordedEvent,
|
|
43
|
+
ForgeRenderedLog,
|
|
34
44
|
ForgeVideoOptions,
|
|
35
45
|
} from './types';
|
|
36
46
|
|
package/src/sections.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) NPAW - All Rights Reserved
|
|
3
|
+
*
|
|
4
|
+
* This source code is protected under international copyright law. All rights
|
|
5
|
+
* reserved and protected by the copyright holders.
|
|
6
|
+
* This file is confidential and only available to authorized individuals with the
|
|
7
|
+
* permission of the copyright holders. If you encounter this file and do not have
|
|
8
|
+
* permission, please contact the copyright holders and delete this file.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* @author: Victor Soto <victor.soto@nicepeopleatwork.com>
|
|
12
|
+
* @description: SECTIONS capture for React Native virtualized lists - turns FlatList/SectionList viewability callbacks into the same four raw section events Web and Roku emit, with matching thresholds and dedupe.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { NPAWForge } from './NPAWForge';
|
|
16
|
+
import type { ForgeParams } from './types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The shape React Native hands to `onViewableItemsChanged`.
|
|
20
|
+
*
|
|
21
|
+
* Declared structurally rather than imported from `react-native` so this module
|
|
22
|
+
* carries no dependency on the RN type surface and stays unit-testable without
|
|
23
|
+
* a renderer.
|
|
24
|
+
*/
|
|
25
|
+
export type ForgeViewToken = {
|
|
26
|
+
key?: string | null;
|
|
27
|
+
index?: number | null;
|
|
28
|
+
isViewable: boolean;
|
|
29
|
+
item?: unknown;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ForgeViewabilityCallbackInfo = {
|
|
33
|
+
viewableItems: ForgeViewToken[];
|
|
34
|
+
changed: ForgeViewToken[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Facts the integrator supplies for a row. Everything is optional but `contentId`. */
|
|
38
|
+
export type ForgeSectionContent = {
|
|
39
|
+
contentId?: string;
|
|
40
|
+
contentTitle?: string;
|
|
41
|
+
contentLink?: string;
|
|
42
|
+
/** Grid coordinates, when the list is not a simple single-axis rail. */
|
|
43
|
+
row?: number;
|
|
44
|
+
column?: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ForgeSectionsCaptureOptions = {
|
|
48
|
+
/** Logical section name — the rail's identity in reporting. */
|
|
49
|
+
section: string;
|
|
50
|
+
/** Zero-based order of this section within the screen. */
|
|
51
|
+
sectionOrder?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Maps a list item to its reporting facts. Return `undefined` to skip an
|
|
54
|
+
* item entirely (placeholders, ad slots, loading skeletons).
|
|
55
|
+
*/
|
|
56
|
+
resolveContent: (item: unknown, index: number) => ForgeSectionContent | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Percentage of a row that must be on screen to count as seen. Mirrors the
|
|
59
|
+
* Web threshold and RN's `itemVisiblePercentThreshold`. Default 50.
|
|
60
|
+
*/
|
|
61
|
+
visibilityPercentThreshold?: number;
|
|
62
|
+
/**
|
|
63
|
+
* How long a row must stay visible before it is an impression. Mirrors RN's
|
|
64
|
+
* `minimumViewTime`. Default 1000 ms.
|
|
65
|
+
*/
|
|
66
|
+
impressionMinVisibleMs?: number;
|
|
67
|
+
/** Minimum visible time that counts as dwell. Default 1000 ms. */
|
|
68
|
+
dwellMs?: number;
|
|
69
|
+
/** Upper bound on tracked rows, mirroring the Web cap. Default 200. */
|
|
70
|
+
maxTrackedItems?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Local half of the capture gate. The kernel is the authority — it drops all
|
|
73
|
+
* four section kinds when the SECTIONS plan bit is off — but returning false
|
|
74
|
+
* here also spares the bridge the traffic. Default: always on.
|
|
75
|
+
*/
|
|
76
|
+
enabled?: () => boolean;
|
|
77
|
+
/** Injectable clock, for tests. */
|
|
78
|
+
now?: () => number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type ForgeSectionsCaptureHandle = {
|
|
82
|
+
/** Pass straight to `FlatList.onViewableItemsChanged`. */
|
|
83
|
+
onViewableItemsChanged: (info: ForgeViewabilityCallbackInfo) => void;
|
|
84
|
+
/** Pass straight to `FlatList.viewabilityConfig`. */
|
|
85
|
+
viewabilityConfig: {
|
|
86
|
+
itemVisiblePercentThreshold: number;
|
|
87
|
+
minimumViewTime: number;
|
|
88
|
+
};
|
|
89
|
+
/** Call from a row's `onPress`. */
|
|
90
|
+
trackClick: (item: unknown, index: number) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Clears dedupe state so the same rows can report again. Call on navigation,
|
|
93
|
+
* matching how the Web implementation resets per route.
|
|
94
|
+
*/
|
|
95
|
+
reset: () => void;
|
|
96
|
+
/** Emits a closing `raw_section_hidden` if the section is visible, and stops. */
|
|
97
|
+
dispose: () => void;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Kernel-mirrored defaults, identical to bindings/web/src/sections-capture.ts.
|
|
101
|
+
const DEFAULT_VISIBILITY_THRESHOLD_PCT = 50;
|
|
102
|
+
const DEFAULT_IMPRESSION_MIN_VISIBLE_MS = 1000;
|
|
103
|
+
const DEFAULT_DWELL_MS = 1000;
|
|
104
|
+
const DEFAULT_MAX_TRACKED_ITEMS = 200;
|
|
105
|
+
|
|
106
|
+
type TrackedRow = {
|
|
107
|
+
key: string;
|
|
108
|
+
index: number;
|
|
109
|
+
content: ForgeSectionContent;
|
|
110
|
+
visibleSince: number;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function tokenKey(token: ForgeViewToken, index: number): string {
|
|
114
|
+
return token.key ?? `index:${index}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a sections capture handle for one list.
|
|
119
|
+
*
|
|
120
|
+
* React Native's virtualized lists are JavaScript-owned: rows are recycled and
|
|
121
|
+
* unmounted as they scroll, so the native Android and Apple sections observers
|
|
122
|
+
* see nothing usable. The viewability callback is the only reliable signal, and
|
|
123
|
+
* it already carries the two knobs the Web implementation derives from
|
|
124
|
+
* IntersectionObserver — a visibility percentage and a minimum view time — so
|
|
125
|
+
* the semantics map across rather than being re-invented.
|
|
126
|
+
*
|
|
127
|
+
* ```tsx
|
|
128
|
+
* const sections = createSectionsCapture(forge, {
|
|
129
|
+
* section: 'Continue watching',
|
|
130
|
+
* resolveContent: (item) => ({ contentId: item.id, contentTitle: item.title }),
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* <FlatList
|
|
134
|
+
* data={rows}
|
|
135
|
+
* onViewableItemsChanged={sections.onViewableItemsChanged}
|
|
136
|
+
* viewabilityConfig={sections.viewabilityConfig}
|
|
137
|
+
* renderItem={({ item, index }) => (
|
|
138
|
+
* <Row onPress={() => sections.trackClick(item, index)} />
|
|
139
|
+
* )}
|
|
140
|
+
* />
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* One handle per list. Call `dispose` when the screen unmounts so the section's
|
|
144
|
+
* `raw_section_hidden` pairs with the `raw_section_visible` it emitted.
|
|
145
|
+
*/
|
|
146
|
+
export function createSectionsCapture(
|
|
147
|
+
forge: NPAWForge,
|
|
148
|
+
options: ForgeSectionsCaptureOptions,
|
|
149
|
+
): ForgeSectionsCaptureHandle {
|
|
150
|
+
const visibilityPercentThreshold =
|
|
151
|
+
options.visibilityPercentThreshold ?? DEFAULT_VISIBILITY_THRESHOLD_PCT;
|
|
152
|
+
const impressionMinVisibleMs =
|
|
153
|
+
options.impressionMinVisibleMs ?? DEFAULT_IMPRESSION_MIN_VISIBLE_MS;
|
|
154
|
+
const dwellMs = options.dwellMs ?? DEFAULT_DWELL_MS;
|
|
155
|
+
const maxTrackedItems = options.maxTrackedItems ?? DEFAULT_MAX_TRACKED_ITEMS;
|
|
156
|
+
const now = options.now ?? (() => Date.now());
|
|
157
|
+
const isEnabled = options.enabled ?? (() => true);
|
|
158
|
+
|
|
159
|
+
const tracked = new Map<string, TrackedRow>();
|
|
160
|
+
const impressionsSeen = new Set<string>();
|
|
161
|
+
const dwellSeen = new Set<string>();
|
|
162
|
+
let sectionVisible = false;
|
|
163
|
+
let disposed = false;
|
|
164
|
+
|
|
165
|
+
function emit(name: string, params: ForgeParams): void {
|
|
166
|
+
// Fire-and-forget: a rejected raw event must never surface inside a list
|
|
167
|
+
// render or a scroll handler.
|
|
168
|
+
void forge.trackRawEvent(name, params).catch(() => undefined);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sectionParams(): ForgeParams {
|
|
172
|
+
const params: ForgeParams = { section: options.section };
|
|
173
|
+
if (options.sectionOrder !== undefined) params.sectionOrder = options.sectionOrder;
|
|
174
|
+
return params;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function dedupeKey(row: TrackedRow): string {
|
|
178
|
+
return row.content.contentId ?? `${options.section}#${row.key}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function contentParams(row: TrackedRow): ForgeParams {
|
|
182
|
+
const params: ForgeParams = { ...sectionParams(), position: row.index };
|
|
183
|
+
const { contentId, contentTitle, contentLink, row: gridRow, column } = row.content;
|
|
184
|
+
if (contentId !== undefined) params.contentId = contentId;
|
|
185
|
+
if (contentTitle !== undefined) params.contentTitle = contentTitle;
|
|
186
|
+
if (contentLink !== undefined) params.contentLink = contentLink;
|
|
187
|
+
if (gridRow !== undefined) params.row = gridRow;
|
|
188
|
+
if (column !== undefined) params.column = column;
|
|
189
|
+
return params;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function openSection(visibleCount: number): void {
|
|
193
|
+
if (sectionVisible) return;
|
|
194
|
+
sectionVisible = true;
|
|
195
|
+
emit('raw_section_visible', {
|
|
196
|
+
...sectionParams(),
|
|
197
|
+
visibilityPct: visibilityPercentThreshold,
|
|
198
|
+
itemCount: visibleCount,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function closeSection(): void {
|
|
203
|
+
if (!sectionVisible) return;
|
|
204
|
+
sectionVisible = false;
|
|
205
|
+
emit('raw_section_hidden', sectionParams());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function rowBecameVisible(token: ForgeViewToken, fallbackIndex: number): void {
|
|
209
|
+
const index = token.index ?? fallbackIndex;
|
|
210
|
+
const key = tokenKey(token, index);
|
|
211
|
+
if (tracked.has(key)) return;
|
|
212
|
+
if (tracked.size >= maxTrackedItems) return;
|
|
213
|
+
|
|
214
|
+
const content = options.resolveContent(token.item, index);
|
|
215
|
+
if (!content) return;
|
|
216
|
+
|
|
217
|
+
tracked.set(key, { key, index, content, visibleSince: now() });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function rowBecameHidden(token: ForgeViewToken, fallbackIndex: number): void {
|
|
221
|
+
const index = token.index ?? fallbackIndex;
|
|
222
|
+
const key = tokenKey(token, index);
|
|
223
|
+
const row = tracked.get(key);
|
|
224
|
+
if (!row) return;
|
|
225
|
+
tracked.delete(key);
|
|
226
|
+
|
|
227
|
+
const elapsed = now() - row.visibleSince;
|
|
228
|
+
const key_ = dedupeKey(row);
|
|
229
|
+
|
|
230
|
+
// Impression first: a row that met the minimum view time was seen, whether
|
|
231
|
+
// or not it also earned a dwell.
|
|
232
|
+
if (elapsed >= impressionMinVisibleMs && !impressionsSeen.has(key_)) {
|
|
233
|
+
impressionsSeen.add(key_);
|
|
234
|
+
emit('raw_content_impression', {
|
|
235
|
+
...contentParams(row),
|
|
236
|
+
visibilityPct: visibilityPercentThreshold,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (elapsed >= dwellMs && !dwellSeen.has(key_)) {
|
|
241
|
+
dwellSeen.add(key_);
|
|
242
|
+
emit('raw_content_dwell', { ...contentParams(row), dwellMs: Math.round(elapsed) });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
viewabilityConfig: {
|
|
248
|
+
itemVisiblePercentThreshold: visibilityPercentThreshold,
|
|
249
|
+
minimumViewTime: impressionMinVisibleMs,
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
onViewableItemsChanged(info) {
|
|
253
|
+
if (disposed || !isEnabled()) return;
|
|
254
|
+
|
|
255
|
+
for (const [position, token] of (info.changed ?? []).entries()) {
|
|
256
|
+
if (token.isViewable) rowBecameVisible(token, position);
|
|
257
|
+
else rowBecameHidden(token, position);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const visible = (info.viewableItems ?? []).length;
|
|
261
|
+
if (visible > 0) openSection(visible);
|
|
262
|
+
else closeSection();
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
trackClick(item, index) {
|
|
266
|
+
if (disposed || !isEnabled()) return;
|
|
267
|
+
const content = options.resolveContent(item, index);
|
|
268
|
+
if (!content) return;
|
|
269
|
+
emit('raw_click', {
|
|
270
|
+
...contentParams({ key: `index:${index}`, index, content, visibleSince: now() }),
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
reset() {
|
|
275
|
+
// Flush what is on screen before forgetting it, so a navigation away does
|
|
276
|
+
// not silently drop rows that had already earned an impression.
|
|
277
|
+
for (const row of [...tracked.values()]) {
|
|
278
|
+
rowBecameHidden({ key: row.key, index: row.index, isViewable: false }, row.index);
|
|
279
|
+
}
|
|
280
|
+
tracked.clear();
|
|
281
|
+
impressionsSeen.clear();
|
|
282
|
+
dwellSeen.clear();
|
|
283
|
+
closeSection();
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
dispose() {
|
|
287
|
+
if (disposed) return;
|
|
288
|
+
this.reset();
|
|
289
|
+
disposed = true;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -33,6 +33,27 @@ export type ForgeOutcome = {
|
|
|
33
33
|
details?: Record<string, string>;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/** Log levels the kernel renders lines at, as they travel on the wire. */
|
|
37
|
+
export type ForgeLogLevelName =
|
|
38
|
+
| 'off'
|
|
39
|
+
| 'error'
|
|
40
|
+
| 'warning'
|
|
41
|
+
| 'info'
|
|
42
|
+
| 'debug'
|
|
43
|
+
| 'trace';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One line of the kernel's rendered decision log - the same text
|
|
47
|
+
* `logLevel: 'trace'` writes to the platform console. Already rendered and
|
|
48
|
+
* redacted natively; `tag` is the kernel's own scope tag.
|
|
49
|
+
*/
|
|
50
|
+
export type ForgeRenderedLog = {
|
|
51
|
+
instanceId: string;
|
|
52
|
+
level: ForgeLogLevelName;
|
|
53
|
+
tag: string;
|
|
54
|
+
message: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
36
57
|
/** One kernel-recorded event from the bounded native snapshot mirror. */
|
|
37
58
|
export type ForgeRecordedEvent = {
|
|
38
59
|
instanceId: string;
|
|
@@ -48,8 +69,8 @@ export type ForgeRecordedEvent = {
|
|
|
48
69
|
* aliases are intentionally not exposed); unknown keys are warn-and-ignore
|
|
49
70
|
* on the native side for forward compatibility.
|
|
50
71
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
72
|
+
* Every key applies on both platforms, including `deviceUuid` and
|
|
73
|
+
* `autoDeviceIdentityEnabled`, which iOS ignored before Forge 9.7.2.
|
|
53
74
|
*/
|
|
54
75
|
export type ForgeConfig = {
|
|
55
76
|
appName?: string;
|