@apex-inc/capacitor-plugin 0.3.9 → 2.2.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/README.md +5 -5
- package/android/build.gradle +4 -2
- package/android/src/main/AndroidManifest.xml +35 -3
- package/android/src/main/java/inc/apex/capacitor/ApexCapacitorPlugin.kt +404 -7
- package/android/src/main/java/inc/apex/capacitor/ApexEvents.kt +122 -0
- package/android/src/main/java/inc/apex/capacitor/ApexFirebaseMessagingService.kt +99 -0
- package/android/src/main/java/inc/apex/capacitor/NativeBatchSender.kt +260 -0
- package/dist/batch-sender.d.ts +16 -4
- package/dist/batch-sender.d.ts.map +1 -1
- package/dist/batch-sender.js +29 -4
- package/dist/batch-sender.js.map +1 -1
- package/dist/cart-helpers.d.ts +63 -0
- package/dist/cart-helpers.d.ts.map +1 -0
- package/dist/cart-helpers.js +50 -0
- package/dist/cart-helpers.js.map +1 -0
- package/dist/definitions.d.ts +150 -5
- package/dist/definitions.d.ts.map +1 -1
- package/dist/esm/batch-sender.d.ts +16 -4
- package/dist/esm/batch-sender.d.ts.map +1 -1
- package/dist/esm/batch-sender.js +29 -4
- package/dist/esm/batch-sender.js.map +1 -1
- package/dist/esm/cart-helpers.d.ts +63 -0
- package/dist/esm/cart-helpers.d.ts.map +1 -0
- package/dist/esm/cart-helpers.js +44 -0
- package/dist/esm/cart-helpers.js.map +1 -0
- package/dist/esm/definitions.d.ts +150 -5
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/events.d.ts +85 -0
- package/dist/esm/events.d.ts.map +1 -0
- package/dist/esm/events.js +96 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +5 -6
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +20 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/offline-queue.d.ts +15 -0
- package/dist/esm/offline-queue.d.ts.map +1 -1
- package/dist/esm/offline-queue.js +35 -0
- package/dist/esm/offline-queue.js.map +1 -1
- package/dist/esm/screen-view.d.ts +18 -0
- package/dist/esm/screen-view.d.ts.map +1 -0
- package/dist/esm/screen-view.js +28 -0
- package/dist/esm/screen-view.js.map +1 -0
- package/dist/esm/web.d.ts +29 -1
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +167 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/events.d.ts +85 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +99 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +5 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -3
- package/dist/index.js.map +1 -1
- package/dist/offline-queue.d.ts +15 -0
- package/dist/offline-queue.d.ts.map +1 -1
- package/dist/offline-queue.js +35 -0
- package/dist/offline-queue.js.map +1 -1
- package/dist/screen-view.d.ts +18 -0
- package/dist/screen-view.d.ts.map +1 -0
- package/dist/screen-view.js +31 -0
- package/dist/screen-view.js.map +1 -0
- package/dist/web.d.ts +29 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +167 -2
- package/dist/web.js.map +1 -1
- package/ios/Sources/ApexCapacitorPlugin/ApexEvents.swift +124 -0
- package/ios/Sources/ApexCapacitorPlugin/BatchSender.swift +24 -6
- package/ios/Sources/ApexCapacitorPlugin/OfflineQueue.swift +19 -0
- package/ios/Sources/ApexCapacitorPlugin/PushNotificationManager.swift +47 -0
- package/ios/Sources/ApexCapacitorPluginBridge/ApexCapacitorPlugin.swift +288 -28
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apex Spec — canonical event name constants for the iOS SDK.
|
|
3
|
+
*
|
|
4
|
+
* Hand-authored from `app/src/lib/core/apex-spec/events.ts`. Kept in sync
|
|
5
|
+
* via the parity test at
|
|
6
|
+
* `app/src/lib/core/apex-spec/__tests__/sdk-parity.test.ts` — CI fails if a
|
|
7
|
+
* canonical name is added/renamed/removed in the registry without a matching
|
|
8
|
+
* update here.
|
|
9
|
+
*
|
|
10
|
+
* Usage from Swift:
|
|
11
|
+
*
|
|
12
|
+
* plugin.track(type: ApexEvents.addToCart, data: ["product_id": "sku_123"])
|
|
13
|
+
*
|
|
14
|
+
* Each constant is the unversioned canonical name as the developer writes it
|
|
15
|
+
* in `track()` calls. Versions are an internal storage primitive; the SDK
|
|
16
|
+
* never carries a version field.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import Foundation
|
|
20
|
+
|
|
21
|
+
public enum ApexEvents {
|
|
22
|
+
// MARK: Commerce
|
|
23
|
+
public static let addToCart = "add_to_cart"
|
|
24
|
+
public static let removeFromCart = "remove_from_cart"
|
|
25
|
+
public static let productView = "product_view"
|
|
26
|
+
public static let addToWishlist = "add_to_wishlist"
|
|
27
|
+
public static let checkoutStarted = "checkout_started"
|
|
28
|
+
public static let inAppPurchase = "in_app_purchase"
|
|
29
|
+
public static let purchaseRefunded = "purchase_refunded"
|
|
30
|
+
public static let subscriptionEvent = "subscription_event"
|
|
31
|
+
public static let orderPlaced = "order_placed"
|
|
32
|
+
public static let firstSaleCompleted = "first_sale_completed"
|
|
33
|
+
|
|
34
|
+
// MARK: Marketing (merchant-fired triggers)
|
|
35
|
+
public static let priceDropped = "price_dropped"
|
|
36
|
+
public static let restockDue = "restock_due"
|
|
37
|
+
public static let reviewRequestDue = "review_request_due"
|
|
38
|
+
|
|
39
|
+
// MARK: Identity
|
|
40
|
+
public static let userSignedUp = "user_signed_up"
|
|
41
|
+
public static let userSignedIn = "user_signed_in"
|
|
42
|
+
public static let userSignedOut = "user_signed_out"
|
|
43
|
+
public static let userIdentified = "user_identified"
|
|
44
|
+
|
|
45
|
+
// MARK: Lifecycle
|
|
46
|
+
public static let appInstall = "app_install"
|
|
47
|
+
public static let appOpen = "app_open"
|
|
48
|
+
public static let appBackground = "app_background"
|
|
49
|
+
public static let appUninstall = "app_uninstall"
|
|
50
|
+
public static let appReinstall = "app_reinstall"
|
|
51
|
+
|
|
52
|
+
// MARK: Session
|
|
53
|
+
public static let sessionStart = "session_start"
|
|
54
|
+
public static let sessionEnd = "session_end"
|
|
55
|
+
|
|
56
|
+
// MARK: Engagement
|
|
57
|
+
public static let pageView = "page_view"
|
|
58
|
+
public static let pageview = "pageview"
|
|
59
|
+
public static let screenView = "screen_view"
|
|
60
|
+
public static let search = "search"
|
|
61
|
+
public static let share = "share"
|
|
62
|
+
public static let contentView = "content_view"
|
|
63
|
+
public static let formSubmit = "form_submit"
|
|
64
|
+
public static let click = "click"
|
|
65
|
+
public static let engagement = "engagement"
|
|
66
|
+
public static let heartbeat = "heartbeat"
|
|
67
|
+
public static let rageClick = "rage_click"
|
|
68
|
+
public static let deadClick = "dead_click"
|
|
69
|
+
public static let goalConversion = "goal_conversion"
|
|
70
|
+
public static let onboardingStepCompleted = "onboarding_step_completed"
|
|
71
|
+
|
|
72
|
+
// MARK: Product (PLG lifecycle outcomes)
|
|
73
|
+
public static let onboardingCompleted = "onboarding_completed"
|
|
74
|
+
public static let activated = "activated"
|
|
75
|
+
|
|
76
|
+
// MARK: Communication
|
|
77
|
+
public static let pushReceived = "push_received"
|
|
78
|
+
public static let pushOpened = "push_opened"
|
|
79
|
+
public static let emailOpened = "email_opened"
|
|
80
|
+
public static let emailClicked = "email_clicked"
|
|
81
|
+
public static let inAppMessageSeen = "in_app_message_seen"
|
|
82
|
+
public static let inAppMessageClicked = "in_app_message_clicked"
|
|
83
|
+
|
|
84
|
+
// MARK: Deep link
|
|
85
|
+
public static let deepLinkOpen = "deep_link_open"
|
|
86
|
+
|
|
87
|
+
// MARK: Attribution
|
|
88
|
+
public static let reattribution = "reattribution"
|
|
89
|
+
public static let reengagement = "reengagement"
|
|
90
|
+
|
|
91
|
+
// MARK: Smart banner
|
|
92
|
+
public static let smartBannerImpression = "smart_banner_impression"
|
|
93
|
+
public static let smartBannerClick = "smart_banner_click"
|
|
94
|
+
public static let smartBannerDismiss = "smart_banner_dismiss"
|
|
95
|
+
|
|
96
|
+
// MARK: System
|
|
97
|
+
public static let custom = "custom"
|
|
98
|
+
public static let lifecycleTransition = "lifecycle_transition"
|
|
99
|
+
public static let jsError = "js_error"
|
|
100
|
+
|
|
101
|
+
/// Apex Spec semver this SDK implements. Bumped together with
|
|
102
|
+
/// `APEX_SPEC_VERSION` in the TypeScript registry.
|
|
103
|
+
public static let specVersion = "1.1.0"
|
|
104
|
+
|
|
105
|
+
/// Complete list of every canonical event name. Used internally for
|
|
106
|
+
/// validation; iterate this set if you need to enumerate the spec.
|
|
107
|
+
public static let all: [String] = [
|
|
108
|
+
addToCart, removeFromCart, productView, addToWishlist, checkoutStarted,
|
|
109
|
+
inAppPurchase, purchaseRefunded, subscriptionEvent,
|
|
110
|
+
userSignedUp, userSignedIn, userSignedOut, userIdentified,
|
|
111
|
+
appInstall, appOpen, appBackground, appUninstall, appReinstall,
|
|
112
|
+
sessionStart, sessionEnd,
|
|
113
|
+
pageView, pageview, screenView, search, share, contentView, formSubmit,
|
|
114
|
+
click, engagement, heartbeat, rageClick, deadClick, goalConversion,
|
|
115
|
+
onboardingStepCompleted,
|
|
116
|
+
onboardingCompleted, activated,
|
|
117
|
+
pushReceived, pushOpened, emailOpened, emailClicked,
|
|
118
|
+
inAppMessageSeen, inAppMessageClicked,
|
|
119
|
+
deepLinkOpen,
|
|
120
|
+
reattribution, reengagement,
|
|
121
|
+
smartBannerImpression, smartBannerClick, smartBannerDismiss,
|
|
122
|
+
custom, lifecycleTransition, jsError,
|
|
123
|
+
]
|
|
124
|
+
}
|
|
@@ -30,8 +30,11 @@ public final class NativeBatchSender {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
private let apiBaseUrl: String
|
|
33
|
-
private let
|
|
33
|
+
private let workspaceKey: String
|
|
34
34
|
private let platformHeader: String
|
|
35
|
+
// Phase 2-SDK — optional workspace-bound API key forwarded as
|
|
36
|
+
// `x-apex-api-key` on every batch.
|
|
37
|
+
private let apiKey: String?
|
|
35
38
|
private let batchSize: Int
|
|
36
39
|
private let maxRetries: Int
|
|
37
40
|
private let baseBackoffMs: Int
|
|
@@ -43,8 +46,9 @@ public final class NativeBatchSender {
|
|
|
43
46
|
|
|
44
47
|
public init(
|
|
45
48
|
apiBaseUrl: String,
|
|
46
|
-
|
|
49
|
+
workspaceKey: String,
|
|
47
50
|
platformHeader: String = "ios",
|
|
51
|
+
apiKey: String? = nil,
|
|
48
52
|
batchSize: Int = 50,
|
|
49
53
|
maxRetries: Int = 3,
|
|
50
54
|
baseBackoffMs: Int = 1000,
|
|
@@ -55,8 +59,9 @@ public final class NativeBatchSender {
|
|
|
55
59
|
var trimmed = apiBaseUrl
|
|
56
60
|
while trimmed.hasSuffix("/") { trimmed.removeLast() }
|
|
57
61
|
self.apiBaseUrl = trimmed
|
|
58
|
-
self.
|
|
62
|
+
self.workspaceKey = workspaceKey
|
|
59
63
|
self.platformHeader = platformHeader
|
|
64
|
+
self.apiKey = apiKey
|
|
60
65
|
self.batchSize = max(1, batchSize)
|
|
61
66
|
self.maxRetries = max(1, maxRetries)
|
|
62
67
|
self.baseBackoffMs = max(0, baseBackoffMs)
|
|
@@ -149,15 +154,25 @@ public final class NativeBatchSender {
|
|
|
149
154
|
return
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
// The server accepts `{
|
|
157
|
+
// The server accepts `{ workspaceKey, events: [{ id, ...payload }] }`.
|
|
153
158
|
// We unwrap the queued envelope so the `id` lives at the top
|
|
154
159
|
// level of each event object (matches what /api/events expects
|
|
155
160
|
// and what the JS-side BatchSender produces).
|
|
161
|
+
//
|
|
162
|
+
// MMP-081 — stamp `clientType` at send time so events from the
|
|
163
|
+
// native iOS plugin land server-side as `native-ios`. We don't
|
|
164
|
+
// overwrite a value the caller already set on the event
|
|
165
|
+
// (preserves test fixtures and unusual workflows); the typical
|
|
166
|
+
// path is that the queued payload doesn't carry one and we
|
|
167
|
+
// fill it in here.
|
|
156
168
|
let body: [String: Any] = [
|
|
157
|
-
"
|
|
169
|
+
"workspaceKey": workspaceKey,
|
|
158
170
|
"events": batch.map { ev -> [String: Any] in
|
|
159
171
|
var flat = ev.payload.mapValues { $0.value }
|
|
160
172
|
flat["id"] = ev.id
|
|
173
|
+
if flat["clientType"] == nil {
|
|
174
|
+
flat["clientType"] = "native-ios"
|
|
175
|
+
}
|
|
161
176
|
return flat
|
|
162
177
|
},
|
|
163
178
|
]
|
|
@@ -165,8 +180,11 @@ public final class NativeBatchSender {
|
|
|
165
180
|
var request = URLRequest(url: url)
|
|
166
181
|
request.httpMethod = "POST"
|
|
167
182
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
168
|
-
request.setValue(
|
|
183
|
+
request.setValue(workspaceKey, forHTTPHeaderField: "X-Apex-Workspace-Key")
|
|
169
184
|
request.setValue(platformHeader, forHTTPHeaderField: "X-Apex-Platform")
|
|
185
|
+
if let key = apiKey {
|
|
186
|
+
request.setValue(key, forHTTPHeaderField: "x-apex-api-key")
|
|
187
|
+
}
|
|
170
188
|
request.timeoutInterval = 20
|
|
171
189
|
|
|
172
190
|
do {
|
|
@@ -165,6 +165,25 @@ public struct AnyCodable: Codable, Equatable {
|
|
|
165
165
|
switch value {
|
|
166
166
|
case is NSNull:
|
|
167
167
|
try container.encodeNil()
|
|
168
|
+
case let n as NSNumber:
|
|
169
|
+
// 2026-05-16 — NSNumber discrimination. Capacitor's JS bridge
|
|
170
|
+
// hands us every JS number as NSNumber, and Objective-C's
|
|
171
|
+
// toll-free bridging means `NSNumber(value: 1)` and
|
|
172
|
+
// `NSNumber(value: true)` both satisfy `as Bool` AND `as Int`.
|
|
173
|
+
// The previous `case let v as Bool:` branch consumed integer
|
|
174
|
+
// 1 / 0 first, encoding `quantity: 1` from JS as `quantity:
|
|
175
|
+
// true` server-side. That broke a) commerce widgets reading
|
|
176
|
+
// quantity as a number, and b) any journey trigger that
|
|
177
|
+
// type-checks numeric fields. CFBooleanGetTypeID is the only
|
|
178
|
+
// reliable way to distinguish a true Bool NSNumber from an
|
|
179
|
+
// integer NSNumber on Apple platforms.
|
|
180
|
+
if CFGetTypeID(n) == CFBooleanGetTypeID() {
|
|
181
|
+
try container.encode(n.boolValue)
|
|
182
|
+
} else if CFNumberIsFloatType(n) {
|
|
183
|
+
try container.encode(n.doubleValue)
|
|
184
|
+
} else {
|
|
185
|
+
try container.encode(n.int64Value)
|
|
186
|
+
}
|
|
168
187
|
case let v as Bool:
|
|
169
188
|
try container.encode(v)
|
|
170
189
|
case let v as Int:
|
|
@@ -60,6 +60,18 @@ public final class PushNotificationManager: NSObject {
|
|
|
60
60
|
private let notificationCenter: NotificationCenter
|
|
61
61
|
private var pendingCompletion: Completion?
|
|
62
62
|
|
|
63
|
+
/// Fired on EVERY token arrival, regardless of whether the
|
|
64
|
+
/// registration was kicked off by an explicit
|
|
65
|
+
/// `requestAuthorizationAndRegister` call or by `silentRegisterIfAuthorized`.
|
|
66
|
+
///
|
|
67
|
+
/// The plugin uses this to forward the token to JS listeners and to
|
|
68
|
+
/// re-register the token with the Apex backend. Setting this from
|
|
69
|
+
/// `initialize` lets us silently refresh the token on every cold
|
|
70
|
+
/// start when permission is already granted — without this hook the
|
|
71
|
+
/// merchant would have to manually tap "Enable push" after every
|
|
72
|
+
/// fresh Xcode install just to surface the token in the dashboard.
|
|
73
|
+
public var onTokenReceived: ((String) -> Void)?
|
|
74
|
+
|
|
63
75
|
public init(
|
|
64
76
|
center: UNUserNotificationCenter = .current(),
|
|
65
77
|
application: UIApplication = .shared,
|
|
@@ -113,6 +125,36 @@ public final class PushNotificationManager: NSObject {
|
|
|
113
125
|
}
|
|
114
126
|
}
|
|
115
127
|
|
|
128
|
+
/// Re-register for remote notifications WITHOUT prompting when
|
|
129
|
+
/// permission has already been granted. iOS's permission survives
|
|
130
|
+
/// app reinstalls (including Xcode debug rebuilds) but the sample
|
|
131
|
+
/// app's in-memory "have we registered yet?" state does not — so
|
|
132
|
+
/// merchants kept telling us push permission was "resetting on every
|
|
133
|
+
/// build". The OS hadn't actually reset anything; we just weren't
|
|
134
|
+
/// surfacing the fact that permission was still granted.
|
|
135
|
+
///
|
|
136
|
+
/// Calling this on `initialize` re-fires `didRegisterForRemoteNotifications`
|
|
137
|
+
/// with the device's current token, which flows through `onTokenReceived`
|
|
138
|
+
/// and refreshes the dashboard registration with the latest plugin
|
|
139
|
+
/// metadata (including the APNs environment field that older builds
|
|
140
|
+
/// didn't report).
|
|
141
|
+
///
|
|
142
|
+
/// No-op when permission is denied or hasn't been requested yet —
|
|
143
|
+
/// the user has to take an explicit action in those cases.
|
|
144
|
+
public func silentRegisterIfAuthorized() {
|
|
145
|
+
center.getNotificationSettings { [weak self] settings in
|
|
146
|
+
guard let self = self else { return }
|
|
147
|
+
switch settings.authorizationStatus {
|
|
148
|
+
case .authorized, .provisional, .ephemeral:
|
|
149
|
+
DispatchQueue.main.async {
|
|
150
|
+
self.application.registerForRemoteNotifications()
|
|
151
|
+
}
|
|
152
|
+
default:
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
116
158
|
// MARK: - AppDelegate bridge
|
|
117
159
|
|
|
118
160
|
private func observeAppDelegateBridge() {
|
|
@@ -137,6 +179,11 @@ public final class PushNotificationManager: NSObject {
|
|
|
137
179
|
}
|
|
138
180
|
let hex = PushTokenHexFormatter.format(data)
|
|
139
181
|
resolvePending(.init(permission: .granted, token: hex))
|
|
182
|
+
// Fire after resolvePending so the JS Promise for an explicit
|
|
183
|
+
// `Apex.registerForPushNotifications()` resolves first, then
|
|
184
|
+
// the plugin's persistent `onTokenReceived` hook runs to forward
|
|
185
|
+
// to listeners + the Apex backend.
|
|
186
|
+
onTokenReceived?(hex)
|
|
140
187
|
}
|
|
141
188
|
|
|
142
189
|
@objc private func handleDidFail(_: Notification) {
|