@apex-inc/capacitor-plugin 0.1.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.
Files changed (83) hide show
  1. package/ApexCapacitorPlugin.podspec +17 -0
  2. package/LICENSE +17 -0
  3. package/README.md +136 -0
  4. package/android/build.gradle +68 -0
  5. package/android/src/main/AndroidManifest.xml +8 -0
  6. package/android/src/main/java/inc/apex/capacitor/ApexCapacitorPlugin.kt +325 -0
  7. package/android/src/main/java/inc/apex/capacitor/DeepLinkManager.kt +47 -0
  8. package/android/src/main/java/inc/apex/capacitor/InstallReferrerParser.kt +123 -0
  9. package/android/src/main/java/inc/apex/capacitor/OfflineQueue.kt +150 -0
  10. package/android/src/main/java/inc/apex/capacitor/SessionManager.kt +108 -0
  11. package/dist/batch-sender.d.ts +60 -0
  12. package/dist/batch-sender.d.ts.map +1 -0
  13. package/dist/batch-sender.js +115 -0
  14. package/dist/batch-sender.js.map +1 -0
  15. package/dist/definitions.d.ts +224 -0
  16. package/dist/definitions.d.ts.map +1 -0
  17. package/dist/definitions.js +14 -0
  18. package/dist/definitions.js.map +1 -0
  19. package/dist/esm/batch-sender.d.ts +60 -0
  20. package/dist/esm/batch-sender.d.ts.map +1 -0
  21. package/dist/esm/batch-sender.js +111 -0
  22. package/dist/esm/batch-sender.js.map +1 -0
  23. package/dist/esm/definitions.d.ts +224 -0
  24. package/dist/esm/definitions.d.ts.map +1 -0
  25. package/dist/esm/definitions.js +13 -0
  26. package/dist/esm/definitions.js.map +1 -0
  27. package/dist/esm/event-id.d.ts +17 -0
  28. package/dist/esm/event-id.d.ts.map +1 -0
  29. package/dist/esm/event-id.js +57 -0
  30. package/dist/esm/event-id.js.map +1 -0
  31. package/dist/esm/index.d.ts +29 -0
  32. package/dist/esm/index.d.ts.map +1 -0
  33. package/dist/esm/index.js +30 -0
  34. package/dist/esm/index.js.map +1 -0
  35. package/dist/esm/offline-queue.d.ts +111 -0
  36. package/dist/esm/offline-queue.d.ts.map +1 -0
  37. package/dist/esm/offline-queue.js +240 -0
  38. package/dist/esm/offline-queue.js.map +1 -0
  39. package/dist/esm/session-manager.d.ts +63 -0
  40. package/dist/esm/session-manager.d.ts.map +1 -0
  41. package/dist/esm/session-manager.js +100 -0
  42. package/dist/esm/session-manager.js.map +1 -0
  43. package/dist/esm/web.d.ts +65 -0
  44. package/dist/esm/web.d.ts.map +1 -0
  45. package/dist/esm/web.js +203 -0
  46. package/dist/esm/web.js.map +1 -0
  47. package/dist/event-id.d.ts +17 -0
  48. package/dist/event-id.d.ts.map +1 -0
  49. package/dist/event-id.js +61 -0
  50. package/dist/event-id.js.map +1 -0
  51. package/dist/index.d.ts +29 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +76 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/offline-queue.d.ts +111 -0
  56. package/dist/offline-queue.d.ts.map +1 -0
  57. package/dist/offline-queue.js +246 -0
  58. package/dist/offline-queue.js.map +1 -0
  59. package/dist/session-manager.d.ts +63 -0
  60. package/dist/session-manager.d.ts.map +1 -0
  61. package/dist/session-manager.js +104 -0
  62. package/dist/session-manager.js.map +1 -0
  63. package/dist/web.d.ts +65 -0
  64. package/dist/web.d.ts.map +1 -0
  65. package/dist/web.js +207 -0
  66. package/dist/web.js.map +1 -0
  67. package/ios/Package.swift +34 -0
  68. package/ios/Sources/ApexCapacitorPlugin/AdvertisingIdProvider.swift +66 -0
  69. package/ios/Sources/ApexCapacitorPlugin/AttManager.swift +82 -0
  70. package/ios/Sources/ApexCapacitorPlugin/DeepLinkManager.swift +64 -0
  71. package/ios/Sources/ApexCapacitorPlugin/DeviceInfo.swift +107 -0
  72. package/ios/Sources/ApexCapacitorPlugin/OfflineQueue.swift +191 -0
  73. package/ios/Sources/ApexCapacitorPlugin/SessionManager.swift +113 -0
  74. package/ios/Sources/ApexCapacitorPlugin/SkanManager.swift +95 -0
  75. package/ios/Sources/ApexCapacitorPluginBridge/ApexCapacitorPlugin.swift +269 -0
  76. package/ios/Tests/ApexCapacitorPluginTests/AdvertisingIdProviderTests.swift +74 -0
  77. package/ios/Tests/ApexCapacitorPluginTests/AttManagerTests.swift +82 -0
  78. package/ios/Tests/ApexCapacitorPluginTests/DeepLinkManagerTests.swift +69 -0
  79. package/ios/Tests/ApexCapacitorPluginTests/DeviceInfoTests.swift +52 -0
  80. package/ios/Tests/ApexCapacitorPluginTests/OfflineQueueTests.swift +134 -0
  81. package/ios/Tests/ApexCapacitorPluginTests/SessionManagerTests.swift +98 -0
  82. package/ios/Tests/ApexCapacitorPluginTests/SkanManagerTests.swift +91 -0
  83. package/package.json +82 -0
package/dist/web.js ADDED
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ /**
3
+ * Web fallback implementation of `ApexCapacitorPlugin`.
4
+ *
5
+ * Runs when the app is in a browser or Capacitor PWA context where no native
6
+ * iOS/Android bridge is present. Provides best-effort behavior for every
7
+ * method — identifiers return null, ATT is a no-op (returns `authorized`),
8
+ * SKAN is a no-op, deep links come from `window.location`, and events are
9
+ * batched + sent over fetch via the normal offline queue.
10
+ *
11
+ * This module deliberately keeps the heavy lifting (queue, session, batch)
12
+ * in separate files so unit tests can cover them without DOM mocking.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ApexCapacitorWeb = void 0;
16
+ const core_1 = require("@capacitor/core");
17
+ const batch_sender_1 = require("./batch-sender");
18
+ const event_id_1 = require("./event-id");
19
+ const offline_queue_1 = require("./offline-queue");
20
+ const session_manager_1 = require("./session-manager");
21
+ const VISITOR_ID_KEY = "apex.visitorId";
22
+ class ApexCapacitorWeb extends core_1.WebPlugin {
23
+ constructor() {
24
+ super(...arguments);
25
+ this.testMode = false;
26
+ this.debug = false;
27
+ this.initialized = false;
28
+ }
29
+ async initialize(options) {
30
+ if (this.initialized)
31
+ return;
32
+ this.testMode = options.testMode ?? false;
33
+ this.debug = options.debug ?? false;
34
+ const storage = typeof indexedDB !== "undefined" ? new offline_queue_1.IndexedDBStorage() : new offline_queue_1.InMemoryStorage();
35
+ this.queue = new offline_queue_1.OfflineQueue({
36
+ storage,
37
+ maxSize: options.offlineQueueMaxSize ?? 1000,
38
+ onEvicted: (ev) => {
39
+ if (this.debug)
40
+ console.warn("[apex-capacitor] queue eviction", ev.event.id);
41
+ },
42
+ });
43
+ this.session = new session_manager_1.SessionManager({
44
+ timeoutMinutes: options.sessionTimeoutMinutes ?? 30,
45
+ onSessionStart: (s) => this.emitSession("sessionStart", s),
46
+ onSessionEnd: (s) => this.emitSession("sessionEnd", s),
47
+ });
48
+ this.sender = new batch_sender_1.BatchSender({
49
+ apiUrl: options.apiUrl,
50
+ projectKey: options.projectKey,
51
+ platformHeader: "web",
52
+ debug: this.debug,
53
+ });
54
+ this.visitorId = this.loadOrCreateVisitorId();
55
+ this.initialized = true;
56
+ }
57
+ async requestTrackingAuthorization() {
58
+ return { status: "authorized" };
59
+ }
60
+ async getTrackingStatus() {
61
+ return { status: "authorized" };
62
+ }
63
+ async getAdvertisingId() {
64
+ return { id: null, fallback: null };
65
+ }
66
+ async getInstallReferrer() {
67
+ return { referrer: null };
68
+ }
69
+ async getVisitorId() {
70
+ if (!this.visitorId) {
71
+ this.visitorId = this.loadOrCreateVisitorId();
72
+ }
73
+ return { visitorId: this.visitorId };
74
+ }
75
+ async setVisitorId(options) {
76
+ this.visitorId = options.visitorId;
77
+ this.saveVisitorId(options.visitorId);
78
+ }
79
+ async updateConversionValue(_options) {
80
+ // No-op on web / non-iOS.
81
+ }
82
+ async getInitialDeepLink() {
83
+ if (typeof window !== "undefined" && window.location) {
84
+ return { url: window.location.href };
85
+ }
86
+ return { url: null };
87
+ }
88
+ async getDeviceInfo() {
89
+ // Best-effort on web — mostly placeholders so callers get consistent shape.
90
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
91
+ const locale = typeof navigator !== "undefined" ? navigator.language : "en-US";
92
+ const tz = typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : "UTC";
93
+ return {
94
+ platform: "android", // Web falls back to android shape; native overrides when present.
95
+ osVersion: "unknown",
96
+ model: ua.substring(0, 64),
97
+ appVersion: "0.0.0",
98
+ bundleId: typeof location !== "undefined" ? location.host : "web",
99
+ timezone: tz,
100
+ locale,
101
+ };
102
+ }
103
+ async startSession() {
104
+ this.ensureInitialized();
105
+ const snapshot = this.session.forceStart();
106
+ return { sessionId: snapshot.sessionId };
107
+ }
108
+ async endSession() {
109
+ if (!this.session)
110
+ return;
111
+ this.session.endSession();
112
+ }
113
+ async getCurrentSession() {
114
+ const current = this.session?.getCurrent() ?? null;
115
+ return {
116
+ sessionId: current?.sessionId ?? null,
117
+ startedAt: current?.startedAt ?? null,
118
+ };
119
+ }
120
+ async track(event) {
121
+ this.ensureInitialized();
122
+ const sessionSnapshot = this.session.recordActivity();
123
+ const stamped = {
124
+ ...event,
125
+ id: event.id ?? (0, event_id_1.generateEventId)(),
126
+ timestamp: event.timestamp ?? new Date().toISOString(),
127
+ sessionId: event.sessionId ?? sessionSnapshot.sessionId,
128
+ };
129
+ if (this.testMode) {
130
+ stamped.data = { ...(stamped.data ?? {}), __testMode: true };
131
+ }
132
+ if (stamped.id && !(0, event_id_1.isValidEventId)(stamped.id)) {
133
+ // Developer mis-set id — overwrite rather than fail the track() call.
134
+ if (this.debug)
135
+ console.warn("[apex-capacitor] invalid event.id, regenerating");
136
+ stamped.id = (0, event_id_1.generateEventId)();
137
+ }
138
+ await this.queue.enqueue(stamped);
139
+ // Attempt delivery; errors are swallowed — events stay queued for next flush.
140
+ void this.sender.flush(this.queue).catch((err) => {
141
+ if (this.debug)
142
+ console.warn("[apex-capacitor] flush error", err);
143
+ });
144
+ }
145
+ async getQueueSize() {
146
+ if (!this.queue)
147
+ return { count: 0, oldestEventAt: null };
148
+ const [count, oldestEventAt] = await Promise.all([
149
+ this.queue.size(),
150
+ this.queue.oldestEventAt(),
151
+ ]);
152
+ return { count, oldestEventAt };
153
+ }
154
+ async flushQueue() {
155
+ this.ensureInitialized();
156
+ const result = await this.sender.flush(this.queue);
157
+ return { flushed: result.flushed, remaining: result.remaining };
158
+ }
159
+ async setTestMode(options) {
160
+ this.testMode = options.enabled;
161
+ }
162
+ async removeAllListeners() {
163
+ await super.removeAllListeners();
164
+ }
165
+ emitSession(eventName, session) {
166
+ const payload = eventName === "sessionStart"
167
+ ? { sessionId: session.sessionId }
168
+ : {
169
+ sessionId: session.sessionId,
170
+ durationSeconds: session.durationSeconds ?? 0,
171
+ };
172
+ this.notifyListeners(eventName, payload);
173
+ }
174
+ loadOrCreateVisitorId() {
175
+ if (typeof localStorage !== "undefined") {
176
+ const existing = localStorage.getItem(VISITOR_ID_KEY);
177
+ if (existing)
178
+ return existing;
179
+ const fresh = (0, event_id_1.generateEventId)();
180
+ try {
181
+ localStorage.setItem(VISITOR_ID_KEY, fresh);
182
+ }
183
+ catch {
184
+ // Storage quota / private mode — fall through to in-memory.
185
+ }
186
+ return fresh;
187
+ }
188
+ return (0, event_id_1.generateEventId)();
189
+ }
190
+ saveVisitorId(id) {
191
+ if (typeof localStorage !== "undefined") {
192
+ try {
193
+ localStorage.setItem(VISITOR_ID_KEY, id);
194
+ }
195
+ catch {
196
+ // ignore
197
+ }
198
+ }
199
+ }
200
+ ensureInitialized() {
201
+ if (!this.initialized || !this.queue || !this.session || !this.sender) {
202
+ throw new Error("Apex plugin not initialized. Call initialize() before using other methods.");
203
+ }
204
+ }
205
+ }
206
+ exports.ApexCapacitorWeb = ApexCapacitorWeb;
207
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ // swift-tools-version: 5.9
2
+ //
3
+ // This manifest builds the pure-logic portion of @apex-inc/capacitor-plugin
4
+ // for standalone test runs via `swift test`. The Capacitor-dependent plugin
5
+ // entry point (ApexCapacitorPlugin.swift) lives in a sibling folder and is
6
+ // compiled only through the CocoaPods build path (see .podspec) because it
7
+ // imports the Capacitor framework.
8
+ //
9
+ // Consumers installing the plugin in a Capacitor app get BOTH sources via
10
+ // `npx cap sync` + CocoaPods — this split is purely a testability concern.
11
+ //
12
+ import PackageDescription
13
+
14
+ let package = Package(
15
+ name: "ApexCapacitorPlugin",
16
+ platforms: [.iOS(.v14)],
17
+ products: [
18
+ .library(
19
+ name: "ApexCapacitorPlugin",
20
+ targets: ["ApexCapacitorPlugin"]
21
+ ),
22
+ ],
23
+ targets: [
24
+ .target(
25
+ name: "ApexCapacitorPlugin",
26
+ path: "Sources/ApexCapacitorPlugin"
27
+ ),
28
+ .testTarget(
29
+ name: "ApexCapacitorPluginTests",
30
+ dependencies: ["ApexCapacitorPlugin"],
31
+ path: "Tests/ApexCapacitorPluginTests"
32
+ ),
33
+ ]
34
+ )
@@ -0,0 +1,66 @@
1
+ //
2
+ // AdvertisingIdProvider.swift
3
+ // Apex Capacitor Plugin
4
+ //
5
+ // Provides IDFA when ATT is authorized, falls back to IDFV (identifier for
6
+ // vendor) when denied or not yet prompted. IDFA may return an all-zero UUID
7
+ // ("00000000-0000-0000-0000-000000000000") when Limit Ad Tracking is on —
8
+ // we detect that and substitute IDFV with an explicit fallback flag.
9
+ //
10
+
11
+ import Foundation
12
+ import AdSupport
13
+ #if canImport(UIKit)
14
+ import UIKit
15
+ #endif
16
+
17
+ public struct AdvertisingIdResult: Equatable {
18
+ public let id: String?
19
+ public let fallback: String? // "idfv" when IDFA unavailable, nil when IDFA is fresh
20
+ }
21
+
22
+ private let zeroIdfa = "00000000-0000-0000-0000-000000000000"
23
+
24
+ public final class AdvertisingIdProvider {
25
+ private let attManager: AttManager
26
+ private let idfaProvider: () -> String?
27
+ private let idfvProvider: () -> String?
28
+
29
+ public init(
30
+ attManager: AttManager = AttManager(),
31
+ idfaProvider: @escaping () -> String? = AdvertisingIdProvider.defaultIdfaProvider,
32
+ idfvProvider: @escaping () -> String? = AdvertisingIdProvider.defaultIdfvProvider
33
+ ) {
34
+ self.attManager = attManager
35
+ self.idfaProvider = idfaProvider
36
+ self.idfvProvider = idfvProvider
37
+ }
38
+
39
+ public func current() -> AdvertisingIdResult {
40
+ let status = attManager.currentStatus()
41
+ if status == .authorized {
42
+ if let idfa = idfaProvider(), idfa != zeroIdfa {
43
+ return AdvertisingIdResult(id: idfa, fallback: nil)
44
+ }
45
+ }
46
+ // ATT denied / restricted / not-determined, OR IDFA returned zero-UUID.
47
+ if let idfv = idfvProvider() {
48
+ return AdvertisingIdResult(id: idfv, fallback: "idfv")
49
+ }
50
+ return AdvertisingIdResult(id: nil, fallback: nil)
51
+ }
52
+
53
+ // MARK: - Defaults
54
+
55
+ public static var defaultIdfaProvider: () -> String? = {
56
+ return ASIdentifierManager.shared().advertisingIdentifier.uuidString.lowercased()
57
+ }
58
+
59
+ public static var defaultIdfvProvider: () -> String? = {
60
+ #if canImport(UIKit)
61
+ return UIDevice.current.identifierForVendor?.uuidString.lowercased()
62
+ #else
63
+ return nil
64
+ #endif
65
+ }
66
+ }
@@ -0,0 +1,82 @@
1
+ //
2
+ // AttManager.swift
3
+ // Apex Capacitor Plugin
4
+ //
5
+ // Wraps iOS App Tracking Transparency (ATT). Reports status and requests
6
+ // authorization. Maps Apple's ATTrackingManager.AuthorizationStatus enum
7
+ // to the stable string contract used by the TS interface.
8
+ //
9
+
10
+ import Foundation
11
+ import AppTrackingTransparency
12
+
13
+ public enum AttStatus: String {
14
+ case authorized = "authorized"
15
+ case denied = "denied"
16
+ case restricted = "restricted"
17
+ case notDetermined = "not-determined"
18
+
19
+ /// Stable mapping from Apple's rawValue-less enum to our string contract.
20
+ /// Apple's constants: notDetermined=0, restricted=1, denied=2, authorized=3.
21
+ public static func from(authorizationRawValue: UInt) -> AttStatus {
22
+ switch authorizationRawValue {
23
+ case 0: return .notDetermined
24
+ case 1: return .restricted
25
+ case 2: return .denied
26
+ case 3: return .authorized
27
+ default: return .notDetermined
28
+ }
29
+ }
30
+ }
31
+
32
+ /// Thin wrapper around ATTrackingManager. The actual UI prompt can only be
33
+ /// driven from a real iOS runtime, so tests cover the enum mapping; the
34
+ /// live prompt is verified on-device.
35
+ public final class AttManager {
36
+ private let tracker: AttTrackerProtocol
37
+
38
+ public init(tracker: AttTrackerProtocol = DefaultAttTracker()) {
39
+ self.tracker = tracker
40
+ }
41
+
42
+ /// Returns the current ATT status without presenting the prompt.
43
+ public func currentStatus() -> AttStatus {
44
+ return tracker.currentStatus()
45
+ }
46
+
47
+ /// Presents the ATT prompt (once per install) and completes with the
48
+ /// resulting status.
49
+ public func request(completion: @escaping (AttStatus) -> Void) {
50
+ tracker.requestAuthorization { rawValue in
51
+ completion(AttStatus.from(authorizationRawValue: rawValue))
52
+ }
53
+ }
54
+ }
55
+
56
+ /// Abstraction around ATTrackingManager so tests can inject fakes.
57
+ public protocol AttTrackerProtocol {
58
+ func currentStatus() -> AttStatus
59
+ func requestAuthorization(completion: @escaping (UInt) -> Void)
60
+ }
61
+
62
+ public final class DefaultAttTracker: AttTrackerProtocol {
63
+ public init() {}
64
+
65
+ public func currentStatus() -> AttStatus {
66
+ if #available(iOS 14, *) {
67
+ let raw = UInt(ATTrackingManager.trackingAuthorizationStatus.rawValue)
68
+ return AttStatus.from(authorizationRawValue: raw)
69
+ }
70
+ return .authorized // Pre-iOS 14 has no ATT, tracking implicitly allowed.
71
+ }
72
+
73
+ public func requestAuthorization(completion: @escaping (UInt) -> Void) {
74
+ if #available(iOS 14, *) {
75
+ ATTrackingManager.requestTrackingAuthorization { status in
76
+ completion(UInt(status.rawValue))
77
+ }
78
+ } else {
79
+ completion(3) // authorized
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,64 @@
1
+ //
2
+ // DeepLinkManager.swift
3
+ // Apex Capacitor Plugin
4
+ //
5
+ // Captures the URL that opens the app (Universal Link or custom scheme).
6
+ // Retains the cold-start URL so `getInitialDeepLink()` can return it after
7
+ // JS is ready. Warm-start URLs are delivered via Capacitor's `deepLink`
8
+ // notification bus.
9
+ //
10
+ // Intentionally does no URL parsing — that's the server's job. This class
11
+ // only cares about preserving + forwarding the URL string.
12
+ //
13
+
14
+ import Foundation
15
+
16
+ public final class DeepLinkManager {
17
+ private var initialUrl: URL?
18
+ private var warmHandler: ((URL) -> Void)?
19
+ private let queue = DispatchQueue(label: "inc.apex.deep-link")
20
+
21
+ public init() {}
22
+
23
+ /// Called from the AppDelegate when the app launches from a Universal Link
24
+ /// or custom URL scheme. Stored for retrieval via `consumeInitialUrl()`.
25
+ public func setInitialUrl(_ url: URL) {
26
+ queue.sync {
27
+ if initialUrl == nil {
28
+ initialUrl = url
29
+ }
30
+ }
31
+ }
32
+
33
+ /// Fetches and clears the cold-start URL. Returns nil if already consumed
34
+ /// or if the app was launched without a link.
35
+ public func consumeInitialUrl() -> URL? {
36
+ return queue.sync { () -> URL? in
37
+ let url = initialUrl
38
+ initialUrl = nil
39
+ return url
40
+ }
41
+ }
42
+
43
+ /// Called from continue(userActivity:) or open(url:) when the app is
44
+ /// already running. Dispatches to registered handler (typically the
45
+ /// Capacitor plugin which forwards to JS listeners).
46
+ public func deliverWarmLink(_ url: URL) {
47
+ queue.sync { warmHandler?(url) }
48
+ }
49
+
50
+ /// Register the warm-link handler. Only one handler at a time — replacing
51
+ /// overwrites the previous.
52
+ public func setWarmLinkHandler(_ handler: @escaping (URL) -> Void) {
53
+ queue.sync { warmHandler = handler }
54
+ }
55
+
56
+ /// Validates that a URL looks like a valid app deep link. Used to filter
57
+ /// spurious launches (e.g. when the app is opened from Spotlight with no
58
+ /// URL context).
59
+ public static func isValidDeepLink(_ url: URL) -> Bool {
60
+ guard let scheme = url.scheme, !scheme.isEmpty else { return false }
61
+ // https and custom schemes are both valid. file:// is not a deep link.
62
+ return scheme != "file"
63
+ }
64
+ }
@@ -0,0 +1,107 @@
1
+ //
2
+ // DeviceInfo.swift
3
+ // Apex Capacitor Plugin
4
+ //
5
+ // Collects device metadata reported on every event and to `getDeviceInfo()`.
6
+ // All values are pulled from UIDevice, Bundle, and Foundation — no PII,
7
+ // no identifiers, just platform/os/model/app/timezone/locale.
8
+ //
9
+
10
+ import Foundation
11
+ #if canImport(UIKit)
12
+ import UIKit
13
+ #endif
14
+
15
+ public struct DeviceMetadata: Equatable {
16
+ public let platform: String // "ios"
17
+ public let osVersion: String // e.g. "17.4"
18
+ public let model: String // e.g. "iPhone15,2" (machine identifier)
19
+ public let appVersion: String // CFBundleShortVersionString
20
+ public let bundleId: String // CFBundleIdentifier
21
+ public let timezone: String // e.g. "America/New_York"
22
+ public let locale: String // e.g. "en-US"
23
+
24
+ public init(
25
+ platform: String,
26
+ osVersion: String,
27
+ model: String,
28
+ appVersion: String,
29
+ bundleId: String,
30
+ timezone: String,
31
+ locale: String
32
+ ) {
33
+ self.platform = platform
34
+ self.osVersion = osVersion
35
+ self.model = model
36
+ self.appVersion = appVersion
37
+ self.bundleId = bundleId
38
+ self.timezone = timezone
39
+ self.locale = locale
40
+ }
41
+ }
42
+
43
+ public final class DeviceInfo {
44
+ private let bundle: Bundle
45
+ private let tzProvider: () -> String
46
+ private let localeProvider: () -> String
47
+ private let modelProvider: () -> String
48
+ private let osVersionProvider: () -> String
49
+
50
+ public init(
51
+ bundle: Bundle = .main,
52
+ tzProvider: @escaping () -> String = { TimeZone.current.identifier },
53
+ localeProvider: @escaping () -> String = { Locale.current.identifier.replacingOccurrences(of: "_", with: "-") },
54
+ modelProvider: @escaping () -> String = DeviceInfo.defaultModelProvider,
55
+ osVersionProvider: @escaping () -> String = DeviceInfo.defaultOsVersionProvider
56
+ ) {
57
+ self.bundle = bundle
58
+ self.tzProvider = tzProvider
59
+ self.localeProvider = localeProvider
60
+ self.modelProvider = modelProvider
61
+ self.osVersionProvider = osVersionProvider
62
+ }
63
+
64
+ public func collect() -> DeviceMetadata {
65
+ return DeviceMetadata(
66
+ platform: "ios",
67
+ osVersion: osVersionProvider(),
68
+ model: modelProvider(),
69
+ appVersion: appVersionFromBundle(),
70
+ bundleId: bundle.bundleIdentifier ?? "",
71
+ timezone: tzProvider(),
72
+ locale: localeProvider()
73
+ )
74
+ }
75
+
76
+ private func appVersionFromBundle() -> String {
77
+ let short = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
78
+ return short
79
+ }
80
+
81
+ // MARK: - Default providers
82
+
83
+ public static var defaultModelProvider: () -> String = {
84
+ #if targetEnvironment(simulator)
85
+ return "iOSSimulator"
86
+ #else
87
+ var sysinfo = utsname()
88
+ uname(&sysinfo)
89
+ let mirror = Mirror(reflecting: sysinfo.machine)
90
+ var identifier = ""
91
+ for child in mirror.children {
92
+ if let value = child.value as? Int8, value != 0 {
93
+ identifier.append(Character(UnicodeScalar(UInt8(value))))
94
+ }
95
+ }
96
+ return identifier.isEmpty ? "iPhone" : identifier
97
+ #endif
98
+ }
99
+
100
+ public static var defaultOsVersionProvider: () -> String = {
101
+ #if canImport(UIKit)
102
+ return UIDevice.current.systemVersion
103
+ #else
104
+ return ProcessInfo.processInfo.operatingSystemVersionString
105
+ #endif
106
+ }
107
+ }