Skip to content

Commit

Permalink
initiate promos from ManageSubscriptionsView
Browse files Browse the repository at this point in the history
  • Loading branch information
vegaro committed Jun 26, 2024
1 parent 8b4aa72 commit 181e73b
Show file tree
Hide file tree
Showing 7 changed files with 418 additions and 61 deletions.
8 changes: 4 additions & 4 deletions RevenueCat.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
"state" : {
"revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00",
"version" : "2.1.2"
"revision" : "3ef6999c73b6938cc0da422f2c912d0158abb0a0",
"version" : "2.2.0"
}
},
{
"identity" : "cwlpreconditiontesting",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
"state" : {
"revision" : "a23ded2c91df9156628a6996ab4f347526f17b6b",
"version" : "2.1.2"
"revision" : "2ef56b2caf25f55fa7eef8784c30d5a767550f54",
"version" : "2.2.1"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,61 @@ import RevenueCat
@MainActor
class FeedbackSurveyViewModel: ObservableObject {

typealias CustomerInfoFetcher = @Sendable () async throws -> CustomerInfo

@Published
var feedbackSurveyData: FeedbackSurveyData
@Published
var isShowingPromotionalOffer: Bool = false
@Published
var selectedPromotionalOffer: CustomerCenterConfigData.HelpPath.PromotionalOffer?
var loadingStates: [String: Bool] = [:]

var promotionalOffer: PromotionalOffer? {
return promotionalOfferViewModel.promotionalOffer
}

var product: StoreProduct? {
return promotionalOfferViewModel.product
}

private var customerInfoFetcher: CustomerInfoFetcher
private var promotionalOfferViewModel: PromotionalOfferViewModel

convenience init(feedbackSurveyData: FeedbackSurveyData) {
self.init(feedbackSurveyData: feedbackSurveyData,
promotionalOfferViewModel: PromotionalOfferViewModel(),
customerInfoFetcher: {
guard Purchases.isConfigured else {
throw PaywallError.purchasesNotConfigured
}

return try await Purchases.shared.customerInfo()
})
}

init(feedbackSurveyData: FeedbackSurveyData) {
// @PublicForExternalTesting
init(feedbackSurveyData: FeedbackSurveyData,
promotionalOfferViewModel: PromotionalOfferViewModel,
customerInfoFetcher: @escaping CustomerInfoFetcher
) {
self.feedbackSurveyData = feedbackSurveyData
self.promotionalOfferViewModel = promotionalOfferViewModel
self.customerInfoFetcher = customerInfoFetcher
}

func handleAction(for option: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) {
func handleAction(for option: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) async {
if let promotionalOffer = option.promotionalOffer {
applyPromotionalOffer(promotionalOffer)
self.loadingStates[option.id] = true
await promotionalOfferViewModel.loadPromo(promotionalOfferId: promotionalOffer.iosOfferId)
self.isShowingPromotionalOffer = true
} else {
feedbackSurveyData.action()
self.feedbackSurveyData.action()
}
}

private func applyPromotionalOffer(_ offer: CustomerCenterConfigData.HelpPath.PromotionalOffer) {
selectedPromotionalOffer = offer
isShowingPromotionalOffer = true
func handleSheetDismiss() {
self.feedbackSurveyData.action()
self.loadingStates.removeAll()
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ class ManageSubscriptionsViewModel: ObservableObject {
var showRestoreAlert: Bool = false
@Published
var feedbackSurveyData: FeedbackSurveyData?
@Published
var isShowingPromotionalOffer: Bool = false
@Published
var loadingPath: CustomerCenterConfigData.HelpPath?

@Published
var state: CustomerCenterViewState {
Expand All @@ -49,18 +53,30 @@ class ManageSubscriptionsViewModel: ObservableObject {
return state != .notLoaded
}

var product: StoreProduct? {
return promotionalOfferViewModel.product
}

var promotionalOffer: PromotionalOffer? {
return promotionalOfferViewModel.promotionalOffer
}

private var purchasesProvider: ManageSubscriptionsPurchaseType
private var promotionalOfferViewModel: PromotionalOfferViewModel

private var error: Error?

convenience init() {
self.init(purchasesProvider: ManageSubscriptionPurchases())
self.init(purchasesProvider: ManageSubscriptionPurchases(),
promotionalOfferViewModel: PromotionalOfferViewModel())
}

// @PublicForExternalTesting
init(purchasesProvider: ManageSubscriptionsPurchaseType) {
init(purchasesProvider: ManageSubscriptionsPurchaseType,
promotionalOfferViewModel: PromotionalOfferViewModel) {
self.state = .notLoaded
self.purchasesProvider = purchasesProvider
self.promotionalOfferViewModel = promotionalOfferViewModel
}

// @PublicForExternalTesting
Expand All @@ -69,6 +85,7 @@ class ManageSubscriptionsViewModel: ObservableObject {
self.configuration = configuration
self.subscriptionInformation = subscriptionInformation
self.purchasesProvider = ManageSubscriptionPurchases()
self.promotionalOfferViewModel = PromotionalOfferViewModel()
state = .success
}

Expand Down Expand Up @@ -115,18 +132,40 @@ class ManageSubscriptionsViewModel: ObservableObject {
}
}

func handleSheetDismiss() {
if let loadingPath = loadingPath {
performAction(for: loadingPath)
self.loadingPath = nil
}
}

#if os(iOS) || targetEnvironment(macCatalyst)
func determineFlow(for path: CustomerCenterConfigData.HelpPath) {
if case let .feedbackSurvey(feedbackSurvey) = path.detail {
func determineFlow(for path: CustomerCenterConfigData.HelpPath) async {
switch path.detail {
case let .feedbackSurvey(feedbackSurvey):
self.feedbackSurveyData = FeedbackSurveyData(configuration: feedbackSurvey) { [weak self] in
self?.performAction(for: path)
await self?.performAction(for: path)
}
} else {
case let .promotionalOffer(promotionalOffer):
self.loadingPath = path
await promotionalOfferViewModel.loadPromo(promotionalOfferId: promotionalOffer.iosOfferId)
self.isShowingPromotionalOffer = true
default:
performAction(for: path)
}
}
#endif

}

func performAction(for path: CustomerCenterConfigData.HelpPath) async {
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
private extension ManageSubscriptionsViewModel {

#if os(iOS) || targetEnvironment(macCatalyst)
private func performAction(for path: CustomerCenterConfigData.HelpPath) async {
switch path.type {
case .missingPurchase:
self.showRestoreAlert = true
Expand Down
120 changes: 120 additions & 0 deletions RevenueCatUI/CustomerCenter/ViewModels/PromotionalOfferViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PromotionalOfferViewModel.swift
//
//
// Created by Cesar de la Vega on 17/6/24.
//

import Foundation
import RevenueCat

#if !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS)

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
@MainActor
class PromotionalOfferViewModel: ObservableObject {

typealias CustomerInfoFetcher = @Sendable () async throws -> CustomerInfo

@Published
var product: StoreProduct?
@Published
var promotionalOffer: PromotionalOffer?
@Published
var transaction: StoreTransaction?

private var customerInfoFetcher: CustomerInfoFetcher

convenience init() {
self.init(product: nil, promotionalOffer: nil)
}

convenience init(product: StoreProduct?,
promotionalOffer: PromotionalOffer?) {
self.init(product: product,
promotionalOffer: promotionalOffer,
customerInfoFetcher: {
guard Purchases.isConfigured else {
throw PaywallError.purchasesNotConfigured
}

return try await Purchases.shared.customerInfo()
})
}

// @PublicForExternalTesting
init(product: StoreProduct?,
promotionalOffer: PromotionalOffer?,
customerInfoFetcher: @escaping CustomerInfoFetcher) {
self.product = product
self.promotionalOffer = promotionalOffer
self.customerInfoFetcher = customerInfoFetcher
}

func purchasePromo() async {
guard let promotionalOffer = self.promotionalOffer,
let product = self.product else {
print("Promotional offer not loaded")
return
}
do {
let purchase = try await Purchases.shared.purchase(product: product, promotionalOffer: promotionalOffer)
self.transaction = purchase.transaction
} catch {
print("Error purchasing product with promotional offer: \(error)")
}
}

func loadPromo(promotionalOfferId: String) async {
do {
let customerInfo = try await self.customerInfoFetcher()
let activeSubscriptionProductIds = customerInfo.activeSubscriptions

guard let appStoreSubscription = customerInfo.entitlements.active.first(where: {
$0.value.store == .appStore
}) else {
print("No active App Store subscriptions found")
return
}

let productId = appStoreSubscription.value.productIdentifier
let products = await Purchases.shared.products([productId])
guard let product = products.first(where: { product in
product.discounts.contains { $0.offerIdentifier == promotionalOfferId }
}) else {
print("No active product found with the given promotional offer ID")
return
}

self.product = product

if let discount = product.discounts.first(where: { $0.offerIdentifier == promotionalOfferId }) {
do {
let promotionalOffer = try await Purchases.shared.promotionalOffer(forProductDiscount: discount,
product: product)
self.promotionalOffer = promotionalOffer
} catch {
print("Error fetching promotional offer")
return
}
}
} catch {
print("Error fetching promotional offer for active product: \(error)")
return
}
}

}

#endif
38 changes: 27 additions & 11 deletions RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,27 @@ struct FeedbackSurveyView: View {

var body: some View {
VStack {
Text(viewModel.feedbackSurveyData.configuration.title)
Text(self.viewModel.feedbackSurveyData.configuration.title)
.font(.title)
.padding()

Spacer()

FeedbackSurveyButtonsView(options: viewModel.feedbackSurveyData.configuration.options,
action: viewModel.handleAction(for:))
}
.sheet(isPresented: $viewModel.isShowingPromotionalOffer) {
if let promotionalOffer = viewModel.selectedPromotionalOffer {
PromotionalOfferView(promotionalOffer: promotionalOffer)
}
FeedbackSurveyButtonsView(options: self.viewModel.feedbackSurveyData.configuration.options,
action: self.viewModel.handleAction(for:),
loadingStates: self.$viewModel.loadingStates)
}
.sheet(
isPresented: self.$viewModel.isShowingPromotionalOffer,
onDismiss: { self.viewModel.handleSheetDismiss() },
content: {
if let promotionalOffer = self.viewModel.promotionalOffer,
let product = self.viewModel.product {
PromotionalOfferView(promotionalOffer: promotionalOffer,
product: product
)
}
})
}

}
Expand All @@ -60,17 +67,26 @@ struct FeedbackSurveyView: View {
struct FeedbackSurveyButtonsView: View {

let options: [CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option]
let action: (CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) -> Void
let action: (CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) async -> Void
@Binding
var loadingStates: [String: Bool]

var body: some View {
VStack(spacing: 16) {
ForEach(options, id: \.id) { option in
Button(option.title) {
Button {
Task {
self.action(option)
await self.action(option)
}
} label: {
if self.loadingStates[option.id] ?? false {
ProgressView()
} else {
Text(option.title)
}
}
.buttonStyle(ManageSubscriptionsButtonStyle())
.disabled(self.loadingStates[option.id] ?? false)
}
}
}
Expand Down
Loading

0 comments on commit 181e73b

Please sign in to comment.