Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions PostHog/PostHogApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,13 @@ class PostHogApi {
distinctId: String,
deviceToken: String,
appId: String,
identityToken: String?,
completion: @escaping (PostHogUploadInfo) -> Void
) {
sendPushSubscription(
httpMethod: "POST", endpointName: "push subscription",
distinctId: distinctId, deviceToken: deviceToken, appId: appId, completion: completion
distinctId: distinctId, deviceToken: deviceToken, appId: appId,
identityToken: identityToken, completion: completion
)
}

Expand All @@ -260,11 +262,13 @@ class PostHogApi {
distinctId: String,
deviceToken: String,
appId: String,
identityToken: String?,
completion: @escaping (PostHogUploadInfo) -> Void
) {
sendPushSubscription(
httpMethod: "DELETE", endpointName: "push unsubscription",
distinctId: distinctId, deviceToken: deviceToken, appId: appId, completion: completion
distinctId: distinctId, deviceToken: deviceToken, appId: appId,
identityToken: identityToken, completion: completion
)
}

Expand All @@ -274,20 +278,26 @@ class PostHogApi {
distinctId: String,
deviceToken: String,
appId: String,
identityToken: String?,
completion: @escaping (PostHogUploadInfo) -> Void
) {
guard let url = getEndpointURL("/api/push_subscriptions/", relativeTo: config.host) else {
hedgeLog("Malformed push subscriptions URL error.")
return completion(PostHogUploadInfo(statusCode: nil, error: nil))
}

let toSend: [String: Any] = [
// The token key is omitted entirely when absent — the backend treats a missing key as
// "unsigned"; an explicit null would fail its string check.
var toSend: [String: Any] = [
"api_key": config.projectToken,
"distinct_id": distinctId,
"device_token": deviceToken,
"platform": "ios",
"app_id": appId,
]
if let identityToken {
toSend["identity_token"] = identityToken
}

guard let data = try? JSONSerialization.data(withJSONObject: toSend) else {
hedgeLog("Error parsing the \(endpointName) body")
Expand Down
21 changes: 21 additions & 0 deletions PostHog/PostHogConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,27 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent?
@objc public var capturePushNotificationOpened: Bool = true
#endif

/// Supplies a signed identity token to attach (as `identity_token`) to push subscription
/// register/unregister requests, for projects that enable identity verification on their push
/// integration. Invoked with the `distinct_id` and `app_id` the request carries; call `completion`
/// **exactly once**, from any thread, with a token minted by your backend — or `nil` to send the
/// request without one. The SDK never mints tokens itself: your backend signs an HS256 JWT with
/// the project's **secret** API key, claims `sub` = distinct id, `app_id`,
/// `aud` = "posthog:push_identity", and an `exp`.
///
/// The token is cached in memory per `(distinctId, appId)` and re-requested on identity change, on
/// the next launch, and once after a 401 rejection. If `completion` isn't called within ~10s the
/// SDK stops waiting and sends the request without an identity token; where your project requires
/// identity verification the backend will likely reject it, so make sure your provider always
/// completes.
///
/// The hook is invoked on a background queue that serializes push-registration work — never on the
/// calling thread — and `completion` may be called from any thread. Still return promptly: blocking
/// here stalls the SDK's push retry/offline-resume flow (though not your app's main thread).
///
/// Default: `nil` (requests carry no identity token).
@objc public var pushIdentityProvider: ((_ distinctId: String, _ appId: String, _ completion: @escaping (String?) -> Void) -> Void)?

#if os(iOS) || targetEnvironment(macCatalyst)
/// Enables UIKit element interaction autocapture on iOS and Mac Catalyst.
///
Expand Down
9 changes: 6 additions & 3 deletions PostHog/PostHogSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2398,6 +2398,8 @@ let maxRetryDelay = 30.0
storage?.setBool(forKey: .optOut, contents: true)
}

pushSubscriptionHandler?.onOptOut()

setupLock.withLock {
uninstallIntegrations()
}
Expand Down Expand Up @@ -2979,9 +2981,10 @@ let maxRetryDelay = 30.0
/// Unregisters this device's push token from PostHog so Workflows stop targeting it — for example
/// from your logout flow.
///
/// Sends a best-effort `DELETE /api/push_subscriptions/` for the current distinct id (the backend
/// unsets the subscription property) and forgets the locally stored token. Unlike registration this
/// is not retried. Call it directly if you manage push subscriptions yourself. On `reset()` the SDK
/// Sends a `DELETE /api/push_subscriptions/` for the current distinct id (the backend unsets the
/// subscription property) and forgets the locally stored token. The delete intent is durable: an
/// offline or failed attempt is retried on `flush()`/next launch until it succeeds or hits a
/// terminal 4xx. Call it directly if you manage push subscriptions yourself. On `reset()` the SDK
/// already moves any registered token to the new anonymous identity (unregister then re-register),
/// independently of `capturePushNotificationSubscriptions` — that flag only gates automatic token
/// subscription at startup.
Expand Down
1 change: 1 addition & 0 deletions PostHog/PostHogStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ class PostHogStorage {
case capturePerformance = "posthog.capturePerformance"
case deviceId = "posthog.deviceId"
case pushSubscription = "posthog.pushSubscription"
case pushPendingUnregister = "posthog.pushPendingUnregister"
}

// The location for storing data that we always want to keep
Expand Down
Loading
Loading