Commit e5ea3459 by Demid Merzlyakov

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

parent db14649d
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
"product_id": "com.onelouder.oneweather.subscription.premium.yearly" "product_id": "com.onelouder.oneweather.subscription.premium.yearly"
}, },
"monthly_discounted": { "monthly_discounted": {
"product_id": "com.onelouder.oneweather.subscription.premium.forpro" "product_id": "com.onelouder.oneweather.subscription.premium.forPro"
}, },
"yearly_discounted": { "yearly_discounted": {
"product_id": "com.onelouder.oneweather.subscription.premium.yearly.forPro" "product_id": "com.onelouder.oneweather.subscription.premium.yearly.forPro"
......
...@@ -37,4 +37,11 @@ class MenuCoordinator: Coordinator { ...@@ -37,4 +37,11 @@ class MenuCoordinator: Coordinator {
childCoordinators.append(settingsCoordinator) childCoordinators.append(settingsCoordinator)
settingsCoordinator.start() 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 @@ ...@@ -77,7 +77,7 @@
"locale" : "en_US" "locale" : "en_US"
} }
], ],
"productID" : "com.onelouder.oneweather.subscription.premium.forpro", "productID" : "com.onelouder.oneweather.subscription.premium.forPro",
"recurringSubscriptionPeriod" : "P1M", "recurringSubscriptionPeriod" : "P1M",
"referenceName" : "Premium Subscription Monthly (for Pro)", "referenceName" : "Premium Subscription Monthly (for Pro)",
"subscriptionGroupID" : "AC6BEB61", "subscriptionGroupID" : "AC6BEB61",
......
...@@ -9,6 +9,7 @@ import Foundation ...@@ -9,6 +9,7 @@ import Foundation
import OneWeatherCore import OneWeatherCore
import OneWeatherAnalytics import OneWeatherAnalytics
import SwiftyStoreKit import SwiftyStoreKit
import StoreKit
public protocol StoreManagerObserver { public protocol StoreManagerObserver {
func storeManagerUpdatedStatus(_ storeManager: StoreManager) func storeManagerUpdatedStatus(_ storeManager: StoreManager)
...@@ -21,6 +22,8 @@ public class StoreManager { ...@@ -21,6 +22,8 @@ public class StoreManager {
private var activeInAppRequests = [String: InAppRequest]() private var activeInAppRequests = [String: InAppRequest]()
private static let sharedSecret = "9462f5b715774b2dafac935098e3f3c6" private static let sharedSecret = "9462f5b715774b2dafac935098e3f3c6"
private var productInfoCache = [String: SKProduct]()
public static let shared = StoreManager() public static let shared = StoreManager()
public init(configManager: ConfigManager = ConfigManager.shared) { public init(configManager: ConfigManager = ConfigManager.shared) {
...@@ -28,6 +31,19 @@ public class StoreManager { ...@@ -28,6 +31,19 @@ public class StoreManager {
self.configManager.add(delegate: self) 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 // MARK: - Get known status
@UserDefaultsValue("com.inmobi.oneweather.WeatherKey", defaultValue: false, userDefaults: UserDefaults.appDefaults) @UserDefaultsValue("com.inmobi.oneweather.WeatherKey", defaultValue: false, userDefaults: UserDefaults.appDefaults)
...@@ -75,6 +91,8 @@ public class StoreManager { ...@@ -75,6 +91,8 @@ public class StoreManager {
self.checkSubscriptionExpirationLocally() self.checkSubscriptionExpirationLocally()
self.completeTransactions() self.completeTransactions()
self.updateProductInfo() self.updateProductInfo()
self.verifySubscriptions()
#warning("Not implemented!") #warning("Not implemented!")
//TODO: implement //TODO: implement
// We need to verify subscription status on startup as well // We need to verify subscription status on startup as well
...@@ -116,22 +134,19 @@ public class StoreManager { ...@@ -116,22 +134,19 @@ public class StoreManager {
log.debug("Update product info: subscriptions disabled. Skip.") log.debug("Update product info: subscriptions disabled. Skip.")
return return
} }
let subscriptions: [SubscriptionConfig] let subscriptions: [SubscriptionConfig] = [config.monthlyDiscounted, config.yearlyDiscounted, config.monthly, config.yearly]
if removeAdsPurchased {
subscriptions = [config.monthlyDiscounted, config.yearlyDiscounted]
}
else {
subscriptions = [config.monthly, config.yearly]
}
let productIds = Set<String>(subscriptions.map { $0.productId }) 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 { if let error = result.error {
log.error("Update info: error: \(error)") log.error("Update info: error: \(error)")
} }
for product in result.retrievedProducts { 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 { ...@@ -155,12 +170,27 @@ public class StoreManager {
activeInAppRequests[productId] = request 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() { public func verifySubscriptions() {
let log = self.log let log = self.log
log.info("Verify subscriptions...") log.info("Verify subscriptions...")
let service: AppleReceiptValidator.VerifyReceiptURLType let service: AppleReceiptValidator.VerifyReceiptURLType
#if DEBUG #if DEBUG
service = .production service = .sandbox
#else #else
service = .production service = .production
#endif #endif
...@@ -225,5 +255,6 @@ public class StoreManager { ...@@ -225,5 +255,6 @@ public class StoreManager {
extension StoreManager: ConfigManagerDelegate { extension StoreManager: ConfigManagerDelegate {
public func dataUpdated(by configManager: ConfigManager) { public func dataUpdated(by configManager: ConfigManager) {
self.updateProductInfo() 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 @@ ...@@ -212,6 +212,14 @@
"menu.deviceId.text" = "Your device identifier is: "; "menu.deviceId.text" = "Your device identifier is: ";
"menu.help.unableToSendEmail.text" = "Device is unable to send email."; "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
"settings.theme.automatic" = "Automatic"; "settings.theme.automatic" = "Automatic";
"settings.theme.automaticDesc" = "Enable light or dark theme based on your device brightness and display settings"; "settings.theme.automaticDesc" = "Enable light or dark theme based on your device brightness and display settings";
......
...@@ -129,4 +129,13 @@ struct DefaultTheme: ThemeProtocol { ...@@ -129,4 +129,13 @@ struct DefaultTheme: ThemeProtocol {
var widgetPromotionText: UIColor { var widgetPromotionText: UIColor {
return UIColor(named: "widget_promotion_text") ?? .red 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 { ...@@ -56,4 +56,8 @@ public protocol ThemeProtocol {
//Widget promotion //Widget promotion
var widgetPromotionBackground: UIColor { get } var widgetPromotionBackground: UIColor { get }
var widgetPromotionText: UIColor { get } var widgetPromotionText: UIColor { get }
// Subscriptions
var subscriptionPurchaseBackgroundColor: UIColor { get }
var subscriptionPurchaseColor: UIColor { get }
} }
...@@ -73,10 +73,6 @@ class MenuViewController: UIViewController { ...@@ -73,10 +73,6 @@ class MenuViewController: UIViewController {
view.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor view.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
tableView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor tableView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
} }
private func upgradeToPro() {
viewModel.updateToPro()
}
} }
//MARK:- Prepare //MARK:- Prepare
...@@ -87,7 +83,7 @@ private extension MenuViewController { ...@@ -87,7 +83,7 @@ private extension MenuViewController {
func prepareTableViewHeader() { func prepareTableViewHeader() {
menuHeaderView.onTapBuy = { [weak self] in menuHeaderView.onTapBuy = { [weak self] in
self?.viewModel.updateToPro() self?.coordinator.openSubscriptions()
} }
} }
...@@ -109,6 +105,9 @@ private extension MenuViewController { ...@@ -109,6 +105,9 @@ private extension MenuViewController {
tableView.snp.makeConstraints { (make) in tableView.snp.makeConstraints { (make) in
make.edges.equalToSuperview() 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 { ...@@ -27,7 +27,7 @@ public struct SubscriptionsListConfig {
self.subscriptionsEnabled = true self.subscriptionsEnabled = true
self.monthly = SubscriptionConfig(productId: "com.onelouder.oneweather.subscription.premium") self.monthly = SubscriptionConfig(productId: "com.onelouder.oneweather.subscription.premium")
self.yearly = SubscriptionConfig(productId: "com.onelouder.oneweather.subscription.premium.yearly") 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") 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