diff --git a/podcasts.xcodeproj/project.pbxproj b/podcasts.xcodeproj/project.pbxproj index 630b78fb4d..030a4d4519 100644 --- a/podcasts.xcodeproj/project.pbxproj +++ b/podcasts.xcodeproj/project.pbxproj @@ -671,6 +671,8 @@ 8BF9CC0C2ADDA90F004E9B65 /* YearOverYearStory2023.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF9CC0B2ADDA90F004E9B65 /* YearOverYearStory2023.swift */; }; 8BFB434E2A1FFB4B00F3D409 /* StatusPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFB434D2A1FFB4B00F3D409 /* StatusPageViewModel.swift */; }; 91319B0B2C171F69000220A4 /* GravatarSafariViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91319B0A2C171F69000220A4 /* GravatarSafariViewController.swift */; }; + 9A156AAD2CF60973007BA8D9 /* CancelSubscriptionPlansView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A156AAC2CF60973007BA8D9 /* CancelSubscriptionPlansView.swift */; }; + 9A156AB32CF8BEE3007BA8D9 /* CancelSubscriptionPlanRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A156AB22CF8BEE3007BA8D9 /* CancelSubscriptionPlanRow.swift */; }; 9A156AB12CF7430A007BA8D9 /* UpNextAnnouncementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A156AB02CF7430A007BA8D9 /* UpNextAnnouncementView.swift */; }; BD001B892174260B00504DD3 /* FilterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD001B882174260B00504DD3 /* FilterManager.swift */; }; BD00CB2B24BD20CD00A10257 /* TimeStepperCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD00CB2924BD20CD00A10257 /* TimeStepperCell.swift */; }; @@ -2642,6 +2644,8 @@ 8BF9CC0B2ADDA90F004E9B65 /* YearOverYearStory2023.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YearOverYearStory2023.swift; sourceTree = ""; }; 8BFB434D2A1FFB4B00F3D409 /* StatusPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPageViewModel.swift; sourceTree = ""; }; 91319B0A2C171F69000220A4 /* GravatarSafariViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GravatarSafariViewController.swift; sourceTree = ""; }; + 9A156AAC2CF60973007BA8D9 /* CancelSubscriptionPlansView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelSubscriptionPlansView.swift; sourceTree = ""; }; + 9A156AB22CF8BEE3007BA8D9 /* CancelSubscriptionPlanRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelSubscriptionPlanRow.swift; sourceTree = ""; }; 9A156AB02CF7430A007BA8D9 /* UpNextAnnouncementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpNextAnnouncementView.swift; sourceTree = ""; }; 9A9517324EFFD2C905890193 /* Pods-podcasts.appstore.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-podcasts.appstore.xcconfig"; path = "Pods/Target Support Files/Pods-podcasts/Pods-podcasts.appstore.xcconfig"; sourceTree = ""; }; 9E6FEF10644B78313185D080 /* Pods-PocketCastsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PocketCastsTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PocketCastsTests/Pods-PocketCastsTests.release.xcconfig"; sourceTree = ""; }; @@ -3912,8 +3916,7 @@ children = ( 1035FFEC2CEE5F4E00C1E80D /* CancelSubscriptionOption.swift */, 109446752CEB3A7E00977161 /* CancelSubscriptionViewModel.swift */, - 100C99542CE7B801008D73E9 /* CancelSubscriptionView.swift */, - 102864A82CECEDB20098B2A9 /* CancelSubscriptionViewRow.swift */, + 9A156AAE2CF61016007BA8D9 /* Cancel Subscription */, ); path = "Cancel Subscription"; sourceTree = ""; @@ -5034,6 +5037,25 @@ path = Folders; sourceTree = ""; }; + 9A156AAE2CF61016007BA8D9 /* Cancel Subscription */ = { + isa = PBXGroup; + children = ( + 100C99542CE7B801008D73E9 /* CancelSubscriptionView.swift */, + 102864A82CECEDB20098B2A9 /* CancelSubscriptionViewRow.swift */, + 9A156AAF2CF61025007BA8D9 /* Plans View */, + ); + path = "Cancel Subscription"; + sourceTree = ""; + }; + 9A156AAF2CF61025007BA8D9 /* Plans View */ = { + isa = PBXGroup; + children = ( + 9A156AAC2CF60973007BA8D9 /* CancelSubscriptionPlansView.swift */, + 9A156AB22CF8BEE3007BA8D9 /* CancelSubscriptionPlanRow.swift */, + ); + path = "Plans View"; + sourceTree = ""; + }; BD03B383173A1D68000A419B /* Video Player */ = { isa = PBXGroup; children = ( @@ -9758,6 +9780,7 @@ 10756F8B2C5945C00089D34F /* KidsProfileSubmitScreen.swift in Sources */, C7080C5F29233BA000D7A432 /* PlusAccountPromptViewModel.swift in Sources */, 40FFAD8A2147831400024FCF /* PlaylistViewController+CollectionView.swift in Sources */, + 9A156AB32CF8BEE3007BA8D9 /* CancelSubscriptionPlanRow.swift in Sources */, BDA1E8161EDD28700098FB9D /* SonosLinkController.swift in Sources */, BD79EACD20D3805900E96572 /* ThemeSecondaryButton.swift in Sources */, F52B4F8E2BB4A9ED00E87BE4 /* CategoriesSelectorView.swift in Sources */, @@ -10201,6 +10224,7 @@ FF91A0FC2B6BC1D2002A0590 /* UpgradePrompt.swift in Sources */, BDCCBC8824BC444F009B4D1D /* CustomTimeStepper.swift in Sources */, BDFB53CB2362B9080001806E /* UpNextViewController.swift in Sources */, + 9A156AAD2CF60973007BA8D9 /* CancelSubscriptionPlansView.swift in Sources */, C7B3C60C2919DCC800054145 /* ActionView.swift in Sources */, BD240C3F231E8BE000FB2CDD /* PCSearchBarController.swift in Sources */, C7DC40242A67054000883D03 /* ToastTheme.swift in Sources */, diff --git a/podcasts/Cancel Subscription/CancelSubscriptionView.swift b/podcasts/Cancel Subscription/Cancel Subscription/CancelSubscriptionView.swift similarity index 98% rename from podcasts/Cancel Subscription/CancelSubscriptionView.swift rename to podcasts/Cancel Subscription/Cancel Subscription/CancelSubscriptionView.swift index 37d8e8e7d9..97a9dcf06d 100644 --- a/podcasts/Cancel Subscription/CancelSubscriptionView.swift +++ b/podcasts/Cancel Subscription/Cancel Subscription/CancelSubscriptionView.swift @@ -50,6 +50,7 @@ struct CancelSubscriptionView: View { .padding(.bottom, 58.0) case .loading, .unknown: ProgressView() + .foregroundStyle(theme.primaryUi01) case .failed: Text(L10n.cancelSubscriptionGenericError) .font(size: 18.0, style: .body, weight: .bold) diff --git a/podcasts/Cancel Subscription/CancelSubscriptionViewRow.swift b/podcasts/Cancel Subscription/Cancel Subscription/CancelSubscriptionViewRow.swift similarity index 100% rename from podcasts/Cancel Subscription/CancelSubscriptionViewRow.swift rename to podcasts/Cancel Subscription/Cancel Subscription/CancelSubscriptionViewRow.swift diff --git a/podcasts/Cancel Subscription/Cancel Subscription/Plans View/CancelSubscriptionPlanRow.swift b/podcasts/Cancel Subscription/Cancel Subscription/Plans View/CancelSubscriptionPlanRow.swift new file mode 100644 index 0000000000..672021ea52 --- /dev/null +++ b/podcasts/Cancel Subscription/Cancel Subscription/Plans View/CancelSubscriptionPlanRow.swift @@ -0,0 +1,106 @@ +import SwiftUI + +struct CancelSubscriptionPlanRow: View { + @EnvironmentObject var theme: Theme + + let product: PlusPricingInfoModel.PlusProductPricingInfo + var selected: Bool + let onTap: (PlusPricingInfoModel.PlusProductPricingInfo) -> Void + + var body: some View { + ZStack { + Rectangle() + .foregroundStyle(.clear) + .background(theme.primaryUi01) + .cornerRadius(8.0) + .frame(height: 64) + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 0) { + Text(product.planTitle) + .font(size: 18.0, style: .body, weight: .bold) + .foregroundStyle(theme.primaryText01) + Spacer() + if selected { + ZStack { + Circle() + .fill(theme.primaryField03Active) + .frame(width: 24, height: 24) + Image("small-tick") + .resizable() + .frame(width: 24, height: 24) + .foregroundColor(theme.primaryInteractive02) + } + } + } + HStack(spacing: 0) { + Text(product.frequencyPrice) + .font(size: 15.0, style: .body, weight: .regular) + .foregroundStyle(theme.primaryText01) + Spacer() + } + } + .padding(.leading, 20.0) + .padding(.trailing, 10.0) + } + .overlay( + RoundedRectangle(cornerRadius: 8.0) + .stroke(theme.primaryField03Active, + lineWidth: selected ? 2 : 0) + ) + .padding(.horizontal, 20.0) + .onTapGesture { + onTap(product) + } + } +} + +extension PlusPricingInfoModel.PlusProductPricingInfo { + fileprivate var planTitle: String { + switch identifier { + case .yearly, .yearlyReferral: + return "Plus \(L10n.yearly.capitalized)" + case .monthly: + return "Plus \(L10n.monthly.capitalized)" + case .patronMonthly: + return "Patron \(L10n.monthly.capitalized)" + case .patronYearly: + return "Patron \(L10n.yearly.capitalized)" + } + } + + fileprivate var frequencyPrice: String { + switch identifier { + case .yearly, .yearlyReferral, .patronYearly: + return L10n.plusYearlyFrequencyPricingFormat(rawPrice) + case .monthly, .patronMonthly: + return L10n.plusMonthlyFrequencyPricingFormat(rawPrice) + } + } +} + +struct CancelSubscriptionPlanRow_Preview: PreviewProvider { + static var previews: some View { + VStack(spacing: 16.0) { + CancelSubscriptionPlanRow( + product: .init( + identifier: .yearly, + price: "", + rawPrice: "$39.99", + offer: nil), + selected: true + ) { _ in } + .environmentObject(Theme.sharedTheme) + CancelSubscriptionPlanRow( + product: .init( + identifier: .monthly, + price: "", + rawPrice: "$3.99", + offer: nil), + selected: false + ) { _ in } + .environmentObject(Theme.sharedTheme) + } + .background(.gray) + .previewLayout(.fixed(width: 393, height: 250)) + } +} diff --git a/podcasts/Cancel Subscription/Cancel Subscription/Plans View/CancelSubscriptionPlansView.swift b/podcasts/Cancel Subscription/Cancel Subscription/Plans View/CancelSubscriptionPlansView.swift new file mode 100644 index 0000000000..9805fd1ea8 --- /dev/null +++ b/podcasts/Cancel Subscription/Cancel Subscription/Plans View/CancelSubscriptionPlansView.swift @@ -0,0 +1,94 @@ +import SwiftUI + +struct CancelSubscriptionPlansView: View { + @EnvironmentObject var theme: Theme + + @ObservedObject var viewModel: CancelSubscriptionViewModel + + init(viewModel: CancelSubscriptionViewModel) { + self.viewModel = viewModel + } + + var body: some View { + ZStack { + switch viewModel.currentProductAvailability { + case .loading: + showLoading() + default: + closeButton + mainView + if viewModel.state == .purchasing { + showLoading(fullScreen: true) + } + } + } + .onAppear { + Task { + await viewModel.loadCurrentProduct() + } + } + .background(theme.primaryUi04) + } + + var closeButton: some View { + VStack { + HStack { + Spacer() + Button(action: { + viewModel.closePlans() + }) { + Image(systemName: "xmark") + .font(.system(size: 14).weight(.bold)) + .frame(width: 30, height: 30) + .foregroundColor(theme.primaryIcon02Active) + .background(theme.primaryUi05) + .clipShape(Circle()) + } + } + .padding(.trailing, 16.0) + .padding(.top, 16.0) + Spacer() + } + } + + var mainView: some View { + VStack(spacing: 16.0) { + Image("cs-app-icon") + .frame(width: 100.0, height: 100.0) + .padding(.top, 88.0) + .padding(.bottom, 20.0) + Text(L10n.cancelSubscriptionAvailablePlansTitle) + .font(size: 28.0, style: .body, weight: .bold) + .foregroundStyle(theme.primaryText01) + .padding(.bottom, 24.0) + ForEach(viewModel.pricingInfo.products, id: \.id) { product in + CancelSubscriptionPlanRow(product: product, + selected: product.identifier == viewModel.currentPricingProduct?.identifier) { selectedProduct in + viewModel.purchase(product: selectedProduct) + } + } + Spacer() + } + } + + @ViewBuilder + func showLoading(fullScreen: Bool = false) -> some View { + if fullScreen { + ZStack { + theme.primaryUi05Selected + .edgesIgnoringSafeArea(.all) + .opacity(0.7) + ProgressView() + .tint(theme.primaryText01) + } + } else { + ProgressView() + .tint(theme.primaryUi01) + } + } +} + +#Preview { + CancelSubscriptionPlansView(viewModel: CancelSubscriptionViewModel(navigationController: UINavigationController())) + .environmentObject(Theme.sharedTheme) +} diff --git a/podcasts/Cancel Subscription/CancelSubscriptionViewModel.swift b/podcasts/Cancel Subscription/CancelSubscriptionViewModel.swift index b75e0b56f3..9404ab8ea6 100644 --- a/podcasts/Cancel Subscription/CancelSubscriptionViewModel.swift +++ b/podcasts/Cancel Subscription/CancelSubscriptionViewModel.swift @@ -1,5 +1,6 @@ import SwiftUI import PocketCastsServer +import PocketCastsUtils class CancelSubscriptionViewModel: PlusPurchaseModel { let navigationController: UINavigationController @@ -8,6 +9,11 @@ class CancelSubscriptionViewModel: PlusPurchaseModel { purchaseHandler.isEligibleForOffer } + private var lastPurchasedProductID: IAPProductID? + + @Published var currentPricingProduct: PlusPricingInfoModel.PlusProductPricingInfo? + @State var currentProductAvailability: CurrentProductAvailability = .idle + init(purchaseHandler: IAPHelper = .shared, navigationController: UINavigationController) { self.navigationController = navigationController @@ -16,6 +22,36 @@ class CancelSubscriptionViewModel: PlusPurchaseModel { self.loadPrices() } + override func didAppear() { + //TODO: Implement analytics + } + + override func didDismiss(type: OnboardingDismissType) { + // Since the view can only be dismissed via swipe, only check for that + guard type == .swipe else { return } + + //TODO: Implement analytics + } + + override func handleNext() { + if SubscriptionHelper.activeTier == .patron { + let controller = PatronWelcomeViewModel.make(in: navigationController) + navigationController.pushViewController(controller, animated: true) + } else { + navigationController.dismiss(animated: true) + } + } + + enum CurrentProductAvailability { + case idle + case loading + case available + case unavailable + } +} + +// IAP +extension CancelSubscriptionViewModel { func monthlyPrice() -> String? { switch SubscriptionHelper.activeTier { case .plus: @@ -27,43 +63,73 @@ class CancelSubscriptionViewModel: PlusPurchaseModel { } } - func cancelSubscriptionTap() { - let viewController = CancelConfirmationViewModel.make(in: navigationController) - navigationController.pushViewController(viewController, animated: true) + func loadCurrentProduct() async { + if currentProductAvailability == .loading { return } + + currentProductAvailability = .loading + if let transaction = await purchaseHandler.findLastSubscriptionPurchased(), + let productID = IAPProductID(rawValue: transaction.productID) { + await MainActor.run { + lastPurchasedProductID = productID + currentProductAvailability = .available + currentPricingProduct = pricingInfo.products.first { $0.identifier == productID } + } + } else { + currentProductAvailability = .unavailable + FileLog.shared.console("[CancelSubscriptionViewModel] Could not find last subscription purchased") + } + } + + func purchase(product: PlusPricingInfoModel.PlusProductPricingInfo) { + currentPricingProduct = product + + if currentPricingProduct?.identifier != lastPurchasedProductID { + purchase(product: product.identifier) + } } func claimOffer() { //TODO: Apply one month free } +} + +// Navigation +extension CancelSubscriptionViewModel { + func cancelSubscriptionTap() { + //TODO: Implement analytics + let viewController = CancelConfirmationViewModel.make(in: navigationController) + navigationController.pushViewController(viewController, animated: true) + } func showPlans() { - //TODO: Show plans + //TODO: Implement analytics + let view = CancelSubscriptionPlansView(viewModel: self).setupDefaultEnvironment() + let controller = OnboardingHostingViewController(rootView: view) + controller.navBarIsHidden = true + navigationController.pushViewController(controller, animated: true) } func showHelp() { + //TODO: Implement analytics let controller = OnlineSupportController() navigationController.navigationBar.isHidden = false navigationController.pushViewController(controller, animated: true) } - override func didAppear() { - //TODO: Implement analytics - } - - override func didDismiss(type: OnboardingDismissType) { - // Since the view can only be dismissed via swipe, only check for that - guard type == .swipe else { return } - + func closePlans() { //TODO: Implement analytics + navigationController.dismiss(animated: true) } } +// Making vew controller extension CancelSubscriptionViewModel { /// Make the view, and allow it to be shown by itself or within another navigation flow static func make() -> UIViewController { // If we're not being presented within another nav controller then wrap ourselves in one let navController = UINavigationController() let viewModel = CancelSubscriptionViewModel(navigationController: navController) + viewModel.parentController = navController // Wrap the SwiftUI view in the hosting view controller let swiftUIView = CancelSubscriptionView(viewModel: viewModel).setupDefaultEnvironment() diff --git a/podcasts/CancelSubscription.xcassets/cs-app-icon.imageset/Contents.json b/podcasts/CancelSubscription.xcassets/cs-app-icon.imageset/Contents.json new file mode 100644 index 0000000000..e70b31bb2b --- /dev/null +++ b/podcasts/CancelSubscription.xcassets/cs-app-icon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "pc-app-icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/podcasts/CancelSubscription.xcassets/cs-app-icon.imageset/pc-app-icon.svg b/podcasts/CancelSubscription.xcassets/cs-app-icon.imageset/pc-app-icon.svg new file mode 100644 index 0000000000..6c882d875a --- /dev/null +++ b/podcasts/CancelSubscription.xcassets/cs-app-icon.imageset/pc-app-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/podcasts/IAPHelper.swift b/podcasts/IAPHelper.swift index 06ec7d060e..7d52857e5e 100644 --- a/podcasts/IAPHelper.swift +++ b/podcasts/IAPHelper.swift @@ -84,7 +84,7 @@ class IAPHelper: NSObject { return nil } - func findLastSubscriptionPurchased() async -> [StoreKit.Transaction] { + func findLastSubscriptionsPurchased() async -> [StoreKit.Transaction] { var transactions: [StoreKit.Transaction] = [] for await result in Transaction.currentEntitlements { guard case .verified(let transaction) = result else { @@ -97,20 +97,17 @@ class IAPHelper: NSObject { return transactions } - func findLastSubscriptionPurchasedGroupID() async -> String? { - return await findLastSubscriptionPurchased() + func findLastSubscriptionPurchased() async -> StoreKit.Transaction? { + return await findLastSubscriptionsPurchased() .filter { $0.expirationDate != nil } .sorted { - if let t0 = $0.expirationDate, let t1 = $1.expirationDate { - return t0 > t1 - } - return false + return $0.purchaseDate < $1.purchaseDate } - .first?.subscriptionGroupID + .last } func showManageSubscriptions(in windowScene: UIWindowScene) async throws { - if let groupID = await findLastSubscriptionPurchasedGroupID(), #available(iOS 17.0, *) { + if let groupID = await findLastSubscriptionPurchased()?.subscriptionGroupID, #available(iOS 17.0, *) { FileLog.shared.console("[CancelConfirmationViewModel] Last subscription purchased group ID: \(groupID)") try await StoreKit.AppStore.showManageSubscriptions(in: windowScene, subscriptionGroupID: groupID) diff --git a/podcasts/Onboarding/Models/PlusPurchaseModel.swift b/podcasts/Onboarding/Models/PlusPurchaseModel.swift index 5ef90cc1be..b5e9d8802d 100644 --- a/podcasts/Onboarding/Models/PlusPurchaseModel.swift +++ b/podcasts/Onboarding/Models/PlusPurchaseModel.swift @@ -74,6 +74,42 @@ class PlusPurchaseModel: PlusPricingInfoModel, OnboardingModel { controller.present(alert, animated: true) } + func handleNext() { + guard let parentController else { return } + + if OnboardingFlow.shared.currentFlow.shouldDismissAfterPurchase { + parentController.dismiss(animated: true) + return + } + + let navigationController = parentController as? UINavigationController + + let controller: UIViewController + if SubscriptionHelper.activeTier == .patron { + controller = PatronWelcomeViewModel.make(in: navigationController) + } else { + controller = WelcomeViewModel.make(in: navigationController, displayType: .plus) + } + + let presentNextBlock: () -> Void = { + guard let navigationController else { + // Present the welcome flow + parentController.present(controller, animated: true) + return + } + + // Reset the nav flow to only show the welcome controller + navigationController.setViewControllers([controller], animated: true) + } + + // Dismiss the current flow + if parentController.presentedViewController != nil { + parentController.dismiss(animated: true, completion: presentNextBlock) + } else { + presentNextBlock() + } + } + // Our internal state enum PurchaseState { case ready @@ -139,44 +175,6 @@ private extension PlusPurchaseModel { } } -private extension PlusPurchaseModel { - private func handleNext() { - guard let parentController else { return } - - if OnboardingFlow.shared.currentFlow.shouldDismissAfterPurchase { - parentController.dismiss(animated: true) - return - } - - let navigationController = parentController as? UINavigationController - - let controller: UIViewController - if SubscriptionHelper.activeTier == .patron { - controller = PatronWelcomeViewModel.make(in: navigationController) - } else { - controller = WelcomeViewModel.make(in: navigationController, displayType: .plus) - } - - let presentNextBlock: () -> Void = { - guard let navigationController else { - // Present the welcome flow - parentController.present(controller, animated: true) - return - } - - // Reset the nav flow to only show the welcome controller - navigationController.setViewControllers([controller], animated: true) - } - - // Dismiss the current flow - if parentController.presentedViewController != nil { - parentController.dismiss(animated: true, completion: presentNextBlock) - } else { - presentNextBlock() - } - } -} - // MARK: - Purchase Notification handlers private extension PlusPurchaseModel { func handlePurchaseCompleted(_ notification: Notification) { diff --git a/podcasts/Strings+Generated.swift b/podcasts/Strings+Generated.swift index c3d30ae9a5..32393c9372 100644 --- a/podcasts/Strings+Generated.swift +++ b/podcasts/Strings+Generated.swift @@ -386,6 +386,8 @@ internal enum L10n { internal static var cancelFailed: String { return L10n.tr("Localizable", "cancel_failed") } /// Cancel Subscription internal static var cancelSubscription: String { return L10n.tr("Localizable", "cancel_subscription") } + /// Available Plans + internal static var cancelSubscriptionAvailablePlansTitle: String { return L10n.tr("Localizable", "cancel_subscription_available_plans_title") } /// Claim offer internal static var cancelSubscriptionClaimOfferButton: String { return L10n.tr("Localizable", "cancel_subscription_claim_offer_button") } /// Continue to Cancellation diff --git a/podcasts/en.lproj/Localizable.strings b/podcasts/en.lproj/Localizable.strings index 2c4edb0e2c..1e7b7fa18a 100644 --- a/podcasts/en.lproj/Localizable.strings +++ b/podcasts/en.lproj/Localizable.strings @@ -4693,6 +4693,9 @@ /* Title for the claim offer button */ "cancel_subscription_claim_offer_button" = "Claim offer"; +/* Title of the Available Plans screen accessible from the Cancel Subscription view */ +"cancel_subscription_available_plans_title" = "Available Plans"; + /* Referrals - Share Pass message. `%1$@' is a placeholder for the duration of free period offered on the Plus subscription*/ "referrals_share_pass_long_message" = "Hi there!\n\nHere is a %1$@ guest pass for Pocket Casts Plus–my favorite podcast player. It's packed with unique features like bookmarks, folders, and more that you won't find anywhere else. I think you'll love it too!\n";