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
1 change: 1 addition & 0 deletions swift-sdk/Core/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ enum JsonKey {
enum Embedded {
static let packageName = "packageName"
static let sdkVersion = "SDKVersion"
static let placementIds = "placementIds"
}

enum Header {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ public protocol IterableEmbeddedManagerProtocol {
func addUpdateListener(_ listener: IterableEmbeddedUpdateDelegate)
func removeUpdateListener(_ listener: IterableEmbeddedUpdateDelegate)

func syncMessages(completion: @escaping () -> Void)
func syncMessages(placementIds: [Int]?, completion: @escaping () -> Void)
func handleEmbeddedClick(message: IterableEmbeddedMessage, buttonIdentifier: String?, clickedUrl: String)
func reset()
}

public extension IterableEmbeddedManagerProtocol {
func syncMessages(completion: @escaping () -> Void) {
syncMessages(placementIds: nil, completion: completion)
}
}
4 changes: 4 additions & 0 deletions swift-sdk/Internal/EmptyEmbeddedManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class EmptyEmbeddedManager: IterableInternalEmbeddedManagerProtocol {

func syncMessages(completion: @escaping () -> Void) {

}

func syncMessages(placementIds: [Int]?, completion: @escaping () -> Void) {

}

public func handleEmbeddedClick(message: IterableEmbeddedMessage, buttonIdentifier: String?, clickedUrl: String) {
Expand Down
25 changes: 20 additions & 5 deletions swift-sdk/Internal/IterableEmbeddedManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ class IterableEmbeddedManager: NSObject, IterableInternalEmbeddedManagerProtocol
syncMessages { }
}

private func retrieveEmbeddedMessages(completion: @escaping () -> Void) {
apiClient.getEmbeddedMessages()
private func retrieveEmbeddedMessages(placementIds: [Int]?, completion: @escaping () -> Void) {
apiClient.getEmbeddedMessages(placementIds: placementIds)
.onCompletion(
receiveValue: { embeddedMessagesPayload in
let placements = embeddedMessagesPayload.placements
Expand All @@ -172,7 +172,18 @@ class IterableEmbeddedManager: NSObject, IterableInternalEmbeddedManagerProtocol
fetchedMessagesDict[placement.placementId!] = placement.embeddedMessages
}

let processor = EmbeddedMessagingProcessor(currentMessages: self.messages,
let currentMessagesSnapshot: [Int: [IterableEmbeddedMessage]] = self.messageProcessingQueue.sync {
self.messages
}

if let placementIds, !placementIds.isEmpty {
let requestedPlacementIds = Set(placementIds)
for (placementId, currentMessages) in currentMessagesSnapshot where !requestedPlacementIds.contains(placementId) {
fetchedMessagesDict[placementId] = currentMessages
}
}

let processor = EmbeddedMessagingProcessor(currentMessages: currentMessagesSnapshot,
fetchedMessages: fetchedMessagesDict)

self.setMessages(processor)
Expand Down Expand Up @@ -260,8 +271,12 @@ class IterableEmbeddedManager: NSObject, IterableInternalEmbeddedManagerProtocol

extension IterableEmbeddedManager: EmbeddedNotifiable {
public func syncMessages(completion: @escaping () -> Void) {
if (enableEmbeddedMessaging) {
retrieveEmbeddedMessages(completion: completion)
syncMessages(placementIds: nil, completion: completion)
}

public func syncMessages(placementIds: [Int]?, completion: @escaping () -> Void) {
if enableEmbeddedMessaging {
retrieveEmbeddedMessages(placementIds: placementIds, completion: completion)
}
}
}
4 changes: 2 additions & 2 deletions swift-sdk/Internal/api-client/ApiClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,8 @@ extension ApiClient: ApiClientProtocol {

// MARK: - Embedded Messaging

func getEmbeddedMessages() -> Pending<PlacementsPayload, SendRequestError> {
let result = createRequestCreator().flatMap { $0.createGetEmbeddedMessagesRequest() }
func getEmbeddedMessages(placementIds: [Int]?) -> Pending<PlacementsPayload, SendRequestError> {
let result = createRequestCreator().flatMap { $0.createGetEmbeddedMessagesRequest(placementIds: placementIds) }
return send(iterableRequestResult: result)
}

Expand Down
8 changes: 7 additions & 1 deletion swift-sdk/Internal/api-client/ApiClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ protocol ApiClientProtocol: AnyObject {

func trackConsent(consentTimestamp: Int64, email: String?, userId: String?, isUserKnown: Bool) -> Pending<SendRequestValue, SendRequestError>

func getEmbeddedMessages() -> Pending<PlacementsPayload, SendRequestError>
func getEmbeddedMessages(placementIds: [Int]?) -> Pending<PlacementsPayload, SendRequestError>

@discardableResult func track(embeddedMessageReceived message: IterableEmbeddedMessage) -> Pending<SendRequestValue, SendRequestError>

Expand All @@ -72,3 +72,9 @@ protocol ApiClientProtocol: AnyObject {

@discardableResult func track(embeddedSession: IterableEmbeddedSession) -> Pending<SendRequestValue, SendRequestError>
}

extension ApiClientProtocol {
func getEmbeddedMessages() -> Pending<PlacementsPayload, SendRequestError> {
getEmbeddedMessages(placementIds: nil)
}
}
26 changes: 22 additions & 4 deletions swift-sdk/Internal/api-client/Request/RequestCreator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ struct RequestCreator {

// MARK: - Embedded Messaging Request Calls

func createGetEmbeddedMessagesRequest() -> Result<IterableRequest, IterableError> {
func createGetEmbeddedMessagesRequest(placementIds: [Int]?) -> Result<IterableRequest, IterableError> {
if case .none = auth.emailOrUserId {
ITBError(Self.authMissingMessage)
return .failure(IterableError.general(description: Self.authMissingMessage))
Expand All @@ -513,10 +513,28 @@ struct RequestCreator {
if let packageName = Bundle.main.appPackageName {
args[JsonKey.Embedded.packageName] = packageName
}

setCurrentUser(inDict: &args)

return .success(.get(createGetRequest(forPath: Const.Path.getEmbeddedMessages, withArgs: args as! [String: String])))

let placementIdQueryItems: [URLQueryItem]
if let placementIds, !placementIds.isEmpty {
placementIdQueryItems = placementIds.map { URLQueryItem(name: JsonKey.Embedded.placementIds, value: String($0)) }
} else {
placementIdQueryItems = []
}

let argsQueryItems: [URLQueryItem] = args.compactMap { key, value in
guard let key = key as? String, let value = value as? String else { return nil }
if key == JsonKey.Embedded.placementIds { return nil }
return URLQueryItem(name: key, value: value)
}

var components = URLComponents()
components.path = Const.Path.getEmbeddedMessages
components.queryItems = argsQueryItems + placementIdQueryItems
let query = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
let pathWithQuery = components.path + (query.map { "?\($0)" } ?? "")

return .success(.get(GetRequest(path: pathWithQuery, args: nil)))
}

func createEmbeddedMessageReceivedRequest(_ message: IterableEmbeddedMessage) -> Result<IterableRequest, IterableError> {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit-tests/BlankApiClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class BlankApiClient: ApiClientProtocol {
Pending()
}

func getEmbeddedMessages() -> Pending<PlacementsPayload, SendRequestError> {
func getEmbeddedMessages(placementIds: [Int]?) -> Pending<PlacementsPayload, SendRequestError> {
Pending()
}

Expand Down
36 changes: 33 additions & 3 deletions tests/unit-tests/EmbeddedManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,33 @@ final class EmbeddedManagerTests: XCTestCase {

wait(for: [syncMessagesExpectation, delegateExpectation, syncSuccessExpectation], timeout: 2)
}

func testSyncMessagesWithPlacementIdsDoesNotClearOtherPlacements() {
let mockApiClient = MockApiClient()
mockApiClient.populateMessages([
1: [IterableEmbeddedMessage(messageId: "1a", placementId: 1)],
2: [IterableEmbeddedMessage(messageId: "2a", placementId: 2)],
])

let manager = IterableEmbeddedManager(apiClient: mockApiClient,
urlDelegate: nil,
customActionDelegate: nil,
urlOpener: MockUrlOpener(),
allowedProtocols: [],
enableEmbeddedMessaging: true)

manager.syncMessages { }
XCTAssertEqual(manager.getMessages(for: 2).map { $0.metadata.messageId }, ["2a"])

// Update only placement 1 on the "server", then request only that placement.
mockApiClient.populateMessages([
1: [IterableEmbeddedMessage(messageId: "1b", placementId: 1)],
])
manager.syncMessages(placementIds: [1]) { }

XCTAssertEqual(manager.getMessages(for: 1).map { $0.metadata.messageId }, ["1b"])
XCTAssertEqual(manager.getMessages(for: 2).map { $0.metadata.messageId }, ["2a"])
}

func testManagerReset() {
let syncMessagesExpectation = expectation(description: "syncMessages should complete")
Expand Down Expand Up @@ -392,16 +419,19 @@ final class EmbeddedManagerTests: XCTestCase {
invalidApiKey = true
}

override func getEmbeddedMessages() -> IterableSDK.Pending<IterableSDK.PlacementsPayload, IterableSDK.SendRequestError> {
override func getEmbeddedMessages(placementIds: [Int]?) -> IterableSDK.Pending<IterableSDK.PlacementsPayload, IterableSDK.SendRequestError> {
if invalidApiKey {
return FailPending(error: IterableSDK.SendRequestError(reason: "Invalid API Key"))
}

if newMessages {
var placements: [Placement] = []
let requested = Set(placementIds ?? [])
for (placementId, messages) in mockMessages {
let placement = Placement(placementId: placementId, embeddedMessages: messages)
placements.append(placement)
if placementIds == nil || requested.contains(placementId) {
let placement = Placement(placementId: placementId, embeddedMessages: messages)
placements.append(placement)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not quite understood this part. is it trying to add placement to list of placement once it sees it has new messages for that placement?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, pretty much. It’s not “seeing new messages per placement”—the newMessages boolean gates the whole response. Once newMessages is true, it adds a placement entry for each placementId in mockMessages that was requested (or all of them if placementIds is nil), using whatever messages are stored there.

}
}

let payload = PlacementsPayload(placements: placements)
Expand Down
52 changes: 52 additions & 0 deletions tests/unit-tests/RequestCreatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,58 @@ class RequestCreatorTests: XCTestCase {
XCTAssertEqual(args[JsonKey.InApp.count], inAppMessageRequestCount.stringValue)
XCTAssertEqual(args[JsonKey.systemVersion], UIDevice.current.systemVersion)
}

func testGetEmbeddedMessagesRequestFailure() {
let auth = Auth(userId: nil, email: nil, authToken: nil, userIdUnknownUser: nil)
let requestCreator = RequestCreator(auth: auth, deviceMetadata: deviceMetadata)

let failingRequest = requestCreator.createGetEmbeddedMessagesRequest(placementIds: nil)

if let _ = try? failingRequest.get() {
XCTFail("request succeeded despite userId and email being nil")
}
}

func testGetEmbeddedMessagesRequest() {
let request = createRequestCreator().createGetEmbeddedMessagesRequest(placementIds: nil)
let urlRequest = convertToUrlRequest(request)

TestUtils.validateHeader(urlRequest, apiKey)
TestUtils.validate(request: urlRequest, requestType: .get, apiEndPoint: Endpoint.api, path: Const.Path.getEmbeddedMessages)

guard let url = urlRequest.url, let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
XCTFail("could not create URLComponents from request url")
return
}

let queryItems = urlComponents.queryItems ?? []
XCTAssertEqual(queryItems.first(where: { $0.name == JsonKey.email })?.value, auth.email)
XCTAssertEqual(queryItems.first(where: { $0.name == JsonKey.Embedded.packageName })?.value, Bundle.main.appPackageName)
XCTAssertEqual(queryItems.first(where: { $0.name == JsonKey.systemVersion })?.value, UIDevice.current.systemVersion)
XCTAssertTrue(queryItems.filter { $0.name == JsonKey.Embedded.placementIds }.isEmpty)
}

func testGetEmbeddedMessagesRequestWithPlacementIds() {
let request = createRequestCreator().createGetEmbeddedMessagesRequest(placementIds: [1, 2, 3])
let urlRequest = convertToUrlRequest(request)

TestUtils.validateHeader(urlRequest, apiKey)
TestUtils.validate(request: urlRequest, requestType: .get, apiEndPoint: Endpoint.api, path: Const.Path.getEmbeddedMessages)

guard let url = urlRequest.url, let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
XCTFail("could not create URLComponents from request url")
return
}

let queryItems = urlComponents.queryItems ?? []
XCTAssertEqual(queryItems.first(where: { $0.name == JsonKey.email })?.value, auth.email)

let placementIds = queryItems
.filter { $0.name == JsonKey.Embedded.placementIds }
.compactMap(\.value)

XCTAssertEqual(placementIds, ["1", "2", "3"])
}

func testTrackEventRequest() {
let eventName = "dsfsdf"
Expand Down
Loading