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
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ struct CancelSubscriptionViewRow: View {
var chevron: some View {
HStack {
Spacer()
Image("chevron-small-right")
Image("cs-chevron")
.renderingMode(.template)
.foregroundStyle(theme.primaryIcon02)
.frame(width: 24, height: 24)
Expand Down
25 changes: 24 additions & 1 deletion podcasts/CancelConfirmationViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SwiftUI
import PocketCastsServer
import PocketCastsUtils
import StoreKit

class CancelConfirmationViewModel: OnboardingModel {
let navigationController: UINavigationController
Expand All @@ -25,7 +26,29 @@ 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 {
if let groupID = await IAPHelper.shared.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)
}

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
27 changes: 26 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,31 @@ 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
}

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

Expand Down
2 changes: 1 addition & 1 deletion podcasts/Strings+Generated.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.