Commit 93871b28 by Demid Merzlyakov

Merge branch 'feature/IOS-175-176-feature-availability-implementation' into…

Merge branch 'feature/IOS-175-176-feature-availability-implementation' into feature/IOS-155-subscriptions

# Conflicts:
#	OneWeatherCore/OneWeatherCore.xcodeproj/project.pbxproj
parents 98ee0597 cdeebaa9
......@@ -115,7 +115,6 @@
CD6B304325726AD1004B34B3 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B304225726AD1004B34B3 /* DefaultTheme.swift */; };
CD6C22EA26677BE000D75659 /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22E926677BDF00D75659 /* ConfigManager.swift */; };
CD6C22EE26677DBC00D75659 /* PushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22ED26677DBC00D75659 /* PushNotificationsManager.swift */; };
CD6C22F1266780BE00D75659 /* AdConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22E726677B4900D75659 /* AdConfig.swift */; };
CD6C22F2266780ED00D75659 /* AdConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22E626677B4900D75659 /* AdConfigManager.swift */; };
CD6C22F32667815000D75659 /* EnvironmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22E526677B4900D75659 /* EnvironmentManager.swift */; };
CD71709025FA317700A63C27 /* ForecastTimePeriodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD71708F25FA317700A63C27 /* ForecastTimePeriodView.swift */; };
......@@ -416,7 +415,6 @@
CD6B304225726AD1004B34B3 /* DefaultTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = "<group>"; };
CD6C22E526677B4900D75659 /* EnvironmentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentManager.swift; sourceTree = "<group>"; };
CD6C22E626677B4900D75659 /* AdConfigManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdConfigManager.swift; sourceTree = "<group>"; };
CD6C22E726677B4900D75659 /* AdConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdConfig.swift; sourceTree = "<group>"; };
CD6C22E926677BDF00D75659 /* ConfigManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigManager.swift; sourceTree = "<group>"; };
CD6C22ED26677DBC00D75659 /* PushNotificationsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationsManager.swift; sourceTree = "<group>"; };
CD71708F25FA317700A63C27 /* ForecastTimePeriodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastTimePeriodView.swift; sourceTree = "<group>"; };
......@@ -986,7 +984,6 @@
children = (
CD6C22E526677B4900D75659 /* EnvironmentManager.swift */,
CD6C22E626677B4900D75659 /* AdConfigManager.swift */,
CD6C22E726677B4900D75659 /* AdConfig.swift */,
);
path = Configuration;
sourceTree = "<group>";
......@@ -1851,7 +1848,6 @@
files = (
CD6C22F32667815000D75659 /* EnvironmentManager.swift in Sources */,
CD6C22F2266780ED00D75659 /* AdConfigManager.swift in Sources */,
CD6C22F1266780BE00D75659 /* AdConfig.swift in Sources */,
CD35DFD0260344A500F2138F /* ForecastConditionView.swift in Sources */,
CD857EA7268B45DD00B5E069 /* PromotionSmallWidgetView.swift in Sources */,
CD2ABF32261489F700C1A92E /* LocationCellFactory.swift in Sources */,
......@@ -1958,7 +1954,6 @@
CD6B303E25726960004B34B3 /* ThemeProtocol.swift in Sources */,
CD6B303B2572680C004B34B3 /* SelfSizingButton.swift in Sources */,
CDC62C4726C13BE300156643 /* OnboardingContentController.swift in Sources */,
CD9B6B1125DBC723001D9B80 /* CubicCurveAlgorithm.swift in Sources */,
CD8B60B4263819790055CB3F /* NotificationsViewController.swift in Sources */,
CD67617026259D220079D273 /* RadarMapLayersController.swift in Sources */,
CEC8FBAF2639756A0001A6BF /* OnboardingViewController.swift in Sources */,
......
......@@ -7,14 +7,13 @@
//
import UIKit
import OneWeatherCore
import OneWeatherAnalytics
extension Notification.Name {
public static let adConfigChanged = Notification.Name(rawValue: "AdConfig.Changed")
}
public typealias AdPlacementName = String
public let placementNameTodayBanner: AdPlacementName = "today_banner"
public let placementNameTodaySquare: AdPlacementName = "today_square"
public let placementNameForecastHourlyBanner: AdPlacementName = "forecast_hourly_banner"
......
......@@ -6,6 +6,7 @@
//
import Foundation
import OneWeatherCore
import GoogleMobileAds
class NativeAdView: GADNativeAdView {
......
......@@ -7,8 +7,9 @@
//
import Foundation
import GoogleMobileAds
import OneWeatherAnalytics
import OneWeatherCore
import GoogleMobileAds
protocol NativeBannerContainerViewDelegate: AnyObject {
func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADNativeAd)
......
......@@ -86,6 +86,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
FirebaseApp.configure()
ConfigManager.shared.updateConfig()
FeatureAvailabilityManager.shared = configureFeatureAvailabilityManager(with: LocationManager.shared, configManager: ConfigManager.shared)
//App UI
let appCoordinator = AppCoordinator(window: self.window!, launchOptions: launchOptions)
appCoordinator.start()
......@@ -189,6 +191,46 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
}
private func configureFeatureAvailabilityManager(with locationManager: LocationManager, configManager: ConfigManager) -> FeatureAvailabilityManager {
let usOnly = USOnlyFeatureAvailabilityChecker { [weak locationManager] in
return locationManager?.selectedLocation
}
let premium = PremiumInAppFeatureAvailabilityChecker()
let proSubscription = ProSubscriptionAvailabilityChecker()
let isPhone = DeviceTypeFeatureAvailabilityChecker(deviceType: .phone)
var availabilityCheckers = [AppFeature: FeatureAvailabilityChecker]()
for feature in AppFeature.allCases {
// To make sure all features are explicitly configured.
switch feature {
case .ads:
availabilityCheckers[feature] = !premium && !proSubscription
case .minutelyForecast:
availabilityCheckers[feature] = proSubscription
case .shorts:
availabilityCheckers[feature] = usOnly && isPhone
case .airQualityIndex:
break
case .attPrompt:
break // config only
case .nwsAlertsViaMoEngage:
break // config only
case .onboarding:
break // config only
case .shortsLastNudge:
break // config only
}
}
let manager = FeatureAvailabilityManager(configFetcher: { [weak configManager] in
configManager?.config
},
checkers: availabilityCheckers)
return manager
}
}
extension AppDelegate: AppsFlyerLibDelegate {
......
......@@ -26,6 +26,9 @@ class AppCoordinator: Coordinator {
var parentCoordinator: Coordinator?
var childCoordinators = [Coordinator]()
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
init(window:UIWindow, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
self.window = window
......@@ -109,7 +112,7 @@ class AppCoordinator: Coordinator {
DispatchQueue.main.async {
self.logAppLaunchEvents()
if ConfigManager.shared.config.showOnboarding {
if self.featureAvailabilityManager.isAvailable(feature: .onboarding) {
if Settings.shared.initialOnboardingShowed {
self.finishInitialOnboarding()
}
......@@ -158,7 +161,7 @@ class AppCoordinator: Coordinator {
firstOpenSource = .url
}
let flow = ConfigManager.shared.config.showOnboarding ? "FTUX" : "default"
let flow = featureAvailabilityManager.isAvailable(feature: .onboarding) ? "FTUX" : "default"
analytics(log: .ANALYTICS_FIRST_OPEN,
params: [.ANALYTICS_KEY_FIRST_OPEN_SOURCE : firstOpenSource.rawValue,
.ANALYTICS_KEY_FIRST_OPEN_FLOW : flow])
......
......@@ -12,39 +12,6 @@ import FirebaseRemoteConfig
import OneWeatherAnalytics
import OneWeatherCore
public struct AppConfig: Codable {
public let popularCities: [GeoNamesPlace]?
public let adConfig: AdConfig
public let ccpaUpdateInterval: TimeInterval?
public let nwsAlertsViaMoEngageEnabled: Bool
public let showAttPrompt: Bool
public let shortsLeftBelowCount: Int
public let shortsLastNudgeEnabled: Bool
public let shortsSwipeUpNudgeCount: Int
public let showOnboarding: Bool
public init(popularCities: [GeoNamesPlace]?,
adConfig: AdConfig,
ccpaUpdateInterval: TimeInterval?,
nwsAlertsViaMoEngageEnabled: Bool,
showAttPrompt: Bool,
shortsLeftBelowCountKey: Int,
shortsLastNudgeEnabledKey: Bool,
shortsSwipeUpNudgeCountKey: Int,
showOnboarding: Bool
) {
self.popularCities = popularCities
self.adConfig = adConfig
self.ccpaUpdateInterval = ccpaUpdateInterval
self.nwsAlertsViaMoEngageEnabled = nwsAlertsViaMoEngageEnabled
self.showAttPrompt = showAttPrompt
self.shortsLeftBelowCount = shortsLeftBelowCountKey
self.shortsLastNudgeEnabled = shortsLastNudgeEnabledKey
self.shortsSwipeUpNudgeCount = shortsSwipeUpNudgeCountKey
self.showOnboarding = showOnboarding
}
}
public protocol ConfigManagerDelegate {
func dataUpdated(by configManager: ConfigManager)
}
......@@ -53,12 +20,8 @@ public class ConfigManager {
private static let adConfigKey = "ads_config_ios"
private static let popularCitiesConfigKey = "search_screen_popular_cities"
private static let ccpaUpdateIntervalConfigKey = "ccpa_update_interval_ios"
private static let nwsAlertsViaMoEngageEnabledKey = "ios_nws_alerts_via_moengage_enabled"
private static let showAttPromptKey = "ios_show_att_prompt"
private static let shortsLeftBelowCountKey = "shorts_left_below_nudge_every_x_cards"
private static let shortsLastNudgeEnabledKey = "shorts_swipe_down_nudge_enabled"
private static let shortsSwipeUpNudgeCountKey = "shorts_swipe_up_nudge_on_x_cards"
private static let showOnboardingKey = "ios_show_onboarding"
private let delegates = MulticastDelegate<ConfigManagerDelegate>()
......@@ -77,12 +40,15 @@ public class ConfigManager {
public var config: AppConfig = AppConfig(popularCities: nil,
adConfig: AdConfig(),
ccpaUpdateInterval: nil,
nwsAlertsViaMoEngageEnabled: true,
showAttPrompt: false,
shortsLeftBelowCountKey: 0,
shortsLastNudgeEnabledKey: false,
shortsSwipeUpNudgeCountKey: 0,
showOnboarding: false)
explicitFeatureAvailability: [
.nwsAlertsViaMoEngage: true,
.attPrompt: false,
.shortsLastNudge: false,
.onboarding: false
]
)
public func updateConfig() {
log.info("update config")
......@@ -120,11 +86,23 @@ public class ConfigManager {
delegates.remove(delegate: delegate)
}
private func parseFeatureAvailability() -> [AppFeature: Bool] {
var featureAvailability = [AppFeature: Bool]()
for feature in AppFeature.allCases {
if let configName = feature.configVariableName {
let configValue = remoteConfig.configValue(forKey: configName)
featureAvailability[feature] = configValue.boolValue
}
}
return featureAvailability
}
private func parseConfigFromFirebase(source: String) {
log.info("Got config from \(source)")
var configErrors = [Error]()
let decoder = JSONDecoder()
var adConfig = AdConfig()
do {
let adConfigData = remoteConfig.configValue(forKey: ConfigManager.adConfigKey).dataValue
......@@ -156,34 +134,22 @@ public class ConfigManager {
if ccpaUpdateIntervalConfigValue.source == RemoteConfigSource.remote {
ccpaUpdateInterval = ccpaUpdateIntervalConfigValue.numberValue.doubleValue
}
let nwsAlertsViaMoEngageEnabledValue = remoteConfig.configValue(forKey: ConfigManager.nwsAlertsViaMoEngageEnabledKey)
let nwsAlertsViaMoEngageEnabled = nwsAlertsViaMoEngageEnabledValue.boolValue
let showAttPromptValue = remoteConfig.configValue(forKey: ConfigManager.showAttPromptKey)
let showAttPrompt = showAttPromptValue.boolValue
let shortsLeftBelowCountValue = remoteConfig.configValue(forKey: ConfigManager.shortsLeftBelowCountKey)
let shortsLeftBelowCount = shortsLeftBelowCountValue.numberValue.intValue
let shortsLastNudgeEnabledValue = remoteConfig.configValue(forKey: ConfigManager.shortsLastNudgeEnabledKey)
let shortsLastNudgeEnabled = shortsLastNudgeEnabledValue.boolValue
let shortsSwipeNudgeCountValue = remoteConfig.configValue(forKey: ConfigManager.shortsSwipeUpNudgeCountKey)
let shortsSwipeNudgeCount = shortsSwipeNudgeCountValue.numberValue.intValue
let showOnboardingValue = remoteConfig.configValue(forKey: ConfigManager.showOnboardingKey)
let showOnboarding = showOnboardingValue.boolValue
let featureAvailability = parseFeatureAvailability()
DispatchQueue.main.async {
self.config = AppConfig(popularCities: popularCities,
adConfig: adConfig,
ccpaUpdateInterval:ccpaUpdateInterval,
nwsAlertsViaMoEngageEnabled: nwsAlertsViaMoEngageEnabled,
showAttPrompt: showAttPrompt,
shortsLeftBelowCountKey: shortsLeftBelowCount,
shortsLastNudgeEnabledKey: shortsLastNudgeEnabled,
shortsSwipeUpNudgeCountKey: shortsSwipeNudgeCount,
showOnboarding: showOnboarding)
explicitFeatureAvailability: featureAvailability
)
self.notifyAboutConfigUpdate()
DispatchQueue.global().async {
let encoder = JSONEncoder()
......@@ -204,3 +170,21 @@ public class ConfigManager {
}
}
}
fileprivate extension AppFeature {
var configVariableName: String? {
switch self {
case .nwsAlertsViaMoEngage:
return "ios_nws_alerts_via_moengage_enabled"
case .attPrompt:
return "ios_show_att_prompt"
case .shortsLastNudge:
return "shorts_swipe_down_nudge_enabled"
case .onboarding:
return "ios_show_onboarding"
case .ads, .airQualityIndex, .minutelyForecast, .shorts:
return nil
// don't use 'default', so that we didn't add new features here in the future.
}
}
}
......@@ -56,8 +56,12 @@ public class PushNotificationsManager: NSObject, PushNotificationsManagerProtoco
internal var lastSetFIPSList: String? = ""
internal var lastSetFIPSCode: String? = ""
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
private func updateNwsSubscriptions(with fipsList: String?, currentFipsCode: String?) {
if configManager.config.nwsAlertsViaMoEngageEnabled {
if featureAvailabilityManager.isAvailable(feature: .nwsAlertsViaMoEngage) {
if fipsList != lastSetFIPSList {
log.info("Set \(AnalyticsAttribute.fipsList.attributeName) to '\(fipsList ?? "")'")
lastSetFIPSList = fipsList
......
......@@ -20,8 +20,12 @@ class ShortsManager {
static let shared = ShortsManager()
let multicastDelegate = MulticastDelegate<ShortsManagerDelegate>()
private(set) var shorts = [ShortsItem]()
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
var shortsAvailable: Bool {
return LocationManager.shared.selectedLocation?.countryCode == "US" && UIDevice.current.userInterfaceIdiom == .phone
return featureAvailabilityManager.isAvailable(feature: .shorts)
}
//Private
......
......@@ -29,7 +29,8 @@ private enum ForecastCellType {
private struct HourlySection {
let rows: [ForecastCellType] = {
let showAds = !isAppPro() && AdConfigManager.shared.adConfig.adsEnabled
if showAds {
//TODO: dependency injection
if FeatureAvailabilityManager.shared.isAvailable(feature: .ads) {
return [.day, .forecastHourly, .adBanner, .precipitation, .wind, .adMREC]
}
else {
......@@ -40,8 +41,8 @@ private struct HourlySection {
private struct DailySection {
let rows: [ForecastCellType] = {
let showAds = !isAppPro() && AdConfigManager.shared.adConfig.adsEnabled
if showAds {
//TODO: dependency injection
if FeatureAvailabilityManager.shared.isAvailable(feature: .ads) {
return [.forecastDaily, .forecastDailyInfo, .adBanner, .sun, .moon, .adMREC]
}
else {
......
......@@ -33,7 +33,8 @@ fileprivate struct HeaderSection: NWSAlertTableViewSection {
}
private let rows: [NWSAlertCellType] = {
if !isAppPro() && AdConfigManager.shared.adConfig.adsEnabled {
//TODO: dependency injection
if FeatureAvailabilityManager.shared.isAvailable(feature: .ads) {
return [.header, .forecastOffice, .adBanner]
}
else {
......
......@@ -50,6 +50,10 @@ class ShortsViewController: UIViewController {
return scheduler
}()
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
deinit {
coordinator.viewControllerDidEnd(controller: self)
NotificationCenter.default.removeObserver(self)
......@@ -150,7 +154,7 @@ class ShortsViewController: UIViewController {
}
//Show the swipe helper view if needed
if ConfigManager.shared.config.shortsLastNudgeEnabled {
if featureAvailabilityManager.isAvailable(feature: .shortsLastNudge) {
//Check for the last row
if rowIndex == viewModel.shorts.count - 1 {
swipeHelperView.configure(forState: .downViewedAll)
......
......@@ -62,6 +62,10 @@ class TodayCellFactory: CellFactory {
private var adViewCache = [IndexPath: AdView]()
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
//Public
public var delegate: CellFactoryDelegate?
init(viewModel: TodayViewModel) {
......@@ -227,7 +231,7 @@ class TodayCellFactory: CellFactory {
private func setupHiddenRows() {
var rowsToHide = Set<TodayCellType>()
if isAppPro() || !AdConfigManager.shared.adConfig.adsEnabled {
if !featureAvailabilityManager.isAvailable(feature: .ads) {
rowsToHide.insert(.adBanner)
rowsToHide.insert(.adMREC)
}
......
......@@ -126,9 +126,13 @@ class MenuViewModel: NSObject, ViewModelProtocol {
// MARK: - Help section
extension MenuViewModel {
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
private func helpRequestBodyString() -> String {
var str = String()
str.append("Weather Alerts Enabled: \(ConfigManager.shared.config.nwsAlertsViaMoEngageEnabled)\n")
str.append("Weather Alerts Enabled: \(featureAvailabilityManager.isAvailable(feature: .nwsAlertsViaMoEngage))\n")
let isRegistered = UIApplication.shared.isRegisteredForRemoteNotifications
str.append("Push Enabled: \(isRegistered)\n")
......
......@@ -35,6 +35,10 @@ class TodayViewModel: ViewModelProtocol {
shortsManager.shorts
}
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
public lazy var todayCellFactory:TodayCellFactory = {
let factory = TodayCellFactory(viewModel: self)
factory.delegate = self
......@@ -127,7 +131,7 @@ class TodayViewModel: ViewModelProtocol {
// not calling onboardingFlowCompleted, because it will be called in the ATT prompt completion handler.
return
}
if self.configManager.config.showAttPrompt
if self.featureAvailabilityManager.isAvailable(feature: .attPrompt)
&& !self.ccpaHelper.isNewUser
&& ATTrackingManager.trackingAuthorizationStatus == .notDetermined {
......
//
// AppFeature.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 24.08.2021.
//
import Foundation
public enum AppFeature: String, Codable, CaseIterable {
case airQualityIndex
case attPrompt
case ads
case nwsAlertsViaMoEngage
case onboarding
case minutelyForecast
case shorts
case shortsLastNudge
}
//
// DeviceTypeFeatureAvailabilityChecker.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 25.08.2021.
//
import UIKit
public struct DeviceTypeFeatureAvailabilityChecker: FeatureAvailabilityChecker {
let deviceType: UIUserInterfaceIdiom
public init(deviceType: UIUserInterfaceIdiom) {
self.deviceType = deviceType
}
public var isAvailable: Bool {
UIDevice.current.userInterfaceIdiom == deviceType
}
}
//
// FeatureAvailabilityChecker.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 24.08.2021.
//
import Foundation
public protocol FeatureAvailabilityChecker {
var isAvailable: Bool { get }
}
//
// FeatureAvailabilityCheckerHelpers.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 25.08.2021.
//
import Foundation
public extension FeatureAvailabilityChecker {
static prefix func ! (checker: Self) -> FeatureAvailabilityChecker {
return FeatureAvailabilityNot(decoratedChecker: checker)
}
}
public func && (first: FeatureAvailabilityChecker, second: FeatureAvailabilityChecker) -> FeatureAvailabilityChecker {
return FeatureAvaillabilityAnd(first: first, second: second)
}
public func || (first: FeatureAvailabilityChecker, second: FeatureAvailabilityChecker) -> FeatureAvailabilityChecker {
return FeatureAvaillabilityOr(first: first, second: second)
}
fileprivate struct FeatureAvailabilityNot: FeatureAvailabilityChecker {
private let decorated: FeatureAvailabilityChecker
public init(decoratedChecker: FeatureAvailabilityChecker) {
decorated = decoratedChecker
}
var isAvailable: Bool {
!decorated.isAvailable
}
}
fileprivate struct FeatureAvaillabilityAnd: FeatureAvailabilityChecker {
private let firstChecker: FeatureAvailabilityChecker
private let secondChecker: FeatureAvailabilityChecker
public init(first: FeatureAvailabilityChecker, second: FeatureAvailabilityChecker) {
firstChecker = first
secondChecker = second
}
var isAvailable: Bool {
firstChecker.isAvailable && secondChecker.isAvailable
}
}
fileprivate struct FeatureAvaillabilityOr: FeatureAvailabilityChecker {
private let firstChecker: FeatureAvailabilityChecker
private let secondChecker: FeatureAvailabilityChecker
public init(first: FeatureAvailabilityChecker, second: FeatureAvailabilityChecker) {
firstChecker = first
secondChecker = second
}
var isAvailable: Bool {
firstChecker.isAvailable || secondChecker.isAvailable
}
}
//
// PremiumInAppFeatureAvailabilityChecker.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 25.08.2021.
//
import Foundation
public struct PremiumInAppFeatureAvailabilityChecker: FeatureAvailabilityChecker {
public init() {}
public var isAvailable: Bool {
isAppPro()
}
}
//
// ProSubscriptionAvailabilityChecker.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 25.08.2021.
//
import Foundation
public struct ProSubscriptionAvailabilityChecker: FeatureAvailabilityChecker {
public init() {}
public var isAvailable: Bool {
#warning("Not implemented!")
//TODO: Implement!
// IOS-155
#if DEBUG
return true
#else
return false
#endif
}
}
//
// USOnlyFeatureAvailabilityChecker.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 25.08.2021.
//
import Foundation
public struct USOnlyFeatureAvailabilityChecker: FeatureAvailabilityChecker {
public typealias LocationFetcher = () -> Location?
private let locationFetcher: LocationFetcher
public init(locationFetcher: @escaping LocationFetcher) {
self.locationFetcher = locationFetcher
}
public var isAvailable: Bool {
if let location = locationFetcher() {
return location.countryCode == "US"
}
return false
}
}
//
// FeatureAvailabilityManager.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 24.08.2021.
//
import Foundation
public class FeatureAvailabilityManager {
public typealias ConfigFetcher = () -> AppConfig?
private let checkers: [AppFeature: FeatureAvailabilityChecker]
private let configFetcher: ConfigFetcher
public static var shared: FeatureAvailabilityManager!
public init(configFetcher: @escaping ConfigFetcher, checkers: [AppFeature: FeatureAvailabilityChecker]) {
self.configFetcher = configFetcher
self.checkers = checkers
}
public func isAvailable(feature: AppFeature) -> Bool {
guard let config = configFetcher() else {
// TODO: log error
return false
}
guard config.isEnabled(feature: feature) else {
return false
}
if let avaiabilityChecker = checkers[feature] {
return avaiabilityChecker.isAvailable
}
return true
}
}
// TODO: make FeatureAvailabilityCheckers observe the values and notify the FeatureAvailabilityManager that the value has been changed. Move from observing different things across the app (ConfigManager, LocationManager, etc.) to observing just the FeatureAvailabilityManager
//public protocol FeatureAvailabilityObserver {
// func manager(_ manager: FeatureAvailabilityManager, updatedAvailability isAvailable: Bool, for feature: AppFeature)
//}
//
// AdConfig.swift
// BaconReader
// OneWeatherCore
//
// Created by Steve Pint on 5/2/19.
// Copyright © 2019 OneLouder Apps. All rights reserved.
// Created by Demid Merzlyakov on 25.08.2021.
//
import UIKit
import Foundation
public struct AdConfig: Codable {
public typealias AdPlacementName = String
public struct AdConfig {
private static let defaultA9MaxCachedPerPlacement: UInt = 2
public var adsEnabled: Bool
......@@ -21,6 +22,22 @@ public struct AdConfig: Codable {
return placements?[name]
}
public init() {
adsEnabled = true
a9RefreshRate = 0
a9MaxCachedPerPlacement = AdConfig.defaultA9MaxCachedPerPlacement
}
}
extension AdConfig: Equatable {
public static func == (lhs: AdConfig, rhs: AdConfig) -> Bool {
return lhs.adsEnabled == rhs.adsEnabled &&
lhs.a9RefreshRate == rhs.a9RefreshRate &&
lhs.placements == rhs.placements
}
}
extension AdConfig: Codable {
struct CodingKeys: CodingKey {
var stringValue: String
init(stringValue: String) {
......@@ -35,12 +52,6 @@ public struct AdConfig: Codable {
static let placements = CodingKeys(stringValue: "placements")
}
init() {
adsEnabled = true
a9RefreshRate = 0
a9MaxCachedPerPlacement = AdConfig.defaultA9MaxCachedPerPlacement
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
......@@ -73,52 +84,3 @@ public struct AdConfig: Codable {
try container.encodeIfPresent(placements, forKey: .placements)
}
}
public enum AdType: String, Codable {
case banner = "banner"
case square = "mrec"
case interstitial = "interstitial"
}
extension AdConfig: Equatable {
public static func == (lhs: AdConfig, rhs: AdConfig) -> Bool {
return lhs.adsEnabled == rhs.adsEnabled &&
lhs.a9RefreshRate == rhs.a9RefreshRate &&
lhs.placements == rhs.placements
}
}
public struct ContentUrlConfig : Codable {
var enabled: Bool
}
extension ContentUrlConfig: Equatable {
public static func == (lhs: ContentUrlConfig, rhs: ContentUrlConfig) -> Bool {
return lhs.enabled == rhs.enabled
}
}
public struct AdPlacement: Codable {
public var name: String
public var adUnitId: String
public var a9SlotId: String?
public var refreshInterval: TimeInterval
public var type: AdType
enum CodingKeys: String, CodingKey {
case name
case adUnitId = "ad_unit_id"
case a9SlotId = "a9_slot_id"
case refreshInterval = "refresh_interval"
case type
}
}
extension AdPlacement: Equatable {
public static func == (lhs: AdPlacement, rhs: AdPlacement) -> Bool {
return lhs.name == rhs.name &&
lhs.adUnitId == rhs.adUnitId &&
lhs.a9SlotId == rhs.a9SlotId &&
lhs.refreshInterval == rhs.refreshInterval
}
}
//
// AdPlacement.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 25.08.2021.
//
import Foundation
public struct AdPlacement: Codable {
public var name: String
public var adUnitId: String
public var a9SlotId: String?
public var refreshInterval: TimeInterval
public var type: AdType
enum CodingKeys: String, CodingKey {
case name
case adUnitId = "ad_unit_id"
case a9SlotId = "a9_slot_id"
case refreshInterval = "refresh_interval"
case type
}
}
extension AdPlacement: Equatable {
public static func == (lhs: AdPlacement, rhs: AdPlacement) -> Bool {
return lhs.name == rhs.name &&
lhs.adUnitId == rhs.adUnitId &&
lhs.a9SlotId == rhs.a9SlotId &&
lhs.refreshInterval == rhs.refreshInterval
}
}
//
// AdTypes.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 25.08.2021.
//
import Foundation
public enum AdType: String, Codable {
case banner = "banner"
case square = "mrec"
case interstitial = "interstitial"
}
//
// AppConfig.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 25.08.2021.
//
import Foundation
public struct AppConfig: Codable {
public let popularCities: [GeoNamesPlace]?
public let adConfig: AdConfig
public let ccpaUpdateInterval: TimeInterval?
public let shortsLeftBelowCount: Int
public let shortsSwipeUpNudgeCount: Int
private let explicitFeatureAvailability: [AppFeature: Bool]
public init(popularCities: [GeoNamesPlace]?,
adConfig: AdConfig,
ccpaUpdateInterval: TimeInterval?,
shortsLeftBelowCountKey: Int,
shortsSwipeUpNudgeCountKey: Int,
explicitFeatureAvailability: [AppFeature: Bool]
) {
self.popularCities = popularCities
self.adConfig = adConfig
self.ccpaUpdateInterval = ccpaUpdateInterval
self.shortsLeftBelowCount = shortsLeftBelowCountKey
self.shortsSwipeUpNudgeCount = shortsSwipeUpNudgeCountKey
self.explicitFeatureAvailability = explicitFeatureAvailability
}
public func isEnabled(feature: AppFeature) -> Bool {
if feature == .ads {
return adConfig.adsEnabled
}
return explicitFeatureAvailability[feature] ?? true
}
}
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