@apex-inc/capacitor-plugin 2.2.7 → 2.4.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/android/src/main/java/inc/apex/capacitor/ApexCapacitorPlugin.kt +167 -0
- package/android/src/main/java/inc/apex/capacitor/ApexEvents.kt +43 -1
- package/dist/definitions.d.ts +27 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/esm/definitions.d.ts +27 -1
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/events.d.ts +27 -1
- package/dist/esm/events.d.ts.map +1 -1
- package/dist/esm/events.js +32 -1
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/web.d.ts +12 -0
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +48 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/events.d.ts +27 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +32 -1
- package/dist/events.js.map +1 -1
- package/dist/web.d.ts +12 -0
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +48 -0
- package/dist/web.js.map +1 -1
- package/ios/Sources/ApexCapacitorPlugin/ApexEvents.swift +43 -1
- package/ios/Sources/ApexCapacitorPluginBridge/ApexCapacitorPlugin.swift +156 -0
- package/package.json +1 -1
|
@@ -41,10 +41,13 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
41
41
|
CAPPluginMethod(name: "endSession", returnType: CAPPluginReturnPromise),
|
|
42
42
|
CAPPluginMethod(name: "getCurrentSession", returnType: CAPPluginReturnPromise),
|
|
43
43
|
CAPPluginMethod(name: "captureVariantScreenshot", returnType: CAPPluginReturnPromise),
|
|
44
|
+
CAPPluginMethod(name: "getVariant", returnType: CAPPluginReturnPromise),
|
|
44
45
|
CAPPluginMethod(name: "track", returnType: CAPPluginReturnPromise),
|
|
45
46
|
CAPPluginMethod(name: "getQueueSize", returnType: CAPPluginReturnPromise),
|
|
46
47
|
CAPPluginMethod(name: "flushQueue", returnType: CAPPluginReturnPromise),
|
|
47
48
|
CAPPluginMethod(name: "setTestMode", returnType: CAPPluginReturnPromise),
|
|
49
|
+
CAPPluginMethod(name: "setConsent", returnType: CAPPluginReturnPromise),
|
|
50
|
+
CAPPluginMethod(name: "getConsent", returnType: CAPPluginReturnPromise),
|
|
48
51
|
]
|
|
49
52
|
|
|
50
53
|
// MARK: - Dependencies (lazy; constructed after initialize())
|
|
@@ -94,6 +97,13 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
94
97
|
private var releaseChannel: String = "unknown"
|
|
95
98
|
private var releaseChannelSource: String = "auto"
|
|
96
99
|
private var lastReleaseChannelDetectedAt: Date = Date(timeIntervalSince1970: 0)
|
|
100
|
+
|
|
101
|
+
// Phase 4a — consent gate. Defaults granted; set false by
|
|
102
|
+
// initialize({ requireConsent: true }) until setConsent({ granted:true }).
|
|
103
|
+
private var consentGranted = true
|
|
104
|
+
// Phase 3 — per-launch dedup so each (experiment, variant) exposure
|
|
105
|
+
// fires once, matching the JS/web fallback.
|
|
106
|
+
private var firedExposures = Set<String>()
|
|
97
107
|
// MMP-207 — debounced flush. Without this, every `track()` call
|
|
98
108
|
// kicked a POST, so 8 quick taps = 8 separate single-event HTTP
|
|
99
109
|
// requests. With debounce, rapid-fire events coalesce into one
|
|
@@ -201,6 +211,8 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
201
211
|
// authorize quarantine-mode auto-stitch.
|
|
202
212
|
let rawKey = call.getString("apiKey") ?? ""
|
|
203
213
|
apiKey = rawKey.isEmpty ? nil : rawKey
|
|
214
|
+
// Phase 4a — consent gate.
|
|
215
|
+
consentGranted = !(call.getBool("requireConsent") ?? false)
|
|
204
216
|
|
|
205
217
|
// MMP-061 — release channel detection. iOS uses the receipt URL
|
|
206
218
|
// + StoreKit 2 AppTransaction. Any JS-supplied override on iOS
|
|
@@ -535,6 +547,12 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
535
547
|
/// guarantees than firing a track event, and merchants tend to
|
|
536
548
|
/// wire identify behind a one-shot boundary.
|
|
537
549
|
@objc public func identify(_ call: CAPPluginCall) {
|
|
550
|
+
// Phase 4a — identify sends PII; collect nothing until consent given.
|
|
551
|
+
if !consentGranted {
|
|
552
|
+
if debug { print("[apex-capacitor] consent not granted; skipping identify") }
|
|
553
|
+
call.resolve()
|
|
554
|
+
return
|
|
555
|
+
}
|
|
538
556
|
guard let email = call.getString("email")?.trimmingCharacters(in: .whitespaces),
|
|
539
557
|
!email.isEmpty else {
|
|
540
558
|
call.reject("email is required")
|
|
@@ -795,6 +813,119 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
795
813
|
}.resume()
|
|
796
814
|
}
|
|
797
815
|
|
|
816
|
+
// MARK: - Experiments (Phase 3 — native getVariant)
|
|
817
|
+
|
|
818
|
+
/// Resolve an experiment variant for this device against
|
|
819
|
+
/// `${apiBaseUrl}/api/experiments/:id/assign?unified=true&source=sdk`,
|
|
820
|
+
/// passing the stable visitor id so assignment buckets on the SAME id the
|
|
821
|
+
/// exposure event carries. Fires `experiment_exposure` once per
|
|
822
|
+
/// (experiment, variant). Falls back to control on any error.
|
|
823
|
+
@objc public func getVariant(_ call: CAPPluginCall) {
|
|
824
|
+
guard let experimentId = call.getString("experimentId"), !experimentId.isEmpty else {
|
|
825
|
+
call.reject("experimentId is required")
|
|
826
|
+
return
|
|
827
|
+
}
|
|
828
|
+
// Phase 4a — without consent, resolve control and fire no exposure.
|
|
829
|
+
if !consentGranted {
|
|
830
|
+
call.resolve([
|
|
831
|
+
"experimentId": experimentId,
|
|
832
|
+
"variant": "control",
|
|
833
|
+
"payload": [String: Any](),
|
|
834
|
+
"eligible": false,
|
|
835
|
+
"reason": "no_consent",
|
|
836
|
+
])
|
|
837
|
+
return
|
|
838
|
+
}
|
|
839
|
+
let fallback: [String: Any] = [
|
|
840
|
+
"experimentId": experimentId,
|
|
841
|
+
"variant": "control",
|
|
842
|
+
"payload": [String: Any](),
|
|
843
|
+
"eligible": false,
|
|
844
|
+
"reason": "not_found",
|
|
845
|
+
]
|
|
846
|
+
guard let encodedId = experimentId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
|
|
847
|
+
let url = URL(string: "\(apiBaseUrl)/api/experiments/\(encodedId)/assign?unified=true&source=sdk") else {
|
|
848
|
+
call.resolve(fallback)
|
|
849
|
+
return
|
|
850
|
+
}
|
|
851
|
+
var request = URLRequest(url: url)
|
|
852
|
+
request.httpMethod = "GET"
|
|
853
|
+
request.setValue("sdk", forHTTPHeaderField: "x-apex-source")
|
|
854
|
+
request.setValue(visitorId, forHTTPHeaderField: "x-apex-visitor-id")
|
|
855
|
+
request.setValue(workspaceKey, forHTTPHeaderField: "x-apex-workspace")
|
|
856
|
+
if let key = apiKey {
|
|
857
|
+
request.setValue(key, forHTTPHeaderField: "x-apex-api-key")
|
|
858
|
+
}
|
|
859
|
+
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
|
860
|
+
guard let self = self else { return }
|
|
861
|
+
if error != nil {
|
|
862
|
+
call.resolve(fallback)
|
|
863
|
+
return
|
|
864
|
+
}
|
|
865
|
+
guard let http = response as? HTTPURLResponse,
|
|
866
|
+
(200...299).contains(http.statusCode),
|
|
867
|
+
let data = data,
|
|
868
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
869
|
+
call.resolve(fallback)
|
|
870
|
+
return
|
|
871
|
+
}
|
|
872
|
+
let variant = (json["variant"] as? String) ?? "control"
|
|
873
|
+
let eligible = (json["eligible"] as? Bool) ?? false
|
|
874
|
+
let payload = (json["payload"] as? [String: Any]) ?? [:]
|
|
875
|
+
var result: [String: Any] = [
|
|
876
|
+
"experimentId": experimentId,
|
|
877
|
+
"variant": variant,
|
|
878
|
+
"payload": payload,
|
|
879
|
+
"eligible": eligible,
|
|
880
|
+
]
|
|
881
|
+
if let reason = json["reason"] as? String {
|
|
882
|
+
result["reason"] = reason
|
|
883
|
+
}
|
|
884
|
+
// Fire one exposure per (experiment, variant) on a real assignment.
|
|
885
|
+
if eligible {
|
|
886
|
+
let key = "\(experimentId):\(variant)"
|
|
887
|
+
if !self.firedExposures.contains(key) {
|
|
888
|
+
self.firedExposures.insert(key)
|
|
889
|
+
self.enqueueExposure(experimentId: experimentId, variant: variant)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
call.resolve(result)
|
|
893
|
+
}.resume()
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/// Enqueue a canonical `experiment_exposure` event, stamped the same way
|
|
897
|
+
/// `track(_:)` stamps every event (platform / timestamp / visitorId /
|
|
898
|
+
/// mobile slot / testMode). Batched through the offline queue.
|
|
899
|
+
private func enqueueExposure(experimentId: String, variant: String) {
|
|
900
|
+
guard let queue = offlineQueue else { return }
|
|
901
|
+
var payload: [String: AnyCodable] = [:]
|
|
902
|
+
payload["type"] = AnyCodable("experiment_exposure")
|
|
903
|
+
payload["data"] = AnyCodable([
|
|
904
|
+
"experiment_id": experimentId,
|
|
905
|
+
"variant_key": variant,
|
|
906
|
+
"surface": "mobile",
|
|
907
|
+
])
|
|
908
|
+
if testMode {
|
|
909
|
+
payload["testMode"] = AnyCodable(true)
|
|
910
|
+
}
|
|
911
|
+
payload["platform"] = AnyCodable("ios")
|
|
912
|
+
payload["timestamp"] = AnyCodable(ISO8601DateFormatter().string(from: Date()))
|
|
913
|
+
payload["visitorId"] = AnyCodable(visitorId)
|
|
914
|
+
let mobileSlot: [String: Any] = [
|
|
915
|
+
"releaseChannel": releaseChannel,
|
|
916
|
+
"releaseChannelSource": releaseChannelSource,
|
|
917
|
+
]
|
|
918
|
+
payload["mobile"] = AnyCodable(mobileSlot)
|
|
919
|
+
let event = NativeQueuedEvent(
|
|
920
|
+
id: UUID().uuidString.lowercased(),
|
|
921
|
+
payload: payload,
|
|
922
|
+
attempts: 0,
|
|
923
|
+
enqueuedAt: Date()
|
|
924
|
+
)
|
|
925
|
+
try? queue.enqueue(event)
|
|
926
|
+
kickFlush(reason: "track")
|
|
927
|
+
}
|
|
928
|
+
|
|
798
929
|
// MARK: - Events & Queue
|
|
799
930
|
|
|
800
931
|
@objc public func track(_ call: CAPPluginCall) {
|
|
@@ -802,6 +933,12 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
802
933
|
call.reject("Plugin not initialized — call initialize() first")
|
|
803
934
|
return
|
|
804
935
|
}
|
|
936
|
+
// Phase 4a — collect nothing until consent is granted.
|
|
937
|
+
if !consentGranted {
|
|
938
|
+
if debug { print("[apex-capacitor] consent not granted; dropping event") }
|
|
939
|
+
call.resolve()
|
|
940
|
+
return
|
|
941
|
+
}
|
|
805
942
|
|
|
806
943
|
let id = call.getString("id") ?? UUID().uuidString.lowercased()
|
|
807
944
|
let payloadDict = call.options.compactMapValues { $0 }
|
|
@@ -901,6 +1038,25 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
901
1038
|
testMode = call.getBool("enabled") ?? false
|
|
902
1039
|
call.resolve()
|
|
903
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
// MARK: - Consent (Phase 4a)
|
|
1043
|
+
|
|
1044
|
+
@objc public func setConsent(_ call: CAPPluginCall) {
|
|
1045
|
+
let granted = call.getBool("granted") ?? false
|
|
1046
|
+
consentGranted = granted
|
|
1047
|
+
if granted {
|
|
1048
|
+
// Resume: flush anything captured before this point.
|
|
1049
|
+
kickFlush(reason: "consent")
|
|
1050
|
+
} else {
|
|
1051
|
+
// Revoke: drop anything queued but not yet sent.
|
|
1052
|
+
try? offlineQueue?.clear()
|
|
1053
|
+
}
|
|
1054
|
+
call.resolve()
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
@objc public func getConsent(_ call: CAPPluginCall) {
|
|
1058
|
+
call.resolve(["granted": consentGranted])
|
|
1059
|
+
}
|
|
904
1060
|
}
|
|
905
1061
|
|
|
906
1062
|
// MARK: - Date ISO 8601 helper
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apex-inc/capacitor-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Apex Capacitor plugin — iOS/Android attribution, events, deep linking, SKAN, and offline-tolerant tracking for Capacitor apps.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|