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 @@ ...@@ -115,7 +115,6 @@
CD6B304325726AD1004B34B3 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B304225726AD1004B34B3 /* DefaultTheme.swift */; }; CD6B304325726AD1004B34B3 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B304225726AD1004B34B3 /* DefaultTheme.swift */; };
CD6C22EA26677BE000D75659 /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22E926677BDF00D75659 /* ConfigManager.swift */; }; CD6C22EA26677BE000D75659 /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22E926677BDF00D75659 /* ConfigManager.swift */; };
CD6C22EE26677DBC00D75659 /* PushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22ED26677DBC00D75659 /* PushNotificationsManager.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 */; }; CD6C22F2266780ED00D75659 /* AdConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22E626677B4900D75659 /* AdConfigManager.swift */; };
CD6C22F32667815000D75659 /* EnvironmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22E526677B4900D75659 /* EnvironmentManager.swift */; }; CD6C22F32667815000D75659 /* EnvironmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22E526677B4900D75659 /* EnvironmentManager.swift */; };
CD71709025FA317700A63C27 /* ForecastTimePeriodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD71708F25FA317700A63C27 /* ForecastTimePeriodView.swift */; }; CD71709025FA317700A63C27 /* ForecastTimePeriodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD71708F25FA317700A63C27 /* ForecastTimePeriodView.swift */; };
...@@ -416,7 +415,6 @@ ...@@ -416,7 +415,6 @@
CD6B304225726AD1004B34B3 /* DefaultTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; CD71708F25FA317700A63C27 /* ForecastTimePeriodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastTimePeriodView.swift; sourceTree = "<group>"; };
...@@ -986,7 +984,6 @@ ...@@ -986,7 +984,6 @@
children = ( children = (
CD6C22E526677B4900D75659 /* EnvironmentManager.swift */, CD6C22E526677B4900D75659 /* EnvironmentManager.swift */,
CD6C22E626677B4900D75659 /* AdConfigManager.swift */, CD6C22E626677B4900D75659 /* AdConfigManager.swift */,
CD6C22E726677B4900D75659 /* AdConfig.swift */,
); );
path = Configuration; path = Configuration;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -1851,7 +1848,6 @@ ...@@ -1851,7 +1848,6 @@
files = ( files = (
CD6C22F32667815000D75659 /* EnvironmentManager.swift in Sources */, CD6C22F32667815000D75659 /* EnvironmentManager.swift in Sources */,
CD6C22F2266780ED00D75659 /* AdConfigManager.swift in Sources */, CD6C22F2266780ED00D75659 /* AdConfigManager.swift in Sources */,
CD6C22F1266780BE00D75659 /* AdConfig.swift in Sources */,
CD35DFD0260344A500F2138F /* ForecastConditionView.swift in Sources */, CD35DFD0260344A500F2138F /* ForecastConditionView.swift in Sources */,
CD857EA7268B45DD00B5E069 /* PromotionSmallWidgetView.swift in Sources */, CD857EA7268B45DD00B5E069 /* PromotionSmallWidgetView.swift in Sources */,
CD2ABF32261489F700C1A92E /* LocationCellFactory.swift in Sources */, CD2ABF32261489F700C1A92E /* LocationCellFactory.swift in Sources */,
...@@ -1958,7 +1954,6 @@ ...@@ -1958,7 +1954,6 @@
CD6B303E25726960004B34B3 /* ThemeProtocol.swift in Sources */, CD6B303E25726960004B34B3 /* ThemeProtocol.swift in Sources */,
CD6B303B2572680C004B34B3 /* SelfSizingButton.swift in Sources */, CD6B303B2572680C004B34B3 /* SelfSizingButton.swift in Sources */,
CDC62C4726C13BE300156643 /* OnboardingContentController.swift in Sources */, CDC62C4726C13BE300156643 /* OnboardingContentController.swift in Sources */,
CD9B6B1125DBC723001D9B80 /* CubicCurveAlgorithm.swift in Sources */,
CD8B60B4263819790055CB3F /* NotificationsViewController.swift in Sources */, CD8B60B4263819790055CB3F /* NotificationsViewController.swift in Sources */,
CD67617026259D220079D273 /* RadarMapLayersController.swift in Sources */, CD67617026259D220079D273 /* RadarMapLayersController.swift in Sources */,
CEC8FBAF2639756A0001A6BF /* OnboardingViewController.swift in Sources */, CEC8FBAF2639756A0001A6BF /* OnboardingViewController.swift in Sources */,
......
...@@ -7,14 +7,13 @@ ...@@ -7,14 +7,13 @@
// //
import UIKit import UIKit
import OneWeatherCore
import OneWeatherAnalytics import OneWeatherAnalytics
extension Notification.Name { extension Notification.Name {
public static let adConfigChanged = Notification.Name(rawValue: "AdConfig.Changed") public static let adConfigChanged = Notification.Name(rawValue: "AdConfig.Changed")
} }
public typealias AdPlacementName = String
public let placementNameTodayBanner: AdPlacementName = "today_banner" public let placementNameTodayBanner: AdPlacementName = "today_banner"
public let placementNameTodaySquare: AdPlacementName = "today_square" public let placementNameTodaySquare: AdPlacementName = "today_square"
public let placementNameForecastHourlyBanner: AdPlacementName = "forecast_hourly_banner" public let placementNameForecastHourlyBanner: AdPlacementName = "forecast_hourly_banner"
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import OneWeatherCore
import GoogleMobileAds import GoogleMobileAds
class NativeAdView: GADNativeAdView { class NativeAdView: GADNativeAdView {
......
...@@ -7,8 +7,9 @@ ...@@ -7,8 +7,9 @@
// //
import Foundation import Foundation
import GoogleMobileAds
import OneWeatherAnalytics import OneWeatherAnalytics
import OneWeatherCore
import GoogleMobileAds
protocol NativeBannerContainerViewDelegate: AnyObject { protocol NativeBannerContainerViewDelegate: AnyObject {
func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADNativeAd) func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADNativeAd)
......
...@@ -86,6 +86,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -86,6 +86,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
FirebaseApp.configure() FirebaseApp.configure()
ConfigManager.shared.updateConfig() ConfigManager.shared.updateConfig()
FeatureAvailabilityManager.shared = configureFeatureAvailabilityManager(with: LocationManager.shared, configManager: ConfigManager.shared)
//App UI //App UI
let appCoordinator = AppCoordinator(window: self.window!, launchOptions: launchOptions) let appCoordinator = AppCoordinator(window: self.window!, launchOptions: launchOptions)
appCoordinator.start() appCoordinator.start()
...@@ -189,6 +191,46 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -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 { extension AppDelegate: AppsFlyerLibDelegate {
......
...@@ -26,6 +26,9 @@ class AppCoordinator: Coordinator { ...@@ -26,6 +26,9 @@ class AppCoordinator: Coordinator {
var parentCoordinator: Coordinator? var parentCoordinator: Coordinator?
var childCoordinators = [Coordinator]() var childCoordinators = [Coordinator]()
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
init(window:UIWindow, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { init(window:UIWindow, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
self.window = window self.window = window
...@@ -109,7 +112,7 @@ class AppCoordinator: Coordinator { ...@@ -109,7 +112,7 @@ class AppCoordinator: Coordinator {
DispatchQueue.main.async { DispatchQueue.main.async {
self.logAppLaunchEvents() self.logAppLaunchEvents()
if ConfigManager.shared.config.showOnboarding { if self.featureAvailabilityManager.isAvailable(feature: .onboarding) {
if Settings.shared.initialOnboardingShowed { if Settings.shared.initialOnboardingShowed {
self.finishInitialOnboarding() self.finishInitialOnboarding()
} }
...@@ -158,7 +161,7 @@ class AppCoordinator: Coordinator { ...@@ -158,7 +161,7 @@ class AppCoordinator: Coordinator {
firstOpenSource = .url firstOpenSource = .url
} }
let flow = ConfigManager.shared.config.showOnboarding ? "FTUX" : "default" let flow = featureAvailabilityManager.isAvailable(feature: .onboarding) ? "FTUX" : "default"
analytics(log: .ANALYTICS_FIRST_OPEN, analytics(log: .ANALYTICS_FIRST_OPEN,
params: [.ANALYTICS_KEY_FIRST_OPEN_SOURCE : firstOpenSource.rawValue, params: [.ANALYTICS_KEY_FIRST_OPEN_SOURCE : firstOpenSource.rawValue,
.ANALYTICS_KEY_FIRST_OPEN_FLOW : flow]) .ANALYTICS_KEY_FIRST_OPEN_FLOW : flow])
......
...@@ -12,39 +12,6 @@ import FirebaseRemoteConfig ...@@ -12,39 +12,6 @@ import FirebaseRemoteConfig
import OneWeatherAnalytics import OneWeatherAnalytics
import OneWeatherCore 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 { public protocol ConfigManagerDelegate {
func dataUpdated(by configManager: ConfigManager) func dataUpdated(by configManager: ConfigManager)
} }
...@@ -53,12 +20,8 @@ public class ConfigManager { ...@@ -53,12 +20,8 @@ public class ConfigManager {
private static let adConfigKey = "ads_config_ios" private static let adConfigKey = "ads_config_ios"
private static let popularCitiesConfigKey = "search_screen_popular_cities" private static let popularCitiesConfigKey = "search_screen_popular_cities"
private static let ccpaUpdateIntervalConfigKey = "ccpa_update_interval_ios" 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 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 shortsSwipeUpNudgeCountKey = "shorts_swipe_up_nudge_on_x_cards"
private static let showOnboardingKey = "ios_show_onboarding"
private let delegates = MulticastDelegate<ConfigManagerDelegate>() private let delegates = MulticastDelegate<ConfigManagerDelegate>()
...@@ -77,12 +40,15 @@ public class ConfigManager { ...@@ -77,12 +40,15 @@ public class ConfigManager {
public var config: AppConfig = AppConfig(popularCities: nil, public var config: AppConfig = AppConfig(popularCities: nil,
adConfig: AdConfig(), adConfig: AdConfig(),
ccpaUpdateInterval: nil, ccpaUpdateInterval: nil,
nwsAlertsViaMoEngageEnabled: true,
showAttPrompt: false,
shortsLeftBelowCountKey: 0, shortsLeftBelowCountKey: 0,
shortsLastNudgeEnabledKey: false,
shortsSwipeUpNudgeCountKey: 0, shortsSwipeUpNudgeCountKey: 0,
showOnboarding: false) explicitFeatureAvailability: [
.nwsAlertsViaMoEngage: true,
.attPrompt: false,
.shortsLastNudge: false,
.onboarding: false
]
)
public func updateConfig() { public func updateConfig() {
log.info("update config") log.info("update config")
...@@ -120,11 +86,23 @@ public class ConfigManager { ...@@ -120,11 +86,23 @@ public class ConfigManager {
delegates.remove(delegate: delegate) 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) { private func parseConfigFromFirebase(source: String) {
log.info("Got config from \(source)") log.info("Got config from \(source)")
var configErrors = [Error]() var configErrors = [Error]()
let decoder = JSONDecoder() let decoder = JSONDecoder()
var adConfig = AdConfig() var adConfig = AdConfig()
do { do {
let adConfigData = remoteConfig.configValue(forKey: ConfigManager.adConfigKey).dataValue let adConfigData = remoteConfig.configValue(forKey: ConfigManager.adConfigKey).dataValue
...@@ -156,34 +134,22 @@ public class ConfigManager { ...@@ -156,34 +134,22 @@ public class ConfigManager {
if ccpaUpdateIntervalConfigValue.source == RemoteConfigSource.remote { if ccpaUpdateIntervalConfigValue.source == RemoteConfigSource.remote {
ccpaUpdateInterval = ccpaUpdateIntervalConfigValue.numberValue.doubleValue 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 shortsLeftBelowCountValue = remoteConfig.configValue(forKey: ConfigManager.shortsLeftBelowCountKey)
let shortsLeftBelowCount = shortsLeftBelowCountValue.numberValue.intValue let shortsLeftBelowCount = shortsLeftBelowCountValue.numberValue.intValue
let shortsLastNudgeEnabledValue = remoteConfig.configValue(forKey: ConfigManager.shortsLastNudgeEnabledKey)
let shortsLastNudgeEnabled = shortsLastNudgeEnabledValue.boolValue
let shortsSwipeNudgeCountValue = remoteConfig.configValue(forKey: ConfigManager.shortsSwipeUpNudgeCountKey) let shortsSwipeNudgeCountValue = remoteConfig.configValue(forKey: ConfigManager.shortsSwipeUpNudgeCountKey)
let shortsSwipeNudgeCount = shortsSwipeNudgeCountValue.numberValue.intValue let shortsSwipeNudgeCount = shortsSwipeNudgeCountValue.numberValue.intValue
let showOnboardingValue = remoteConfig.configValue(forKey: ConfigManager.showOnboardingKey) let featureAvailability = parseFeatureAvailability()
let showOnboarding = showOnboardingValue.boolValue
DispatchQueue.main.async { DispatchQueue.main.async {
self.config = AppConfig(popularCities: popularCities, self.config = AppConfig(popularCities: popularCities,
adConfig: adConfig, adConfig: adConfig,
ccpaUpdateInterval:ccpaUpdateInterval, ccpaUpdateInterval:ccpaUpdateInterval,
nwsAlertsViaMoEngageEnabled: nwsAlertsViaMoEngageEnabled,
showAttPrompt: showAttPrompt,
shortsLeftBelowCountKey: shortsLeftBelowCount, shortsLeftBelowCountKey: shortsLeftBelowCount,
shortsLastNudgeEnabledKey: shortsLastNudgeEnabled,
shortsSwipeUpNudgeCountKey: shortsSwipeNudgeCount, shortsSwipeUpNudgeCountKey: shortsSwipeNudgeCount,
showOnboarding: showOnboarding) explicitFeatureAvailability: featureAvailability
)
self.notifyAboutConfigUpdate() self.notifyAboutConfigUpdate()
DispatchQueue.global().async { DispatchQueue.global().async {
let encoder = JSONEncoder() let encoder = JSONEncoder()
...@@ -204,3 +170,21 @@ public class ConfigManager { ...@@ -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 ...@@ -56,8 +56,12 @@ public class PushNotificationsManager: NSObject, PushNotificationsManagerProtoco
internal var lastSetFIPSList: String? = "" internal var lastSetFIPSList: String? = ""
internal var lastSetFIPSCode: String? = "" internal var lastSetFIPSCode: String? = ""
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
private func updateNwsSubscriptions(with fipsList: String?, currentFipsCode: String?) { private func updateNwsSubscriptions(with fipsList: String?, currentFipsCode: String?) {
if configManager.config.nwsAlertsViaMoEngageEnabled { if featureAvailabilityManager.isAvailable(feature: .nwsAlertsViaMoEngage) {
if fipsList != lastSetFIPSList { if fipsList != lastSetFIPSList {
log.info("Set \(AnalyticsAttribute.fipsList.attributeName) to '\(fipsList ?? "")'") log.info("Set \(AnalyticsAttribute.fipsList.attributeName) to '\(fipsList ?? "")'")
lastSetFIPSList = fipsList lastSetFIPSList = fipsList
......
...@@ -20,8 +20,12 @@ class ShortsManager { ...@@ -20,8 +20,12 @@ class ShortsManager {
static let shared = ShortsManager() static let shared = ShortsManager()
let multicastDelegate = MulticastDelegate<ShortsManagerDelegate>() let multicastDelegate = MulticastDelegate<ShortsManagerDelegate>()
private(set) var shorts = [ShortsItem]() private(set) var shorts = [ShortsItem]()
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
var shortsAvailable: Bool { var shortsAvailable: Bool {
return LocationManager.shared.selectedLocation?.countryCode == "US" && UIDevice.current.userInterfaceIdiom == .phone return featureAvailabilityManager.isAvailable(feature: .shorts)
} }
//Private //Private
......
...@@ -29,7 +29,8 @@ private enum ForecastCellType { ...@@ -29,7 +29,8 @@ private enum ForecastCellType {
private struct HourlySection { private struct HourlySection {
let rows: [ForecastCellType] = { let rows: [ForecastCellType] = {
let showAds = !isAppPro() && AdConfigManager.shared.adConfig.adsEnabled 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] return [.day, .forecastHourly, .adBanner, .precipitation, .wind, .adMREC]
} }
else { else {
...@@ -40,8 +41,8 @@ private struct HourlySection { ...@@ -40,8 +41,8 @@ private struct HourlySection {
private struct DailySection { private struct DailySection {
let rows: [ForecastCellType] = { let rows: [ForecastCellType] = {
let showAds = !isAppPro() && AdConfigManager.shared.adConfig.adsEnabled //TODO: dependency injection
if showAds { if FeatureAvailabilityManager.shared.isAvailable(feature: .ads) {
return [.forecastDaily, .forecastDailyInfo, .adBanner, .sun, .moon, .adMREC] return [.forecastDaily, .forecastDailyInfo, .adBanner, .sun, .moon, .adMREC]
} }
else { else {
......
...@@ -33,7 +33,8 @@ fileprivate struct HeaderSection: NWSAlertTableViewSection { ...@@ -33,7 +33,8 @@ fileprivate struct HeaderSection: NWSAlertTableViewSection {
} }
private let rows: [NWSAlertCellType] = { private let rows: [NWSAlertCellType] = {
if !isAppPro() && AdConfigManager.shared.adConfig.adsEnabled { //TODO: dependency injection
if FeatureAvailabilityManager.shared.isAvailable(feature: .ads) {
return [.header, .forecastOffice, .adBanner] return [.header, .forecastOffice, .adBanner]
} }
else { else {
......
...@@ -50,6 +50,10 @@ class ShortsViewController: UIViewController { ...@@ -50,6 +50,10 @@ class ShortsViewController: UIViewController {
return scheduler return scheduler
}() }()
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
deinit { deinit {
coordinator.viewControllerDidEnd(controller: self) coordinator.viewControllerDidEnd(controller: self)
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
...@@ -150,7 +154,7 @@ class ShortsViewController: UIViewController { ...@@ -150,7 +154,7 @@ class ShortsViewController: UIViewController {
} }
//Show the swipe helper view if needed //Show the swipe helper view if needed
if ConfigManager.shared.config.shortsLastNudgeEnabled { if featureAvailabilityManager.isAvailable(feature: .shortsLastNudge) {
//Check for the last row //Check for the last row
if rowIndex == viewModel.shorts.count - 1 { if rowIndex == viewModel.shorts.count - 1 {
swipeHelperView.configure(forState: .downViewedAll) swipeHelperView.configure(forState: .downViewedAll)
......
...@@ -62,6 +62,10 @@ class TodayCellFactory: CellFactory { ...@@ -62,6 +62,10 @@ class TodayCellFactory: CellFactory {
private var adViewCache = [IndexPath: AdView]() private var adViewCache = [IndexPath: AdView]()
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
//Public //Public
public var delegate: CellFactoryDelegate? public var delegate: CellFactoryDelegate?
init(viewModel: TodayViewModel) { init(viewModel: TodayViewModel) {
...@@ -227,7 +231,7 @@ class TodayCellFactory: CellFactory { ...@@ -227,7 +231,7 @@ class TodayCellFactory: CellFactory {
private func setupHiddenRows() { private func setupHiddenRows() {
var rowsToHide = Set<TodayCellType>() var rowsToHide = Set<TodayCellType>()
if isAppPro() || !AdConfigManager.shared.adConfig.adsEnabled { if !featureAvailabilityManager.isAvailable(feature: .ads) {
rowsToHide.insert(.adBanner) rowsToHide.insert(.adBanner)
rowsToHide.insert(.adMREC) rowsToHide.insert(.adMREC)
} }
......
...@@ -126,9 +126,13 @@ class MenuViewModel: NSObject, ViewModelProtocol { ...@@ -126,9 +126,13 @@ class MenuViewModel: NSObject, ViewModelProtocol {
// MARK: - Help section // MARK: - Help section
extension MenuViewModel { extension MenuViewModel {
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
private func helpRequestBodyString() -> String { private func helpRequestBodyString() -> String {
var str = 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 let isRegistered = UIApplication.shared.isRegisteredForRemoteNotifications
str.append("Push Enabled: \(isRegistered)\n") str.append("Push Enabled: \(isRegistered)\n")
......
...@@ -35,6 +35,10 @@ class TodayViewModel: ViewModelProtocol { ...@@ -35,6 +35,10 @@ class TodayViewModel: ViewModelProtocol {
shortsManager.shorts shortsManager.shorts
} }
private var featureAvailabilityManager: FeatureAvailabilityManager {
FeatureAvailabilityManager.shared
}
public lazy var todayCellFactory:TodayCellFactory = { public lazy var todayCellFactory:TodayCellFactory = {
let factory = TodayCellFactory(viewModel: self) let factory = TodayCellFactory(viewModel: self)
factory.delegate = self factory.delegate = self
...@@ -127,7 +131,7 @@ class TodayViewModel: ViewModelProtocol { ...@@ -127,7 +131,7 @@ class TodayViewModel: ViewModelProtocol {
// not calling onboardingFlowCompleted, because it will be called in the ATT prompt completion handler. // not calling onboardingFlowCompleted, because it will be called in the ATT prompt completion handler.
return return
} }
if self.configManager.config.showAttPrompt if self.featureAvailabilityManager.isAvailable(feature: .attPrompt)
&& !self.ccpaHelper.isNewUser && !self.ccpaHelper.isNewUser
&& ATTrackingManager.trackingAuthorizationStatus == .notDetermined { && 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 // AdConfig.swift
// BaconReader // OneWeatherCore
// //
// Created by Steve Pint on 5/2/19. // Created by Demid Merzlyakov on 25.08.2021.
// Copyright © 2019 OneLouder Apps. All rights reserved.
// //
import UIKit import Foundation
public struct AdConfig: Codable { public typealias AdPlacementName = String
public struct AdConfig {
private static let defaultA9MaxCachedPerPlacement: UInt = 2 private static let defaultA9MaxCachedPerPlacement: UInt = 2
public var adsEnabled: Bool public var adsEnabled: Bool
...@@ -21,6 +22,22 @@ public struct AdConfig: Codable { ...@@ -21,6 +22,22 @@ public struct AdConfig: Codable {
return placements?[name] 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 { struct CodingKeys: CodingKey {
var stringValue: String var stringValue: String
init(stringValue: String) { init(stringValue: String) {
...@@ -35,12 +52,6 @@ public struct AdConfig: Codable { ...@@ -35,12 +52,6 @@ public struct AdConfig: Codable {
static let placements = CodingKeys(stringValue: "placements") static let placements = CodingKeys(stringValue: "placements")
} }
init() {
adsEnabled = true
a9RefreshRate = 0
a9MaxCachedPerPlacement = AdConfig.defaultA9MaxCachedPerPlacement
}
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
...@@ -73,52 +84,3 @@ public struct AdConfig: Codable { ...@@ -73,52 +84,3 @@ public struct AdConfig: Codable {
try container.encodeIfPresent(placements, forKey: .placements) 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