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
61 changes: 61 additions & 0 deletions swift-sdk/SDK/IterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,29 @@ import UIKit
public static func disableDeviceForCurrentUser() {
disableDeviceForCurrentUser(withOnSuccess: nil, onFailure: nil)
}

/// Disable this device's token in Iterable, for the current user.
@nonobjc
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public static func disableDeviceForCurrentUser() async throws {
try await disableDeviceAsync { onSuccess, onFailure in
disableDeviceForCurrentUser(withOnSuccess: onSuccess, onFailure: onFailure)
}
}

/// Disable this device's token in Iterable, for all users on this device.
public static func disableDeviceForAllUsers() {
disableDeviceForAllUsers(withOnSuccess: nil, onFailure: nil)
}

/// Disable this device's token in Iterable, for all users on this device.
@nonobjc
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public static func disableDeviceForAllUsers() async throws {
try await disableDeviceAsync { onSuccess, onFailure in
disableDeviceForAllUsers(withOnSuccess: onSuccess, onFailure: onFailure)
}
}

/// Disable this device's token in Iterable, for the current user, with custom completion blocks
///
Expand Down Expand Up @@ -377,6 +395,29 @@ import UIKit

implementation.disableDeviceForAllUsers(withOnSuccess: onSuccess, onFailure: onFailure)
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
private static func disableDeviceAsync(
_ disableDevice: (_ onSuccess: OnSuccessHandler?, _ onFailure: OnFailureHandler?) -> Void
) async throws {
guard let implementation, implementation.isSDKInitialized() else {
throw SendRequestError(reason: sdkNotInitializedErrorReason)
}

try await withCheckedThrowingContinuation { continuation in
let resumeGuard = AsyncContinuationResumeGuard()

disableDevice({ _ in
resumeGuard.resume {
continuation.resume(returning: ())
}
}, { reason, data in
resumeGuard.resume {
continuation.resume(throwing: SendRequestError(reason: reason, data: data))
}
})
}
}

/// Updates the available user fields
///
Expand Down Expand Up @@ -911,7 +952,27 @@ import UIKit

// MARK: - Private/Internal

private static let sdkNotInitializedErrorReason = "Iterable SDK is not initialized"

static var implementation: InternalIterableAPI?

override private init() { super.init() }
}

private final class AsyncContinuationResumeGuard {
private let lock = NSLock()
private var didResume = false

func resume(_ block: () -> Void) {
lock.lock()
let shouldResume = !didResume
if shouldResume {
didResume = true
}
lock.unlock()

if shouldResume {
block()
}
}
}
146 changes: 146 additions & 0 deletions tests/unit-tests/IterableAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,102 @@ class IterableAPITests: XCTestCase {

wait(for: [expectation], timeout: testExpectationTimeout)
}

@available(iOS 13.0, *)
func testDisableDeviceForCurrentUserAsyncSuccess() async throws {
let oldImplementation = IterableAPI.implementation
defer { IterableAPI.implementation = oldImplementation }

let networkSession = MockNetworkSession(statusCode: 200)
let token = try await setUpIterableAPIForAsyncDisableDevice(networkSession: networkSession)

try await IterableAPI.disableDeviceForCurrentUser()

guard let request = networkSession.getRequest(withEndPoint: Const.Path.disableDevice) else {
return XCTFail("Expected disableDevice request")
}
guard let body = TestUtils.getRequestBody(request: request) else {
return XCTFail("Expected disableDevice request body")
}

TestUtils.validate(request: request,
requestType: .post,
apiEndPoint: Endpoint.api,
path: Const.Path.disableDevice,
queryParams: [])
TestUtils.validateElementPresent(withName: JsonKey.token, andValue: token.hexString(), inDictionary: body)
TestUtils.validateElementPresent(withName: JsonKey.email, andValue: "user@example.com", inDictionary: body)
}

@available(iOS 13.0, *)
func testDisableDeviceForAllUsersAsyncSuccess() async throws {
let oldImplementation = IterableAPI.implementation
defer { IterableAPI.implementation = oldImplementation }

let networkSession = MockNetworkSession(statusCode: 200)
let token = try await setUpIterableAPIForAsyncDisableDevice(networkSession: networkSession)

try await IterableAPI.disableDeviceForAllUsers()

guard let request = networkSession.getRequest(withEndPoint: Const.Path.disableDevice) else {
return XCTFail("Expected disableDevice request")
}
guard let body = TestUtils.getRequestBody(request: request) else {
return XCTFail("Expected disableDevice request body")
}

TestUtils.validate(request: request,
requestType: .post,
apiEndPoint: Endpoint.api,
path: Const.Path.disableDevice,
queryParams: [])
TestUtils.validateElementPresent(withName: JsonKey.token, andValue: token.hexString(), inDictionary: body)
TestUtils.validateElementNotPresent(withName: JsonKey.email, inDictionary: body)
TestUtils.validateElementNotPresent(withName: JsonKey.userId, inDictionary: body)
}

@available(iOS 13.0, *)
func testDisableDeviceForCurrentUserAsyncFailureThrowsSendRequestError() async throws {
let oldImplementation = IterableAPI.implementation
defer { IterableAPI.implementation = oldImplementation }

let failureReason = "disable failed"
let networkSession = MockNetworkSession(responseCallback: { url in
if url.absoluteString.contains(Const.Path.disableDevice) {
return MockNetworkSession.MockResponse(statusCode: 400,
data: ["msg": failureReason].toJsonData())
}
return MockNetworkSession.MockResponse(statusCode: 200)
})
_ = try await setUpIterableAPIForAsyncDisableDevice(networkSession: networkSession)

do {
try await IterableAPI.disableDeviceForCurrentUser()
XCTFail("Expected disableDeviceForCurrentUser async API to throw")
} catch let error as SendRequestError {
XCTAssertEqual(error.reason, failureReason)
XCTAssertEqual(error.data, ["msg": failureReason].toJsonData())
} catch {
XCTFail("Expected SendRequestError, got \(error)")
}
}

@available(iOS 13.0, *)
func testDisableDeviceForCurrentUserAsyncNotInitializedThrowsSendRequestError() async {
let oldImplementation = IterableAPI.implementation
IterableAPI.implementation = nil
defer { IterableAPI.implementation = oldImplementation }

do {
try await IterableAPI.disableDeviceForCurrentUser()
XCTFail("Expected disableDeviceForCurrentUser async API to throw")
} catch let error as SendRequestError {
XCTAssertEqual(error.reason, "Iterable SDK is not initialized")
XCTAssertNil(error.data)
} catch {
XCTFail("Expected SendRequestError, got \(error)")
}
}

func testUpdateCart() {
let condition1 = XCTestExpectation(description: #function)
Expand Down Expand Up @@ -1478,4 +1574,54 @@ class IterableAPITests: XCTestCase {
XCTAssertEqual(dateFromMilliseconds, testDate)
}

@available(iOS 13.0, *)
private func setUpIterableAPIForAsyncDisableDevice(networkSession: MockNetworkSession) async throws -> Data {
let config = IterableConfig()
config.pushIntegrationName = "my-push-integration"

IterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey,
config: config,
networkSession: networkSession)
IterableAPI.email = "user@example.com"

let token = "zeeToken".data(using: .utf8)!
try await registerTokenForAsyncDisableDevice(token)
return token
}

@available(iOS 13.0, *)
private func registerTokenForAsyncDisableDevice(_ token: Data) async throws {
try await withCheckedThrowingContinuation { continuation in
let resumeGuard = TestAsyncContinuationResumeGuard()

IterableAPI.register(token: token, onSuccess: { _ in
resumeGuard.resume {
continuation.resume(returning: ())
}
}, onFailure: { reason, data in
resumeGuard.resume {
continuation.resume(throwing: SendRequestError(reason: reason, data: data))
}
})
}
}

}

private final class TestAsyncContinuationResumeGuard {
private let lock = NSLock()
private var didResume = false

func resume(_ block: () -> Void) {
lock.lock()
let shouldResume = !didResume
if shouldResume {
didResume = true
}
lock.unlock()

if shouldResume {
block()
}
}
}
Loading