@npaw/forge-react-native 9.6.1 → 9.7.0

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.
@@ -28,7 +28,7 @@ def safeExtGet(prop, fallback) {
28
28
  // FORGE_CORE_VERSION (see scripts/react-native/hydrate-natives.sh).
29
29
  def forgeCoreVersion = project.hasProperty('NpawForge_forgeCoreVersion')
30
30
  ? project.property('NpawForge_forgeCoreVersion')
31
- : (System.getenv('FORGE_CORE_VERSION') ?: '9.6.1')
31
+ : (System.getenv('FORGE_CORE_VERSION') ?: '9.7.0')
32
32
 
33
33
  android {
34
34
  namespace 'com.npaw.forge.reactnative'
@@ -10,15 +10,18 @@
10
10
  <key>HeadersPath</key>
11
11
  <string>Headers</string>
12
12
  <key>LibraryIdentifier</key>
13
- <string>tvos-arm64</string>
13
+ <string>ios-arm64_x86_64-simulator</string>
14
14
  <key>LibraryPath</key>
15
15
  <string>libForgeKernelFFI.a</string>
16
16
  <key>SupportedArchitectures</key>
17
17
  <array>
18
18
  <string>arm64</string>
19
+ <string>x86_64</string>
19
20
  </array>
20
21
  <key>SupportedPlatform</key>
21
- <string>tvos</string>
22
+ <string>ios</string>
23
+ <key>SupportedPlatformVariant</key>
24
+ <string>simulator</string>
22
25
  </dict>
23
26
  <dict>
24
27
  <key>BinaryPath</key>
@@ -26,7 +29,7 @@
26
29
  <key>HeadersPath</key>
27
30
  <string>Headers</string>
28
31
  <key>LibraryIdentifier</key>
29
- <string>ios-arm64</string>
32
+ <string>tvos-arm64</string>
30
33
  <key>LibraryPath</key>
31
34
  <string>libForgeKernelFFI.a</string>
32
35
  <key>SupportedArchitectures</key>
@@ -34,7 +37,7 @@
34
37
  <string>arm64</string>
35
38
  </array>
36
39
  <key>SupportedPlatform</key>
37
- <string>ios</string>
40
+ <string>tvos</string>
38
41
  </dict>
39
42
  <dict>
40
43
  <key>BinaryPath</key>
@@ -42,16 +45,15 @@
42
45
  <key>HeadersPath</key>
43
46
  <string>Headers</string>
44
47
  <key>LibraryIdentifier</key>
45
- <string>ios-arm64_x86_64-simulator</string>
48
+ <string>tvos-arm64-simulator</string>
46
49
  <key>LibraryPath</key>
47
50
  <string>libForgeKernelFFI.a</string>
48
51
  <key>SupportedArchitectures</key>
49
52
  <array>
50
53
  <string>arm64</string>
51
- <string>x86_64</string>
52
54
  </array>
53
55
  <key>SupportedPlatform</key>
54
- <string>ios</string>
56
+ <string>tvos</string>
55
57
  <key>SupportedPlatformVariant</key>
56
58
  <string>simulator</string>
57
59
  </dict>
@@ -61,7 +63,7 @@
61
63
  <key>HeadersPath</key>
62
64
  <string>Headers</string>
63
65
  <key>LibraryIdentifier</key>
64
- <string>tvos-arm64-simulator</string>
66
+ <string>ios-arm64</string>
65
67
  <key>LibraryPath</key>
66
68
  <string>libForgeKernelFFI.a</string>
67
69
  <key>SupportedArchitectures</key>
@@ -69,9 +71,7 @@
69
71
  <string>arm64</string>
70
72
  </array>
71
73
  <key>SupportedPlatform</key>
72
- <string>tvos</string>
73
- <key>SupportedPlatformVariant</key>
74
- <string>simulator</string>
74
+ <string>ios</string>
75
75
  </dict>
76
76
  </array>
77
77
  <key>CFBundlePackageType</key>
@@ -1891,11 +1891,13 @@ public struct AppleRuntimeConfig: Equatable, Hashable {
1891
1891
  public var analyticsUserAgent: String?
1892
1892
  public var allowLocalHttp: Bool
1893
1893
  public var logLevel: String
1894
+ public var deviceUuid: String?
1895
+ public var deviceInfoJson: String?
1894
1896
  public var preSessionEventPolicy: ApplePreSessionEventPolicy
1895
1897
 
1896
1898
  // Default memberwise initializers are never public by default, so we
1897
1899
  // declare one manually.
1898
- public init(accountCode: String, pluginVersion: String, appName: String, buildVariant: String, appReleaseVersion: String?, lmaEndpoint: String, analyticsUserAgent: String?, allowLocalHttp: Bool, logLevel: String, preSessionEventPolicy: ApplePreSessionEventPolicy) {
1900
+ public init(accountCode: String, pluginVersion: String, appName: String, buildVariant: String, appReleaseVersion: String?, lmaEndpoint: String, analyticsUserAgent: String?, allowLocalHttp: Bool, logLevel: String, deviceUuid: String?, deviceInfoJson: String?, preSessionEventPolicy: ApplePreSessionEventPolicy) {
1899
1901
  self.accountCode = accountCode
1900
1902
  self.pluginVersion = pluginVersion
1901
1903
  self.appName = appName
@@ -1905,6 +1907,8 @@ public struct AppleRuntimeConfig: Equatable, Hashable {
1905
1907
  self.analyticsUserAgent = analyticsUserAgent
1906
1908
  self.allowLocalHttp = allowLocalHttp
1907
1909
  self.logLevel = logLevel
1910
+ self.deviceUuid = deviceUuid
1911
+ self.deviceInfoJson = deviceInfoJson
1908
1912
  self.preSessionEventPolicy = preSessionEventPolicy
1909
1913
  }
1910
1914
 
@@ -1933,6 +1937,8 @@ public struct FfiConverterTypeAppleRuntimeConfig: FfiConverterRustBuffer {
1933
1937
  analyticsUserAgent: FfiConverterOptionString.read(from: &buf),
1934
1938
  allowLocalHttp: FfiConverterBool.read(from: &buf),
1935
1939
  logLevel: FfiConverterString.read(from: &buf),
1940
+ deviceUuid: FfiConverterOptionString.read(from: &buf),
1941
+ deviceInfoJson: FfiConverterOptionString.read(from: &buf),
1936
1942
  preSessionEventPolicy: FfiConverterTypeApplePreSessionEventPolicy.read(from: &buf)
1937
1943
  )
1938
1944
  }
@@ -1947,6 +1953,8 @@ public struct FfiConverterTypeAppleRuntimeConfig: FfiConverterRustBuffer {
1947
1953
  FfiConverterOptionString.write(value.analyticsUserAgent, into: &buf)
1948
1954
  FfiConverterBool.write(value.allowLocalHttp, into: &buf)
1949
1955
  FfiConverterString.write(value.logLevel, into: &buf)
1956
+ FfiConverterOptionString.write(value.deviceUuid, into: &buf)
1957
+ FfiConverterOptionString.write(value.deviceInfoJson, into: &buf)
1950
1958
  FfiConverterTypeApplePreSessionEventPolicy.write(value.preSessionEventPolicy, into: &buf)
1951
1959
  }
1952
1960
  }
@@ -0,0 +1,194 @@
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: Automatic Apple device identity (deviceInfo fields and persistent deviceUUID) for Forge analytics.
13
+ */
14
+ import Foundation
15
+
16
+ #if canImport(UIKit)
17
+ import UIKit
18
+ #endif
19
+
20
+ /// Device identity facts reported through the `deviceInfo` analytics parameter.
21
+ ///
22
+ /// The JSON key names are a wire contract fixed by the v7 plugins, because the
23
+ /// NQS device resolver reads those exact names. Note `deviceType`/`deviceCode`
24
+ /// keep their prefix while `model`/`brand`/`osName`/`osVersion` do not, and
25
+ /// vendor is `brand` — `deviceVendor` is the backend's column name and has
26
+ /// never been a wire key.
27
+ public struct ForgeDeviceInfo: Equatable {
28
+ public var brand: String?
29
+ public var code: String?
30
+ public var model: String?
31
+ public var type: String?
32
+ public var name: String?
33
+ public var osName: String?
34
+ public var osVersion: String?
35
+
36
+ public init(
37
+ brand: String? = nil,
38
+ code: String? = nil,
39
+ model: String? = nil,
40
+ type: String? = nil,
41
+ name: String? = nil,
42
+ osName: String? = nil,
43
+ osVersion: String? = nil
44
+ ) {
45
+ self.brand = brand
46
+ self.code = code
47
+ self.model = model
48
+ self.type = type
49
+ self.name = name
50
+ self.osName = osName
51
+ self.osVersion = osVersion
52
+ }
53
+
54
+ /// Ordered non-blank fields keyed by the `deviceInfo` JSON key names.
55
+ /// Blank values are omitted rather than sent empty, matching v7.
56
+ public func orderedFields() -> [(String, String)] {
57
+ var fields: [(String, String)] = []
58
+ func append(_ key: String, _ value: String?) {
59
+ guard let value, !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
60
+ return
61
+ }
62
+ fields.append((key, value))
63
+ }
64
+ append("model", model)
65
+ append("osVersion", osVersion)
66
+ append("brand", brand)
67
+ append("deviceType", type)
68
+ append("deviceCode", code)
69
+ append("deviceName", name)
70
+ append("osName", osName)
71
+ return fields
72
+ }
73
+
74
+ public var isEmpty: Bool { orderedFields().isEmpty }
75
+
76
+ /// Serializes to the JSON object string the kernel expects. Returns `nil`
77
+ /// when no fact is known, so the param stays off the wire entirely.
78
+ public func toRuntimeJson() -> String? {
79
+ let fields = orderedFields()
80
+ guard !fields.isEmpty else { return nil }
81
+ let body = fields
82
+ .map { "\(Self.quoted($0.0)):\(Self.quoted($0.1))" }
83
+ .joined(separator: ",")
84
+ return "{\(body)}"
85
+ }
86
+
87
+ private static func quoted(_ value: String) -> String {
88
+ var escaped = ""
89
+ for character in value.unicodeScalars {
90
+ switch character {
91
+ case "\"": escaped.append("\\\"")
92
+ case "\\": escaped.append("\\\\")
93
+ case "\n": escaped.append("\\n")
94
+ case "\r": escaped.append("\\r")
95
+ case "\t": escaped.append("\\t")
96
+ default:
97
+ if character.value < 0x20 {
98
+ escaped.append(String(format: "\\u%04x", character.value))
99
+ } else {
100
+ escaped.unicodeScalars.append(character)
101
+ }
102
+ }
103
+ }
104
+ return "\"\(escaped)\""
105
+ }
106
+ }
107
+
108
+ public enum DeviceIdentity {
109
+ public static let deviceUuidStorageKey = "npaw_forge_device_uuid"
110
+
111
+ /// Collects the automatic device facts available on this platform.
112
+ ///
113
+ /// Matches the v7 iOS plugin where it reports at all — `brand` is the
114
+ /// literal `Apple` and `osVersion` comes from the system — and adds the
115
+ /// two facts v7 left to the backend's User-Agent parsing: `osName` and the
116
+ /// hardware `model`. That addition is deliberate. Forge's analytics
117
+ /// User-Agent is `NPAWForgeApple/<version> (<app>; <variant>)`, which
118
+ /// carries no device identity at all, unlike v7's
119
+ /// `<app>/<version>/<UIDevice.model>/<hw.machine>/<systemVersion>`. Without
120
+ /// these two fields NPAW would have no way to resolve an Apple device.
121
+ ///
122
+ /// `deviceType` is deliberately not inferred: v7 iOS emits no type literal
123
+ /// and the backend vocabulary is not ours to invent. Integrators can still
124
+ /// set it explicitly.
125
+ public static func collectDeviceInfo() -> ForgeDeviceInfo {
126
+ ForgeDeviceInfo(
127
+ brand: "Apple",
128
+ model: hardwareModel(),
129
+ osName: systemName(),
130
+ osVersion: systemVersion()
131
+ )
132
+ }
133
+
134
+ /// The hardware identifier, e.g. `iPhone16,1` or `AppleTV11,1`. This is the
135
+ /// same value v7 embedded in its User-Agent via `sysctlbyname("hw.machine")`.
136
+ /// On the simulator it reports the host architecture, which is expected.
137
+ public static func hardwareModel() -> String? {
138
+ var systemInfo = utsname()
139
+ uname(&systemInfo)
140
+ let machine = withUnsafePointer(to: &systemInfo.machine) { pointer in
141
+ pointer.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: pointer.pointee)) {
142
+ String(validatingUTF8: $0)
143
+ }
144
+ }
145
+ guard let machine, !machine.isEmpty else { return nil }
146
+ return machine
147
+ }
148
+
149
+ private static func systemName() -> String? {
150
+ #if os(tvOS)
151
+ return "tvOS"
152
+ #elseif os(visionOS)
153
+ return "visionOS"
154
+ #elseif os(watchOS)
155
+ return "watchOS"
156
+ #elseif os(macOS)
157
+ return "macOS"
158
+ #else
159
+ return "iOS"
160
+ #endif
161
+ }
162
+
163
+ private static func systemVersion() -> String? {
164
+ #if canImport(UIKit)
165
+ let version = UIDevice.current.systemVersion
166
+ return version.isEmpty ? nil : version
167
+ #else
168
+ let version = ProcessInfo.processInfo.operatingSystemVersion
169
+ return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
170
+ #endif
171
+ }
172
+
173
+ /// Returns a stable device identifier.
174
+ ///
175
+ /// Prefers `identifierForVendor`, which is what v7 sends and what NPAW
176
+ /// already associates with Apple installs. When it is unavailable — it can
177
+ /// be `nil` before first unlock, and does not exist off UIKit — falls back
178
+ /// to a random UUID persisted in [storage], so a device is never left
179
+ /// without an identifier.
180
+ public static func persistentDeviceUuid(storage: ForgeStorage) -> String {
181
+ #if canImport(UIKit)
182
+ if let vendorId = UIDevice.current.identifierForVendor?.uuidString, !vendorId.isEmpty {
183
+ return vendorId
184
+ }
185
+ #endif
186
+ if let stored = storage.read(deviceUuidStorageKey),
187
+ !stored.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
188
+ return stored
189
+ }
190
+ let generated = UUID().uuidString
191
+ _ = storage.write(deviceUuidStorageKey, value: generated)
192
+ return generated
193
+ }
194
+ }
@@ -28,6 +28,8 @@ public struct ForgeAppleConfig: Equatable {
28
28
  public var preSessionEventQueueLimitEvents: Int
29
29
  public var preSessionEventQueueMaxAgeMs: Int64
30
30
  public var logLevel: ForgeLogLevel
31
+ public var deviceUuid: String?
32
+ public var deviceInfo: ForgeDeviceInfo?
31
33
 
32
34
  public init(
33
35
  accountCode: String,
@@ -42,7 +44,9 @@ public struct ForgeAppleConfig: Equatable {
42
44
  processEventsBeforeSessionStart: Bool = false,
43
45
  preSessionEventQueueLimitEvents: Int = 100,
44
46
  preSessionEventQueueMaxAgeMs: Int64 = 30_000,
45
- logLevel: ForgeLogLevel = .warning
47
+ logLevel: ForgeLogLevel = .warning,
48
+ deviceUuid: String? = nil,
49
+ deviceInfo: ForgeDeviceInfo? = nil
46
50
  ) {
47
51
  self.accountCode = accountCode
48
52
  self.pluginVersion = pluginVersion
@@ -57,6 +61,8 @@ public struct ForgeAppleConfig: Equatable {
57
61
  self.preSessionEventQueueLimitEvents = preSessionEventQueueLimitEvents
58
62
  self.preSessionEventQueueMaxAgeMs = preSessionEventQueueMaxAgeMs
59
63
  self.logLevel = logLevel
64
+ self.deviceUuid = deviceUuid
65
+ self.deviceInfo = deviceInfo
60
66
  }
61
67
  }
62
68
 
@@ -40,7 +40,7 @@ public final class NPAWForge: NSObject {
40
40
  /// `scripts/apple/publish-local-shipped.sh` rewrites this literal to the
41
41
  /// release version when staging the shipped package, and fails loudly if
42
42
  /// it cannot find it. Keep it a plain stored literal for that reason.
43
- public static let defaultPluginVersion = "9.6.1"
43
+ public static let defaultPluginVersion = "9.7.0"
44
44
  public static let defaultAppName = "apple"
45
45
  public static let defaultBuildVariant = "release"
46
46
  public static let defaultLmaEndpoint = "https://lma.npaw.com"
@@ -298,6 +298,9 @@ public final class NPAWForge: NSObject {
298
298
  private var preSessionEventQueueLimitEvents = 100
299
299
  private var preSessionEventQueueMaxAgeMs: Int64 = 30_000
300
300
  private var logLevel: ForgeLogLevel = .warning
301
+ private var autoDeviceIdentityEnabled = true
302
+ private var deviceUuid: String?
303
+ private var deviceInfo: ForgeDeviceInfo?
301
304
  private var services: ForgeAppleServices?
302
305
 
303
306
  public init(accountCode: String) {
@@ -378,7 +381,40 @@ public final class NPAWForge: NSObject {
378
381
  return self
379
382
  }
380
383
 
384
+ /// Disables the automatic device identity collection (`deviceInfo` from
385
+ /// the system plus the `deviceUUID`). `deviceUUID` is a persistent
386
+ /// identifier, so integrators can opt out; an explicit
387
+ /// ``setDeviceUuid(_:)`` value is still honored when disabled.
388
+ @discardableResult
389
+ public func setAutoDeviceIdentityEnabled(_ enabled: Bool) -> Builder {
390
+ autoDeviceIdentityEnabled = enabled
391
+ return self
392
+ }
393
+
394
+ /// Overrides the automatically collected device identifier.
395
+ @discardableResult
396
+ public func setDeviceUuid(_ value: String?) -> Builder {
397
+ deviceUuid = value
398
+ return self
399
+ }
400
+
401
+ /// Overrides the automatically collected `deviceInfo` facts wholesale.
402
+ @discardableResult
403
+ public func setDeviceInfo(_ value: ForgeDeviceInfo?) -> Builder {
404
+ deviceInfo = value
405
+ return self
406
+ }
407
+
381
408
  public func build() -> NPAWForge {
409
+ // Services first: resolving the device UUID needs the storage the
410
+ // integrator supplied, so it cannot wait until after the config.
411
+ let resolvedServices = services ?? ForgeAppleServices(storage: UserDefaultsStorage())
412
+ let resolvedDeviceUuid = deviceUuid ?? (autoDeviceIdentityEnabled
413
+ ? DeviceIdentity.persistentDeviceUuid(storage: resolvedServices.storage)
414
+ : nil)
415
+ let resolvedDeviceInfo = deviceInfo ?? (autoDeviceIdentityEnabled
416
+ ? DeviceIdentity.collectDeviceInfo()
417
+ : nil)
382
418
  let config = ForgeAppleConfig(
383
419
  accountCode: accountCode,
384
420
  pluginVersion: NPAWForge.defaultPluginVersion,
@@ -392,9 +428,10 @@ public final class NPAWForge: NSObject {
392
428
  processEventsBeforeSessionStart: processEventsBeforeSessionStart,
393
429
  preSessionEventQueueLimitEvents: preSessionEventQueueLimitEvents,
394
430
  preSessionEventQueueMaxAgeMs: preSessionEventQueueMaxAgeMs,
395
- logLevel: logLevel
431
+ logLevel: logLevel,
432
+ deviceUuid: resolvedDeviceUuid,
433
+ deviceInfo: resolvedDeviceInfo?.isEmpty == true ? nil : resolvedDeviceInfo
396
434
  )
397
- let resolvedServices = services ?? ForgeAppleServices(storage: UserDefaultsStorage())
398
435
  let forge = NPAWForge(kernel: ForgeApple(config: config, services: resolvedServices))
399
436
  ForgeAutomaticCaptureRegistry.installIfAvailable(forge)
400
437
  return forge
@@ -38,6 +38,8 @@ final class UniFFIForgeRuntime: ForgeRuntime {
38
38
  analyticsUserAgent: config.analyticsUserAgent,
39
39
  allowLocalHttp: config.allowLocalHttp,
40
40
  logLevel: config.logLevel.wireName,
41
+ deviceUuid: config.deviceUuid,
42
+ deviceInfoJson: config.deviceInfo?.toRuntimeJson(),
41
43
  preSessionEventPolicy: ApplePreSessionEventPolicy(
42
44
  processEventsBeforeSessionStart: config.processEventsBeforeSessionStart,
43
45
  queueLimitEvents: UInt64(max(0, config.preSessionEventQueueLimitEvents)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npaw/forge-react-native",
3
- "version": "9.6.1",
3
+ "version": "9.7.0",
4
4
  "description": "NPAW Forge analytics for React Native: New Architecture TurboModule facade over the native Forge Android/Apple SDKs.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Victor Soto <victor.soto@nicepeopleatwork.com>",