This repository has been archived by the owner on May 6, 2024. It is now read-only.
/
CourseUpgradeHandler.swift
250 lines (217 loc) · 8.68 KB
/
CourseUpgradeHandler.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
//
// CourseUpgradeHandler.swift
// edX
//
// Created by Saeed Bashir on 8/16/21.
// Copyright © 2021 edX. All rights reserved.
//
import Foundation
class CourseUpgradeHandler: NSObject {
enum CourseUpgradeState {
case initial
case basket
case checkout
case payment
case verify
case complete
case error(type: PurchaseError, error: Error?)
}
enum CourseUpgradeMode: String {
case silent = "silent"
case userInitiated = "user_initiated"
case restore = "restore"
}
typealias Environment = NetworkManagerProvider & OEXConfigProvider
typealias UpgradeCompletionHandler = (CourseUpgradeState) -> Void
private var environment: Environment? = nil
private var completion: UpgradeCompletionHandler?
private(set) var course: OEXCourse?
private var basketID: Int = 0
private var courseSku: String = ""
private(set) var upgradeMode: CourseUpgradeMode = .userInitiated
private(set) var price: NSDecimalNumber?
private(set) var currencyCode: String?
private(set) var state: CourseUpgradeState = .initial {
didSet {
switch state {
case .basket:
CourseUpgradeHelper.shared.saveIAPInKeychain(courseSku)
break
case .complete:
CourseUpgradeHelper.shared.markIAPSKUCompleteInKeychain(courseSku)
break
case .error(let error, _):
if error != .verifyReceiptError && upgradeMode == .userInitiated {
CourseUpgradeHelper.shared.removeIAPSKUFromKeychain(courseSku)
}
break
default:
break
}
completion?(state)
}
}
init(for course: OEXCourse, environment: Environment) {
self.course = course
self.environment = environment
super.init()
}
func upgradeCourse(with upgradeMode: CourseUpgradeMode = .userInitiated, price: NSDecimalNumber?, currencyCode: String?, completion: UpgradeCompletionHandler?) {
self.completion = completion
self.upgradeMode = upgradeMode
self.price = price
self.currencyCode = currencyCode
guard let course = self.course,
let coursePurchaseSku = course.sku else {
state = .error(type: .generalError, error: error(message: "course sku is missing"))
return
}
state = .initial
courseSku = coursePurchaseSku
proceedWithUpgrade()
}
private func proceedWithUpgrade() {
addToBasket { [weak self] (orderBasket, error) in
if let basketID = orderBasket?.basketID {
self?.basketID = basketID
self?.checkout()
} else {
self?.state = .error(type: .basketError, error: error)
}
}
}
private func addToBasket(completion: @escaping (OrderBasket?, Error?) -> ()) {
state = .basket
let baseURL = CourseUpgradeAPI.baseURL
let request = CourseUpgradeAPI.basketAPI(with: courseSku)
environment?.networkManager.taskForRequest(base: baseURL, request) { response in
completion(response.data, response.error)
}
}
private func checkout() {
// Checkout API
guard basketID > 0 else {
state = .error(type: .checkoutError, error: error(message: "invalid basket id < zero"))
return
}
state = .checkout
let baseURL = CourseUpgradeAPI.baseURL
let request = CourseUpgradeAPI.checkoutAPI(basketID: basketID)
environment?.networkManager.taskForRequest(base: baseURL, request) { [weak self] response in
if response.error == nil {
if self?.upgradeMode != .userInitiated {
self?.reverifyPayment()
} else {
self?.makePayment()
}
} else {
self?.state = .error(type: .checkoutError, error: response.error)
}
}
}
private func makePayment() {
state = .payment
PaymentManager.shared.purchaseProduct(courseSku) { [weak self] success, receipt, error in
if let receipt = receipt, success {
self?.verifyPayment(receipt)
} else {
self?.state = .error(type: (error?.type ?? .paymentError), error: error?.error)
}
}
}
private func verifyPayment(_ receipt: String) {
state = .verify
// Execute API, pass the payment receipt to complete the course upgrade
let baseURL = CourseUpgradeAPI.baseURL
let request = CourseUpgradeAPI.executeAPI(basketID: basketID, price: price ?? 0, currencyCode: currencyCode ?? "", receipt: receipt)
environment?.networkManager.taskForRequest(base: baseURL, request) { [weak self] response in
if response.error == nil {
PaymentManager.shared.markPurchaseComplete(self?.courseSku ?? "", type: (self?.upgradeMode == .userInitiated) ? .purchase : .transction)
self?.state = .complete
} else {
self?.state = .error(type: .verifyReceiptError, error: response.error)
}
}
}
// Give an option of retry to learner
func reverifyPayment() {
PaymentManager.shared.purchaseReceipt { [weak self] success, receipt, error in
if let receipt = receipt, success {
self?.verifyPayment(receipt)
} else {
self?.state = .error(type: (error?.type ?? .receiptNotAvailable), error: error?.error)
}
}
}
}
extension CourseUpgradeHandler {
// IAP error messages
var errorMessage: String {
if case .error (let type, let error) = state {
guard let error = error as NSError? else { return Strings.CourseUpgrade.FailureAlert.generalErrorMessage }
switch type {
case .basketError:
return basketErrorMessage(for: error)
case .checkoutError:
return checkoutErrorMessage(for: error)
case .paymentError:
return Strings.CourseUpgrade.FailureAlert.paymentNotProcessed
case .verifyReceiptError:
return executeErrorMessage(for: error)
default:
return Strings.CourseUpgrade.FailureAlert.paymentNotProcessed
}
}
return Strings.CourseUpgrade.FailureAlert.generalErrorMessage
}
private func basketErrorMessage(for error: NSError) -> String {
switch error.code {
case 400:
return Strings.CourseUpgrade.FailureAlert.courseNotFount
case 403:
return Strings.CourseUpgrade.FailureAlert.authenticationErrorMessage
case 406:
return Strings.CourseUpgrade.FailureAlert.courseAlreadyPaid
default:
return Strings.CourseUpgrade.FailureAlert.paymentNotProcessed
}
}
private func checkoutErrorMessage(for error: NSError) -> String {
switch error.code {
case 403:
return Strings.CourseUpgrade.FailureAlert.authenticationErrorMessage
default:
return Strings.CourseUpgrade.FailureAlert.paymentNotProcessed
}
}
private func executeErrorMessage(for error: NSError) -> String {
switch error.code {
case 409:
return Strings.CourseUpgrade.FailureAlert.courseAlreadyPaid
default:
return Strings.CourseUpgrade.FailureAlert.courseNotFullfilled
}
}
var formattedError: String {
let unhandledError = "unhandledError"
if case .error(let type, let error) = state {
guard let error = error as NSError? else { return unhandledError }
if case .paymentError = type {
return "\(type.errorString)-\(error.code)-\(error.localizedDescription)"
}
if let errorResponse = error.userInfo.values.first,
let json = errorResponse as? JSON,
let errorMessage = json.dictionary?.values.first {
return "\(type.errorString)-\(error.code)-\(errorMessage)"
}
else if let errorMessage = error.userInfo[NSLocalizedDescriptionKey] as? String {
return errorMessage
}
return "\(type.errorString)-\(error.code)-\(unhandledError)"
}
return unhandledError
}
fileprivate func error(message: String) -> NSError {
return NSError(domain:"edx.app.courseupgrade", code: 1010, userInfo: [NSLocalizedDescriptionKey: JSON(["error": message])])
}
}