Commit e5ea3459 by Demid Merzlyakov

IOS-155: basic layout of the subscription overview screeen.

parent db14649d
......@@ -235,6 +235,14 @@
CE578FE725FB415F00E8B85D /* LocationsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE578FE425FB415F00E8B85D /* LocationsViewModel.swift */; };
CE5F0CBC268A031800B99572 /* OneWeatherWidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5F0CBB268A031800B99572 /* OneWeatherWidgetsBundle.swift */; };
CE6BE4942634170800626822 /* USStateCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6BE4932634170800626822 /* USStateCode.swift */; };
CE6E410326EBA3B0009829AE /* SubscriptionStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E410226EBA3B0009829AE /* SubscriptionStoreViewController.swift */; };
CE6E410626EBA3EB009829AE /* SubscriptionOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E410526EBA3EB009829AE /* SubscriptionOverviewViewController.swift */; };
CE6E410826EBA7C0009829AE /* SubscriptionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E410726EBA7C0009829AE /* SubscriptionCoordinator.swift */; };
CE6E410A26EBA800009829AE /* SubscriptionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E410926EBA800009829AE /* SubscriptionViewModel.swift */; };
CE6E410C26EBAE84009829AE /* SubscriptionTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E410B26EBAE84009829AE /* SubscriptionTopView.swift */; };
CE6E410E26EBBB3B009829AE /* SubscriptionDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E410D26EBBB3B009829AE /* SubscriptionDescriptionView.swift */; };
CE6E411026EBBB57009829AE /* SubscriptionProDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E410F26EBBB57009829AE /* SubscriptionProDescriptionView.swift */; };
CE6E411226EBBB64009829AE /* SubscriptionPurchaseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E411126EBBB64009829AE /* SubscriptionPurchaseButton.swift */; };
CE6E411426EBC0E9009829AE /* LocalizationChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E411326EBC0E9009829AE /* LocalizationChangeObserver.swift */; };
CE7298C9267A34F3002745D0 /* BlendFIPSSource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEF40FF265E47FF00425D8F /* BlendFIPSSource.framework */; };
CE7298CC267A34F5002745D0 /* BlendHealthSource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD3883C12657B6A10070FD6F /* BlendHealthSource.framework */; };
......@@ -538,6 +546,14 @@
CE5F0CBB268A031800B99572 /* OneWeatherWidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneWeatherWidgetsBundle.swift; sourceTree = "<group>"; };
CE6BE4932634170800626822 /* USStateCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = USStateCode.swift; sourceTree = "<group>"; };
CE6E410026EB4A92009829AE /* StoreKitTestCertificate.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = StoreKitTestCertificate.cer; sourceTree = "<group>"; };
CE6E410226EBA3B0009829AE /* SubscriptionStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionStoreViewController.swift; sourceTree = "<group>"; };
CE6E410526EBA3EB009829AE /* SubscriptionOverviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionOverviewViewController.swift; sourceTree = "<group>"; };
CE6E410726EBA7C0009829AE /* SubscriptionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionCoordinator.swift; sourceTree = "<group>"; };
CE6E410926EBA800009829AE /* SubscriptionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionViewModel.swift; sourceTree = "<group>"; };
CE6E410B26EBAE84009829AE /* SubscriptionTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionTopView.swift; sourceTree = "<group>"; };
CE6E410D26EBBB3B009829AE /* SubscriptionDescriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionDescriptionView.swift; sourceTree = "<group>"; };
CE6E410F26EBBB57009829AE /* SubscriptionProDescriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionProDescriptionView.swift; sourceTree = "<group>"; };
CE6E411126EBBB64009829AE /* SubscriptionPurchaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPurchaseButton.swift; sourceTree = "<group>"; };
CE6E411326EBC0E9009829AE /* LocalizationChangeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationChangeObserver.swift; sourceTree = "<group>"; };
CE81A421266E289E00800EFF /* NativeAdView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAdView.swift; sourceTree = "<group>"; };
CE849DB52638C33600DEFFBD /* OneWeatherNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OneWeatherNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
......@@ -729,6 +745,7 @@
CEC8FBB1263976240001A6BF /* OnboardingCoordinator.swift */,
CD7D3188268F33CC000D01FA /* WidgetPromotionCoordinator.swift */,
CD8579692671FA8100CC4CDA /* ShortsCoordinator.swift */,
CE6E410726EBA7C0009829AE /* SubscriptionCoordinator.swift */,
);
path = Coordinators;
sourceTree = "<group>";
......@@ -922,6 +939,7 @@
CD6761872625C3360079D273 /* RadarViewModel.swift */,
CEC8FBB4263976400001A6BF /* OnboardingViewModel.swift */,
CD85796E26721C2900CC4CDA /* ShortsViewModel.swift */,
CE6E410926EBA800009829AE /* SubscriptionViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
......@@ -947,6 +965,7 @@
CD6B3038257267E2004B34B3 /* View controllers */ = {
isa = PBXGroup;
children = (
CE6E410126EBA386009829AE /* Subscriptions */,
CD857EA0268B290500B5E069 /* WidgetPromotion */,
CD5692B22653D46100A3CDBE /* SplashAnimation */,
CEC8FBAD263975170001A6BF /* Onboarding */,
......@@ -1405,6 +1424,27 @@
path = Widgets;
sourceTree = "<group>";
};
CE6E410126EBA386009829AE /* Subscriptions */ = {
isa = PBXGroup;
children = (
CE6E410426EBA3B4009829AE /* Views */,
CE6E410226EBA3B0009829AE /* SubscriptionStoreViewController.swift */,
CE6E410526EBA3EB009829AE /* SubscriptionOverviewViewController.swift */,
);
path = Subscriptions;
sourceTree = "<group>";
};
CE6E410426EBA3B4009829AE /* Views */ = {
isa = PBXGroup;
children = (
CE6E410B26EBAE84009829AE /* SubscriptionTopView.swift */,
CE6E410D26EBBB3B009829AE /* SubscriptionDescriptionView.swift */,
CE6E410F26EBBB57009829AE /* SubscriptionProDescriptionView.swift */,
CE6E411126EBBB64009829AE /* SubscriptionPurchaseButton.swift */,
);
path = Views;
sourceTree = "<group>";
};
CE81A420266E288C00800EFF /* NativeAdViews */ = {
isa = PBXGroup;
children = (
......@@ -1861,6 +1901,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CE6E410E26EBBB3B009829AE /* SubscriptionDescriptionView.swift in Sources */,
CD6C22F32667815000D75659 /* EnvironmentManager.swift in Sources */,
CD6C22F2266780ED00D75659 /* AdConfigManager.swift in Sources */,
CD35DFD0260344A500F2138F /* ForecastConditionView.swift in Sources */,
......@@ -1878,6 +1919,7 @@
CD2C22812670C36A001ADA9A /* ShortsView.swift in Sources */,
CD866A6C260F676400E96A5C /* SettingsDetailsCellFactory.swift in Sources */,
CDDE8D7F262EED4D00267931 /* MapLegendWeatherView.swift in Sources */,
CE6E410C26EBAE84009829AE /* SubscriptionTopView.swift in Sources */,
87C171F425FF7A4000DA3464 /* PopularCitiesManager.swift in Sources */,
CD85797326721DD400CC4CDA /* UIColor+Highlight.swift in Sources */,
CD67616A262575CD0079D273 /* MapLegendGradientView.swift in Sources */,
......@@ -1891,6 +1933,7 @@
CD67617C2625A60B0079D273 /* MapLayersDismissAnimator.swift in Sources */,
CD18728B2624763000AFEDAA /* MapLegendView.swift in Sources */,
CEEB3549266F5DA900E16F90 /* MRECAdCell.swift in Sources */,
CE6E410626EBA3EB009829AE /* SubscriptionOverviewViewController.swift in Sources */,
CDB0D4CA2670CAD00081C773 /* ShortsCollectionViewCell.swift in Sources */,
CDDCD5092680C18B00E089AD /* ShortsUnreadNudgeView.swift in Sources */,
CE13B818262480B3007CBD4D /* A9BidObject.swift in Sources */,
......@@ -1907,8 +1950,10 @@
CD67617726259DD70079D273 /* MapLayersPresentationAnimator.swift in Sources */,
CD58529226A02B3200D61021 /* PromotionWidgetViewWrapper.swift in Sources */,
CD3D567F268C705900DB99B6 /* PromotionPresentationAnimator.swift in Sources */,
CE6E411026EBBB57009829AE /* SubscriptionProDescriptionView.swift in Sources */,
CD7BF1582620410800A30DF5 /* RadarCoordinator.swift in Sources */,
CDD0F1EE25725BCF00CF5017 /* ThemeManager.swift in Sources */,
CE6E411226EBBB64009829AE /* SubscriptionPurchaseButton.swift in Sources */,
CD1237F4255D889F00C98139 /* GradientView.swift in Sources */,
CD2C22832670C579001ADA9A /* ShortsCollectionViewLayout.swift in Sources */,
CD37D3EF260DF4E6002669D6 /* SettingsViewController.swift in Sources */,
......@@ -1924,6 +1969,7 @@
CD866A6F260F67F200E96A5C /* SettingsDetailsViewModel.swift in Sources */,
CD71709025FA317700A63C27 /* ForecastTimePeriodView.swift in Sources */,
CE13B812262480B3007CBD4D /* NativeAdItem.swift in Sources */,
CE6E410826EBA7C0009829AE /* SubscriptionCoordinator.swift in Sources */,
CD857EA5268B36EA00B5E069 /* PromotionHeaderView.swift in Sources */,
CD37D3E5260CB05C002669D6 /* MenuFooterView.swift in Sources */,
CDE18DD825D16CB200C80ED9 /* NavigationCityButton.swift in Sources */,
......@@ -1937,6 +1983,7 @@
CD37D401260DF744002669D6 /* SettingsCell.swift in Sources */,
CDC6125725E7AB1A00188DA7 /* TodayAirQualityCell.swift in Sources */,
CD8B60AE263819400055CB3F /* NWSAlertViewController.swift in Sources */,
CE6E410326EBA3B0009829AE /* SubscriptionStoreViewController.swift in Sources */,
CD593BCF2608A50900C93428 /* ForecastHourlyCell.swift in Sources */,
CDB0D4CC2670D12F0081C773 /* TodayShortsCell.swift in Sources */,
CD1DDD332602305200AC62B2 /* ForecastInfoCell.swift in Sources */,
......@@ -2002,6 +2049,7 @@
CD8B60B3263819790055CB3F /* NWSAlertCell.swift in Sources */,
CE13B81C262480B3007CBD4D /* Interstitial.swift in Sources */,
CD7D3189268F33CC000D01FA /* WidgetPromotionCoordinator.swift in Sources */,
CE6E410A26EBA800009829AE /* SubscriptionViewModel.swift in Sources */,
CDDE8D7C262EED3C00267931 /* MapLegendSevereView.swift in Sources */,
CD6761882625C3360079D273 /* RadarViewModel.swift in Sources */,
CDF8F12A262089A200DB384A /* MapTimeView.swift in Sources */,
......
......@@ -12,7 +12,7 @@
&quot;product_id&quot;: &quot;com.onelouder.oneweather.subscription.premium.yearly&quot;
},
&quot;monthly_discounted&quot;: {
&quot;product_id&quot;: &quot;com.onelouder.oneweather.subscription.premium.forpro&quot;
&quot;product_id&quot;: &quot;com.onelouder.oneweather.subscription.premium.forPro&quot;
},
&quot;yearly_discounted&quot;: {
&quot;product_id&quot;: &quot;com.onelouder.oneweather.subscription.premium.yearly.forPro&quot;
......
......@@ -37,4 +37,11 @@ class MenuCoordinator: Coordinator {
childCoordinators.append(settingsCoordinator)
settingsCoordinator.start()
}
func openSubscriptions() {
let subscriptionsCoordinator = SubscriptionCoordinator(parentViewController: self.navigationController)
subscriptionsCoordinator.parentCoordinator = self
childCoordinators.append(subscriptionsCoordinator)
subscriptionsCoordinator.start()
}
}
//
// SubscriptionCoordinator.swift
// 1Weather
//
// Created by Demid Merzlyakov on 10.09.2021.
//
import UIKit
import OneWeatherCore
class SubscriptionCoordinator: Coordinator {
private let parentViewController: UIViewController
public var childCoordinators = [Coordinator]()
public var parentCoordinator: Coordinator?
private let storeManager: StoreManager
public init(parentViewController: UIViewController, storeManager: StoreManager = StoreManager.shared) {
self.parentViewController = parentViewController
self.storeManager = storeManager
}
public func start() {
let viewModel = SubscriptionViewModel(storeManager: storeManager)
let vc = SubscriptionStoreViewController(coordinator: self, viewModel: viewModel)
self.parentViewController.present(vc, animated: true)
}
func viewControllerDidEnd(controller: UIViewController) {
parentCoordinator?.childDidFinish(child: self)
}
}
......@@ -77,7 +77,7 @@
"locale" : "en_US"
}
],
"productID" : "com.onelouder.oneweather.subscription.premium.forpro",
"productID" : "com.onelouder.oneweather.subscription.premium.forPro",
"recurringSubscriptionPeriod" : "P1M",
"referenceName" : "Premium Subscription Monthly (for Pro)",
"subscriptionGroupID" : "AC6BEB61",
......
......@@ -9,6 +9,7 @@ import Foundation
import OneWeatherCore
import OneWeatherAnalytics
import SwiftyStoreKit
import StoreKit
public protocol StoreManagerObserver {
func storeManagerUpdatedStatus(_ storeManager: StoreManager)
......@@ -21,6 +22,8 @@ public class StoreManager {
private var activeInAppRequests = [String: InAppRequest]()
private static let sharedSecret = "9462f5b715774b2dafac935098e3f3c6"
private var productInfoCache = [String: SKProduct]()
public static let shared = StoreManager()
public init(configManager: ConfigManager = ConfigManager.shared) {
......@@ -28,6 +31,19 @@ public class StoreManager {
self.configManager.add(delegate: self)
}
public var monthly: SKProduct? {
productInfoCache[configManager.config.subscriptionsConfig.monthly.productId]
}
public var yearly: SKProduct? {
productInfoCache[configManager.config.subscriptionsConfig.yearly.productId]
}
public var monthlyDiscounted: SKProduct? {
productInfoCache[configManager.config.subscriptionsConfig.monthlyDiscounted.productId]
}
public var yearlyDiscounted: SKProduct? {
productInfoCache[configManager.config.subscriptionsConfig.yearlyDiscounted.productId]
}
// MARK: - Get known status
@UserDefaultsValue("com.inmobi.oneweather.WeatherKey", defaultValue: false, userDefaults: UserDefaults.appDefaults)
......@@ -75,6 +91,8 @@ public class StoreManager {
self.checkSubscriptionExpirationLocally()
self.completeTransactions()
self.updateProductInfo()
self.verifySubscriptions()
#warning("Not implemented!")
//TODO: implement
// We need to verify subscription status on startup as well
......@@ -116,22 +134,19 @@ public class StoreManager {
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 subscriptions: [SubscriptionConfig] = [config.monthlyDiscounted, config.yearlyDiscounted, config.monthly, config.yearly]
let productIds = Set<String>(subscriptions.map { $0.productId })
SwiftyStoreKit.retrieveProductsInfo(productIds) { result in
SwiftyStoreKit.retrieveProductsInfo(productIds) { [weak self] result in
guard let self = self else { return }
if let error = result.error {
log.error("Update info: error: \(error)")
}
for product in result.retrievedProducts {
log.debug("For \(product.productIdentifier): \(product.debugDescription)")
self.productInfoCache[product.productIdentifier] = product
}
log.info("Product info cache contains: \(self.productInfoCache.keys.joined(separator: ", "))")
}
}
......@@ -155,12 +170,27 @@ public class StoreManager {
activeInAppRequests[productId] = request
}
public func purchase(product: SKProduct) {
let log = self.log
log.info("Purchase \(product.productIdentifier) start (SK).")
SwiftyStoreKit.purchaseProduct(product) { result in
switch result {
case .success(let product):
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
case .error(error: let error):
log.error("Purchase: error for \(product.productIdentifier): \(error)")
}
}
}
public func verifySubscriptions() {
let log = self.log
log.info("Verify subscriptions...")
let service: AppleReceiptValidator.VerifyReceiptURLType
#if DEBUG
service = .production
service = .sandbox
#else
service = .production
#endif
......@@ -225,5 +255,6 @@ public class StoreManager {
extension StoreManager: ConfigManagerDelegate {
public func dataUpdated(by configManager: ConfigManager) {
self.updateProductInfo()
self.verifySubscriptions()
}
}
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"images" : [
{
"filename" : "1WeatherPremium.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0.922",
"green" : "0.399",
"red" : "0.205"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0.922",
"green" : "0.399",
"red" : "0.205"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
......@@ -212,6 +212,14 @@
"menu.deviceId.text" = "Your device identifier is: ";
"menu.help.unableToSendEmail.text" = "Device is unable to send email.";
//Subscriptions
"subscription.header.oneWeather" = "1Weather";
"subscription.header.premium" = "PREMIUM";
"subscription.button.buy.yearly" = "Subscribe yearly for #PRICE#";
"subscription.button.buy.monthly" = "Subscribe monthly for #PRICE#";
"subscription.button.upgrade.yearly" = "Upgrade for #PRICE# #DISCOUNT_PRICE# / year";
"subscription.button.upgrade.monthly" = "Upgrade for #PRICE# #DISCOUNT_PRICE# / month";
//Settings
"settings.theme.automatic" = "Automatic";
"settings.theme.automaticDesc" = "Enable light or dark theme based on your device brightness and display settings";
......
......@@ -129,4 +129,13 @@ struct DefaultTheme: ThemeProtocol {
var widgetPromotionText: UIColor {
return UIColor(named: "widget_promotion_text") ?? .red
}
// Subscriptions
var subscriptionPurchaseBackgroundColor: UIColor {
return UIColor(named: "subscription_button_background") ?? .red
}
var subscriptionPurchaseColor: UIColor {
return UIColor(named: "subscription_button") ?? .red
}
}
......@@ -56,4 +56,8 @@ public protocol ThemeProtocol {
//Widget promotion
var widgetPromotionBackground: UIColor { get }
var widgetPromotionText: UIColor { get }
// Subscriptions
var subscriptionPurchaseBackgroundColor: UIColor { get }
var subscriptionPurchaseColor: UIColor { get }
}
......@@ -73,10 +73,6 @@ class MenuViewController: UIViewController {
view.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
tableView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
}
private func upgradeToPro() {
viewModel.updateToPro()
}
}
//MARK:- Prepare
......@@ -87,7 +83,7 @@ private extension MenuViewController {
func prepareTableViewHeader() {
menuHeaderView.onTapBuy = { [weak self] in
self?.viewModel.updateToPro()
self?.coordinator.openSubscriptions()
}
}
......@@ -109,6 +105,9 @@ private extension MenuViewController {
tableView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
if self.viewModel.storeManager.hasSubscription || !self.viewModel.featureAvailabilityManager.isAvailable(feature: .subscription) {
self.tableView.tableHeaderView = nil
}
}
}
......
//
// SubscriptionOverviewViewController.swift
// 1Weather
//
// Created by Demid Merzlyakov on 10.09.2021.
//
import UIKit
class SubscriptionOverviewViewController: UIViewController {
}
//
// SubscriptionStoreViewController.swift
// 1Weather
//
// Created by Demid Merzlyakov on 10.09.2021.
//
import UIKit
import SnapKit
import StoreKit
/// A screen containing buttons to purchase a subscription.
/// Relevant design URLs:
/// For users who previously purchased an in-app to disable ads: https://zpl.io/anzPMKr
/// For usual users: https://zpl.io/bLJ8rOd
public class SubscriptionStoreViewController: UIViewController {
private let coordinator: SubscriptionCoordinator
private let viewModel: SubscriptionViewModel
private var localizationObserver: LocalizationChangeObserver!
private let scrollView = UIScrollView()
private let scrollViewContent = UIView()
private let topHeaderView = SubscriptionTopView()
private let dynamicContentView = UIView()
init(coordinator: SubscriptionCoordinator, viewModel: SubscriptionViewModel) {
self.coordinator = coordinator
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) { return nil }
public override func viewDidLoad() {
super.viewDidLoad()
prepareViewController()
prepareScrollView()
rebuildUI()
}
}
//MARK: - UI Setup
extension SubscriptionStoreViewController {
private func prepareViewController() {
view.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
localizationObserver = LocalizationChangeObserver { [weak self] in
self?.rebuildUI()
}
}
private func prepareScrollView() {
view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
scrollView.addSubview(scrollViewContent)
scrollViewContent.snp.makeConstraints { make in
make.top.bottom.left.right.equalToSuperview()
make.width.equalToSuperview()
}
scrollViewContent.addSubview(topHeaderView)
topHeaderView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
}
scrollViewContent.addSubview(dynamicContentView)
dynamicContentView.translatesAutoresizingMaskIntoConstraints = false
dynamicContentView.snp.makeConstraints { make in
make.top.equalTo(topHeaderView.snp.bottom)
make.left.right.equalToSuperview().inset(24)
make.bottom.equalToSuperview()
}
}
private func rebuildUI() {
for subview in dynamicContentView.subviews {
subview.removeFromSuperview()
}
let descriptionView: UIView
if viewModel.isProUser {
descriptionView = SubscriptionProDescriptionView()
}
else {
descriptionView = SubscriptionDescriptionView()
}
dynamicContentView.addSubview(descriptionView)
descriptionView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
}
var lastView: UIView = descriptionView
if let yearly = viewModel.storeManager.yearly {
let yearlyButton = SubscriptionPurchaseButton(type: .yearly, subscription: yearly, discountSubscription: viewModel.isProUser ? viewModel.storeManager.yearlyDiscounted : nil)
dynamicContentView.addSubview(yearlyButton)
yearlyButton.snp.makeConstraints { make in
make.top.equalTo(lastView.snp.bottom).offset(20)
make.left.right.equalToSuperview()
}
lastView = yearlyButton
yearlyButton.delegate = self
}
if let monthly = viewModel.storeManager.monthly {
let monthlyButton = SubscriptionPurchaseButton(type: .monthly, subscription: monthly, discountSubscription: viewModel.isProUser ? viewModel.storeManager.monthlyDiscounted : nil)
dynamicContentView.addSubview(monthlyButton)
monthlyButton.snp.makeConstraints { make in
make.top.equalTo(lastView.snp.bottom).offset(20)
make.left.right.equalToSuperview()
}
lastView = monthlyButton
monthlyButton.delegate = self
}
lastView.snp.makeConstraints { make in
make.bottom.equalToSuperview()
}
}
}
extension SubscriptionStoreViewController: SubscriptionPurchaseButtonDelegate {
func button(_ button: SubscriptionPurchaseButton, triggeredPurchaseOf product: SKProduct) {
viewModel.purchase(subscription: product)
}
}
//
// SubscriptionDescriptionView.swift
// 1Weather
//
// Created by Demid Merzlyakov on 10.09.2021.
//
import UIKit
/// A view containing decription of features of premium subscription for users who HAVE NOT previously purchased an in-app to remove ads.
class SubscriptionDescriptionView: UIView {
public init() {
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//
// SubscriptionProDescriptionView.swift
// 1Weather
//
// Created by Demid Merzlyakov on 10.09.2021.
//
import UIKit
/// A view containing decription of features of upgrade to premium subscription for pro users (users who have previously purchased an in-app to remove ads).
class SubscriptionProDescriptionView: UIView {
public init() {
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//
// SubscriptionPurchaseButton.swift
// 1Weather
//
// Created by Demid Merzlyakov on 10.09.2021.
//
import UIKit
import StoreKit
protocol SubscriptionPurchaseButtonDelegate: AnyObject {
func button(_ button: SubscriptionPurchaseButton, triggeredPurchaseOf product: SKProduct)
}
class SubscriptionPurchaseButton: UIButton {
public enum SubscriptionType: String {
case monthly
case yearly
}
private let discountSubscription: SKProduct?
private let subscription: SKProduct
private let subscriptionType: SubscriptionPurchaseButton.SubscriptionType
public weak var delegate: SubscriptionPurchaseButtonDelegate?
public init(type: SubscriptionPurchaseButton.SubscriptionType, subscription: SKProduct, discountSubscription: SKProduct? = nil) {
self.discountSubscription = discountSubscription
self.subscription = subscription
self.subscriptionType = type
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.setAttributedTitle(buildTitle(), for: .normal)
self.backgroundColor = ThemeManager.currentTheme.subscriptionPurchaseBackgroundColor
self.setTitleColor(ThemeManager.currentTheme.subscriptionPurchaseColor, for: .normal)
self.clipsToBounds = true
self.layer.cornerRadius = 6
self.snp.makeConstraints { make in
make.height.equalTo(52)
}
addTarget(self, action: #selector(handleTap), for: .touchUpInside)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func buildTitle() -> NSAttributedString {
guard let price = subscription.localizedPrice else {
return NSAttributedString()
}
let template = "subscription.button.\(showUpgradeText ? "upgrade" : "buy").\(subscriptionType.rawValue)".localized()
var withPrices = template.replacingOccurrences(of: "#PRICE#", with: price)
if let discountSubscription = self.discountSubscription {
guard let discountPrice = discountSubscription.localizedPrice else {
return NSAttributedString()
}
withPrices = withPrices.replacingOccurrences(of: "#DISCOUNT_PRICE#", with: discountPrice)
}
var result = NSMutableAttributedString(string: withPrices)
#warning("Not implemented markup!")
//TODO: implement markup
return result
}
private var subscriptionToBuy: SKProduct {
discountSubscription ?? subscription
}
private var showUpgradeText: Bool {
discountSubscription != nil
}
@objc
private func handleTap() {
delegate?.button(self, triggeredPurchaseOf: subscriptionToBuy)
}
}
//
// SubscriptionTopView.swift
// 1Weather
//
// Created by Demid Merzlyakov on 10.09.2021.
//
import UIKit
import SnapKit
class SubscriptionTopView: UIView {
private let oneWeatherLabel = UILabel()
private let premiumLabel = UILabel()
private let logoView = UIImageView()
private var localizationObserver: LocalizationChangeObserver!
public init() {
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
localizationObserver = LocalizationChangeObserver { [weak self] in
self?.updateButtonTexts()
}
backgroundColor = ThemeManager.currentTheme.subscriptionPurchaseBackgroundColor
oneWeatherLabel.font = AppFont.SFPro.bold(size: 32)
premiumLabel.font = AppFont.SFPro.bold(size: 24)
oneWeatherLabel.textColor = ThemeManager.currentTheme.subscriptionPurchaseColor
premiumLabel.textColor = ThemeManager.currentTheme.subscriptionPurchaseColor
updateButtonTexts()
logoView.image = UIImage(named: "subscription_logo")
logoView.contentMode = .scaleAspectFit
addSubview(oneWeatherLabel)
addSubview(premiumLabel)
addSubview(logoView)
oneWeatherLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(28)
make.top.equalToSuperview().offset(81)
make.trailing.lessThanOrEqualTo(logoView.snp.leading).priority(.low)
}
premiumLabel.snp.makeConstraints { make in
make.top.equalTo(oneWeatherLabel.snp.bottom).offset(4)
make.leading.equalTo(oneWeatherLabel.snp.leading)
make.trailing.lessThanOrEqualTo(logoView.snp.leading).priority(.low)
}
logoView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(46)
make.trailing.equalToSuperview().offset(-18)
make.bottom.equalToSuperview().offset(-14)
make.width.equalTo(190)
make.height.equalTo(160)
}
}
private func updateButtonTexts() {
oneWeatherLabel.text = "subscription.header.oneWeather".localized()
premiumLabel.text = "subscription.header.premium".localized()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//
// SubscriptionViewModel.swift
// 1Weather
//
// Created by Demid Merzlyakov on 10.09.2021.
//
import Foundation
import OneWeatherCore
import StoreKit
class SubscriptionViewModel: ViewModelProtocol {
public let storeManager: StoreManager
public init(storeManager: StoreManager) {
self.storeManager = storeManager
}
public var isProUser: Bool {
storeManager.removeAdsPurchased
}
public func purchase(subscription: SKProduct) {
storeManager.purchase(product: subscription)
}
}
......@@ -27,7 +27,7 @@ public struct SubscriptionsListConfig {
self.subscriptionsEnabled = true
self.monthly = SubscriptionConfig(productId: "com.onelouder.oneweather.subscription.premium")
self.yearly = SubscriptionConfig(productId: "com.onelouder.oneweather.subscription.premium.yearly")
self.monthlyDiscounted = SubscriptionConfig(productId: "com.onelouder.oneweather.subscription.premium.forpro")
self.monthlyDiscounted = SubscriptionConfig(productId: "com.onelouder.oneweather.subscription.premium.forPro")
self.yearlyDiscounted = SubscriptionConfig(productId: "com.onelouder.oneweather.subscription.premium.yearly.forPro")
}
}
......
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