@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.
- package/LICENSE +10 -0
- package/NPAWForgeReactNative.podspec +44 -0
- package/README.md +51 -0
- package/android/build.gradle +77 -0
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +295 -0
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +61 -0
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +735 -0
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgePackage.java +53 -0
- package/android/src/test/java/com/npaw/forge/reactnative/ForgeDtoMapperTest.java +133 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +82 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +3931 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.modulemap +7 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAccessLogFacts.swift +152 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +877 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/AttributionCapture.swift +158 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/CapturePlanGate.swift +189 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/CrashCapture.swift +120 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/ForgeAutomaticCapture.swift +639 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsCapture.swift +1176 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsEngine.swift +674 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsMarkers.swift +105 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsSwiftUI.swift +284 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SwiftUICapture.swift +44 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/AdAdapter.swift +199 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/AdSemantics.swift +136 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +596 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +297 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeClock.swift +29 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +109 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeOutcome.swift +89 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +291 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeStorage.swift +70 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ManualAdReporter.swift +180 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +453 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +594 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NetworkRequestRecord.swift +191 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ObjectiveCWrappers.swift +191 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +299 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerSemantics.swift +266 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PrivacySanitizer.swift +59 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/VideoAdapter.swift +478 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/VideoOptions.swift +431 -0
- package/ios/ForgeApple/Sources/NPAWForgeNetworkCapture/ForgeNetworkCapture.swift +116 -0
- package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/NPAWForgeNetworkSwizzler.m +293 -0
- package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/include/NPAWForgeNetworkSwizzler.h +33 -0
- package/ios/ForgeModuleImpl.swift +1131 -0
- package/ios/NPAWForgeTurboModule.mm +399 -0
- package/package.json +33 -0
- package/src/ForgeAdReporter.ts +101 -0
- package/src/ForgeVideo.ts +169 -0
- package/src/NPAWForge.ts +177 -0
- package/src/capture.ts +192 -0
- package/src/events.ts +40 -0
- package/src/index.ts +37 -0
- package/src/specs/NativeNPAWForge.ts +168 -0
- 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
|
+
}
|