@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.
- package/ApexCapacitorPlugin.podspec +17 -0
- package/LICENSE +17 -0
- package/README.md +136 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/inc/apex/capacitor/ApexCapacitorPlugin.kt +325 -0
- package/android/src/main/java/inc/apex/capacitor/DeepLinkManager.kt +47 -0
- package/android/src/main/java/inc/apex/capacitor/InstallReferrerParser.kt +123 -0
- package/android/src/main/java/inc/apex/capacitor/OfflineQueue.kt +150 -0
- package/android/src/main/java/inc/apex/capacitor/SessionManager.kt +108 -0
- package/dist/batch-sender.d.ts +60 -0
- package/dist/batch-sender.d.ts.map +1 -0
- package/dist/batch-sender.js +115 -0
- package/dist/batch-sender.js.map +1 -0
- package/dist/definitions.d.ts +224 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/definitions.js +14 -0
- package/dist/definitions.js.map +1 -0
- package/dist/esm/batch-sender.d.ts +60 -0
- package/dist/esm/batch-sender.d.ts.map +1 -0
- package/dist/esm/batch-sender.js +111 -0
- package/dist/esm/batch-sender.js.map +1 -0
- package/dist/esm/definitions.d.ts +224 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +13 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/event-id.d.ts +17 -0
- package/dist/esm/event-id.d.ts.map +1 -0
- package/dist/esm/event-id.js +57 -0
- package/dist/esm/event-id.js.map +1 -0
- package/dist/esm/index.d.ts +29 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +30 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/offline-queue.d.ts +111 -0
- package/dist/esm/offline-queue.d.ts.map +1 -0
- package/dist/esm/offline-queue.js +240 -0
- package/dist/esm/offline-queue.js.map +1 -0
- package/dist/esm/session-manager.d.ts +63 -0
- package/dist/esm/session-manager.d.ts.map +1 -0
- package/dist/esm/session-manager.js +100 -0
- package/dist/esm/session-manager.js.map +1 -0
- package/dist/esm/web.d.ts +65 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +203 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/event-id.d.ts +17 -0
- package/dist/event-id.d.ts.map +1 -0
- package/dist/event-id.js +61 -0
- package/dist/event-id.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/offline-queue.d.ts +111 -0
- package/dist/offline-queue.d.ts.map +1 -0
- package/dist/offline-queue.js +246 -0
- package/dist/offline-queue.js.map +1 -0
- package/dist/session-manager.d.ts +63 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +104 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/web.d.ts +65 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +207 -0
- package/dist/web.js.map +1 -0
- package/ios/Package.swift +34 -0
- package/ios/Sources/ApexCapacitorPlugin/AdvertisingIdProvider.swift +66 -0
- package/ios/Sources/ApexCapacitorPlugin/AttManager.swift +82 -0
- package/ios/Sources/ApexCapacitorPlugin/DeepLinkManager.swift +64 -0
- package/ios/Sources/ApexCapacitorPlugin/DeviceInfo.swift +107 -0
- package/ios/Sources/ApexCapacitorPlugin/OfflineQueue.swift +191 -0
- package/ios/Sources/ApexCapacitorPlugin/SessionManager.swift +113 -0
- package/ios/Sources/ApexCapacitorPlugin/SkanManager.swift +95 -0
- package/ios/Sources/ApexCapacitorPluginBridge/ApexCapacitorPlugin.swift +269 -0
- package/ios/Tests/ApexCapacitorPluginTests/AdvertisingIdProviderTests.swift +74 -0
- package/ios/Tests/ApexCapacitorPluginTests/AttManagerTests.swift +82 -0
- package/ios/Tests/ApexCapacitorPluginTests/DeepLinkManagerTests.swift +69 -0
- package/ios/Tests/ApexCapacitorPluginTests/DeviceInfoTests.swift +52 -0
- package/ios/Tests/ApexCapacitorPluginTests/OfflineQueueTests.swift +134 -0
- package/ios/Tests/ApexCapacitorPluginTests/SessionManagerTests.swift +98 -0
- package/ios/Tests/ApexCapacitorPluginTests/SkanManagerTests.swift +91 -0
- 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
|
package/dist/web.js.map
ADDED
|
@@ -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
|
+
}
|