Skip to content

Commit

Permalink
Manage Expired Subscription (#2764)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1204099484721401/1207112724044306/f

Description:

Allows repurchasing an expired subscription, whether it was purchased on Apple or via Stripe or Google
Properly caches Settings Subscription-State and uses it by default (Until the backend says otherwise)
  • Loading branch information
afterxleep committed May 3, 2024
1 parent e13bc13 commit 3c86f9e
Show file tree
Hide file tree
Showing 19 changed files with 381 additions and 166 deletions.
4 changes: 4 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@
D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */; };
D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */; };
D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */; };
D6A4645C2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4645B2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift */; };
D6ACEA322BBD55BF008FADDF /* TabURLInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6ACEA312BBD55BF008FADDF /* TabURLInterceptor.swift */; };
D6BFCB5F2B7524AA0051FF81 /* SubscriptionPIRView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */; };
D6BFCB612B7525160051FF81 /* SubscriptionPIRViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BFCB602B7525160051FF81 /* SubscriptionPIRViewModel.swift */; };
Expand Down Expand Up @@ -2384,6 +2385,7 @@
D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreView.swift; sourceTree = "<group>"; };
D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreViewModel.swift; sourceTree = "<group>"; };
D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionView.swift; sourceTree = "<group>"; };
D6A4645B2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsCacheKey.swift; sourceTree = "<group>"; };
D6ACEA312BBD55BF008FADDF /* TabURLInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabURLInterceptor.swift; sourceTree = "<group>"; };
D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPIRView.swift; sourceTree = "<group>"; };
D6BFCB602B7525160051FF81 /* SubscriptionPIRViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPIRViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5141,6 +5143,7 @@
85C8E61C2B0E47380029A6BD /* BookmarksDatabaseSetup.swift */,
9821234D2B6D0A6300F08C57 /* UserAuthenticator.swift */,
9821234F2B6D233E00F08C57 /* UserSession.swift */,
D6A4645B2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift */,
);
name = Application;
sourceTree = "<group>";
Expand Down Expand Up @@ -6605,6 +6608,7 @@
D60B1F272B9DDE5A00AE4760 /* SubscriptionGoogleView.swift in Sources */,
984D035C24AE15CD0066CFB8 /* TabSwitcherSettings.swift in Sources */,
D6E83C562B21ECC1006C8AFB /* SettingsLegacyViewProvider.swift in Sources */,
D6A4645C2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift in Sources */,
98B31292218CCB8C00E54DE1 /* AppDependencyProvider.swift in Sources */,
D670E5BD2BB6AA0000941A42 /* View+AppearModifiers.swift in Sources */,
D6ACEA322BBD55BF008FADDF /* TabURLInterceptor.swift in Sources */,
Expand Down
4 changes: 0 additions & 4 deletions DuckDuckGo/SettingsRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ struct SettingsRootView: View {
viewModel.onAppear()
}

.onDisappear {
viewModel.onDissapear()
}

// MARK: Deeplink Modifiers

.sheet(isPresented: $shouldDisplayDeepLinkSheet,
Expand Down
16 changes: 10 additions & 6 deletions DuckDuckGo/SettingsState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ struct SettingsState {
var enabled: Bool
var status: String
}

struct Subscription {
struct Subscription: Codable {
var enabled: Bool
var canPurchase: Bool
var isSignedIn: Bool
var hasActiveSubscription: Bool
var isSubscriptionPendingActivation: Bool
var isRestoring: Bool
var shouldDisplayRestoreSubscriptionError: Bool
var entitlements: [Entitlement.ProductName]
var platform: DDGSubscription.Platform
var isShowingStripeView: Bool
}

struct SyncSettings {
Expand Down Expand Up @@ -96,7 +98,7 @@ struct SettingsState {

// Sync Properties
var sync: SyncSettings

static var defaults: SettingsState {
return SettingsState(
appTheme: .systemDefault,
Expand All @@ -122,11 +124,13 @@ struct SettingsState {
networkProtection: NetworkProtection(enabled: false, status: ""),
subscription: Subscription(enabled: false,
canPurchase: false,
isSignedIn: false,
hasActiveSubscription: false,
isSubscriptionPendingActivation: false,
isRestoring: false,
shouldDisplayRestoreSubscriptionError: false,
entitlements: []),
entitlements: [],
platform: .unknown,
isShowingStripeView: false),
sync: SyncSettings(enabled: false, title: "")
)
}
Expand Down
148 changes: 101 additions & 47 deletions DuckDuckGo/SettingsSubscriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
// limitations under the License.
//

import Core
import Subscription
import SwiftUI
import UIKit

import Subscription
import Core
@available(iOS 15.0, *)
struct SettingsSubscriptionView: View {

Expand All @@ -31,6 +31,8 @@ struct SettingsSubscriptionView: View {
@State var isShowingITP = false
@State var isShowingRestoreFlow = false
@State var isShowingSubscribeFlow = false
@State var isShowingGoogleView = false
@State var isShowingStripeView = false
@State var isShowingSubscriptionError = false

enum Constants {
Expand All @@ -39,6 +41,7 @@ struct SettingsSubscriptionView: View {
static let noEntitlementsIconWidth = 20.0
static let navigationDelay = 0.3
static let infoIcon = "info-16"
static let alertIcon = "Exclamation-Color-16"
}

private var subscriptionDescriptionView: some View {
Expand Down Expand Up @@ -102,6 +105,41 @@ struct SettingsSubscriptionView: View {
}
}

@ViewBuilder
private var subscriptionExpiredView: some View {
Group {
SettingsCustomCell(content: {
HStack(alignment: .top) {
Image(Constants.alertIcon)
.frame(width: Constants.noEntitlementsIconWidth)
.padding(.top, Constants.topCellPadding)
VStack(alignment: .leading) {
Text(UserText.settingsPProSubscriptionExpiredTitle).daxBodyRegular()
Text(UserText.settingsPProSubscribeAgain).daxFootnoteRegular()
.padding(.bottom, Constants.purchaseDescriptionPadding)
}.foregroundColor(Color(designSystemColor: .textSecondary))
}
})

let subscribeView = SubscriptionContainerView(currentView: .subscribe)
.navigationViewStyle(.stack)
.environmentObject(subscriptionNavigationCoordinator)
NavigationLink(
destination: subscribeView,
isActive: $isShowingSubscribeFlow,
label: { SettingsCellView(label: UserText.subscriptionRestoreNotFoundPlans) })

// Renew Subscription (Expired)
let settingsView = SubscriptionSettingsView(viewPlans: {
isShowingSubscribeFlow = true
})
.environmentObject(subscriptionNavigationCoordinator)
NavigationLink(destination: settingsView) {
SettingsCustomCell(content: { manageSubscriptionView })
}
}
}

@ViewBuilder
private var noEntitlementsAvailableView: some View {
Group {
Expand All @@ -124,58 +162,74 @@ struct SettingsSubscriptionView: View {
@ViewBuilder
private var subscriptionDetailsView: some View {

if viewModel.state.subscription.entitlements.contains(.networkProtection) {
SettingsCellView(label: UserText.settingsPProVPNTitle,
subtitle: viewModel.state.networkProtection.status != "" ? viewModel.state.networkProtection.status : nil,
action: { viewModel.presentLegacyView(.netP) },
disclosureIndicator: true,
isButton: true)
}

if viewModel.state.subscription.entitlements.contains(.dataBrokerProtection) {
NavigationLink(destination: SubscriptionPIRView(),
isActive: $isShowingDBP,
label: {
SettingsCellView(label: UserText.settingsPProDBPTitle,
subtitle: UserText.settingsPProDBPSubTitle)
if viewModel.state.subscription.entitlements.contains(.networkProtection) {
SettingsCellView(
label: UserText.settingsPProVPNTitle,
subtitle: viewModel.state.networkProtection.status != ""
? viewModel.state.networkProtection.status : nil,
action: { viewModel.presentLegacyView(.netP) },
disclosureIndicator: true,
isButton: true)
}

if viewModel.state.subscription.entitlements.contains(.dataBrokerProtection) {
NavigationLink(
destination: SubscriptionPIRView(),
isActive: $isShowingDBP,
label: {
SettingsCellView(
label: UserText.settingsPProDBPTitle,
subtitle: UserText.settingsPProDBPSubTitle)
})

}

if viewModel.state.subscription.entitlements.contains(.identityTheftRestoration) {
NavigationLink(destination: SubscriptionITPView(),
isActive: $isShowingITP,
label: {
SettingsCellView(label: UserText.settingsPProITRTitle,
subtitle: UserText.settingsPProITRSubTitle)

}

if viewModel.state.subscription.entitlements.contains(.identityTheftRestoration) {
NavigationLink(
destination: SubscriptionITPView(),
isActive: $isShowingITP,
label: {
SettingsCellView(
label: UserText.settingsPProITRTitle,
subtitle: UserText.settingsPProITRSubTitle)
})

}

NavigationLink(destination: SubscriptionSettingsView().environmentObject(subscriptionNavigationCoordinator)) {
SettingsCustomCell(content: { manageSubscriptionView })

}


NavigationLink(
destination: SubscriptionSettingsView().environmentObject(subscriptionNavigationCoordinator)
) {
SettingsCustomCell(content: { manageSubscriptionView })
}

}

var body: some View {
if viewModel.state.subscription.enabled && viewModel.state.subscription.canPurchase {

Section(header: Text(UserText.settingsPProSection)) {
if viewModel.state.subscription.hasActiveSubscription {

// Allow managing the subscription if we have some entitlements
if !viewModel.state.subscription.entitlements.isEmpty {
subscriptionDetailsView

// If no entitlements it should mean the backend is still out of sync
} else {
noEntitlementsAvailableView
}

switch (
viewModel.state.subscription.isSignedIn,
viewModel.state.subscription.hasActiveSubscription,
viewModel.state.subscription.entitlements.isEmpty
) {

// Signed In, Subscription Expired
case (true, false, _):
subscriptionExpiredView

// Signed in, Subscription Active, Valid entitlements
case (true, true, false):
subscriptionDetailsView // View for valid subscription details

} else if viewModel.state.subscription.isSubscriptionPendingActivation {
noEntitlementsAvailableView
} else {
purchaseSubscriptionView
// Signed in, Subscription Active, Empty Entitlements
case (true, true, true):
noEntitlementsAvailableView // View for no entitlements

// Signed out
case (false, _, _):
purchaseSubscriptionView // View for signing up or purchasing a subscription
}
}

Expand All @@ -191,7 +245,7 @@ struct SettingsSubscriptionView: View {
isShowingSubscribeFlow = false
}
}

}
}
}
2 changes: 1 addition & 1 deletion DuckDuckGo/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ struct SettingsView: View {
}

.onDisappear {
viewModel.onDissapear()
viewModel.onDisappear()
}

// MARK: Deeplink Modifiers
Expand Down

0 comments on commit 3c86f9e

Please sign in to comment.