diff --git a/HabitRPG/UI/Inventory/ShopViewController.swift b/HabitRPG/UI/Inventory/ShopViewController.swift index 0ebfd05d1..71b3e99ec 100644 --- a/HabitRPG/UI/Inventory/ShopViewController.swift +++ b/HabitRPG/UI/Inventory/ShopViewController.swift @@ -79,6 +79,10 @@ class ShopViewController: BaseCollectionViewController, ShopCollectionViewDataSo refresher = HabiticaRefresControl() refresher.addTarget(self, action: #selector(refresh), for: .valueChanged) collectionView.addSubview(refresher) + + userRepository.getUser().on(value: {[weak self] user in + self?.updateNavBar(gold: Int(user.stats?.gold ?? 0.0), gems: user.gemCount, hourglasses: user.purchased?.subscriptionPlan?.consecutive?.hourglasses ?? 0) + }).start() } private var isSubscribed: Bool? diff --git a/HabitRPG/UI/Purchases/SubscriptionBenefitView.swift b/HabitRPG/UI/Purchases/SubscriptionBenefitView.swift index 1b5a66701..870af7445 100644 --- a/HabitRPG/UI/Purchases/SubscriptionBenefitView.swift +++ b/HabitRPG/UI/Purchases/SubscriptionBenefitView.swift @@ -27,7 +27,7 @@ struct SubscriptionBenefitView: View .cornerRadius(8) VStack(alignment: .leading, spacing: 4) { title.font(.system(size: 15, weight: .semibold)) - description.font(.system(size: 13)).lineSpacing(3) + description.font(.system(size: 13)).lineSpacing(2) }.frame(maxWidth: .infinity, alignment: .leading) }.padding(.leading, 12) .padding(.vertical, 8) diff --git a/HabitRPG/UI/Purchases/SubscriptionDetailViewUI.swift b/HabitRPG/UI/Purchases/SubscriptionDetailViewUI.swift index caf0486c8..d15579c63 100644 --- a/HabitRPG/UI/Purchases/SubscriptionDetailViewUI.swift +++ b/HabitRPG/UI/Purchases/SubscriptionDetailViewUI.swift @@ -79,7 +79,7 @@ struct SubscriptionDetailViewUI: View { var paymentTypeText: String { if plan.isGifted { return L10n.Subscription.gifted - } else if plan.dateTerminated != nil { + } else if plan.dateTerminated != nil || PurchaseHandler.shared.wasSubscriptionCancelled == true { return L10n.cancelled } else if plan.paymentMethod == "Apple" { if let nextEstimatedPayment = plan.nextEstimatedPayment { @@ -272,24 +272,26 @@ struct SubscriptionDetailViewUI: View { Image(Asset.hourglassBannerRight.name) } } - DetailContainer(verticalPadding: 16) { - VStack(alignment: .leading, spacing: 6) { - Group { - if plan.isGifted { - Text(L10n.continueBenefits) - } else if plan.dateTerminated != nil { - Text(L10n.resubscribe) - } else if plan.paymentMethod == "Apple" { - Text(L10n.editCancelSubscription) - } else { - Text(L10n.cancelSubscription) + if !(plan.isGifted && plan.isActive) { + DetailContainer(verticalPadding: 16) { + VStack(alignment: .leading, spacing: 6) { + Group { + if plan.isGifted { + Text(L10n.continueBenefits) + } else if plan.dateTerminated != nil { + Text(L10n.resubscribe) + } else if plan.paymentMethod == "Apple" { + Text(L10n.editCancelSubscription) + } else { + Text(L10n.cancelSubscription) + } + }.font(.system(size: 15, weight: .semibold)) + Text(cancelDescription).font(.system(size: 13)) + if let text = cancelButtonText { + HabiticaButtonUI(label: Text(text).foregroundColor(.purple100), color: .yellow100, size: .compact) { + cancelSubscription() + }.padding(.top, 7) } - }.font(.system(size: 15, weight: .semibold)) - Text(cancelDescription).font(.system(size: 13)) - if let text = cancelButtonText { - HabiticaButtonUI(label: Text(text).foregroundColor(.purple100), color: .yellow100, size: .compact) { - cancelSubscription() - }.padding(.top, 7) } } } diff --git a/HabitRPG/UI/Purchases/SubscriptionPage.swift b/HabitRPG/UI/Purchases/SubscriptionPage.swift index 3f9ee122d..7186a6640 100644 --- a/HabitRPG/UI/Purchases/SubscriptionPage.swift +++ b/HabitRPG/UI/Purchases/SubscriptionPage.swift @@ -111,13 +111,17 @@ class SubscriptionViewModel: BaseSubscriptionViewModel { super.init() userRepository.getUser().on(value: {[weak self] user in - if (self?.scrollToTop == nil && self?.isSubscribed == false && user.isSubscribed) { - self?.scrollToTop = Date() + if self?.isSubscribed == false && user.isSubscribed { + if self?.scrollToTop == nil { + self?.scrollToTop = Date() + } + if presentationPoint != nil { + self?.dismiss() + } } self?.isSubscribed = user.isSubscribed self?.subscriptionPlan = user.purchased?.subscriptionPlan self?.showHourglassPromo = user.purchased?.subscriptionPlan?.isEligableForHourglassPromo == true - }).start() if presentationPoint != nil { @@ -125,12 +129,12 @@ class SubscriptionViewModel: BaseSubscriptionViewModel { availableSubscriptions.remove(at: 1) } - disposable.inner.add(inventoryRepository.getLatestMysteryGear().on(value: { gear in - self.mysteryGear = gear + disposable.inner.add(inventoryRepository.getLatestMysteryGear().on(value: {[weak self] gear in + self?.mysteryGear = gear }).start()) - disposable.inner.add(inventoryRepository.getLatestMysteryGearSet().on(value: { set in - self.mysteryGearSet = set + disposable.inner.add(inventoryRepository.getLatestMysteryGearSet().on(value: {[weak self] set in + self?.mysteryGearSet = set }).start()) retrieveProductList() @@ -161,41 +165,9 @@ class SubscriptionViewModel: BaseSubscriptionViewModel { withAnimation { isSubscribing = true } - SwiftyStoreKit.purchaseProduct(selectedSubscription, atomically: false) { result in + PurchaseHandler.shared.purchaseSubscription(selectedSubscription) {[weak self] _ in withAnimation { - self.isSubscribing = false - } - switch result { - case .success(let product): - self.verifyAndSubscribe(product) - logger.log("Purchase Success: \(product.productId)") - case .error(let error): - Analytics.logEvent("purchase_failed", parameters: ["error": error.localizedDescription, "code": error.errorCode]) - - logger.log("Purchase Failed: \(error)", level: .error) - case .deferred: - return - } - } - } - - func verifyAndSubscribe(_ product: PurchaseDetails) { - SwiftyStoreKit.verifyReceipt(using: appleValidator, forceRefresh: true) { result in - switch result { - case .success(let receipt): - // Verify the purchase of a Subscription - if self.isValidSubscription(product.productId, receipt: receipt) { - self.activateSubscription(product.productId, receipt: receipt) { status in - if status { - if product.needsFinishTransaction { - SwiftyStoreKit.finishTransaction(product.transaction) - } - } - self.dismiss() - } - } - case .error(let error): - logger.log("Receipt verification failed: \(error)", level: .error) + self?.isSubscribing = false } } } @@ -229,22 +201,7 @@ class SubscriptionViewModel: BaseSubscriptionViewModel { return false } } - - func activateSubscription(_ identifier: String, receipt: ReceiptInfo, completion: @escaping (Bool) -> Void) { - if let lastReceipt = receipt["latest_receipt"] as? String { - userRepository.subscribe(sku: identifier, receipt: lastReceipt).observeResult { (result) in - switch result { - case .success: - completion(true) - self.isSubscribed = true - self.scrollToTop = Date() - case .failure: - completion(false) - } - } - } - } - + func checkForExistingSubscription() { isRestoringPurchase = true SwiftyStoreKit.verifyReceipt(using: self.appleValidator, forceRefresh: true) { result in @@ -257,11 +214,7 @@ class SubscriptionViewModel: BaseSubscriptionViewModel { for purchase in purchases { if let identifier = purchase["product_id"] as? String { if self.isValidSubscription(identifier, receipt: verifiedReceipt) { - self.activateSubscription(identifier, receipt: verifiedReceipt) {status in - if status { - return - } - } + PurchaseHandler.shared.activateSubscription(identifier, receipt: verifiedReceipt, completion: { _ in }) } } } @@ -319,7 +272,8 @@ struct SubscriptionBenefitListView: View { if presentationPoint != .armoire { SubscriptionBenefitView(icon: Image(Asset.subBenefitsArmoire.name), title: Text(L10n.Subscription.infoArmoireTitle), description: Text(L10n.Subscription.infoArmoireDescription)) } - SubscriptionBenefitView(icon: Image(Asset.subBenefitDrops.name), title: Text(L10n.subscriptionInfo5Title), description: Text(L10n.subscriptionInfo5Description)).padding(.bottom, 16) + SubscriptionBenefitView(icon: Image(Asset.subBenefitDrops.name), title: Text(L10n.subscriptionInfo5Title), description: Text(L10n.subscriptionInfo5Description)) + .padding(.bottom, 16) } } @@ -416,10 +370,12 @@ struct SubscriptionPage: View { .frame(width: reader.size.width * (CGFloat(viewModel.subscriptionPlan?.gemCapTotal ?? 0) / 50.0), height: 8) } } + .frame(height: 8) .padding(.top, 8) .padding(.horizontal, 41) } - }.padding(.bottom, 12) + } + .padding(.bottom, 16) } SubscriptionBenefitListView(presentationPoint: viewModel.presentationPoint, mysteryGear: viewModel.mysteryGear, mysteryGearSet: viewModel.mysteryGearSet) .padding(.horizontal, 24) diff --git a/HabitRPG/Utilities/PurchaseHandler.swift b/HabitRPG/Utilities/PurchaseHandler.swift index 550c8f8f4..b294d298d 100644 --- a/HabitRPG/Utilities/PurchaseHandler.swift +++ b/HabitRPG/Utilities/PurchaseHandler.swift @@ -179,6 +179,39 @@ class PurchaseHandler: NSObject, SKPaymentTransactionObserver { } } + func purchaseSubscription(_ identifier: String, completion: @escaping (Bool) -> Void) { + SwiftyStoreKit.purchaseProduct(identifier, atomically: false) { result in + switch result { + case .success(let product): + SwiftyStoreKit.verifyReceipt(using: self.appleValidator) { verificationResult in + switch verificationResult { + case .success(let receipt): + if self.isValidSubscription(identifier, receipt: receipt) == true { + self.activateSubscription(identifier, receipt: receipt) { status in + if status { + SwiftyStoreKit.finishTransaction(product.transaction) + } + } + } else { + SwiftyStoreKit.finishTransaction(product.transaction) + } + case .error(let error): + if error.localizedDescription.contains("Code: 1") { + return + } + self.handle(error: error) + } + } + case .error(let error): + self.handle(error: error) + completion(false) + case .deferred: + completion(false) + return + } + } + } + func verifyPurchase(_ product: PurchaseDetails) { SwiftyStoreKit.fetchReceipt(forceRefresh: false) { result in switch result { @@ -210,7 +243,7 @@ class PurchaseHandler: NSObject, SKPaymentTransactionObserver { self?.pendingGifts.removeValue(forKey: identifier) } completion(true) - self?.userRepository.retrieveUser().observeCompleted {} + self?.userRepository.retrieveUser(forced: true).observeCompleted {} } else { completion(false) } @@ -229,7 +262,7 @@ class PurchaseHandler: NSObject, SKPaymentTransactionObserver { if result != nil { self?.pendingGifts.removeValue(forKey: identifier) completion(true) - self?.userRepository.retrieveUser().observeCompleted {} + self?.userRepository.retrieveUser(forced: true).observeCompleted {} } else { completion(false) } @@ -240,9 +273,17 @@ class PurchaseHandler: NSObject, SKPaymentTransactionObserver { return PurchaseHandler.IAPIdentifiers.contains(identifier) } + private var isActivatingSubscription = false func activateSubscription(_ identifier: String, receipt: ReceiptInfo, completion: @escaping (Bool) -> Void) { + if isActivatingSubscription { + return + } if let lastReceipt = receipt["latest_receipt"] as? String { - userRepository.subscribe(sku: identifier, receipt: lastReceipt).observeResult { (result) in + isActivatingSubscription = true + userRepository.subscribe(sku: identifier, receipt: lastReceipt).on(completed: { + self.userRepository.retrieveUser(forced: true).observeCompleted {} + }).observeResult { (result) in + self.isActivatingSubscription = false switch result { case .success: completion(true) @@ -293,7 +334,7 @@ class PurchaseHandler: NSObject, SKPaymentTransactionObserver { } } - private func applySubscription(transaction: SKPaymentTransaction) { + func applySubscription(transaction: SKPaymentTransaction) { if SwiftyStoreKit.localReceiptData == nil { return } @@ -336,7 +377,26 @@ class PurchaseHandler: NSObject, SKPaymentTransactionObserver { case .expired(expiryDate: _, items: let items): self.processItemsForCancellation(items: items, searchedID: searchedID) default: - return + if #available(iOS 15.0, *) { + Task { + do { + let statuses = try await Product.SubscriptionInfo.status(for: "20345996") + for status in statuses { + guard case .verified(let renewalInfo) = status.renewalInfo else { + continue + } + let isCancelled = renewalInfo.expirationReason != nil && renewalInfo.expirationReason != .billingError + if !renewalInfo.willAutoRenew || isCancelled || (renewalInfo.expirationReason == .billingError && !renewalInfo.isInBillingRetry) { + self.userRepository.cancelSubscription().observeCompleted { + self.wasSubscriptionCancelled = true + } + } + } + } catch let error { + print(error) + } + } + } } case .error: return diff --git a/Habitica API Client/Habitica API Client/User/CancelSubscribeCall.swift b/Habitica API Client/Habitica API Client/User/CancelSubscribeCall.swift index 40b08dfa5..86125bdd2 100644 --- a/Habitica API Client/Habitica API Client/User/CancelSubscribeCall.swift +++ b/Habitica API Client/Habitica API Client/User/CancelSubscribeCall.swift @@ -13,5 +13,6 @@ import ReactiveSwift public class CancelSubscribeCall: ResponseObjectCall { public init() { super.init(httpMethod: .GET, endpoint: "iap/ios/subscribe/cancel") + customErrorHandler = PrintNetworkErrorHandler() } } diff --git a/Habitica Database/Habitica Database/Repositories/ContentLocalRepository.swift b/Habitica Database/Habitica Database/Repositories/ContentLocalRepository.swift index 9441b3945..e3f0f30af 100644 --- a/Habitica Database/Habitica Database/Repositories/ContentLocalRepository.swift +++ b/Habitica Database/Habitica Database/Repositories/ContentLocalRepository.swift @@ -48,6 +48,9 @@ public class ContentLocalRepository: BaseLocalRepository { content.customizations.forEach({ (customization) in newObjects.append(RealmCustomization(customization)) }) + content.mystery.forEach({ mysterySet in + newObjects.append(RealmGearSet(mysterySet)) + }) saveMysteryItem() diff --git a/Habitica Models/Habitica Models/User/SubscriptionPlanProtocol.swift b/Habitica Models/Habitica Models/User/SubscriptionPlanProtocol.swift index f6a391d97..8a7de80ba 100644 --- a/Habitica Models/Habitica Models/User/SubscriptionPlanProtocol.swift +++ b/Habitica Models/Habitica Models/User/SubscriptionPlanProtocol.swift @@ -74,11 +74,7 @@ public extension SubscriptionPlanProtocol { } var monthsUntilNextHourglass: Int { - if isMonthlyRenewal { - return 3 - perkMonthCount - } else { - return (consecutive?.offset ?? 0) + 1 - } + return 1 } var isEligableForHourglassPromo: Bool { @@ -93,7 +89,7 @@ public extension SubscriptionPlanProtocol { let now = Date() let calendar = Calendar.current while (startDate < now) { - var newDate = calendar.date(byAdding: .month, value: interval, to: startDate) + let newDate = calendar.date(byAdding: .month, value: interval, to: startDate) if let date = newDate { startDate = date } else {