Skip to content

Commit 3ac2f02

Browse files
sumeruchatclaude
andauthored
SDK-99 Add async/await variants for disablePush (#1067)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2f85293 commit 3ac2f02

2 files changed

Lines changed: 207 additions & 0 deletions

File tree

swift-sdk/SDK/IterableAPI.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,11 +340,29 @@ import UIKit
340340
public static func disableDeviceForCurrentUser() {
341341
disableDeviceForCurrentUser(withOnSuccess: nil, onFailure: nil)
342342
}
343+
344+
/// Disable this device's token in Iterable, for the current user.
345+
@nonobjc
346+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
347+
public static func disableDeviceForCurrentUser() async throws {
348+
try await disableDeviceAsync { onSuccess, onFailure in
349+
disableDeviceForCurrentUser(withOnSuccess: onSuccess, onFailure: onFailure)
350+
}
351+
}
343352

344353
/// Disable this device's token in Iterable, for all users on this device.
345354
public static func disableDeviceForAllUsers() {
346355
disableDeviceForAllUsers(withOnSuccess: nil, onFailure: nil)
347356
}
357+
358+
/// Disable this device's token in Iterable, for all users on this device.
359+
@nonobjc
360+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
361+
public static func disableDeviceForAllUsers() async throws {
362+
try await disableDeviceAsync { onSuccess, onFailure in
363+
disableDeviceForAllUsers(withOnSuccess: onSuccess, onFailure: onFailure)
364+
}
365+
}
348366

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

378396
implementation.disableDeviceForAllUsers(withOnSuccess: onSuccess, onFailure: onFailure)
379397
}
398+
399+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
400+
private static func disableDeviceAsync(
401+
_ disableDevice: (_ onSuccess: OnSuccessHandler?, _ onFailure: OnFailureHandler?) -> Void
402+
) async throws {
403+
guard let implementation, implementation.isSDKInitialized() else {
404+
throw SendRequestError(reason: sdkNotInitializedErrorReason)
405+
}
406+
407+
try await withCheckedThrowingContinuation { continuation in
408+
let resumeGuard = AsyncContinuationResumeGuard()
409+
410+
disableDevice({ _ in
411+
resumeGuard.resume {
412+
continuation.resume(returning: ())
413+
}
414+
}, { reason, data in
415+
resumeGuard.resume {
416+
continuation.resume(throwing: SendRequestError(reason: reason, data: data))
417+
}
418+
})
419+
}
420+
}
380421

381422
/// Updates the available user fields
382423
///
@@ -911,7 +952,27 @@ import UIKit
911952

912953
// MARK: - Private/Internal
913954

955+
private static let sdkNotInitializedErrorReason = "Iterable SDK is not initialized"
956+
914957
static var implementation: InternalIterableAPI?
915958

916959
override private init() { super.init() }
917960
}
961+
962+
private final class AsyncContinuationResumeGuard {
963+
private let lock = NSLock()
964+
private var didResume = false
965+
966+
func resume(_ block: () -> Void) {
967+
lock.lock()
968+
let shouldResume = !didResume
969+
if shouldResume {
970+
didResume = true
971+
}
972+
lock.unlock()
973+
974+
if shouldResume {
975+
block()
976+
}
977+
}
978+
}

tests/unit-tests/IterableAPITests.swift

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,102 @@ class IterableAPITests: XCTestCase {
681681

682682
wait(for: [expectation], timeout: testExpectationTimeout)
683683
}
684+
685+
@available(iOS 13.0, *)
686+
func testDisableDeviceForCurrentUserAsyncSuccess() async throws {
687+
let oldImplementation = IterableAPI.implementation
688+
defer { IterableAPI.implementation = oldImplementation }
689+
690+
let networkSession = MockNetworkSession(statusCode: 200)
691+
let token = try await setUpIterableAPIForAsyncDisableDevice(networkSession: networkSession)
692+
693+
try await IterableAPI.disableDeviceForCurrentUser()
694+
695+
guard let request = networkSession.getRequest(withEndPoint: Const.Path.disableDevice) else {
696+
return XCTFail("Expected disableDevice request")
697+
}
698+
guard let body = TestUtils.getRequestBody(request: request) else {
699+
return XCTFail("Expected disableDevice request body")
700+
}
701+
702+
TestUtils.validate(request: request,
703+
requestType: .post,
704+
apiEndPoint: Endpoint.api,
705+
path: Const.Path.disableDevice,
706+
queryParams: [])
707+
TestUtils.validateElementPresent(withName: JsonKey.token, andValue: token.hexString(), inDictionary: body)
708+
TestUtils.validateElementPresent(withName: JsonKey.email, andValue: "user@example.com", inDictionary: body)
709+
}
710+
711+
@available(iOS 13.0, *)
712+
func testDisableDeviceForAllUsersAsyncSuccess() async throws {
713+
let oldImplementation = IterableAPI.implementation
714+
defer { IterableAPI.implementation = oldImplementation }
715+
716+
let networkSession = MockNetworkSession(statusCode: 200)
717+
let token = try await setUpIterableAPIForAsyncDisableDevice(networkSession: networkSession)
718+
719+
try await IterableAPI.disableDeviceForAllUsers()
720+
721+
guard let request = networkSession.getRequest(withEndPoint: Const.Path.disableDevice) else {
722+
return XCTFail("Expected disableDevice request")
723+
}
724+
guard let body = TestUtils.getRequestBody(request: request) else {
725+
return XCTFail("Expected disableDevice request body")
726+
}
727+
728+
TestUtils.validate(request: request,
729+
requestType: .post,
730+
apiEndPoint: Endpoint.api,
731+
path: Const.Path.disableDevice,
732+
queryParams: [])
733+
TestUtils.validateElementPresent(withName: JsonKey.token, andValue: token.hexString(), inDictionary: body)
734+
TestUtils.validateElementNotPresent(withName: JsonKey.email, inDictionary: body)
735+
TestUtils.validateElementNotPresent(withName: JsonKey.userId, inDictionary: body)
736+
}
737+
738+
@available(iOS 13.0, *)
739+
func testDisableDeviceForCurrentUserAsyncFailureThrowsSendRequestError() async throws {
740+
let oldImplementation = IterableAPI.implementation
741+
defer { IterableAPI.implementation = oldImplementation }
742+
743+
let failureReason = "disable failed"
744+
let networkSession = MockNetworkSession(responseCallback: { url in
745+
if url.absoluteString.contains(Const.Path.disableDevice) {
746+
return MockNetworkSession.MockResponse(statusCode: 400,
747+
data: ["msg": failureReason].toJsonData())
748+
}
749+
return MockNetworkSession.MockResponse(statusCode: 200)
750+
})
751+
_ = try await setUpIterableAPIForAsyncDisableDevice(networkSession: networkSession)
752+
753+
do {
754+
try await IterableAPI.disableDeviceForCurrentUser()
755+
XCTFail("Expected disableDeviceForCurrentUser async API to throw")
756+
} catch let error as SendRequestError {
757+
XCTAssertEqual(error.reason, failureReason)
758+
XCTAssertEqual(error.data, ["msg": failureReason].toJsonData())
759+
} catch {
760+
XCTFail("Expected SendRequestError, got \(error)")
761+
}
762+
}
763+
764+
@available(iOS 13.0, *)
765+
func testDisableDeviceForCurrentUserAsyncNotInitializedThrowsSendRequestError() async {
766+
let oldImplementation = IterableAPI.implementation
767+
IterableAPI.implementation = nil
768+
defer { IterableAPI.implementation = oldImplementation }
769+
770+
do {
771+
try await IterableAPI.disableDeviceForCurrentUser()
772+
XCTFail("Expected disableDeviceForCurrentUser async API to throw")
773+
} catch let error as SendRequestError {
774+
XCTAssertEqual(error.reason, "Iterable SDK is not initialized")
775+
XCTAssertNil(error.data)
776+
} catch {
777+
XCTFail("Expected SendRequestError, got \(error)")
778+
}
779+
}
684780

685781
func testUpdateCart() {
686782
let condition1 = XCTestExpectation(description: #function)
@@ -1478,4 +1574,54 @@ class IterableAPITests: XCTestCase {
14781574
XCTAssertEqual(dateFromMilliseconds, testDate)
14791575
}
14801576

1577+
@available(iOS 13.0, *)
1578+
private func setUpIterableAPIForAsyncDisableDevice(networkSession: MockNetworkSession) async throws -> Data {
1579+
let config = IterableConfig()
1580+
config.pushIntegrationName = "my-push-integration"
1581+
1582+
IterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey,
1583+
config: config,
1584+
networkSession: networkSession)
1585+
IterableAPI.email = "user@example.com"
1586+
1587+
let token = "zeeToken".data(using: .utf8)!
1588+
try await registerTokenForAsyncDisableDevice(token)
1589+
return token
1590+
}
1591+
1592+
@available(iOS 13.0, *)
1593+
private func registerTokenForAsyncDisableDevice(_ token: Data) async throws {
1594+
try await withCheckedThrowingContinuation { continuation in
1595+
let resumeGuard = TestAsyncContinuationResumeGuard()
1596+
1597+
IterableAPI.register(token: token, onSuccess: { _ in
1598+
resumeGuard.resume {
1599+
continuation.resume(returning: ())
1600+
}
1601+
}, onFailure: { reason, data in
1602+
resumeGuard.resume {
1603+
continuation.resume(throwing: SendRequestError(reason: reason, data: data))
1604+
}
1605+
})
1606+
}
1607+
}
1608+
1609+
}
1610+
1611+
private final class TestAsyncContinuationResumeGuard {
1612+
private let lock = NSLock()
1613+
private var didResume = false
1614+
1615+
func resume(_ block: () -> Void) {
1616+
lock.lock()
1617+
let shouldResume = !didResume
1618+
if shouldResume {
1619+
didResume = true
1620+
}
1621+
lock.unlock()
1622+
1623+
if shouldResume {
1624+
block()
1625+
}
1626+
}
14811627
}

0 commit comments

Comments
 (0)