@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,453 @@
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: Public Swift-first Forge Apple facade.
13
+ */
14
+
15
+ import Foundation
16
+
17
+ // The generated kernel bridge is only present when the Rust runtime is linked;
18
+ // guarded exactly as in NativeUniFFIRuntime.swift.
19
+ #if FORGE_HAS_UNIFFI
20
+ #if canImport(ForgeKernelUniFFI)
21
+ import ForgeKernelUniFFI
22
+ #endif
23
+ #endif
24
+
25
+ @objc(NPAWForge)
26
+ public final class NPAWForge: NSObject {
27
+ public static let defaultVideoAdapterIdentifier = "default"
28
+ /// Marker meaning "this build does not know its own version".
29
+ ///
30
+ /// The authoritative Forge version is resolved by
31
+ /// `scripts/version/resolve-version.sh` and compiled into the Rust
32
+ /// runtime, which substitutes it whenever the Swift layer sends this
33
+ /// marker — so the wire never carries the marker when a Forge runtime is
34
+ /// linked. Only builds with no runtime linked see it.
35
+ ///
36
+ /// Deliberately NOT release-shaped: it used to be `9.0.0-local`, which
37
+ /// reached the wire as a plausible-looking version and made internal
38
+ /// builds indistinguishable from a real 9.0.0 release.
39
+ ///
40
+ /// `scripts/apple/publish-local-shipped.sh` rewrites this literal to the
41
+ /// release version when staging the shipped package, and fails loudly if
42
+ /// it cannot find it. Keep it a plain stored literal for that reason.
43
+ public static let defaultPluginVersion = "9.6.1"
44
+ public static let defaultAppName = "apple"
45
+ public static let defaultBuildVariant = "release"
46
+ public static let defaultLmaEndpoint = "https://lma.npaw.com"
47
+
48
+ /// The Forge plugin version this build actually reports, matching Android's
49
+ /// `NPAWForge.pluginVersion`.
50
+ ///
51
+ /// Precedence deliberately mirrors the runtime's own
52
+ /// `effective_plugin_version`, so this property and the wire always agree:
53
+ ///
54
+ /// 1. A patched `defaultPluginVersion` wins. That only happens in a staged
55
+ /// shipped package, where `publish-local-shipped.sh` has written the
56
+ /// release version in, and the release version must stay authoritative.
57
+ /// 2. Otherwise the version compiled into the linked Rust runtime.
58
+ /// 3. Otherwise the marker, for builds with no runtime linked.
59
+ public static var pluginVersion: String {
60
+ if defaultPluginVersion != unresolvedPluginVersionMarker {
61
+ return defaultPluginVersion
62
+ }
63
+ return runtimePluginVersion ?? defaultPluginVersion
64
+ }
65
+
66
+ /// Kept separate from `defaultPluginVersion` so the release patch can
67
+ /// rewrite that literal without also rewriting the comparison value.
68
+ private static let unresolvedPluginVersionMarker = "0.0.0-unresolved"
69
+
70
+ /// The version embedded in the linked Rust runtime, when one is linked.
71
+ ///
72
+ /// `forgePluginVersion()` is the UniFFI-generated Swift name for the UDL
73
+ /// declaration `string forge_plugin_version();` — the generator camelCases
74
+ /// it.
75
+ private static var runtimePluginVersion: String? {
76
+ #if FORGE_HAS_UNIFFI
77
+ #if canImport(ForgeKernelUniFFI)
78
+ let version = forgePluginVersion()
79
+ return version.isEmpty ? nil : version
80
+ #else
81
+ return nil
82
+ #endif
83
+ #else
84
+ return nil
85
+ #endif
86
+ }
87
+
88
+ public let kernel: ForgeApple
89
+ private var destroyed = false
90
+ private let lock = NSRecursiveLock()
91
+ private var videoAdapters: [VideoAdapter] = []
92
+
93
+ public init(kernel: ForgeApple) {
94
+ self.kernel = kernel
95
+ }
96
+
97
+ public func videoBuilder() -> VideoBuilder {
98
+ VideoBuilder(forge: self)
99
+ }
100
+
101
+ public func subscribeEvents(_ observer: @escaping (ForgeRecordedEvent, ForgeOutcome) -> Void) -> ForgeEventSubscription {
102
+ kernel.subscribeEvents(observer)
103
+ }
104
+
105
+ /// Observes kernel-derived capture plans (bitmask + focus/limit knobs +
106
+ /// behavior hints). The latest plan is replayed to listeners added after
107
+ /// the first emission. The kernel intake gates remain the semantic
108
+ /// authority; the plan only tells observers what is worth installing.
109
+ public func addCapturePlanListener(_ listener: @escaping (ForgeCapturePlan) -> Void) -> ForgeEventSubscription {
110
+ kernel.addCapturePlanListener(listener)
111
+ }
112
+
113
+ /// Latest kernel-emitted capture plan, or `nil` before the first
114
+ /// emission (pre-first-plan behavior is local options only).
115
+ public func capturePlan() -> ForgeCapturePlan? {
116
+ kernel.capturePlan()
117
+ }
118
+
119
+ public func addOnSendRequestListener(_ listener: @escaping (String, inout [String: String]) -> Void) -> ForgeEventSubscription {
120
+ kernel.addOnSendRequestListener(listener)
121
+ }
122
+
123
+ public func snapshotEvents() -> [ForgeRecordedEvent] {
124
+ kernel.snapshotEvents()
125
+ }
126
+
127
+ @objc public func setLogLevel(_ value: Any?) -> ForgeOutcome {
128
+ kernel.setLogLevel(ForgeLogLevel.from(value, default: kernel.effectiveLogLevel()))
129
+ }
130
+
131
+ @objc public func startSession(_ metadata: [String: Any] = [:]) -> ForgeOutcome {
132
+ kernel.startSession(metadata: metadata.toStringMap())
133
+ }
134
+
135
+ @objc public func endSession(_ metadata: [String: Any] = [:]) -> ForgeOutcome {
136
+ kernel.endSession(metadata: metadata.toStringMap())
137
+ }
138
+
139
+ public func trackSessionNavigation(_ metadata: [String: Any]) -> ForgeOutcome {
140
+ kernel.trackSessionNavigation(metadata.toStringMap())
141
+ }
142
+
143
+ public func trackSessionEvent(name: String, metadata: [String: Any] = [:]) -> ForgeOutcome {
144
+ kernel.trackSessionEvent(name: name, metadata: metadata.toStringMap())
145
+ }
146
+
147
+ public func trackSessionEventStop(name: String, metadata: [String: Any] = [:]) -> ForgeOutcome {
148
+ kernel.trackSessionEventStop(name: name, metadata: metadata.toStringMap())
149
+ }
150
+
151
+ public func trackSessionError(errorCode: String, errorMessage: String, metadata: [String: Any] = [:]) -> ForgeOutcome {
152
+ kernel.trackSessionError(errorCode: errorCode, errorMessage: errorMessage, metadata: metadata.toStringMap())
153
+ }
154
+
155
+ /// Emits a raw event: the name gains the `raw_` prefix when missing and
156
+ /// travels on the session-event path, matching the web binding shape.
157
+ public func trackRawEvent(name: String, params: [String: Any] = [:]) -> ForgeOutcome {
158
+ kernel.trackRawEvent(name: name, params: params.toStringMap())
159
+ }
160
+
161
+ /// Emits a `raw_search` event with the query and optional result count.
162
+ public func trackSearch(query: String, resultCount: Int? = nil) -> ForgeOutcome {
163
+ kernel.trackSearch(query: query, resultCount: resultCount)
164
+ }
165
+
166
+ /// Emits a `raw_search_result_click` event for a result chosen from the
167
+ /// results produced by the query last reported via `trackSearch`.
168
+ public func trackSearchResultClick(query: String, options: ForgeSearchResultClickOptions = ForgeSearchResultClickOptions()) -> ForgeOutcome {
169
+ kernel.trackSearchResultClick(query: query, options: options)
170
+ }
171
+
172
+ /// Emits a typed business raw event whose attributes travel under the
173
+ /// `target.attr.*` convention.
174
+ public func trackBusinessEvent(name: String, attributes: [String: Any] = [:]) -> ForgeOutcome {
175
+ kernel.trackBusinessEvent(name: name, attributes: attributes.toStringMap())
176
+ }
177
+
178
+ /// Emits a `raw_attribution` event for apps that resolve their own
179
+ /// campaign parameters.
180
+ public func trackAttribution(options: ForgeAttributionOptions = ForgeAttributionOptions()) -> ForgeOutcome {
181
+ kernel.trackAttribution(options: options)
182
+ }
183
+
184
+ /// Reports a successful login; the kernel emits `login
185
+ /// {authStatus:"success"}` and atomically restarts the app session when
186
+ /// the effective identity changed.
187
+ public func loginSuccess(userId: String, options: [String: String] = [:]) -> ForgeOutcome {
188
+ kernel.loginSuccess(userId: userId, options: options)
189
+ }
190
+
191
+ /// Reports a failed login attempt: `login {authStatus:"failure"}`.
192
+ public func loginFailed(options: [String: String] = [:]) -> ForgeOutcome {
193
+ kernel.loginFailed(options: options)
194
+ }
195
+
196
+ /// Reports a logout; the kernel clears the identity to Anonymous and
197
+ /// atomically restarts the app session.
198
+ public func logout(options: [String: String] = [:]) -> ForgeOutcome {
199
+ kernel.logout(options: options)
200
+ }
201
+
202
+ /// Reports a profile creation: `profile_created {profileId, profileType?}`.
203
+ public func profileCreated(profileId: String, options: [String: String] = [:]) -> ForgeOutcome {
204
+ kernel.profileCreated(profileId: profileId, options: options)
205
+ }
206
+
207
+ /// Reports a profile selection; the kernel stamps `profileId` on the
208
+ /// session identity and restarts the app session when it changed.
209
+ public func profileSelected(profileId: String, options: [String: String] = [:]) -> ForgeOutcome {
210
+ kernel.profileSelected(profileId: profileId, options: options)
211
+ }
212
+
213
+ /// Reports a profile deletion: `profile_deleted {profileId}`.
214
+ public func profileDeleted(profileId: String) -> ForgeOutcome {
215
+ kernel.profileDeleted(profileId: profileId)
216
+ }
217
+
218
+ public func trackNetworkRequest(_ record: NetworkRequestRecord) -> ForgeOutcome {
219
+ guard !destroyed else { return .ignoredInvalidState }
220
+ return kernel.trackNetworkRequest(record)
221
+ }
222
+
223
+ public func getVideoAdapter(identifier: String? = nil) -> VideoAdapter? {
224
+ let id = identifier?.nilIfBlank ?? Self.defaultVideoAdapterIdentifier
225
+ lock.lock()
226
+ defer { lock.unlock() }
227
+ return videoAdapters.first { $0.identifier == id && !$0.isDestroyed }
228
+ }
229
+
230
+ public func destroy() -> ForgeOutcome {
231
+ lock.lock()
232
+ defer { lock.unlock() }
233
+ guard !destroyed else { return .ignoredDuplicate }
234
+ destroyed = true
235
+ let adapters = videoAdapters
236
+ videoAdapters.removeAll()
237
+ adapters.forEach { _ = $0.destroy() }
238
+ return kernel.destroy()
239
+ }
240
+
241
+ func buildVideoAdapter(
242
+ identifier: String,
243
+ options: VideoOptions,
244
+ playerAdapter: PlayerAdapter?,
245
+ adAdapter: AdAdapter?
246
+ ) -> VideoAdapter {
247
+ lock.lock()
248
+ defer { lock.unlock() }
249
+ destroyAdapters(identifier: identifier)
250
+ destroyAdapters(using: playerAdapter, owner: nil)
251
+ destroyAdapters(using: adAdapter, owner: nil)
252
+ let adapter = VideoAdapter(forge: self, identifier: identifier, options: options)
253
+ videoAdapters.append(adapter)
254
+ _ = adapter.replacePlayerAdapter(playerAdapter, checkDuplicates: false)
255
+ _ = adapter.replaceAdAdapter(adAdapter, checkDuplicates: false)
256
+ return adapter
257
+ }
258
+
259
+ func onPlayerAdapterRegistered(owner: VideoAdapter, playerAdapter: PlayerAdapter?) {
260
+ lock.lock()
261
+ defer { lock.unlock() }
262
+ destroyAdapters(using: playerAdapter, owner: owner)
263
+ }
264
+
265
+ func onAdAdapterRegistered(owner: VideoAdapter, adAdapter: AdAdapter?) {
266
+ lock.lock()
267
+ defer { lock.unlock() }
268
+ destroyAdapters(using: adAdapter, owner: owner)
269
+ }
270
+
271
+ func onVideoAdapterDestroyed(_ adapter: VideoAdapter) {
272
+ lock.lock()
273
+ defer { lock.unlock() }
274
+ videoAdapters.removeAll { $0 === adapter }
275
+ }
276
+
277
+ private func destroyAdapters(identifier: String) {
278
+ videoAdapters.filter { $0.identifier == identifier && !$0.isDestroyed }.forEach { _ = $0.destroy() }
279
+ }
280
+
281
+ private func destroyAdapters(using candidate: AnyObject?, owner: VideoAdapter?) {
282
+ guard let candidate else { return }
283
+ videoAdapters
284
+ .filter { $0 !== owner && !$0.isDestroyed && $0.usesAdapterOrPlayer(candidate) }
285
+ .forEach { _ = $0.destroy() }
286
+ }
287
+
288
+ public final class Builder {
289
+ private var accountCode: String
290
+ private var appName = Bundle.main.displayName ?? NPAWForge.defaultAppName
291
+ private var appReleaseVersion = Bundle.main.releaseVersion
292
+ private var buildVariant = NPAWForge.defaultBuildVariant
293
+ private var lmaEndpoint = NPAWForge.defaultLmaEndpoint
294
+ private var analyticsUserAgent: String?
295
+ private var allowLocalHttp = false
296
+ private var startSessionOnInitialize = false
297
+ private var processEventsBeforeSessionStart = false
298
+ private var preSessionEventQueueLimitEvents = 100
299
+ private var preSessionEventQueueMaxAgeMs: Int64 = 30_000
300
+ private var logLevel: ForgeLogLevel = .warning
301
+ private var services: ForgeAppleServices?
302
+
303
+ public init(accountCode: String) {
304
+ self.accountCode = accountCode
305
+ }
306
+
307
+ @available(*, deprecated, message: "pluginVersion is owned by the Forge SDK build and this setter is ignored.")
308
+ @discardableResult public func setPluginVersion(_ value: String?) -> Builder {
309
+ _ = value
310
+ return self
311
+ }
312
+
313
+ @discardableResult public func setAppName(_ value: String?) -> Builder {
314
+ appName = value?.nilIfBlank ?? appName
315
+ return self
316
+ }
317
+
318
+ @discardableResult public func setApplicationName(_ value: String?) -> Builder { setAppName(value) }
319
+
320
+ @discardableResult public func setAppReleaseVersion(_ value: String?) -> Builder {
321
+ appReleaseVersion = value?.nilIfBlank
322
+ return self
323
+ }
324
+
325
+ @discardableResult public func setAppVersion(_ value: String?) -> Builder { setAppReleaseVersion(value) }
326
+ @discardableResult public func setApplicationVersion(_ value: String?) -> Builder { setAppReleaseVersion(value) }
327
+
328
+ @discardableResult public func setLmaEndpoint(_ value: String?) -> Builder {
329
+ lmaEndpoint = value?.nilIfBlank ?? lmaEndpoint
330
+ return self
331
+ }
332
+
333
+ @discardableResult public func setAnalyticsHost(_ value: String?) -> Builder { setLmaEndpoint(value) }
334
+ @discardableResult public func setHost(_ value: String?) -> Builder { setLmaEndpoint(value) }
335
+
336
+ @discardableResult public func setAnalyticsUserAgent(_ value: String?) -> Builder {
337
+ analyticsUserAgent = value?.nilIfBlank
338
+ return self
339
+ }
340
+
341
+ @discardableResult public func setLocalPlaintextTransportForTesting(_ enabled: Bool) -> Builder {
342
+ allowLocalHttp = enabled
343
+ return self
344
+ }
345
+
346
+ @discardableResult public func setStartSessionOnInitialize(_ enabled: Bool) -> Builder {
347
+ startSessionOnInitialize = enabled
348
+ return self
349
+ }
350
+
351
+ @discardableResult public func setProcessEventsBeforeSessionStart(_ enabled: Bool) -> Builder {
352
+ processEventsBeforeSessionStart = enabled
353
+ return self
354
+ }
355
+
356
+ @discardableResult public func setPreSessionEventQueueLimitEvents(_ limit: Int) -> Builder {
357
+ preSessionEventQueueLimitEvents = max(0, limit)
358
+ return self
359
+ }
360
+
361
+ @discardableResult public func setPreSessionEventQueueMaxAgeMs(_ maxAgeMs: Int64) -> Builder {
362
+ preSessionEventQueueMaxAgeMs = max(0, maxAgeMs)
363
+ return self
364
+ }
365
+
366
+ @discardableResult public func setLogLevel(_ value: Any?) -> Builder {
367
+ logLevel = ForgeLogLevel.from(value)
368
+ return self
369
+ }
370
+
371
+ @discardableResult public func setBuildVariant(_ value: String?) -> Builder {
372
+ buildVariant = value?.nilIfBlank ?? buildVariant
373
+ return self
374
+ }
375
+
376
+ @discardableResult public func setServices(_ value: ForgeAppleServices?) -> Builder {
377
+ services = value
378
+ return self
379
+ }
380
+
381
+ public func build() -> NPAWForge {
382
+ let config = ForgeAppleConfig(
383
+ accountCode: accountCode,
384
+ pluginVersion: NPAWForge.defaultPluginVersion,
385
+ appName: appName,
386
+ buildVariant: buildVariant,
387
+ appReleaseVersion: appReleaseVersion,
388
+ lmaEndpoint: lmaEndpoint,
389
+ analyticsUserAgent: analyticsUserAgent,
390
+ allowLocalHttp: allowLocalHttp,
391
+ startSessionOnInitialize: startSessionOnInitialize,
392
+ processEventsBeforeSessionStart: processEventsBeforeSessionStart,
393
+ preSessionEventQueueLimitEvents: preSessionEventQueueLimitEvents,
394
+ preSessionEventQueueMaxAgeMs: preSessionEventQueueMaxAgeMs,
395
+ logLevel: logLevel
396
+ )
397
+ let resolvedServices = services ?? ForgeAppleServices(storage: UserDefaultsStorage())
398
+ let forge = NPAWForge(kernel: ForgeApple(config: config, services: resolvedServices))
399
+ ForgeAutomaticCaptureRegistry.installIfAvailable(forge)
400
+ return forge
401
+ }
402
+ }
403
+ }
404
+
405
+ public enum ForgeAutomaticCaptureRegistry {
406
+ private static let lock = NSLock()
407
+ private static var installers: [(NPAWForge) -> Void] = []
408
+
409
+ public static func register(_ installer: @escaping (NPAWForge) -> Void) {
410
+ lock.lock()
411
+ defer { lock.unlock() }
412
+ installers.append(installer)
413
+ }
414
+
415
+ static func installIfAvailable(_ forge: NPAWForge) {
416
+ lock.lock()
417
+ let snapshot = installers
418
+ lock.unlock()
419
+ snapshot.forEach { $0(forge) }
420
+ }
421
+ }
422
+
423
+ private extension Dictionary where Key == String, Value == Any {
424
+ func toStringMap() -> [String: String] {
425
+ reduce(into: [String: String]()) { output, element in
426
+ let key = element.key.trimmingCharacters(in: .whitespacesAndNewlines)
427
+ guard !key.isEmpty else { return }
428
+ let value = String(describing: element.value)
429
+ guard !value.isEmpty else { return }
430
+ output[key] = value
431
+ }
432
+ }
433
+ }
434
+
435
+ private extension String {
436
+ var nilIfBlank: String? {
437
+ let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
438
+ return trimmed.isEmpty ? nil : trimmed
439
+ }
440
+ }
441
+
442
+ private extension Bundle {
443
+ var displayName: String? {
444
+ object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
445
+ ?? object(forInfoDictionaryKey: "CFBundleName") as? String
446
+ ?? bundleIdentifier
447
+ }
448
+
449
+ var releaseVersion: String? {
450
+ object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
451
+ ?? object(forInfoDictionaryKey: "CFBundleVersion") as? String
452
+ }
453
+ }