Skip to content

Commit

Permalink
Merge pull request #267 from klaviyo/ab/CHNL-16694/handle-aggregate-e…
Browse files Browse the repository at this point in the history
…vents

[CHNL-16694] handle aggregate events
  • Loading branch information
ab1470 authored Feb 4, 2025
2 parents 89cd817 + 6f50739 commit 9336cb8
Show file tree
Hide file tree
Showing 13 changed files with 405 additions and 51 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.6
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand Down
10 changes: 10 additions & 0 deletions Sources/KlaviyoCore/Models/APIModels/AggregateEventPayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// AggregateEventPayload.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 1/31/25.
//

import Foundation

public typealias AggregateEventPayload = Data
7 changes: 6 additions & 1 deletion Sources/KlaviyoCore/Networking/KlaviyoEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ public enum KlaviyoEndpoint: Equatable, Codable {
case createEvent(CreateEventPayload)
case registerPushToken(PushTokenPayload)
case unregisterPushToken(UnregisterPushTokenPayload)
case aggregateEvent(AggregateEventPayload)

var httpScheme: String { "https" }

var httpMethod: RequestMethod {
switch self {
case .createProfile, .createEvent, .registerPushToken, .unregisterPushToken:
case .createProfile, .createEvent, .registerPushToken, .unregisterPushToken, .aggregateEvent:
return .post
}
}
Expand All @@ -33,6 +34,8 @@ public enum KlaviyoEndpoint: Equatable, Codable {
return "/client/push-tokens/"
case .unregisterPushToken:
return "/client/push-token-unregister/"
case .aggregateEvent:
return "/onsite/track-analytics"
}
}

Expand All @@ -46,6 +49,8 @@ public enum KlaviyoEndpoint: Equatable, Codable {
return try environment.encodeJSON(payload)
case let .unregisterPushToken(payload):
return try environment.encodeJSON(payload)
case let .aggregateEvent(payload):
return payload
}
}
}
7 changes: 5 additions & 2 deletions Sources/KlaviyoCore/Networking/SDKRequestIterator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ public struct SDKRequest: Identifiable, Equatable {
}

case createEvent(EventInfo, ProfileInfo)
case createAggregateEvent(Data)
case createProfile(ProfileInfo)
case saveToken(token: String, info: ProfileInfo)
case unregisterToken(token: String, info: ProfileInfo)

static func fromEndpoint(request: KlaviyoRequest) -> RequestType {
fileprivate static func fromEndpoint(request: KlaviyoRequest) -> RequestType {
switch request.endpoint {
case let .createProfile(payload):
return .createProfile(ProfileInfo(
Expand All @@ -61,6 +62,8 @@ public struct SDKRequest: Identifiable, Equatable {
phoneNumber: payload.data.attributes.profile.data.attributes.phoneNumber,
externalId: payload.data.attributes.profile.data.attributes.externalId,
anonymousId: payload.data.attributes.profile.data.attributes.anonymousId))
case let .aggregateEvent(payload):
return .createAggregateEvent(payload)
case let .registerPushToken(payload):
return .saveToken(token: payload.data.attributes.token, info:
ProfileInfo(email: payload.data.attributes.profile.data.attributes.email,
Expand All @@ -85,7 +88,7 @@ public struct SDKRequest: Identifiable, Equatable {
case requestError(String, Double)
}

static func fromAPIRequest(request: KlaviyoRequest, urlRequest: URLRequest?, response: SDKRequest.Response) -> SDKRequest {
fileprivate static func fromAPIRequest(request: KlaviyoRequest, urlRequest: URLRequest?, response: SDKRequest.Response) -> SDKRequest {
let type = RequestType.fromEndpoint(request: request)
let method = urlRequest?.httpMethod ?? "Unknown"
let url = urlRequest?.url?.description ?? "Unknown"
Expand Down
17 changes: 17 additions & 0 deletions Sources/KlaviyoSwift/KlaviyoInternal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// KlaviyoSDK.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 2/4/25.
//

import KlaviyoCore

/// The internal interface for the Klaviyo SDK. Can only be accessed from other modules within the Klaviyo-Swift-SDK package; cannot be accessed from the host app.
package struct KlaviyoInternal {
/// Create and send an aggregate event.
/// - Parameter event: the event to be tracked in Klaviyo
package static func create(aggregateEvent: AggregateEventPayload) {
dispatchOnMainThread(action: .enqueueAggregateEvent(aggregateEvent))
}
}
1 change: 1 addition & 0 deletions Sources/KlaviyoSwift/StateManagement/KlaviyoState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct KlaviyoState: Equatable, Codable {

enum PendingRequest: Equatable {
case event(Event)
case aggregateEvent(Data)
case profile(Profile)
case pushToken(String, PushEnablement)
case setEmail(String)
Expand Down
22 changes: 21 additions & 1 deletion Sources/KlaviyoSwift/StateManagement/StateManagement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ enum KlaviyoAction: Equatable {
/// when there is an event to be sent to klaviyo it's added to the queue
case enqueueEvent(Event)

/// when there is an aggregate event to be sent to klaviyo it's added to the queue
case enqueueAggregateEvent(Data)

/// when there is an profile to be sent to klaviyo it's added to the queue
case enqueueProfile(Profile)

Expand All @@ -105,7 +108,7 @@ enum KlaviyoAction: Equatable {
case let .enqueueEvent(event) where event.metric.name == ._openedPush:
return false

case .enqueueEvent, .enqueueProfile, .resetProfile, .resetStateAndDequeue, .setBadgeCount, .setEmail, .setExternalId, .setPhoneNumber, .setProfileProperty, .setPushEnablement, .setPushToken:
case .enqueueAggregateEvent, .enqueueEvent, .enqueueProfile, .resetProfile, .resetStateAndDequeue, .setBadgeCount, .setEmail, .setExternalId, .setPhoneNumber, .setProfileProperty, .setPushEnablement, .setPushToken:
return true

case .cancelInFlightRequests, .completeInitialization, .deQueueCompletedResults, .flushQueue, .initialize, .networkConnectivityChanged, .requestFailed, .sendRequest, .start, .stop, .syncBadgeCount:
Expand Down Expand Up @@ -185,6 +188,8 @@ struct KlaviyoReducer: ReducerProtocol {
switch request {
case let .event(event):
await send(.enqueueEvent(event))
case let .aggregateEvent(payload):
await send(.enqueueAggregateEvent(payload))
case let .profile(profile):
await send(.enqueueProfile(profile))
case let .pushToken(token, enablement):
Expand Down Expand Up @@ -453,6 +458,21 @@ struct KlaviyoReducer: ReducerProtocol {
*/
return event.metric.name == ._openedPush ? .task { .flushQueue } : .none

case let .enqueueAggregateEvent(payload):
guard case .initialized = state.initalizationState,
let apiKey = state.apiKey
else {
state.pendingRequests.append(.aggregateEvent(payload))
return .none
}

let endpoint = KlaviyoEndpoint.aggregateEvent(payload)
let request = KlaviyoRequest(apiKey: apiKey, endpoint: endpoint)

state.enqueueRequest(request: request)

return .none

case let .enqueueProfile(profile):
guard case .initialized = state.initalizationState
else {
Expand Down
2 changes: 1 addition & 1 deletion Sources/KlaviyoUI/InAppForms/IAFPresentationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class IAFPresentationManager {

isLoading = true

let viewModel = IafWebViewModel(url: fileUrl)
let viewModel = IAFWebViewModel(url: fileUrl)
let viewController = KlaviyoWebViewController(viewModel: viewModel)
viewController.modalPresentationStyle = .overCurrentContext

Expand Down
70 changes: 70 additions & 0 deletions Sources/KlaviyoUI/InAppForms/IAFWebViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// IAFWebViewModel.swift
// TestApp
//
// Created by Andrew Balmer on 1/27/25.
//

import Combine
import Foundation
import KlaviyoSwift
import WebKit

class IAFWebViewModel: KlaviyoWebViewModeling {
private enum MessageHandler: String, CaseIterable {
case klaviyoNativeBridge = "KlaviyoNativeBridge"
}

weak var delegate: KlaviyoWebViewDelegate?

let url: URL
var loadScripts: Set<WKUserScript>?
var messageHandlers: Set<String>? = Set(MessageHandler.allCases.map(\.rawValue))

public let (navEventStream, navEventContinuation) = AsyncStream.makeStream(of: WKNavigationEvent.self)

init(url: URL) {
self.url = url
}

// MARK: handle WKWebView events

func handleScriptMessage(_ message: WKScriptMessage) {
guard let handler = MessageHandler(rawValue: message.name) else {
// script message has no handler
return
}

switch handler {
case .klaviyoNativeBridge:
guard let jsonString = message.body as? String else { return }

do {
let jsonData = Data(jsonString.utf8) // Convert string to Data
let messageBusEvent = try JSONDecoder().decode(IAFNativeBridgeEvent.self, from: jsonData)
handleNativeBridgeEvent(messageBusEvent)
} catch {
print("Failed to decode JSON: \(error)")
}
}
}

private func handleNativeBridgeEvent(_ event: IAFNativeBridgeEvent) {
switch event {
case .formsDataLoaded:
// TODO: handle formsDataLoaded
()
case .formAppeared:
// TODO: handle formAppeared
()
case let .trackAggregateEvent(data):
KlaviyoInternal.create(aggregateEvent: data)
case .trackProfileEvent:
// TODO: handle tracktProfileEvent
()
case .openDeepLink:
// TODO: handle openDeepLink
()
}
}
}
45 changes: 0 additions & 45 deletions Sources/KlaviyoUI/InAppForms/IafWebViewModel.swift

This file was deleted.

51 changes: 51 additions & 0 deletions Sources/KlaviyoUI/InAppForms/Models/IAFNativeBridgeEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// IAFNativeBridgeEvent.swift
// TestApp
//
// Created by Andrew Balmer on 2/3/25.
//

import AnyCodable
import Foundation

enum IAFNativeBridgeEvent: Decodable, Equatable {
// TODO: add associated values with the appropriate data types
case formsDataLoaded
case formAppeared
case trackAggregateEvent(Data)
case trackProfileEvent
case openDeepLink

private enum CodingKeys: String, CodingKey {
case type
case data
}

private enum TypeIdentifier: String, Decodable {
case formsDataLoaded
case formAppeared
case trackAggregateEvent
case trackProfileEvent
case openDeepLink
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeIdentifier = try container.decode(TypeIdentifier.self, forKey: .type)

switch typeIdentifier {
case .formsDataLoaded:
self = .formsDataLoaded
case .formAppeared:
self = .formAppeared
case .trackAggregateEvent:
let decodedData = try container.decode(AnyCodable.self, forKey: .data)
let data = try JSONEncoder().encode(decodedData)
self = .trackAggregateEvent(data)
case .trackProfileEvent:
self = .trackProfileEvent
case .openDeepLink:
self = .openDeepLink
}
}
}
46 changes: 46 additions & 0 deletions Tests/KlaviyoSwiftTests/StateManagementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -619,4 +619,50 @@ class StateManagementTests: XCTestCase {
await store.receive(.setPushEnablement(PushEnablement.authorized), timeout: TIMEOUT_NANOSECONDS)
await store.receive(.setBadgeCount(0))
}

// MARK: - Test enqueue aggregate event

@MainActor
func testEnqueueAggregateEvent() async throws {
let initialState = INITIALIZED_TEST_STATE()
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

let data = Data()
await store.send(.enqueueAggregateEvent(data)) {
try $0.enqueueRequest(
request: KlaviyoRequest(
apiKey: XCTUnwrap($0.apiKey),
endpoint: .aggregateEvent(AggregateEventPayload(data)))
)
}
}

@MainActor
func testEnqueueAggregateEventWhenInitilizingSendsEvent() async throws {
let initialState = INITILIZING_TEST_STATE()
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

let data = Data()
await store.send(.enqueueAggregateEvent(data)) {
$0.pendingRequests = [KlaviyoState.PendingRequest.aggregateEvent(data)]
}

await store.send(.completeInitialization(initialState)) {
$0.pendingRequests = []
$0.initalizationState = .initialized
}

await store.receive(.enqueueAggregateEvent(data), timeout: TIMEOUT_NANOSECONDS) {
try $0.enqueueRequest(
request: KlaviyoRequest(
apiKey: XCTUnwrap($0.apiKey),
endpoint: .aggregateEvent(AggregateEventPayload(data)))
)
}

await store.receive(.start, timeout: TIMEOUT_NANOSECONDS)
await store.receive(.flushQueue, timeout: TIMEOUT_NANOSECONDS)
await store.receive(.setPushEnablement(PushEnablement.authorized), timeout: TIMEOUT_NANOSECONDS)
await store.receive(.setBadgeCount(0))
}
}
Loading

0 comments on commit 9336cb8

Please sign in to comment.