@apex-inc/capacitor-plugin 2.4.1 → 2.5.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 (63) hide show
  1. package/README.md +41 -4
  2. package/android/src/main/java/inc/apex/capacitor/ApexCapacitorPlugin.kt +80 -5
  3. package/android/src/main/java/inc/apex/capacitor/ApexEvents.kt +11 -4
  4. package/android/src/main/java/inc/apex/capacitor/DeepLinkManager.kt +27 -0
  5. package/dist/deferred-link.d.ts +73 -0
  6. package/dist/deferred-link.d.ts.map +1 -0
  7. package/dist/deferred-link.js +71 -0
  8. package/dist/deferred-link.js.map +1 -0
  9. package/dist/definitions.d.ts +76 -4
  10. package/dist/definitions.d.ts.map +1 -1
  11. package/dist/esm/deferred-link.d.ts +73 -0
  12. package/dist/esm/deferred-link.d.ts.map +1 -0
  13. package/dist/esm/deferred-link.js +68 -0
  14. package/dist/esm/deferred-link.js.map +1 -0
  15. package/dist/esm/definitions.d.ts +76 -4
  16. package/dist/esm/definitions.d.ts.map +1 -1
  17. package/dist/esm/events.d.ts +8 -1
  18. package/dist/esm/events.d.ts.map +1 -1
  19. package/dist/esm/events.js +8 -1
  20. package/dist/esm/events.js.map +1 -1
  21. package/dist/esm/index.d.ts +9 -0
  22. package/dist/esm/index.d.ts.map +1 -1
  23. package/dist/esm/index.js +157 -1
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/referrer-link.d.ts +36 -0
  26. package/dist/esm/referrer-link.d.ts.map +1 -0
  27. package/dist/esm/referrer-link.js +103 -0
  28. package/dist/esm/referrer-link.js.map +1 -0
  29. package/dist/esm/skan-conversion-values.d.ts +175 -0
  30. package/dist/esm/skan-conversion-values.d.ts.map +1 -0
  31. package/dist/esm/skan-conversion-values.js +323 -0
  32. package/dist/esm/skan-conversion-values.js.map +1 -0
  33. package/dist/esm/web.d.ts +6 -1
  34. package/dist/esm/web.d.ts.map +1 -1
  35. package/dist/esm/web.js +26 -3
  36. package/dist/esm/web.js.map +1 -1
  37. package/dist/events.d.ts +8 -1
  38. package/dist/events.d.ts.map +1 -1
  39. package/dist/events.js +8 -1
  40. package/dist/events.js.map +1 -1
  41. package/dist/index.d.ts +9 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +166 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/referrer-link.d.ts +36 -0
  46. package/dist/referrer-link.d.ts.map +1 -0
  47. package/dist/referrer-link.js +107 -0
  48. package/dist/referrer-link.js.map +1 -0
  49. package/dist/skan-conversion-values.d.ts +175 -0
  50. package/dist/skan-conversion-values.d.ts.map +1 -0
  51. package/dist/skan-conversion-values.js +331 -0
  52. package/dist/skan-conversion-values.js.map +1 -0
  53. package/dist/web.d.ts +6 -1
  54. package/dist/web.d.ts.map +1 -1
  55. package/dist/web.js +26 -3
  56. package/dist/web.js.map +1 -1
  57. package/ios/Sources/ApexCapacitorPlugin/ApexEvents.swift +11 -4
  58. package/ios/Sources/ApexCapacitorPlugin/DeepLinkManager.swift +16 -2
  59. package/ios/Sources/ApexCapacitorPlugin/SkanManager.swift +18 -4
  60. package/ios/Sources/ApexCapacitorPluginBridge/ApexCapacitorPlugin.swift +22 -6
  61. package/ios/Tests/ApexCapacitorPluginTests/DeepLinkManagerTests.swift +26 -0
  62. package/ios/Tests/ApexCapacitorPluginTests/SkanManagerTests.swift +22 -1
  63. package/package.json +1 -1
@@ -5,8 +5,10 @@
5
5
  // Wraps SKAdNetwork conversion value updates.
6
6
  //
7
7
  // - SKAN 3.0 (iOS 11.3-14.x): `SKAdNetwork.updateConversionValue(Int)` — fine value 0-63.
8
- // - SKAN 4.0 (iOS 16.1+): `SKAdNetwork.updatePostbackConversionValue(_:coarseValue:completionHandler:)`
9
- // — fine value 0-63 plus optional coarse value (`.low`, `.medium`, `.high`).
8
+ // - SKAN 4.0 (iOS 16.1+): `SKAdNetwork.updatePostbackConversionValue(_:coarseValue:lockWindow:completionHandler:)`
9
+ // — fine value 0-63 plus optional coarse value (`.low`, `.medium`, `.high`)
10
+ // and an optional `lockWindow` that asks Apple to send the current
11
+ // window's postback without waiting the window out.
10
12
  //
11
13
  // The class validates inputs (0-63 fine) and routes to the correct API
12
14
  // based on iOS availability and whether a coarse value is provided.
@@ -45,6 +47,7 @@ public final class SkanManager {
45
47
  public func update(
46
48
  fineValue: Int,
47
49
  coarseValue: SkanCoarseValue? = nil,
50
+ lockWindow: Bool = false,
48
51
  completion: @escaping (Error?) -> Void
49
52
  ) {
50
53
  do {
@@ -53,7 +56,12 @@ public final class SkanManager {
53
56
  completion(error)
54
57
  return
55
58
  }
56
- updater.updateConversionValue(fineValue: fineValue, coarseValue: coarseValue, completion: completion)
59
+ updater.updateConversionValue(
60
+ fineValue: fineValue,
61
+ coarseValue: coarseValue,
62
+ lockWindow: lockWindow,
63
+ completion: completion
64
+ )
57
65
  }
58
66
  }
59
67
 
@@ -61,6 +69,7 @@ public protocol SkanUpdaterProtocol {
61
69
  func updateConversionValue(
62
70
  fineValue: Int,
63
71
  coarseValue: SkanCoarseValue?,
72
+ lockWindow: Bool,
64
73
  completion: @escaping (Error?) -> Void
65
74
  )
66
75
  }
@@ -71,6 +80,7 @@ public final class DefaultSkanUpdater: SkanUpdaterProtocol {
71
80
  public func updateConversionValue(
72
81
  fineValue: Int,
73
82
  coarseValue: SkanCoarseValue?,
83
+ lockWindow: Bool,
74
84
  completion: @escaping (Error?) -> Void
75
85
  ) {
76
86
  if let coarse = coarseValue, #available(iOS 16.1, *) {
@@ -80,7 +90,11 @@ public final class DefaultSkanUpdater: SkanUpdaterProtocol {
80
90
  case .medium: apiCoarse = .medium
81
91
  case .high: apiCoarse = .high
82
92
  }
83
- SKAdNetwork.updatePostbackConversionValue(fineValue, coarseValue: apiCoarse) { error in
93
+ SKAdNetwork.updatePostbackConversionValue(
94
+ fineValue,
95
+ coarseValue: apiCoarse,
96
+ lockWindow: lockWindow
97
+ ) { error in
84
98
  completion(error)
85
99
  }
86
100
  } else if #available(iOS 15.4, *) {
@@ -116,7 +116,10 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
116
116
  defaults.set(visitorId, forKey: "apex.visitorId")
117
117
 
118
118
  deepLinks.setWarmLinkHandler { [weak self] url in
119
- self?.notifyListeners("deepLink", data: ["url": url.absoluteString])
119
+ self?.notifyListeners("deepLink", data: [
120
+ "url": url.absoluteString,
121
+ "deepLinkHint": DeepLinkManager.apexHint(from: url) ?? NSNull(),
122
+ ])
120
123
  }
121
124
 
122
125
  // MMP-177 — observe foreground pushes posted by the app's
@@ -514,8 +517,10 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
514
517
  }
515
518
 
516
519
  @objc public func getInstallReferrer(_ call: CAPPluginCall) {
517
- // iOS has no equivalent of Play Install Referrer.
518
- call.resolve(["referrer": NSNull()])
520
+ // iOS has no equivalent of Play Install Referrer: the App Store
521
+ // forwards nothing across an install, so deferred deep linking
522
+ // here falls back to the probabilistic server match.
523
+ call.resolve(["referrer": NSNull(), "clickTimestamp": 0])
519
524
  }
520
525
 
521
526
  @objc public func getVisitorId(_ call: CAPPluginCall) {
@@ -618,7 +623,8 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
618
623
  if let raw = call.getString("coarseValue") {
619
624
  coarse = SkanCoarseValue(rawValue: raw)
620
625
  }
621
- skanManager.update(fineValue: fineValue, coarseValue: coarse) { error in
626
+ let lockWindow = call.getBool("lockWindow") ?? false
627
+ skanManager.update(fineValue: fineValue, coarseValue: coarse, lockWindow: lockWindow) { error in
622
628
  if let error = error {
623
629
  call.reject("SKAN update failed: \(error.localizedDescription)")
624
630
  } else {
@@ -631,9 +637,12 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
631
637
 
632
638
  @objc public func getInitialDeepLink(_ call: CAPPluginCall) {
633
639
  if let url = deepLinks.consumeInitialUrl() {
634
- call.resolve(["url": url.absoluteString])
640
+ call.resolve([
641
+ "url": url.absoluteString,
642
+ "deepLinkHint": DeepLinkManager.apexHint(from: url) ?? NSNull(),
643
+ ])
635
644
  } else {
636
- call.resolve(["url": NSNull()])
645
+ call.resolve(["url": NSNull(), "deepLinkHint": NSNull()])
637
646
  }
638
647
  }
639
648
 
@@ -853,6 +862,7 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
853
862
  request.setValue("sdk", forHTTPHeaderField: "x-apex-source")
854
863
  request.setValue(visitorId, forHTTPHeaderField: "x-apex-visitor-id")
855
864
  request.setValue(workspaceKey, forHTTPHeaderField: "x-apex-workspace")
865
+ request.setValue(releaseChannel, forHTTPHeaderField: "x-apex-release-channel")
856
866
  if let key = apiKey {
857
867
  request.setValue(key, forHTTPHeaderField: "x-apex-api-key")
858
868
  }
@@ -898,12 +908,18 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
898
908
  /// mobile slot / testMode). Batched through the offline queue.
899
909
  private func enqueueExposure(experimentId: String, variant: String) {
900
910
  guard let queue = offlineQueue else { return }
911
+ // The app version that saw the variant — the mobile equivalent of the
912
+ // web build id. On mobile the experiment logic often ships inside the
913
+ // binary, so "which version was exposed" is what makes results honest
914
+ // during a staged rollout + enables version-gating (Phase 5, 2026-07-20).
915
+ let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
901
916
  var payload: [String: AnyCodable] = [:]
902
917
  payload["type"] = AnyCodable("experiment_exposure")
903
918
  payload["data"] = AnyCodable([
904
919
  "experiment_id": experimentId,
905
920
  "variant_key": variant,
906
921
  "surface": "mobile",
922
+ "app_version": appVersion,
907
923
  ])
908
924
  if testMode {
909
925
  payload["testMode"] = AnyCodable(true)
@@ -66,4 +66,30 @@ final class DeepLinkManagerTests: XCTestCase {
66
66
  func testIsValidDeepLinkRejectsFileUrl() {
67
67
  XCTAssertFalse(DeepLinkManager.isValidDeepLink(URL(string: "file:///tmp/test")!))
68
68
  }
69
+
70
+ func testApexHintReadsParam() {
71
+ let url = URL(string: "https://acme.example/summer?apex_dl=%2Foffers%2Fsummer")!
72
+ XCTAssertEqual(DeepLinkManager.apexHint(from: url), "/offers/summer")
73
+ }
74
+
75
+ func testApexHintFindsParamAmongOthers() {
76
+ let url = URL(
77
+ string: "https://acme.example/s?utm_source=linkedin&apex_dl=screen-42&utm_medium=social"
78
+ )!
79
+ XCTAssertEqual(DeepLinkManager.apexHint(from: url), "screen-42")
80
+ }
81
+
82
+ func testApexHintNilWhenAbsent() {
83
+ let url = URL(string: "https://acme.example/summer?utm_source=x")!
84
+ XCTAssertNil(DeepLinkManager.apexHint(from: url))
85
+ }
86
+
87
+ func testApexHintNilWithNoQuery() {
88
+ XCTAssertNil(DeepLinkManager.apexHint(from: URL(string: "https://acme.example/s")!))
89
+ }
90
+
91
+ func testApexHintNilWhenEmpty() {
92
+ let url = URL(string: "https://acme.example/s?apex_dl=")!
93
+ XCTAssertNil(DeepLinkManager.apexHint(from: url))
94
+ }
69
95
  }
@@ -9,6 +9,7 @@ final class RecordingSkanUpdater: SkanUpdaterProtocol {
9
9
  struct Call: Equatable {
10
10
  let fineValue: Int
11
11
  let coarseValue: SkanCoarseValue?
12
+ let lockWindow: Bool
12
13
  }
13
14
  var calls: [Call] = []
14
15
  var nextError: Error?
@@ -16,9 +17,10 @@ final class RecordingSkanUpdater: SkanUpdaterProtocol {
16
17
  func updateConversionValue(
17
18
  fineValue: Int,
18
19
  coarseValue: SkanCoarseValue?,
20
+ lockWindow: Bool,
19
21
  completion: @escaping (Error?) -> Void
20
22
  ) {
21
- calls.append(Call(fineValue: fineValue, coarseValue: coarseValue))
23
+ calls.append(Call(fineValue: fineValue, coarseValue: coarseValue, lockWindow: lockWindow))
22
24
  completion(nextError)
23
25
  }
24
26
  }
@@ -88,4 +90,23 @@ final class SkanManagerTests: XCTestCase {
88
90
  XCTAssertEqual(SkanCoarseValue.medium.rawValue, "medium")
89
91
  XCTAssertEqual(SkanCoarseValue.high.rawValue, "high")
90
92
  }
93
+
94
+ func testUpdatePassesLockWindowThrough() {
95
+ let updater = RecordingSkanUpdater()
96
+ let manager = SkanManager(updater: updater)
97
+ let exp = expectation(description: "update")
98
+ manager.update(fineValue: 32, coarseValue: .high, lockWindow: true) { _ in exp.fulfill() }
99
+ wait(for: [exp], timeout: 1)
100
+ XCTAssertEqual(updater.calls.count, 1)
101
+ XCTAssertTrue(updater.calls[0].lockWindow)
102
+ }
103
+
104
+ func testUpdateDefaultsLockWindowToFalse() {
105
+ let updater = RecordingSkanUpdater()
106
+ let manager = SkanManager(updater: updater)
107
+ let exp = expectation(description: "update")
108
+ manager.update(fineValue: 8, coarseValue: .medium) { _ in exp.fulfill() }
109
+ wait(for: [exp], timeout: 1)
110
+ XCTAssertFalse(updater.calls[0].lockWindow)
111
+ }
91
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-inc/capacitor-plugin",
3
- "version": "2.4.1",
3
+ "version": "2.5.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",