Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Winback] Apply Apple API for subscription cancellation #2474

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import PocketCastsUtils
import SwiftProtobuf

class SubscriptionStatusTask: ApiBaseTask {
var completion: ((Bool) -> Void)?

override func apiTokenAcquired(token: String) {
let url = ServerConstants.Urls.api() + "subscription/status"
do {
let (response, httpStatus) = getToServer(url: url, token: token)

guard let responseData = response, httpStatus?.statusCode == ServerConstants.HttpConstants.ok else {
FileLog.shared.addMessage("Subscription status failed \(httpStatus?.statusCode ?? -1)")
completion?(false)
return
}
do {
Expand All @@ -35,9 +38,11 @@ class SubscriptionStatusTask: ApiBaseTask {
if originalSubscriptionStatus, !SubscriptionHelper.hasActiveSubscription() {
ServerConfig.shared.syncDelegate?.cleanupCloudOnlyFiles()
}
completion?(true)
}
} catch {
FileLog.shared.addMessage("SubscriptionStatusTask: Protobuf Encoding failed \(error.localizedDescription)")
completion?(false)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,17 @@ public extension ApiServerHandler {
apiQueue.addOperation(subscriptionStatusTask)
}

@discardableResult
func retrieveSubscriptionStatus() async -> Bool {
return await withCheckedContinuation { continuation in
let operation = SubscriptionStatusTask()
operation.completion = { success in
continuation.resume(returning: success)
}
apiQueue.addOperation(operation)
}
}

// MARK: - Subscription Promotion Codes

func redeemPromoCode(promoCode: String, completion: @escaping (Int, String?, APIError?) -> Void) {
Expand Down
3 changes: 2 additions & 1 deletion podcasts/Cancel Subscription/CancelSubscriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ struct CancelSubscriptionView: View {

ForEach(CancelSubscriptionOption.allCases, id: \.id) { option in
if case .promotion = option {
if viewModel.isEligibleForOffer, case .promotion = option, let price = viewModel.monthlyPrice() {
//TODO: Need to check the if the promotion can be applied
if case .promotion = option, let price = viewModel.monthlyPrice() {
CancelSubscriptionViewRow(option: .promotion(price: price),
viewModel: viewModel)
}
Expand Down
18 changes: 17 additions & 1 deletion podcasts/CancelConfirmationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,23 @@ class CancelConfirmationViewModel: OnboardingModel {
Analytics.track(.cancelConfirmationCancelButtonTapped)

if FeatureFlag.winback.enabled {
//TODO: Add Apple API
Task {
guard let windowScene = await navigationController.view.window?.windowScene else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this code to be refactored to a separated function that could part of IAPHelper in order to better contain any StoreKit code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@SergioEstevao I part of it to the IAPHandler. All the part related to the groupID and StoreKit, so now everything is well decoupled

FileLog.shared.console("[CancelConfirmationViewModel] No window scene available")
return
}
do {
try await IAPHelper.shared.showManageSubscriptions(in: windowScene)

await ApiServerHandler.shared.retrieveSubscriptionStatus()

await MainActor.run {
navigationController.dismiss(animated: true)
}
} catch {
FileLog.shared.console("[StoreKit] Error showing manage subscriptions: \(error.localizedDescription)")
}
}
} else {
let controller = CancelInfoViewController()
navigationController.pushViewController(controller, animated: true)
Expand Down
37 changes: 36 additions & 1 deletion podcasts/IAPHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class IAPHelper: NSObject {
private var isRequestingProducts = false

/// Whether purchasing is allowed in the current environment or not
private (set) var canMakePurchases = true
private(set) var canMakePurchases = true

private var settings: IAPHelperSettings
private var networking: IAPHelperNetworking
Expand Down Expand Up @@ -84,6 +84,41 @@ class IAPHelper: NSObject {
return nil
}

func findLastSubscriptionPurchased() async -> [StoreKit.Transaction] {
var transactions: [StoreKit.Transaction] = []
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
if transaction.revocationDate == nil {
transactions.append(transaction)
}
}
return transactions
}

func findLastSubscriptionPurchasedGroupID() async -> String? {
return await findLastSubscriptionPurchased()
.filter { $0.expirationDate != nil }
.sorted {
if let t0 = $0.expirationDate, let t1 = $1.expirationDate {
return t0 > t1
}
return false
}
.first?.subscriptionGroupID
}

func showManageSubscriptions(in windowScene: UIWindowScene) async throws {
if let groupID = await findLastSubscriptionPurchasedGroupID(), #available(iOS 17.0, *) {
FileLog.shared.console("[CancelConfirmationViewModel] Last subscription purchased group ID: \(groupID)")

try await StoreKit.AppStore.showManageSubscriptions(in: windowScene, subscriptionGroupID: groupID)
} else {
try await StoreKit.AppStore.showManageSubscriptions(in: windowScene)
}
}

/// Whether the products have been loaded from StoreKit
var hasLoadedProducts: Bool { productsArray.count > 0 }

Expand Down