@npaw/forge-react-native 9.6.1

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 (74) hide show
  1. package/LICENSE +10 -0
  2. package/NPAWForgeReactNative.podspec +44 -0
  3. package/README.md +51 -0
  4. package/android/build.gradle +77 -0
  5. package/android/src/main/AndroidManifest.xml +14 -0
  6. package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +295 -0
  7. package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +61 -0
  8. package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +735 -0
  9. package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgePackage.java +53 -0
  10. package/android/src/test/java/com/npaw/forge/reactnative/ForgeDtoMapperTest.java +133 -0
  11. package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +82 -0
  12. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/ForgeKernelFFI.h +17 -0
  13. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +873 -0
  14. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/module.modulemap +7 -0
  15. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
  16. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/ForgeKernelFFI.h +17 -0
  17. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +873 -0
  18. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/module.modulemap +7 -0
  19. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
  20. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/ForgeKernelFFI.h +17 -0
  21. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +873 -0
  22. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/module.modulemap +7 -0
  23. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
  24. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/ForgeKernelFFI.h +17 -0
  25. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +873 -0
  26. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/module.modulemap +7 -0
  27. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
  28. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +3931 -0
  29. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +873 -0
  30. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.modulemap +7 -0
  31. package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAccessLogFacts.swift +152 -0
  32. package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +877 -0
  33. package/ios/ForgeApple/Sources/NPAWForgeCapture/AttributionCapture.swift +158 -0
  34. package/ios/ForgeApple/Sources/NPAWForgeCapture/CapturePlanGate.swift +189 -0
  35. package/ios/ForgeApple/Sources/NPAWForgeCapture/CrashCapture.swift +120 -0
  36. package/ios/ForgeApple/Sources/NPAWForgeCapture/ForgeAutomaticCapture.swift +639 -0
  37. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsCapture.swift +1176 -0
  38. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsEngine.swift +674 -0
  39. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsMarkers.swift +105 -0
  40. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsSwiftUI.swift +284 -0
  41. package/ios/ForgeApple/Sources/NPAWForgeCapture/SwiftUICapture.swift +44 -0
  42. package/ios/ForgeApple/Sources/NPAWForgeCore/AdAdapter.swift +199 -0
  43. package/ios/ForgeApple/Sources/NPAWForgeCore/AdSemantics.swift +136 -0
  44. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +596 -0
  45. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +297 -0
  46. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeClock.swift +29 -0
  47. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +109 -0
  48. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeOutcome.swift +89 -0
  49. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +291 -0
  50. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeStorage.swift +70 -0
  51. package/ios/ForgeApple/Sources/NPAWForgeCore/ManualAdReporter.swift +180 -0
  52. package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +453 -0
  53. package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +594 -0
  54. package/ios/ForgeApple/Sources/NPAWForgeCore/NetworkRequestRecord.swift +191 -0
  55. package/ios/ForgeApple/Sources/NPAWForgeCore/ObjectiveCWrappers.swift +191 -0
  56. package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +299 -0
  57. package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerSemantics.swift +266 -0
  58. package/ios/ForgeApple/Sources/NPAWForgeCore/PrivacySanitizer.swift +59 -0
  59. package/ios/ForgeApple/Sources/NPAWForgeCore/VideoAdapter.swift +478 -0
  60. package/ios/ForgeApple/Sources/NPAWForgeCore/VideoOptions.swift +431 -0
  61. package/ios/ForgeApple/Sources/NPAWForgeNetworkCapture/ForgeNetworkCapture.swift +116 -0
  62. package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/NPAWForgeNetworkSwizzler.m +293 -0
  63. package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/include/NPAWForgeNetworkSwizzler.h +33 -0
  64. package/ios/ForgeModuleImpl.swift +1131 -0
  65. package/ios/NPAWForgeTurboModule.mm +399 -0
  66. package/package.json +33 -0
  67. package/src/ForgeAdReporter.ts +101 -0
  68. package/src/ForgeVideo.ts +169 -0
  69. package/src/NPAWForge.ts +177 -0
  70. package/src/capture.ts +192 -0
  71. package/src/events.ts +40 -0
  72. package/src/index.ts +37 -0
  73. package/src/specs/NativeNPAWForge.ts +168 -0
  74. package/src/types.ts +114 -0
@@ -0,0 +1,105 @@
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 parsing for the F3 sections accessibilityIdentifier marker
13
+ * convention (`forge-section:<name>[:<order>]` and
14
+ * `forge-item:<contentId>[:<position>]`) plus node classification
15
+ * combining Tier 1 markers with the Tier 2 remote native-id lists
16
+ * from the kernel capture plan. Platform-free so the parsing rules
17
+ * are exhaustively testable without UIKit.
18
+ */
19
+
20
+ import Foundation
21
+
22
+ /// Parsed sections marker from an `accessibilityIdentifier`.
23
+ public enum ForgeSectionMarker: Equatable {
24
+ case section(name: String, order: Int?)
25
+ case item(contentId: String, position: Int?)
26
+ }
27
+
28
+ /// Role resolved for one view/node during sections scanning.
29
+ public enum SectionNodeRole: Equatable {
30
+ case section(name: String, order: Int?)
31
+ case item(contentId: String?, position: Int?)
32
+ }
33
+
34
+ /// Marker grammar: `forge-section:<name>[:<order>]` /
35
+ /// `forge-item:<contentId>[:<position>]`. Prefix matching is exact and
36
+ /// case-sensitive. The trailing component is treated as the numeric field only
37
+ /// when it is a plain non-negative decimal integer that fits `Int` AND at
38
+ /// least one other component precedes it; otherwise it stays part of the
39
+ /// name/content id (names may therefore contain colons). Empty names/ids are
40
+ /// malformed and yield `nil`.
41
+ public enum ForgeSectionMarkerParser {
42
+ public static let sectionPrefix = "forge-section:"
43
+ public static let itemPrefix = "forge-item:"
44
+
45
+ public static func parse(_ identifier: String?) -> ForgeSectionMarker? {
46
+ guard let identifier else { return nil }
47
+ if identifier.hasPrefix(sectionPrefix) {
48
+ let payload = String(identifier.dropFirst(sectionPrefix.count))
49
+ guard let (name, number) = split(payload) else { return nil }
50
+ return .section(name: name, order: number)
51
+ }
52
+ if identifier.hasPrefix(itemPrefix) {
53
+ let payload = String(identifier.dropFirst(itemPrefix.count))
54
+ guard let (contentId, number) = split(payload) else { return nil }
55
+ return .item(contentId: contentId, position: number)
56
+ }
57
+ return nil
58
+ }
59
+
60
+ /// Classifies one node: Tier 1 markers win, then Tier 2 exact native-id
61
+ /// matches (section list checked before item list). Non-matching or empty
62
+ /// identifiers resolve to `nil`.
63
+ public static func classify(
64
+ identifier: String?,
65
+ sectionNativeIds: Set<String>,
66
+ itemNativeIds: Set<String>
67
+ ) -> SectionNodeRole? {
68
+ if let marker = parse(identifier) {
69
+ switch marker {
70
+ case let .section(name, order):
71
+ return .section(name: name, order: order)
72
+ case let .item(contentId, position):
73
+ return .item(contentId: contentId, position: position)
74
+ }
75
+ }
76
+ guard let identifier, !identifier.isEmpty else { return nil }
77
+ if sectionNativeIds.contains(identifier) {
78
+ return .section(name: identifier, order: nil)
79
+ }
80
+ if itemNativeIds.contains(identifier) {
81
+ return .item(contentId: identifier, position: nil)
82
+ }
83
+ return nil
84
+ }
85
+
86
+ private static func split(_ payload: String) -> (text: String, number: Int?)? {
87
+ guard !payload.isEmpty else { return nil }
88
+ let components = payload.components(separatedBy: ":")
89
+ if components.count >= 2,
90
+ let last = components.last,
91
+ let number = plainNonNegativeInt(last) {
92
+ let text = components.dropLast().joined(separator: ":")
93
+ guard !text.isEmpty else { return nil }
94
+ return (text, number)
95
+ }
96
+ return (payload, nil)
97
+ }
98
+
99
+ /// Strict digits-only parse: no signs, no whitespace, no decimal points.
100
+ /// Values that overflow `Int` are not numbers and stay part of the text.
101
+ private static func plainNonNegativeInt(_ value: String) -> Int? {
102
+ guard !value.isEmpty, value.allSatisfy(\.isASCII), value.allSatisfy(\.isNumber) else { return nil }
103
+ return Int(value)
104
+ }
105
+ }
@@ -0,0 +1,284 @@
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: SwiftUI view modifiers for F3 sections auto-capture. `.forgeSection`
13
+ * marks a container and `.forgeSectionItem` marks content inside it;
14
+ * both publish their global frame continuously through a geometry
15
+ * preference (throttled ~100 ms with a trailing flush) so scrolling
16
+ * keeps visibility percentages fresh in the sections coordinator —
17
+ * impressions re-verify against real values at fire time — while
18
+ * `onDisappear` stays the remove path. Visibility clips against the
19
+ * foreground window-scene bounds (split-screen correct) with a
20
+ * `UIScreen.main` fallback. The section modifier propagates its
21
+ * engine key through the environment so nested items attach to the
22
+ * right section automatically.
23
+ */
24
+
25
+ import Foundation
26
+ #if canImport(NPAWForgeCore)
27
+ import NPAWForgeCore
28
+ #endif
29
+ #if canImport(SwiftUI)
30
+ import SwiftUI
31
+ #if canImport(UIKit)
32
+ import UIKit
33
+ #endif
34
+
35
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
36
+ private struct ForgeSectionKeyEnvironmentKey: EnvironmentKey {
37
+ static let defaultValue: String? = nil
38
+ }
39
+
40
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
41
+ extension EnvironmentValues {
42
+ var forgeSectionKey: String? {
43
+ get { self[ForgeSectionKeyEnvironmentKey.self] }
44
+ set { self[ForgeSectionKeyEnvironmentKey.self] = newValue }
45
+ }
46
+ }
47
+
48
+ /// Window-bounds visibility approximation for SwiftUI global frames: on
49
+ /// UIKit platforms the ratio of the frame inside the container bounds, and
50
+ /// a plain appear=100/disappear=0 signal elsewhere.
51
+ enum ForgeSwiftUIVisibility {
52
+ static func pct(ofGlobalFrame frame: CGRect) -> Int {
53
+ guard frame.width > 0, frame.height > 0 else { return 0 }
54
+ #if canImport(UIKit)
55
+ let visible = frame.intersection(containerBounds())
56
+ guard !visible.isNull, !visible.isEmpty else { return 0 }
57
+ let ratio = (visible.width * visible.height) / (frame.width * frame.height)
58
+ return max(0, min(100, Int((ratio * 100).rounded())))
59
+ #else
60
+ return 100
61
+ #endif
62
+ }
63
+
64
+ #if canImport(UIKit)
65
+ /// SwiftUI `.global` frames are window-relative, so clip against the
66
+ /// foreground window-scene bounds when one is resolvable (a split-screen
67
+ /// app does not span the full screen); `UIScreen.main` stays as the
68
+ /// fallback for hosts without a resolvable scene and off-main callers.
69
+ private static func containerBounds() -> CGRect {
70
+ if Thread.isMainThread {
71
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
72
+ let scene = scenes.first { $0.activationState == .foregroundActive } ?? scenes.first
73
+ if let scene {
74
+ return scene.coordinateSpace.bounds
75
+ }
76
+ }
77
+ return UIScreen.main.bounds
78
+ }
79
+ #endif
80
+ }
81
+
82
+ /// Main-thread leading+trailing throttle (~100 ms) for continuous SwiftUI
83
+ /// frame publishing: the first change in a window delivers immediately, rapid
84
+ /// follow-ups coalesce into one trailing delivery of the latest frame. Scroll
85
+ /// bursts cannot flood the engine, while the final resting frame always lands
86
+ /// so a stale visibility percentage never survives a fast scroll.
87
+ final class ForgeSwiftUIFrameThrottle {
88
+ static let defaultIntervalMs: Int64 = 100
89
+
90
+ private let intervalMs: Int64
91
+ private let nowMs: () -> Int64
92
+ private let scheduleOnMain: (Int64, @escaping () -> Void) -> Void
93
+ private var lastDeliveryAtMs: Int64?
94
+ private var pending: (frame: CGRect, deliver: (CGRect) -> Void)?
95
+ private var trailingScheduled = false
96
+
97
+ init(
98
+ intervalMs: Int64 = ForgeSwiftUIFrameThrottle.defaultIntervalMs,
99
+ nowMs: @escaping () -> Int64 = { ForgeClock.elapsedRealtimeMs() },
100
+ scheduleOnMain: @escaping (Int64, @escaping () -> Void) -> Void = { delayMs, block in
101
+ DispatchQueue.main.asyncAfter(
102
+ deadline: .now() + .milliseconds(Int(clamping: delayMs)),
103
+ execute: block
104
+ )
105
+ }
106
+ ) {
107
+ self.intervalMs = max(0, intervalMs)
108
+ self.nowMs = nowMs
109
+ self.scheduleOnMain = scheduleOnMain
110
+ }
111
+
112
+ func push(frame: CGRect, deliver: @escaping (CGRect) -> Void) {
113
+ let now = nowMs()
114
+ if let last = lastDeliveryAtMs, now - last < intervalMs {
115
+ pending = (frame, deliver)
116
+ if !trailingScheduled {
117
+ trailingScheduled = true
118
+ scheduleOnMain(max(1, intervalMs - (now - last))) { [weak self] in
119
+ self?.deliverPending()
120
+ }
121
+ }
122
+ return
123
+ }
124
+ lastDeliveryAtMs = now
125
+ pending = nil
126
+ deliver(frame)
127
+ }
128
+
129
+ private func deliverPending() {
130
+ trailingScheduled = false
131
+ guard let pending else { return }
132
+ self.pending = nil
133
+ lastDeliveryAtMs = nowMs()
134
+ pending.deliver(pending.frame)
135
+ }
136
+ }
137
+
138
+ /// Geometry preference carrying a modifier's global frame. One shared key
139
+ /// type is safe: every modifier instance publishes on its own `Color.clear`
140
+ /// and consumes the value inside that same background subtree, so instances
141
+ /// never observe each other's frames.
142
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
143
+ private struct ForgeFramePreferenceKey: PreferenceKey {
144
+ static let defaultValue = CGRect.zero
145
+
146
+ static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
147
+ value = nextValue()
148
+ }
149
+ }
150
+
151
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
152
+ public struct ForgeSectionCaptureModifier: ViewModifier {
153
+ private let handle: AutomaticCaptureHandle
154
+ private let name: String
155
+ private let order: Int?
156
+ @State private var instanceKey = "swiftui-section-\(UUID().uuidString)"
157
+ @State private var frameThrottle = ForgeSwiftUIFrameThrottle()
158
+
159
+ public init(handle: AutomaticCaptureHandle, name: String, order: Int?) {
160
+ self.handle = handle
161
+ self.name = name
162
+ self.order = order
163
+ }
164
+
165
+ public func body(content: Content) -> some View {
166
+ content
167
+ .background(
168
+ GeometryReader { proxy in
169
+ Color.clear
170
+ .preference(key: ForgeFramePreferenceKey.self, value: proxy.frame(in: .global))
171
+ .onPreferenceChange(ForgeFramePreferenceKey.self) { frame in
172
+ // Continuous visibility: scroll/layout changes
173
+ // republish the frame so the engine store stays
174
+ // fresh and impressions re-verify against real
175
+ // percentages at fire time.
176
+ frameThrottle.push(frame: frame) { latest in
177
+ pushSection(frame: latest)
178
+ }
179
+ }
180
+ .onAppear {
181
+ pushSection(frame: proxy.frame(in: .global))
182
+ }
183
+ .onDisappear {
184
+ handle.sectionsCoordinator.removeSection(source: .swiftui, key: instanceKey)
185
+ }
186
+ }
187
+ )
188
+ .environment(\.forgeSectionKey, instanceKey)
189
+ }
190
+
191
+ private func pushSection(frame: CGRect) {
192
+ handle.sectionsCoordinator.updateSection(
193
+ source: .swiftui,
194
+ SectionSnapshot(
195
+ key: instanceKey,
196
+ name: name,
197
+ order: order,
198
+ visibilityPct: ForgeSwiftUIVisibility.pct(ofGlobalFrame: frame),
199
+ itemCount: 0
200
+ )
201
+ )
202
+ }
203
+ }
204
+
205
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
206
+ public struct ForgeSectionItemCaptureModifier: ViewModifier {
207
+ private let handle: AutomaticCaptureHandle
208
+ private let contentId: String
209
+ private let position: Int?
210
+ private let title: String?
211
+ @State private var instanceKey = "swiftui-item-\(UUID().uuidString)"
212
+ @State private var frameThrottle = ForgeSwiftUIFrameThrottle()
213
+ @Environment(\.forgeSectionKey) private var sectionKey: String?
214
+
215
+ public init(handle: AutomaticCaptureHandle, contentId: String, position: Int?, title: String?) {
216
+ self.handle = handle
217
+ self.contentId = contentId
218
+ self.position = position
219
+ self.title = title
220
+ }
221
+
222
+ public func body(content: Content) -> some View {
223
+ content.background(
224
+ GeometryReader { proxy in
225
+ Color.clear
226
+ .preference(key: ForgeFramePreferenceKey.self, value: proxy.frame(in: .global))
227
+ .onPreferenceChange(ForgeFramePreferenceKey.self) { frame in
228
+ // Resolve the environment section key before the
229
+ // throttled (possibly deferred) delivery so the
230
+ // escaping closure never reads @Environment late.
231
+ let resolvedSectionKey = sectionKey
232
+ frameThrottle.push(frame: frame) { latest in
233
+ pushItem(resolvedSectionKey: resolvedSectionKey, frame: latest)
234
+ }
235
+ }
236
+ .onAppear {
237
+ pushItem(resolvedSectionKey: sectionKey, frame: proxy.frame(in: .global))
238
+ }
239
+ .onDisappear {
240
+ handle.sectionsCoordinator.removeItem(source: .swiftui, key: instanceKey)
241
+ }
242
+ }
243
+ )
244
+ }
245
+
246
+ private func pushItem(resolvedSectionKey: String?, frame: CGRect) {
247
+ let coordinator = handle.sectionsCoordinator
248
+ coordinator.updateItem(
249
+ source: .swiftui,
250
+ ItemSnapshot(
251
+ key: instanceKey,
252
+ contentId: contentId,
253
+ position: position ?? coordinator.assignedPosition(
254
+ sectionKey: resolvedSectionKey ?? "swiftui",
255
+ itemKey: instanceKey
256
+ ),
257
+ sectionKey: resolvedSectionKey,
258
+ visibilityPct: ForgeSwiftUIVisibility.pct(ofGlobalFrame: frame),
259
+ title: title
260
+ )
261
+ )
262
+ }
263
+ }
264
+
265
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
266
+ public extension View {
267
+ /// Marks this view as an F3 section container named `name`. An explicit
268
+ /// `order` wins over first-seen ordering across the whole screen.
269
+ func forgeSection(_ name: String, order: Int? = nil, handle: AutomaticCaptureHandle) -> some View {
270
+ modifier(ForgeSectionCaptureModifier(handle: handle, name: name, order: order))
271
+ }
272
+
273
+ /// Marks this view as F3 section content. Nest it inside a `.forgeSection`
274
+ /// container to attach the item to that section automatically.
275
+ func forgeSectionItem(
276
+ contentId: String,
277
+ position: Int? = nil,
278
+ title: String? = nil,
279
+ handle: AutomaticCaptureHandle
280
+ ) -> some View {
281
+ modifier(ForgeSectionItemCaptureModifier(handle: handle, contentId: contentId, position: position, title: title))
282
+ }
283
+ }
284
+ #endif
@@ -0,0 +1,44 @@
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: SwiftUI helper for reporting visible routes through the automatic capture pipeline.
13
+ */
14
+
15
+ #if canImport(NPAWForgeCore)
16
+ import NPAWForgeCore
17
+ #endif
18
+ #if canImport(SwiftUI)
19
+ import SwiftUI
20
+
21
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
22
+ public struct ForgeScreenCaptureModifier: ViewModifier {
23
+ private let handle: AutomaticCaptureHandle
24
+ private let name: String
25
+
26
+ public init(handle: AutomaticCaptureHandle, name: String) {
27
+ self.handle = handle
28
+ self.name = name
29
+ }
30
+
31
+ public func body(content: Content) -> some View {
32
+ content.onAppear {
33
+ _ = handle.screenView(name)
34
+ }
35
+ }
36
+ }
37
+
38
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
39
+ public extension View {
40
+ func forgeScreen(_ name: String, handle: AutomaticCaptureHandle) -> some View {
41
+ modifier(ForgeScreenCaptureModifier(handle: handle, name: name))
42
+ }
43
+ }
44
+ #endif
@@ -0,0 +1,199 @@
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: Apple ad adapter base class and public manual fire API.
13
+ */
14
+
15
+ import Foundation
16
+
17
+ open class AdAdapter: NSObject {
18
+ public let adapterInstanceId = UUID()
19
+ open var descriptor = AdapterDescriptor(
20
+ adapterId: "generic-ad",
21
+ category: .ad,
22
+ playerFamily: "Generic Ads",
23
+ version: "unknown"
24
+ )
25
+ open var playerObject: AnyObject? {
26
+ didSet {
27
+ if oldValue === playerObject { return }
28
+ if isBound { unregisterListeners() }
29
+ if isBound, playerObject != nil { registerListeners() }
30
+ }
31
+ }
32
+ private var isBound = false
33
+ public private(set) weak var videoAdapter: VideoAdapter?
34
+
35
+ public init(player: AnyObject? = nil) {
36
+ self.playerObject = player
37
+ super.init()
38
+ }
39
+
40
+ func bindToVideoAdapter(_ videoAdapter: VideoAdapter) {
41
+ self.videoAdapter = videoAdapter
42
+ isBound = true
43
+ if playerObject != nil { registerListeners() }
44
+ }
45
+
46
+ func unbindFromVideoAdapter() {
47
+ if isBound { unregisterListeners() }
48
+ isBound = false
49
+ videoAdapter = nil
50
+ }
51
+
52
+ open func registerListeners() {}
53
+ open func unregisterListeners() {}
54
+
55
+ open func metadata() -> [String: String] {
56
+ [
57
+ "adapterId": descriptor.adapterId,
58
+ "category": descriptor.category.rawValue,
59
+ "playerFamily": descriptor.playerFamily,
60
+ "version": descriptor.version,
61
+ ]
62
+ }
63
+
64
+ open func onAdEvent(
65
+ _ event: AdEvent,
66
+ timestampMs: Int64 = 0,
67
+ metadata: AdMetadataSnapshot? = nil
68
+ ) -> ForgeOutcome {
69
+ let resolved = metadata ?? defaultMetadata(nil)
70
+ switch event {
71
+ case .manifest: return fireManifest(metadata: resolved, timestampMs: timestampMs)
72
+ case .breakStart: return fireBreakStart(metadata: resolved, timestampMs: timestampMs)
73
+ case .adInit: return fireAdInit(metadata: resolved, timestampMs: timestampMs)
74
+ case .start: return fireStart(metadata: resolved, timestampMs: timestampMs)
75
+ case .join: return fireJoin(metadata: resolved, timestampMs: timestampMs)
76
+ case .pause: return firePause(metadata: resolved, timestampMs: timestampMs)
77
+ case .resume: return fireResume(metadata: resolved, timestampMs: timestampMs)
78
+ case .bufferBegin: return fireBufferBegin(metadata: resolved, timestampMs: timestampMs)
79
+ case .bufferEnd: return fireBufferEnd(metadata: resolved, timestampMs: timestampMs)
80
+ case .quartile: return fireQuartile(resolved.quartile, timestampMs: timestampMs)
81
+ case .click: return fireClick(metadata: resolved, timestampMs: timestampMs)
82
+ case .skip: return fireSkip(timestampMs: timestampMs)
83
+ case .stop: return fireStop(metadata: resolved, timestampMs: timestampMs)
84
+ case .error: return fireError(errorCode: resolved.errorCode, errorMessage: resolved.errorMessage, errorMetadata: resolved.errorMetadata, timestampMs: timestampMs)
85
+ case .fatalError: return fireFatalError(errorCode: resolved.errorCode, errorMessage: resolved.errorMessage, errorMetadata: resolved.errorMetadata, timestampMs: timestampMs)
86
+ case .breakStop: return fireBreakStop(metadata: resolved, timestampMs: timestampMs)
87
+ }
88
+ }
89
+
90
+ open func fireManifest(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
91
+ record(.manifest, metadata ?? defaultMetadata(params), timestampMs)
92
+ }
93
+ open func fireBreakStart(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
94
+ record(.breakStart, metadata ?? defaultMetadata(params), timestampMs)
95
+ }
96
+ open func fireAdInit(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
97
+ record(.adInit, metadata ?? defaultMetadata(params), timestampMs)
98
+ }
99
+ open func fireStart(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
100
+ record(.start, metadata ?? defaultMetadata(params), timestampMs)
101
+ }
102
+ open func fireJoin(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
103
+ record(.join, metadata ?? defaultMetadata(params), timestampMs)
104
+ }
105
+ open func firePause(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
106
+ record(.pause, metadata ?? defaultMetadata(params), timestampMs)
107
+ }
108
+ open func fireResume(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
109
+ record(.resume, metadata ?? defaultMetadata(params), timestampMs)
110
+ }
111
+ open func fireBufferBegin(convertFromSeek: Bool? = false, params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
112
+ record(.bufferBegin, metadata ?? defaultMetadata(params), timestampMs)
113
+ }
114
+ open func fireBufferEnd(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
115
+ record(.bufferEnd, metadata ?? defaultMetadata(params), timestampMs)
116
+ }
117
+ open func fireQuartile(_ quartile: Int? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
118
+ var metadata = defaultMetadata(nil)
119
+ metadata.quartile = quartile
120
+ return record(.quartile, metadata, timestampMs)
121
+ }
122
+ open func fireClick(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
123
+ record(.click, metadata ?? defaultMetadata(params), timestampMs)
124
+ }
125
+ open func fireClick(adUrl: String?) -> ForgeOutcome {
126
+ fireClick(params: adUrl.map { ["adUrl": $0] } ?? [:])
127
+ }
128
+ open func fireSkip(timestampMs: Int64 = 0) -> ForgeOutcome {
129
+ record(.skip, defaultMetadata(nil), timestampMs)
130
+ }
131
+ open func fireStop(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
132
+ record(.stop, metadata ?? defaultMetadata(params), timestampMs)
133
+ }
134
+ open func fireError(errorCode: String? = nil, errorMessage: String? = nil, errorMetadata: String? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
135
+ var metadata = defaultMetadata(nil)
136
+ metadata.errorCode = errorCode
137
+ metadata.errorMessage = errorMessage
138
+ metadata.errorMetadata = errorMetadata
139
+ return record(.error, metadata, timestampMs)
140
+ }
141
+ open func fireFatalError(errorCode: String? = nil, errorMessage: String? = nil, errorMetadata: String? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
142
+ var metadata = defaultMetadata(nil)
143
+ metadata.errorCode = errorCode
144
+ metadata.errorMessage = errorMessage
145
+ metadata.errorMetadata = errorMetadata
146
+ metadata.errorLevel = "fatal"
147
+ return record(.fatalError, metadata, timestampMs)
148
+ }
149
+ open func fireBreakStop(params: [String: String]? = [:], metadata: AdMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
150
+ record(.breakStop, metadata ?? defaultMetadata(params), timestampMs)
151
+ }
152
+ open func fireDestroy(timestampMs: Int64 = 0) -> ForgeOutcome { .unsupportedCapability }
153
+
154
+ open func defaultMetadata(_ params: [String: String]?) -> AdMetadataSnapshot {
155
+ AdMetadataSnapshot(
156
+ adId: params?["adId"] ?? params?["ad_id"],
157
+ creativeId: params?["adCreativeId"] ?? params?["creative_id"],
158
+ adUrl: params?["adUrl"] ?? params?["ad_url"],
159
+ adTitle: params?["adTitle"],
160
+ adDurationMs: params?["adDurationMs"].flatMap(Int64.init) ?? params?["adDuration"].flatMap(Self.secondsToMillis),
161
+ adPlayheadMs: params?["adPlayheadMs"].flatMap(Int64.init) ?? params?["adPlayhead"].flatMap(Self.secondsToMillis),
162
+ adBitrate: params?["adBitrate"].flatMap(Int64.init),
163
+ position: params?["position"].flatMap(AdPosition.from),
164
+ quartile: params?["quartile"].flatMap(Int.init),
165
+ insertionType: (params?["adInsertionType"] ?? params?["insertion_type"]).flatMap(AdInsertionType.from) ?? .unknown,
166
+ errorMetadata: params?["errorMetadata"],
167
+ errorLevel: params?["errorLevel"]
168
+ )
169
+ }
170
+
171
+ func record(_ event: AdEvent, _ metadata: AdMetadataSnapshot, _ timestampMs: Int64) -> ForgeOutcome {
172
+ videoAdapter?.recordAdEvent(event: event, timestampMs: timestampMs, metadata: metadata) ?? .ignoredInvalidState
173
+ }
174
+
175
+ private static func secondsToMillis(_ value: String) -> Int64? {
176
+ Double(value).map { Int64($0 * 1000.0) }
177
+ }
178
+ }
179
+
180
+ private extension AdPosition {
181
+ static func from(_ value: String) -> AdPosition {
182
+ switch value.lowercased() {
183
+ case "pre": return .pre
184
+ case "mid": return .mid
185
+ case "post": return .post
186
+ default: return .unknown
187
+ }
188
+ }
189
+ }
190
+
191
+ private extension AdInsertionType {
192
+ static func from(_ value: String) -> AdInsertionType {
193
+ switch value.lowercased() {
194
+ case "csai", "client_side": return .clientSide
195
+ case "ssai", "server_side": return .serverSide
196
+ default: return .unknown
197
+ }
198
+ }
199
+ }