Commit 0b88a0b4 by Demid Merzlyakov

IOS-155: [WIP] purchase & verification methods.

parent eaad2e8f
......@@ -47,7 +47,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
ThemeManager.refreshAppearance()
UserDefaults.migrateUserDefaultsToAppGroupsIfNeeded()
storeManager.completeTransactions()
storeManager.onStartup()
if let launchOptions = launchOptions {
log.debug("Launch options: \(launchOptions)")
......@@ -205,12 +205,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
return storeManager.removeAdsPurchased
}
let proSubscription = ClosureFeatureAvailabilityChecker { [weak self] in
let activeProSubscription = ClosureFeatureAvailabilityChecker { [weak self] in
guard let storeManager = self?.storeManager else {
return false
}
return storeManager.hasSubscription
}
let everSubscribed = ClosureFeatureAvailabilityChecker { [weak self] in
guard let storeManager = self?.storeManager else {
return false
}
return storeManager.everHadSubscription
}
let isPhone = DeviceTypeFeatureAvailabilityChecker(deviceType: .phone)
var availabilityCheckers = [AppFeature: FeatureAvailabilityChecker]()
......@@ -219,9 +226,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// To make sure all features are explicitly configured.
switch feature {
case .ads:
availabilityCheckers[feature] = !premium && !proSubscription
availabilityCheckers[feature] = !premium && !activeProSubscription && !everSubscribed
case .minutelyForecast:
availabilityCheckers[feature] = proSubscription
availabilityCheckers[feature] = activeProSubscription
case .shorts:
availabilityCheckers[feature] = usOnly && isPhone
case .airQualityIndex:
......
......@@ -7,6 +7,7 @@
import Foundation
import OneWeatherCore
import OneWeatherAnalytics
import SwiftyStoreKit
public protocol StoreManagerObserver {
......@@ -15,15 +16,24 @@ public protocol StoreManagerObserver {
public class StoreManager {
private let observers = MulticastDelegate<StoreManagerObserver>()
private let configManager: ConfigManager
private let log = Logger(componentName: "StoreManager 💸")
private var activeInAppRequests = [String: InAppRequest]()
private static let sharedSecret = "9462f5b715774b2dafac935098e3f3c6"
public static let shared = StoreManager()
public init(configManager: ConfigManager = ConfigManager.shared) {
self.configManager = configManager
self.configManager.add(delegate: self)
}
// MARK: - Get known status
@UserDefaultsValue("com.inmobi.oneweather.WeatherKey", defaultValue: false, userDefaults: UserDefaults.appDefaults)
public var removeAdsPurchased: Bool {
didSet {
if removeAdsPurchased {
if removeAdsPurchased && removeAdsPurchased != oldValue {
observers.invoke { observer in
observer.storeManagerUpdatedStatus(self)
}
......@@ -31,31 +41,64 @@ public class StoreManager {
}
}
@UserDefaultsValue("com.inmobi.oneweather.WeatherSubKey", defaultValue: false, userDefaults: UserDefaults.appDefaults)
public var hasSubscription: Bool {
#warning("Not implemented!")
//TODO: Implement!
#if DEBUG
//TODO: REMOVE THIS'
return false
#warning("THIS IS A DEBUG-ONLY PIECE OF CODE! Not even temporary! Remove before making a production build.")
#else
#error("THIS IS A DEBUG-ONLY PIECE OF CODE! Not even temporary! Remove before making a production build.")
#endif
didSet {
if hasSubscription != oldValue {
observers.invoke { observer in
observer.storeManagerUpdatedStatus(self)
}
}
if hasSubscription {
self.everHadSubscription = true
}
}
}
@UserDefaultsValue("com.inmobi.oneweather.WeatherSubKeyEver", defaultValue: false, userDefaults: UserDefaults.appDefaults)
public var everHadSubscription: Bool {
didSet {
if everHadSubscription && everHadSubscription != oldValue {
observers.invoke { observer in
observer.storeManagerUpdatedStatus(self)
}
}
}
}
@UserDefaultsOptionalValue("com.inmobi.oneweather.WeatherSubKeyDate", userDefaults: UserDefaults.appDefaults)
public var subscriptionExpirationDate: Date?
// MARK: - Actions
public func completeTransactions() {
public func onStartup() {
self.checkSubscriptionExpirationLocally()
self.completeTransactions()
self.updateProductInfo()
#warning("Not implemented!")
//TODO: implement
// We need to verify subscription status on startup as well
}
private func completeTransactions() {
// This SHOULD ONLY BE CALLED ONCE, in the AppDelegate
let log = self.log
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
log.info("Complete transactions for \(purchases.count) purchases")
for purchase in purchases {
log.info("Purchase \(purchase.productId): \(purchase.transaction.transactionState)")
switch purchase.transaction.transactionState {
case .purchased, .restored:
if purchase.needsFinishTransaction {
// Deliver content from server, then:
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
// Unlock content
if self.isPremiumSubscription(productId: purchase.productId) {
self.hasSubscription = true
self.everHadSubscription = true
}
else if purchase.productId == kInAppOneWeatherProId {
self.removeAdsPurchased = true
}
case .failed, .purchasing, .deferred:
break // do nothing
@unknown default:
......@@ -65,14 +108,86 @@ public class StoreManager {
}
}
public func purchase(subscription: SubscriptionConfig) {
public func updateProductInfo() {
let log = self.log
log.debug("Updating product info...")
let config = configManager.config.subscriptionsConfig
guard config.subscriptionsEnabled else {
log.debug("Update product info: subscriptions disabled. Skip.")
return
}
let subscriptions: [SubscriptionConfig]
if removeAdsPurchased {
subscriptions = [config.monthlyDiscounted, config.yearlyDiscounted]
}
else {
subscriptions = [config.monthly, config.yearly]
}
let productIds = Set<String>(subscriptions.map { $0.productId })
SwiftyStoreKit.retrieveProductsInfo(productIds) { result in
if let error = result.error {
log.error("Update info: error: \(error)")
}
for product in result.retrievedProducts {
log.debug("For \(product.productIdentifier): \(product.debugDescription)")
}
}
}
public func updatePurchaseState() {
public func purchase(subscription: SubscriptionConfig) {
let log = self.log
let productId = subscription.productId
log.info("Purchase \(productId) start.")
let request: InAppRequest = SwiftyStoreKit.purchaseProduct(productId, atomically: true) { result in
defer {
self.activeInAppRequests[productId] = nil
}
switch result {
case .success(let product):
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
case .error(error: let error):
log.error("Purchase: error for \(productId): \(error)")
}
}
activeInAppRequests[productId] = request
}
public func verifySubscriptions() {
let log = self.log
log.info("Verify subscriptions...")
let service: AppleReceiptValidator.VerifyReceiptURLType
#if DEBUG
service = .production
#else
service = .production
#endif
let appleValidator = AppleReceiptValidator(service: service, sharedSecret: StoreManager.sharedSecret)
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
switch result {
case .success(let receipt):
let purchaseResult = SwiftyStoreKit.verifySubscriptions(productIds: self.allSubscriptionIds, inReceipt: receipt)
switch purchaseResult {
case .purchased(let expiryDate, let items):
log.debug("Verify: purchased subscription (valid until \(expiryDate))! Items: \(items)")
self.hasSubscription = true
self.everHadSubscription = true
self.subscriptionExpirationDate = expiryDate
case .expired(let expiryDate, let items):
log.debug("Subscription expired since \(expiryDate)\n\(items)\n")
self.everHadSubscription = true
case .notPurchased:
log.debug("The user has never purchased a subscription.")
self.hasSubscription = false
self.everHadSubscription = false
}
case .error(let error):
log.error("Receipt verification failed: \(error)")
}
}
}
//MARK: - Observers management
......@@ -83,4 +198,32 @@ public class StoreManager {
public func remove(observer: StoreManagerObserver) {
observers.remove(delegate: observer)
}
//MARK: - Private helper methods and vars
private var allSubscriptionIds: Set<String> {
let config = configManager.config.subscriptionsConfig
let subscriptionsList = [config.monthly, config.yearly, config.monthlyDiscounted, config.yearlyDiscounted].map {
$0.productId
}
return Set<String>(subscriptionsList)
}
private func isPremiumSubscription(productId: String) -> Bool {
return allSubscriptionIds.contains(productId)
}
private func checkSubscriptionExpirationLocally() {
guard let expirationDate = self.subscriptionExpirationDate else {
return
}
if expirationDate < Date() {
self.hasSubscription = false
}
}
}
extension StoreManager: ConfigManagerDelegate {
public func dataUpdated(by configManager: ConfigManager) {
self.updateProductInfo()
}
}
......@@ -14,7 +14,7 @@ public struct AppConfig: Codable {
public let shortsLeftBelowCount: Int
public let shortsSwipeUpNudgeCount: Int
private let explicitFeatureAvailability: [AppFeature: Bool]
private let subscriptionsConfig: SubscriptionsListConfig
public let subscriptionsConfig: SubscriptionsListConfig
public init(popularCities: [GeoNamesPlace]?,
adConfig: AdConfig,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment