Skip to content

Commit

Permalink
Subscription Email restore & Other flows (#2365)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/72649045549333/1205054784245717/f

Description:

Allows restoring a subscription using your email
Allows adding email to a subscription
Allows managing email from a subscription
  • Loading branch information
afterxleep authored Jan 24, 2024
1 parent 55445fb commit bcf8c70
Show file tree
Hide file tree
Showing 22 changed files with 443 additions and 89 deletions.
14 changes: 13 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,8 @@
CBEFB9142AE0844700DEDE7B /* CriticalAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEFB9102ADFFE7900DEDE7B /* CriticalAlerts.swift */; };
CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */; };
D63657192A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63657182A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift */; };
D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */; };
D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */; };
D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; };
D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */; };
D664C7B72B289AA200CBFA76 /* Subscription.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* Subscription.storekit */; };
Expand Down Expand Up @@ -803,6 +805,7 @@
D6D12CAB2B291CAA0054390C /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9C2B291CA90054390C /* APIService.swift */; };
D6D12CAC2B291CAA0054390C /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9D2B291CA90054390C /* AuthService.swift */; };
D6D12CAD2B291CAA0054390C /* PurchaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9E2B291CA90054390C /* PurchaseManager.swift */; };
D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */; };
D6E83C122B1E6AB3006C8AFB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */; };
D6E83C2E2B1EA06E006C8AFB /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */; };
D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C302B1EA309006C8AFB /* SettingsCell.swift */; };
Expand Down Expand Up @@ -2426,6 +2429,8 @@
CBF14FC627970C8A001D94D0 /* HomeMessageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessageCollectionViewCell.swift; sourceTree = "<group>"; };
CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationURLDebugViewController.swift; sourceTree = "<group>"; };
D63657182A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailManagerRequestDelegate.swift; sourceTree = "<group>"; };
D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailView.swift; sourceTree = "<group>"; };
D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailViewModel.swift; sourceTree = "<group>"; };
D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsViewModel.swift; sourceTree = "<group>"; };
D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModel.swift; sourceTree = "<group>"; };
D664C7952B289AA000CBFA76 /* Subscription.storekit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Subscription.storekit; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2453,6 +2458,7 @@
D6D12C9C2B291CA90054390C /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
D6D12C9D2B291CA90054390C /* AuthService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = "<group>"; };
D6D12C9E2B291CA90054390C /* PurchaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseManager.swift; sourceTree = "<group>"; };
D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowNavController.swift; sourceTree = "<group>"; };
D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
D6E83C302B1EA309006C8AFB /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4556,9 +4562,11 @@
D664C7932B289AA000CBFA76 /* ViewModel */ = {
isa = PBXGroup;
children = (
D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */,
D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */,
D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */,
D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */,
D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */,
D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
Expand All @@ -4579,6 +4587,7 @@
D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */,
D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */,
D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */,
D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -6525,6 +6534,7 @@
1E162610296C5C630004127F /* CustomDaxDialogViewModel.swift in Sources */,
8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */,
D6D12CA62B291CAA0054390C /* AppStoreRestoreFlow.swift in Sources */,
D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */,
C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */,
F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */,
37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */,
Expand Down Expand Up @@ -6623,6 +6633,7 @@
CB258D1329A4F24E00DEBA24 /* ConfigurationStore.swift in Sources */,
85058370219F424500ED4EDB /* SearchBarExtension.swift in Sources */,
310D09212799FD1A00DC0060 /* MIMEType.swift in Sources */,
D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */,
BD862E032B30DA170073E2EE /* VPNFeedbackFormViewModel.swift in Sources */,
F4147354283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift in Sources */,
986DA94A24884B18004A7E39 /* WebViewTransition.swift in Sources */,
Expand Down Expand Up @@ -6886,6 +6897,7 @@
F103073B1E7C91330059FEC7 /* BookmarksDataSource.swift in Sources */,
EE0153E62A6FE106002A8B26 /* NetworkProtectionRootViewModel.swift in Sources */,
85864FBC24D31EF300E756FF /* SuggestionTrayViewController.swift in Sources */,
D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */,
1EF24235273BB9D200DE3D02 /* IntervalSlider.swift in Sources */,
027F48782A4B663C001A1C6C /* AppTPFAQView.swift in Sources */,
D6E83C3D2B1F2C03006C8AFB /* SettingsLoginsView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
{
"identity" : "trackerradarkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/TrackerRadarKit.git",
"location" : "https://github.com/duckduckgo/TrackerRadarKit",
"state" : {
"revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff",
"version" : "1.2.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ public final class AppStoreAccountManagementFlow {
if #available(macOS 12.0, iOS 15.0, *) {
// In case of invalid token attempt store based authentication to obtain a new one
guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else {
return .failure(.noPastTransaction)
}
return .failure(.noPastTransaction) }

switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) {
case .success(let response):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ public final class AppStorePurchaseFlow {
if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(response.authToken),
case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) {
accountManager.storeAuthToken(token: response.authToken)
accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID)
accountManager.storeAccount(token: accessToken,
email: accountDetails.email,
externalID: accountDetails.externalID)
}
case .failure:
return .failure(.accountCreationFailed)
Expand All @@ -104,7 +106,7 @@ public final class AppStorePurchaseFlow {
@discardableResult
public static func completeSubscriptionPurchase() async -> Result<PurchaseUpdate, AppStorePurchaseFlow.Error> {

let result = await checkForEntitlements(wait: 2.0, retry: 30)
let result = await checkForEntitlements(wait: 2.0, retry: 10)

return result ? .success(PurchaseUpdate(type: "completed")) : .failure(.missingEntitlements)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ struct ErrorResponse: Decodable {
}

public protocol APIService {
static var logger: OSLog { get }
static var baseURL: URL { get }
static var session: URLSession { get }
static func executeAPICall<T>(method: String, endpoint: String, headers: [String: String]?, body: Data?) async -> Result<T, APIServiceError> where T: Decodable
Expand Down Expand Up @@ -64,7 +63,7 @@ public extension APIService {
}
}
} catch {
os_log("Service error: %{public}@", log: .error, error.localizedDescription)
os_log(.error, log: .subscription, "Service error: %{public}@", error.localizedDescription)
return .failure(.connectionError)
}
}
Expand Down Expand Up @@ -94,7 +93,8 @@ public extension APIService {
private static func printDebugInfo(method: String, endpoint: String, data: Data, response: URLResponse) {
let statusCode = (response as? HTTPURLResponse)!.statusCode
let stringData = String(data: data, encoding: .utf8) ?? ""
os_log("[%d] %{public}@ /%{public}@ :: %{public}@", log: logger, statusCode, method, endpoint, stringData)

os_log(.info, log: .subscription, "[API] %d %{public}s /%{public}s :: %{private}s", statusCode, method, endpoint, stringData)
}

static func makeAuthorizationHeader(for token: String) -> [String: String] {
Expand Down
12 changes: 5 additions & 7 deletions DuckDuckGo/Subscription/Subscription/Services/AuthService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import Common

public struct AuthService: APIService {

public static let logger: OSLog = .authService
public static let session = {
let configuration = URLSessionConfiguration.ephemeral
return URLSession(configuration: configuration)
Expand Down Expand Up @@ -107,13 +106,12 @@ public struct AuthService: APIService {
public let externalID: String
public let id: Int
public let status: String

// swiftlint:disable:next nesting
// swiftlint:disable nesting
enum CodingKeys: String, CodingKey {
case authToken = "authToken",
email, externalID = "externalId",
id,
status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase
// no underscores due to keyDecodingStrategy = .convertFromSnakeCase
case authToken = "authToken", email, externalID = "externalId", id, status
}
// swiftlint:enable nesting
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ import Foundation
import Common

public struct SubscriptionService: APIService {

public static let logger: OSLog = .subscriptionService

public static let session = {
let configuration = URLSessionConfiguration.ephemeral
return URLSession(configuration: configuration)
Expand All @@ -32,9 +31,10 @@ public struct SubscriptionService: APIService {
// MARK: -

public static func getSubscriptionDetails(token: String) async -> Result<GetSubscriptionDetailsResponse, APIServiceError> {
let result: Result<GetSubscriptionDetailsResponse, APIServiceError> = await executeAPICall(method: "GET",
endpoint: "subscription",
headers: makeAuthorizationHeader(for: token))
let result: Result<GetSubscriptionDetailsResponse,
APIServiceError> = await executeAPICall(method: "GET",
endpoint: "subscription",
headers: makeAuthorizationHeader(for: token))

switch result {
case .success(let response):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@
//

import Foundation
import Common

public final class SubscriptionPurchaseEnvironment {

public enum Environment {
public enum Environment: String {
case appStore, stripe
}

public static var current: Environment = .appStore {
didSet {
os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %@", current.rawValue)

canPurchase = false

switch current {
Expand All @@ -38,8 +41,12 @@ public final class SubscriptionPurchaseEnvironment {
}
}

public static var canPurchase: Bool = false

public static var canPurchase: Bool = false {
didSet {
os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] canPurchase %@", (canPurchase ? "true" : "false"))
}
}

private static func setupForAppStore() {
if #available(macOS 12.0, iOS 15.0, *) {
Task {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
@Published var hasActiveSubscription = false
@Published var purchaseError: AppStorePurchaseFlow.Error?
@Published var activateSubscription: Bool = false
@Published var emailActivationComplete: Bool = false

var broker: UserScriptMessageBroker?

Expand Down Expand Up @@ -141,7 +142,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
case .success(let subscriptionOptions):
return subscriptionOptions
case .failure:

os_log(.info, log: .subscription, "Failed to obtain subscription options")
return nil
}

Expand Down Expand Up @@ -206,6 +207,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) {
accountManager.storeAuthToken(token: authToken)
accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID)
} else {
os_log(.info, log: .subscription, "Failed to obtain subscription options")
}

return nil
Expand All @@ -216,8 +219,10 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
if let accessToken = accountManager.accessToken,
case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) {
accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID)
emailActivationComplete = true
} else {
os_log(.info, log: .subscription, "Failed to restore subscription from Email")
}

return nil
}

Expand Down
80 changes: 80 additions & 0 deletions DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// SubscriptionEmailViewModel.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import UserScript
import Combine
import Core

#if SUBSCRIPTION
@available(iOS 15.0, *)
final class SubscriptionEmailViewModel: ObservableObject {

let accountManager: AccountManager
let userScript: SubscriptionPagesUserScript
let subFeature: SubscriptionPagesUseSubscriptionFeature

var emailURL = URL.activateSubscriptionViaEmail
var viewTitle = UserText.subscriptionRestoreEmail
@Published var subscriptionEmail: String?
@Published var shouldReloadWebView = false
@Published var activateSubscription = false
@Published var managingSubscriptionEmail = false

private var cancellables = Set<AnyCancellable>()

init(userScript: SubscriptionPagesUserScript,
subFeature: SubscriptionPagesUseSubscriptionFeature,
accountManager: AccountManager) {
self.userScript = userScript
self.subFeature = subFeature
self.accountManager = accountManager
initializeView()
setupTransactionObservers()
}

private func initializeView() {
if accountManager.isUserAuthenticated {
// If user is authenticated, we want to "Add or manage email" instead of activating
emailURL = accountManager.email == nil ? URL.addEmailToSubscription : URL.manageSubscriptionEmail
viewTitle = accountManager.email == nil ? UserText.subscriptionRestoreAddEmailTitle : UserText.subscriptionManageEmailTitle

// Also we assume subscription requires managing, and not activation
managingSubscriptionEmail = true
}
}

private func setupTransactionObservers() {
subFeature.$emailActivationComplete
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
if value {
self?.completeActivation()
}
}
.store(in: &cancellables)
}

private func completeActivation() {
subFeature.emailActivationComplete = false
activateSubscription = true
}

}
#endif
Loading

0 comments on commit bcf8c70

Please sign in to comment.