-
Notifications
You must be signed in to change notification settings - Fork 406
/
SubscriptionPagesUseSubscriptionFeature.swift
277 lines (227 loc) · 9.84 KB
/
SubscriptionPagesUseSubscriptionFeature.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
//
// SubscriptionPagesUseSubscriptionFeature.swift
// DuckDuckGo
//
// Copyright © 2023 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.
//
#if SUBSCRIPTION
import BrowserServicesKit
import Common
import Foundation
import WebKit
import UserScript
import Combine
@available(iOS 15.0, *)
final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObject {
struct Constants {
static let featureName = "useSubscription"
static let os = "ios"
static let empty = ""
}
struct OriginDomains {
static let duckduckgo = "duckduckgo.com"
static let abrown = "abrown.duckduckgo.com"
}
struct Handlers {
static let getSubscription = "getSubscription"
static let setSubscription = "setSubscription"
static let backToSettings = "backToSettings"
static let getSubscriptionOptions = "getSubscriptionOptions"
static let subscriptionSelected = "subscriptionSelected"
static let activateSubscription = "activateSubscription"
static let featureSelected = "featureSelected"
}
struct ProductIDs {
static let monthly = "1month"
static let yearly = "1year"
}
struct RecurrenceOptions {
static let month = "monthly"
static let year = "yearly"
}
enum TransactionStatus {
case idle, purchasing, restoring, polling
}
struct FeatureSelection: Codable {
let feature: String
}
@Published var transactionStatus: TransactionStatus = .idle
@Published var hasActiveSubscription = false
@Published var purchaseError: AppStorePurchaseFlow.Error?
@Published var activateSubscription: Bool = false
@Published var emailActivationComplete: Bool = false
@Published var selectedFeature: FeatureSelection?
var broker: UserScriptMessageBroker?
var featureName = Constants.featureName
var messageOriginPolicy: MessageOriginPolicy = .only(rules: [
.exact(hostname: OriginDomains.duckduckgo),
.exact(hostname: OriginDomains.abrown)
])
var originalMessage: WKScriptMessage?
func with(broker: UserScriptMessageBroker) {
self.broker = broker
}
func handler(forMethodNamed methodName: String) -> Subfeature.Handler? {
switch methodName {
case Handlers.getSubscription: return getSubscription
case Handlers.setSubscription: return setSubscription
case Handlers.backToSettings: return backToSettings
case Handlers.getSubscriptionOptions: return getSubscriptionOptions
case Handlers.subscriptionSelected: return subscriptionSelected
case Handlers.activateSubscription: return activateSubscription
case Handlers.featureSelected: return featureSelected
default:
return nil
}
}
/// Values that the Frontend can use to determine the current state.
// swiftlint:disable nesting
struct SubscriptionValues: Codable {
enum CodingKeys: String, CodingKey {
case token
}
let token: String
}
// swiftlint:enable nesting
// Manage transation in progress flag
private func withTransactionInProgress<T>(_ work: () async throws -> T) async rethrows -> T {
transactionStatus = transactionStatus
defer {
transactionStatus = .idle
}
return try await work()
}
private func resetSubscriptionFlow() {
hasActiveSubscription = false
purchaseError = nil
}
func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? {
let authToken = AccountManager().authToken ?? Constants.empty
return Subscription(token: authToken)
}
func getSubscriptionOptions(params: Any, original: WKScriptMessage) async throws -> Encodable? {
await withTransactionInProgress {
transactionStatus = .purchasing
resetSubscriptionFlow()
switch await AppStorePurchaseFlow.subscriptionOptions() {
case .success(let subscriptionOptions):
return subscriptionOptions
case .failure:
os_log(.info, log: .subscription, "Failed to obtain subscription options")
return nil
}
}
}
func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? {
await withTransactionInProgress {
transactionStatus = .purchasing
resetSubscriptionFlow()
struct SubscriptionSelection: Decodable {
let id: String
}
let message = original
guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else {
assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection")
return nil
}
// Check for active subscriptions
if await PurchaseManager.hasActiveSubscription() {
hasActiveSubscription = true
return nil
}
let emailAccessToken = try? EmailManager().getToken()
switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) {
case .success:
break
case .failure:
purchaseError = .purchaseFailed
originalMessage = original
return nil
}
transactionStatus = .polling
switch await AppStorePurchaseFlow.completeSubscriptionPurchase() {
case .success(let purchaseUpdate):
await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate)
case .failure:
purchaseError = .missingEntitlements
await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed"))
}
return nil
}
}
func setSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? {
guard let subscriptionValues: SubscriptionValues = DecodableHelper.decode(from: params) else {
assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionValues")
return nil
}
let authToken = subscriptionValues.token
let accountManager = AccountManager()
if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(authToken),
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
}
func backToSettings(params: Any, original: WKScriptMessage) async throws -> Encodable? {
let accountManager = AccountManager()
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
}
func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? {
activateSubscription = true
return nil
}
func featureSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? {
guard let featureSelection: FeatureSelection = DecodableHelper.decode(from: params) else {
assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection")
return nil
}
selectedFeature = featureSelection
return nil
}
// MARK: Push actions
enum SubscribeActionName: String {
case onPurchaseUpdate
}
@MainActor
func pushPurchaseUpdate(originalMessage: WKScriptMessage, purchaseUpdate: PurchaseUpdate) async {
pushAction(method: .onPurchaseUpdate, webView: originalMessage.webView!, params: purchaseUpdate)
}
func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) {
let broker = UserScriptMessageBroker(context: SubscriptionPagesUserScript.context, requiresRunInPageContentWorld: true )
broker.push(method: method.rawValue, params: params, for: self, into: webView)
}
func restoreAccountFromAppStorePurchase() async -> Bool {
await withTransactionInProgress {
transactionStatus = .restoring
switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() {
case .success(let update):
return true
case .failure:
return false
}
}
}
}
#endif