Commit aced138f by Demid Merzlyakov

Merge branch 'feature/push-notifications'

parents 8f30fea7 cf8f3803
......@@ -190,6 +190,7 @@
CE13B97B2626FB11007CBD4D /* PSMLocationSDK.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE13B7DC262478E7007CBD4D /* PSMLocationSDK.xcframework */; };
CE13B97C2626FB11007CBD4D /* PSMLocationSDK.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE13B7DC262478E7007CBD4D /* PSMLocationSDK.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE13B98726273236007CBD4D /* NWSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B98626273236007CBD4D /* NWSAlert.swift */; };
CE14445F2638B6CF008E2162 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE14445E2638B6CF008E2162 /* StoreKit.framework */; };
CE28474F26159857006C8DC5 /* HealthSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE28474E26159857006C8DC5 /* HealthSource.swift */; };
CE28475226159A32006C8DC5 /* BlendHealthModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE28475126159A32006C8DC5 /* BlendHealthModels.swift */; };
CE28475D2615A5B3006C8DC5 /* Health.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE28475C2615A5B3006C8DC5 /* Health.swift */; };
......@@ -199,6 +200,8 @@
CE308B2A2637EA8E001ECD8A /* _CoreNWSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE308B282637EA8E001ECD8A /* _CoreNWSAlert.swift */; };
CE308B2B2637EA8E001ECD8A /* _CoreNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE308B292637EA8E001ECD8A /* _CoreNotifications.swift */; };
CE376C98261EE484000B1159 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE376C97261EE484000B1159 /* LaunchScreen.storyboard */; };
CE3A36C72638A77E002CACC3 /* BlendFIPSSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3A36C62638A77E002CACC3 /* BlendFIPSSource.swift */; };
CE3A36ED2638A825002CACC3 /* FIPSResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3A36EC2638A825002CACC3 /* FIPSResponse.swift */; };
CE578FD325F7E89400E8B85D /* DayTimeWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE578FD225F7E89400E8B85D /* DayTimeWeather.swift */; };
CE578FE525FB415F00E8B85D /* CityCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE578FE225FB415F00E8B85D /* CityCell.swift */; };
CE578FE625FB415F00E8B85D /* LocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE578FE325FB415F00E8B85D /* LocationViewController.swift */; };
......@@ -234,6 +237,9 @@
CEAFF08F25DFC6ED00DF4EBF /* HourlyWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAFF08E25DFC6ED00DF4EBF /* HourlyWeather.swift */; };
CEAFF09225DFC71D00DF4EBF /* HelperTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAFF09125DFC71D00DF4EBF /* HelperTypes.swift */; };
CEAFF0A325E0FF0800DF4EBF /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAFF0A225E0FF0800DF4EBF /* LocationManager.swift */; };
CEBAC1C62638236D00A89681 /* PushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBAC1C52638236D00A89681 /* PushNotificationsManager.swift */; };
CEBAC1C82638240800A89681 /* DeeplinksRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBAC1C72638240800A89681 /* DeeplinksRouter.swift */; };
CEBAC2122638968D00A89681 /* FIPSSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBAC2112638968D00A89681 /* FIPSSource.swift */; };
CEC526FA25E7959A00DA58A5 /* WeatherSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC526F925E7959A00DA58A5 /* WeatherSource.swift */; };
CEC526FD25E795F700DA58A5 /* WdtWeatherSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC526FC25E795F700DA58A5 /* WdtWeatherSource.swift */; };
CEC5270025E7BACB00DA58A5 /* WdtLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC526FF25E7BACB00DA58A5 /* WdtLocation.swift */; };
......@@ -473,6 +479,8 @@
CE13B88C26248A77007CBD4D /* GoogleService-Info-Production.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info-Production.plist"; sourceTree = "<group>"; };
CE13B88D26248A77007CBD4D /* GoogleService-Info-Staging.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info-Staging.plist"; sourceTree = "<group>"; };
CE13B98626273236007CBD4D /* NWSAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSAlert.swift; sourceTree = "<group>"; };
CE14445D2638B6A8008E2162 /* 1Weather.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = 1Weather.entitlements; sourceTree = "<group>"; };
CE14445E2638B6CF008E2162 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
CE28474E26159857006C8DC5 /* HealthSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthSource.swift; sourceTree = "<group>"; };
CE28475126159A32006C8DC5 /* BlendHealthModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlendHealthModels.swift; sourceTree = "<group>"; };
CE28475C2615A5B3006C8DC5 /* Health.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Health.swift; sourceTree = "<group>"; };
......@@ -482,6 +490,8 @@
CE308B282637EA8E001ECD8A /* _CoreNWSAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _CoreNWSAlert.swift; sourceTree = "<group>"; };
CE308B292637EA8E001ECD8A /* _CoreNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _CoreNotifications.swift; sourceTree = "<group>"; };
CE376C97261EE484000B1159 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
CE3A36C62638A77E002CACC3 /* BlendFIPSSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlendFIPSSource.swift; sourceTree = "<group>"; };
CE3A36EC2638A825002CACC3 /* FIPSResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FIPSResponse.swift; sourceTree = "<group>"; };
CE578FD225F7E89400E8B85D /* DayTimeWeather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayTimeWeather.swift; sourceTree = "<group>"; };
CE578FE225FB415F00E8B85D /* CityCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CityCell.swift; sourceTree = "<group>"; };
CE578FE325FB415F00E8B85D /* LocationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationViewController.swift; sourceTree = "<group>"; };
......@@ -517,6 +527,9 @@
CEAFF08E25DFC6ED00DF4EBF /* HourlyWeather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HourlyWeather.swift; sourceTree = "<group>"; };
CEAFF09125DFC71D00DF4EBF /* HelperTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperTypes.swift; sourceTree = "<group>"; };
CEAFF0A225E0FF0800DF4EBF /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
CEBAC1C52638236D00A89681 /* PushNotificationsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationsManager.swift; sourceTree = "<group>"; };
CEBAC1C72638240800A89681 /* DeeplinksRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeeplinksRouter.swift; sourceTree = "<group>"; };
CEBAC2112638968D00A89681 /* FIPSSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FIPSSource.swift; sourceTree = "<group>"; };
CEC526F925E7959A00DA58A5 /* WeatherSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSource.swift; sourceTree = "<group>"; };
CEC526FC25E795F700DA58A5 /* WdtWeatherSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WdtWeatherSource.swift; sourceTree = "<group>"; };
CEC526FF25E7BACB00DA58A5 /* WdtLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WdtLocation.swift; sourceTree = "<group>"; };
......@@ -554,6 +567,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CE14445F2638B6CF008E2162 /* StoreKit.framework in Frameworks */,
CE13B97B2626FB11007CBD4D /* PSMLocationSDK.xcframework in Frameworks */,
34EAFD887EF2D1D7449A016C /* Pods_1Weather.framework in Frameworks */,
);
......@@ -624,6 +638,7 @@
CD1237C1255D5C5900C98139 /* 1Weather */ = {
isa = PBXGroup;
children = (
CE14445D2638B6A8008E2162 /* 1Weather.entitlements */,
CE13B78926247474007CBD4D /* External */,
CE13B7EC262480B3007CBD4D /* Ads */,
CEF959632600C2E300975FAA /* Analytics */,
......@@ -689,6 +704,7 @@
CD17C5F925D15B5500EE884E /* Coordinators */ = {
isa = PBXGroup;
children = (
CEBAC1C72638240800A89681 /* DeeplinksRouter.swift */,
CD17C60125D15C8500EE884E /* CoordinatorProtocol.swift */,
CD17C5FA25D15B6B00EE884E /* AppCoordinator.swift */,
CD17C5FE25D15B7C00EE884E /* TodayCoordinator.swift */,
......@@ -1222,6 +1238,8 @@
CE13B98026272A13007CBD4D /* Model */,
CE0456232629C04C003D252B /* NWSAlertsManager.swift */,
CEE0A1A326317A8F0044C257 /* NWSAlertInfoParser.swift */,
CEBAC2112638968D00A89681 /* FIPSSource.swift */,
CE3A36C62638A77E002CACC3 /* BlendFIPSSource.swift */,
);
path = Notifications;
sourceTree = "<group>";
......@@ -1234,6 +1252,7 @@
CEE0A1A126317A3F0044C257 /* NWSSeverityLevel.swift */,
CEE0A19F26317A1E0044C257 /* NWSAlertExtendedInfo.swift */,
CEE0A17A263179E50044C257 /* NWSAlertInfoBlock.swift */,
CE3A36EC2638A825002CACC3 /* FIPSResponse.swift */,
);
path = Model;
sourceTree = "<group>";
......@@ -1381,6 +1400,7 @@
CEAFF09925DFC78200DF4EBF /* Network */ = {
isa = PBXGroup;
children = (
CEBAC1C52638236D00A89681 /* PushNotificationsManager.swift */,
87C1724825FF94F400DA3464 /* ConfigManager.swift */,
87C171F325FF7A4000DA3464 /* PopularCitiesManager.swift */,
87C171F125FF7A3300DA3464 /* Weather */,
......@@ -1486,6 +1506,7 @@
DBFD169AA2AA6A3CB5B68BB5 /* Frameworks */ = {
isa = PBXGroup;
children = (
CE14445E2638B6CF008E2162 /* StoreKit.framework */,
6B543196B99BA697763514F6 /* Pods_1Weather.framework */,
);
name = Frameworks;
......@@ -1731,6 +1752,7 @@
CD67617C2625A60B0079D273 /* MapLayersDismissAnimator.swift in Sources */,
CEAFF08C25DFC6BD00DF4EBF /* DailyWeather.swift in Sources */,
CEDE4F0B25EFA3A7007457E9 /* UpdatableModelObject.swift in Sources */,
CE3A36C72638A77E002CACC3 /* BlendFIPSSource.swift in Sources */,
CE13B814262480B3007CBD4D /* BRNativeBannerView.swift in Sources */,
CE13B7E126247BF9007CBD4D /* UserDefaultsOptionalValue.swift in Sources */,
CE8962A626175DF500CA274A /* _CoreHealth.swift in Sources */,
......@@ -1833,6 +1855,7 @@
CE9F01C1261B3776009BA500 /* CoreDataUtils.swift in Sources */,
CD67616D262587D30079D273 /* UITabBarController+Hide.swift in Sources */,
CDC6126225E8DAB800188DA7 /* MoonPhaseCell.swift in Sources */,
CEBAC2122638968D00A89681 /* FIPSSource.swift in Sources */,
CD37D3D6260C93B3002669D6 /* MenuCellFactory.swift in Sources */,
CD8B60AD263819400055CB3F /* NWSAlertInfoBlockTableViewCell.swift in Sources */,
87D815AC2636D61D0015A6D1 /* NWSAlertViewModel.swift in Sources */,
......@@ -1871,6 +1894,7 @@
CD32CE0B260C744A00235081 /* MenuCoordinator.swift in Sources */,
CD55E0BB2615EE2400CC4DC7 /* PollutantView.swift in Sources */,
CE308B2A2637EA8E001ECD8A /* _CoreNWSAlert.swift in Sources */,
CE3A36ED2638A825002CACC3 /* FIPSResponse.swift in Sources */,
CD8B60B3263819790055CB3F /* NWSAlertCell.swift in Sources */,
CE13B81C262480B3007CBD4D /* Interstitial.swift in Sources */,
CDDE8D7C262EED3C00267931 /* MapLegendSevereView.swift in Sources */,
......@@ -1898,11 +1922,13 @@
CD80917B2578E4A8003541A4 /* UIViewController+Alert.swift in Sources */,
CEF959932600C63500975FAA /* Analytics.swift in Sources */,
CEE0A1A426317A8F0044C257 /* NWSAlertInfoParser.swift in Sources */,
CEBAC1C62638236D00A89681 /* PushNotificationsManager.swift in Sources */,
CE13B821262480B3007CBD4D /* Scheduler.swift in Sources */,
CEDE4F0F25EFA3B4007457E9 /* UpdatableModelObjectInTime.swift in Sources */,
CE13B81E262480B3007CBD4D /* AdCacheManager.swift in Sources */,
CE13B72B26245D0D007CBD4D /* HealthStatus.swift in Sources */,
CE8962A426175DF500CA274A /* _CorePollutant.swift in Sources */,
CEBAC1C82638240800A89681 /* DeeplinksRouter.swift in Sources */,
CD3F6E6925FA59D4002DB99B /* ForecastDetailPeriodButton.swift in Sources */,
CD37D405260DFFDD002669D6 /* CellFactory.swift in Sources */,
CD37D3F6260DF5BA002669D6 /* SettingsViewModel.swift in Sources */,
......@@ -2072,6 +2098,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = 1Weather/1Weather.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = SET_BY_BUILD_SCRIPT;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
......@@ -2101,6 +2128,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = 1Weather/1Weather.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = SET_BY_BUILD_SCRIPT;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.onelouder.oneweather.MoEngage</string>
<string>group.com.onelouder.oneweather</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)psm.lsdk</string>
</array>
</dict>
</plist>
......@@ -18,4 +18,5 @@ public enum AnalyticsParameter: String {
case ANALYTICS_KEY_PLACEMENT_NAME = "placement_name"
case ANALYTICS_KEY_AD_UNIT_ID = "AD_PLACEMENT_ID"
case ANALYTICS_KEY_AD_ADAPTER = "AD_ADAPTER"
case ANALYTICS_KEY_PUSH_NOTIFICATION_SOURCE = "source"
}
......@@ -52,13 +52,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
.withCrashReporting(false)
.withAppVersion(appVersion))
// TODO: Radar Setup
// WDT radar setup
// SwarmManager.sharedManager.authentication = SkywiseAuthentication(
// app_id: WDT_APP_ID,
// app_key: WDT_APP_KEY
// )
//MoEngage setup
var moEngageConfig = MOSDKConfig(appID: kMoEngageAppId)
moEngageConfig.appGroupID = "group.com.onelouder.oneweather.MoEngage"
......@@ -70,11 +63,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
MoEngage.sharedInstance().initializeLive(with: moEngageConfig, andLaunchOptions: launchOptions)
#endif
PushNotificationsManager.shared.registerForRemoteNotifications()
return true
}
func applicationDidBecomeActive(_ application: UIApplication) {
LocationManager.shared.updateAllWeatherIfNeeded()
LocationManager.shared.updateEverythingIfNeeded()
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
PushNotificationsManager.shared.set(pushToken: deviceToken)
}
}
......@@ -12,12 +12,17 @@ class AppCoordinator: Coordinator {
private let tabBarController = AppTabBarController()
//Public
static var instance: AppCoordinator!
var parentCoordinator: Coordinator?
var childCoordinators = [Coordinator]()
init(window:UIWindow) {
window.rootViewController = tabBarController
window.makeKeyAndVisible()
//TODO: this is very bad, fix it
AppCoordinator.instance = self
}
func start() {
......@@ -40,6 +45,30 @@ class AppCoordinator: Coordinator {
tabBarController.setupTabBar()
}
public func openToday() {
tabBarController.selectedIndex = AppTabBarController.AppTab.today.rawValue
}
public func openForecast(timePeriod: TimePeriod?) {
let forecastIndex = AppTabBarController.AppTab.forecast.rawValue
tabBarController.selectedIndex = forecastIndex
if let timePeriod = timePeriod {
if let forecastCoordinator = childCoordinators[forecastIndex] as? ForecastCoordinator {
forecastCoordinator.open(timePeriod: timePeriod)
}
}
}
public func openRadar() {
tabBarController.selectedIndex = AppTabBarController.AppTab.radar.rawValue
}
public func openNotifications() {
let notificationsCoordinator = NotificationsCoordinator(parentViewController: tabBarController)
notificationsCoordinator.parentCoordinator = self
notificationsCoordinator.start()
}
func viewControllerDidEnd(controller: UIViewController) {
//
}
......
//
// DeeplinksRouter.swift
// OneWeather
//
// Created by Demid Merzlyakov on 28.01.2021.
// Copyright © 2021 OneLouder, Inc. All rights reserved.
//
import Foundation
class DeeplinksRouter {
static let urlScheme = "oneweather"
enum UrlPathComponent: String {
case home = "home"
case today = "today"
case forecast = "forecast"
case hourly = "hourly"
case daily = "daily"
case precipitation = "precipitation"
case radar = "radar"
case sunMoon = "sun-moon"
}
enum UrlQueryParam: String {
case cityId = "cityid"
case lat = "lat"
case lon = "lon"
case fipsCode = "fipsCode"
}
private var appCoordinator: AppCoordinator {
AppCoordinator.instance
}
private let log = Logger(componentName: "Router")
private var locationManager: LocationManager {
return LocationManager.shared
}
public init() {
}
private func is1WUrl(_ url: URL) -> Bool {
if let scheme = url.scheme {
if scheme.lowercased() == "oneweather" {
return true
}
}
if let host = url.host {
if host == "1weatherapp.com" || host == "oneweatherapp.com" {
return true
}
}
return false
}
public func parseLocation(from url: URL) -> PartialLocation? {
guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems else {
return nil
}
var queryItemsLookup = [UrlQueryParam: String](minimumCapacity: queryItems.count)
for item: URLQueryItem in queryItems {
if let knownParam = UrlQueryParam(rawValue: item.name.lowercased()) {
queryItemsLookup[knownParam] = item.value
}
}
guard let cityId = queryItemsLookup[.cityId] else {
return nil
}
let result = GeoNamesPlace()
result.latitude = queryItemsLookup[.lat]
result.longitude = queryItemsLookup[.lon]
result.fipsCode = queryItemsLookup[.fipsCode]
result.optionalCityId = cityId
return result
}
public func open(url: URL) {
guard self.is1WUrl(url) else {
log.info("not 1Weather URL, ignore: \(url)")
return
}
log.info("open URL: \(url)")
var pathComponents = url.pathComponents
while let currentComponent = pathComponents.popLast() {
if let parsedComponent = UrlPathComponent(rawValue: currentComponent.lowercased()) {
log.debug("Parsed path: \(parsedComponent.rawValue)")
if let location = parseLocation(from: url) {
log.debug("Location found: \(location)")
locationManager.addIfNeeded(partialLocation: location, selectLocation: true)
}
else {
log.debug("No location.")
}
switch parsedComponent {
case .home:
openToday()
case .today:
openToday()
case .forecast:
openForecast(timePeriod: nil)
case .hourly:
openForecast(timePeriod: .hourly)
case .daily:
openForecast(timePeriod: .daily)
case .precipitation:
openPrecipitation()
case .radar:
openRadar()
case .sunMoon:
openSunMoon()
}
break
}
else {
log.debug("Skip unknown path component: \(currentComponent)")
}
}
}
public func openToday() {
DispatchQueue.main.async {
self.log.info("open Today")
self.appCoordinator.openToday()
}
}
public func openForecast(timePeriod: TimePeriod?) {
DispatchQueue.main.async {
self.log.info("open Forecast")
self.appCoordinator.openForecast(timePeriod: timePeriod)
}
}
public func openAlerts() {
DispatchQueue.main.async {
self.log.info("open Alerts")
self.appCoordinator.openNotifications()
}
}
public func openPrecipitation() {
DispatchQueue.main.async {
self.log.info("open Precipitation")
//not implemented
self.openToday()
}
}
public func openRadar() {
DispatchQueue.main.async {
self.log.info("open Radar")
self.appCoordinator.openRadar()
}
}
public func openSunMoon() {
DispatchQueue.main.async {
self.log.info("open SunMoon")
//not implemented
self.openToday()
}
}
public func openVideo() {
DispatchQueue.main.async {
self.log.info("open Video")
//not implemented
self.openToday()
}
}
public func openWeatherDetail() {
DispatchQueue.main.async {
self.log.info("open WeatherDetail")
//not implemented
self.openToday()
}
}
}
......@@ -12,6 +12,7 @@ class ForecastCoordinator: Coordinator {
private let forecastViewModel = ForecastViewModel(locationManager: LocationManager.shared)
private let navigationController = UINavigationController(nibName: nil, bundle: nil)
private var tabBarController:UITabBarController?
private var forecastViewController: ForecastViewController?
//Public
var childCoordinators = [Coordinator]()
......@@ -25,6 +26,11 @@ class ForecastCoordinator: Coordinator {
let forecastViewController = ForecastViewController(viewModel: forecastViewModel)
navigationController.viewControllers = [forecastViewController]
tabBarController?.add(viewController: navigationController)
self.forecastViewController = forecastViewController
}
public func open(timePeriod: TimePeriod) {
self.forecastViewController?.switchTo(timePeriod: timePeriod)
}
func viewControllerDidEnd(controller: UIViewController) {
......
......@@ -39,6 +39,10 @@
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
......
......@@ -103,6 +103,14 @@ internal class DeviceLocationMonitor: NSObject {
// this should never be called.
return ""
}
public var fipsCode: String? {
nil
}
public var optionalCityId: String? {
return nil
}
}
public typealias CurrentLocationCompletion = (LocationRequestResult) -> ()
......
......@@ -21,7 +21,9 @@ public class LocationManager {
private let weatherUpdateSource: WeatherSource
private let healthSource: HealthSource
private let fipsSource: FIPSSource
public let nwsAlertsManager: NWSAlertsManager
private let pushNotificationsManager: PushNotificationsManager
private let storage: Storage
private var defaultLocation = Location(deviceLocation: false,
coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino
......@@ -53,7 +55,7 @@ public class LocationManager {
self?._locations = locations
self?._selectedLocationIndex = selectedIndex
self?.handleLocationsChange(locationsChanged: true, selectedLocationChanged: true)
self?.updateAllWeatherIfNeeded()
self?.updateEverythingIfNeeded()
}
}
......@@ -154,15 +156,19 @@ public class LocationManager {
weatherUpdateSource: WdtWeatherSource(),
healthSource: BlendHealthSource(),
nwsAlertsManager: NWSAlertsManager(),
fipsSource: BlendFIPSSource(),
pushNotificationsManager: PushNotificationsManager.shared,
storage: DelayedSaveStorage(storage: CoreDataStorage(), delay: 2))
public let maxLocationsCount = 12
public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, nwsAlertsManager: NWSAlertsManager, storage: Storage) {
public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, nwsAlertsManager: NWSAlertsManager, fipsSource: FIPSSource, pushNotificationsManager: PushNotificationsManager, storage: Storage) {
self.weatherUpdateSource = weatherUpdateSource
self.healthSource = healthSource
self.deviceLocationMonitor = DeviceLocationMonitor()
self.nwsAlertsManager = nwsAlertsManager
self.fipsSource = fipsSource
self.pushNotificationsManager = pushNotificationsManager
self.storage = storage
self.deviceLocationMonitor.delegate = self
......@@ -185,7 +191,7 @@ public class LocationManager {
}
}
public func updateAllWeatherIfNeeded() {
public func updateEverythingIfNeeded() {
let locations = self.locations
guard locations.count > 0 else {
log.info("Update all: update default location if needed.")
......@@ -204,6 +210,25 @@ public class LocationManager {
}
updateHealth(for: location)
updateNotifications(for: location)
getFipsIfNeeded(for: location)
}
pushNotificationsManager.updateNwsSubscriptions(for: locations)
}
public func getFipsIfNeeded(for location: Location) {
if location.fipsCode == nil {
fipsSource.getFipsCode(for: location) { [weak self] (fipsCode) in
if let fipsCode = fipsCode {
self?.makeChanges(to: location, in: "getFIPS", changes: { (location) -> Location in
var updatedLocation = location
updatedLocation.fipsCode = fipsCode
return updatedLocation
}, completion: { [weak self] in
guard let self = self else { return }
self.pushNotificationsManager.updateNwsSubscriptions(for: self.locations)
})
}
}
}
}
......@@ -321,8 +346,11 @@ public class LocationManager {
}
}
private func makeChanges(to location: Location, in operation: String, changes: @escaping (Location) -> Location) {
private func makeChanges(to location: Location, in operation: String, changes: @escaping (Location) -> Location, completion: (() -> ())? = nil) {
DispatchQueue.main.async {
defer {
completion?()
}
if let indexToUpdate = self.locations.firstIndex(where: { $0 == location }) {
self.locations[indexToUpdate] = changes(self.locations[indexToUpdate])
}
......@@ -365,7 +393,7 @@ public class LocationManager {
selectedLocationIndex = locations.count - 1
}
}
updateAllWeatherIfNeeded()
updateEverythingIfNeeded()
}
public func addIfNeeded(partialLocation: PartialLocation, selectLocation: Bool) {
......@@ -421,6 +449,20 @@ public class LocationManager {
}
private func makeLocation(from partialLocation: PartialLocation, completion: @escaping (Location?) -> ()) {
if let fipsCode = partialLocation.fipsCode {
if let existingLocation = locations.first(where: { $0.fipsCode == fipsCode }) {
log.info("Geo lookup: found location using FIPS code: \(existingLocation)")
completion(existingLocation)
return
}
}
if let cityId = partialLocation.optionalCityId {
if let existingLocation = locations.first(where: { $0.cityId == cityId }) {
log.info("Geo lookup: found location using city ID: \(existingLocation)")
completion(existingLocation)
return
}
}
guard let latStr = partialLocation.lat, let lonStr = partialLocation.lon, let lat = CLLocationDegrees(latStr), let lon = CLLocationDegrees(lonStr) else {
log.error("Geo lookup: no coordinates present: \(partialLocation)")
var location: Location? = nil
......@@ -431,6 +473,7 @@ public class LocationManager {
completion(location)
return
}
let location = CLLocation(latitude: lat, longitude: lon)
let geocodeCompletion: CLGeocodeCompletionHandler = { [weak self] (placemarks, error) in
......
......@@ -19,6 +19,8 @@ final class GeoNamesPlace: NSObject {
var countryCode : String?
var fcodeName : String? // airport if airport
var toponymName : String? // airport name if fcodeName is airport
var fipsCode: String?
var optionalCityId: String?
func detailName() -> String {
var sb = String()
......
......@@ -225,4 +225,8 @@ extension Location: PartialLocation {
return sb
}
}
public var optionalCityId: String? {
cityId
}
}
......@@ -15,6 +15,8 @@ public protocol PartialLocation {
var countryName: String? { get }
var region: String? { get }
var cityName: String? { get }
var fipsCode: String? { get }
var optionalCityId: String? { get }
var nameForDisplay: String { get }
}
//
// BlendFIPSSource.swift
// 1Weather
//
// Created by Demid Merzlyakov on 28.04.2021.
//
import Foundation
class BlendFIPSSource: FIPSSource {
private let log = Logger(componentName: "BlendFIPSSource")
private let baseUrlProduction = "https://nwsalert.onelouder.com"
private let baseUrlStaging = "https://sta-nwsalert.onelouder.com"
private static let blendAPIKeyHeaderName = "blend-api-key"
private static let blendAPIKey = "0imfnc8mVLWwsAawjYr4Rx-Af50DDqtlx"
#if DEBUG
public var useStaging = true
#else
public var useStaging = false
#endif
private var baseUrl: String {
useStaging ? baseUrlStaging : baseUrlProduction
}
/// This queue is needed to synchronize access to locationsBeingUpdated. Also, to make logging more clear.
private let internalQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = "BlendHealthSource Queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
private var locationsBeingUpdated = Set<Location>()
public func getFipsCode(for location: Location, completion: @escaping FIPSSourceeCompletion) {
internalQueue.addOperation { [weak self] in
let extendedCompletion: FIPSSourceeCompletion = { [weak self] (fipsCode) in
self?.internalQueue.addOperation {
completion(fipsCode)
self?.locationsBeingUpdated.remove(location)
}
}
self?.getFipsCodeInternal(for: location, completion: extendedCompletion)
}
}
private func getFipsCodeInternal(for location: Location, completion: @escaping FIPSSourceeCompletion) {
guard !locationsBeingUpdated.contains(location) else {
completion(nil)
return
}
locationsBeingUpdated.insert(location)
guard let url = URL(string: self.baseUrl + "/1weather/api/v1/location") else {
assertionFailure("Should never happen. The URL should be correct.")
return
}
var queryParameters = [String: String]()
if let coordinates = location.coordinates {
queryParameters["lat"] = String(format: "%.5f", coordinates.latitude)
queryParameters["lon"] = String(format: "%.5f", coordinates.longitude)
}
queryParameters["zip"] = location.zip
queryParameters["city"] = location.cityName
queryParameters["state"] = location.region
queryParameters["country"] = location.countryCode
guard !queryParameters.isEmpty else {
completion(nil)
log.error("Not enough information about location.")
return
}
log.debug("Network request (\(location)): \(url)")
var request = URLRequest(url: url)
let encoder = JSONEncoder()
guard let requestBody = try? encoder.encode(queryParameters) else {
completion(nil)
log.error("Couldn't encode request body: \(queryParameters)")
return
}
request.httpBody = requestBody
var headers = request.allHTTPHeaderFields ?? [String: String]()
headers[BlendFIPSSource.blendAPIKeyHeaderName] = BlendFIPSSource.blendAPIKey
headers["Content-Type"] = "application/json"
request.allHTTPHeaderFields = headers
request.httpMethod = "POST"
let urlSession = URLSession.shared
let dataTask = urlSession.dataTask(with: request) { (data, reponse, error) in
// TODO: check response HTTP code
guard let data = data else {
self.log.debug("Network response (\(location)): error \(String(describing: error))")
completion(nil)
return
}
let responseBodyString = String(data: data, encoding: .utf8) ?? "<couldn't show as string"
self.log.debug("Network response (\(location)): \(responseBodyString)")
completion(FIPSResponse(data: data)?.fipsCode)
}
dataTask.resume()
}
}
//
// FIPSSource.swift
// 1Weather
//
// Created by Demid Merzlyakov on 27.04.2021.
//
import Foundation
public typealias FIPSSourceeCompletion = (String?) -> ()
public protocol FIPSSource {
func getFipsCode(for location: Location, completion: @escaping FIPSSourceeCompletion)
}
//
// FIPSResponse.swift
// 1Weather
//
// Created by Demid Merzlyakov on 28.04.2021.
//
import Foundation
struct FIPSResponse: Codable {
let fipsCode, s2CellID: String
enum CodingKeys: String, CodingKey {
case fipsCode = "fips_code"
case s2CellID = "s2_cell_id"
}
}
// MARK: Convenience initializers
extension FIPSResponse {
init?(data: Data) {
guard let me = try? JSONDecoder().decode(FIPSResponse.self, from: data) else { return nil }
self = me
}
init?(_ json: String, using encoding: String.Encoding = .utf8) {
guard let data = json.data(using: encoding) else { return nil }
self.init(data: data)
}
init?(fromURL url: String) {
guard let url = URL(string: url) else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil }
self.init(data: data)
}
var jsonData: Data? {
return try? JSONEncoder().encode(self)
}
var json: String? {
guard let data = self.jsonData else { return nil }
return String(data: data, encoding: .utf8)
}
}
//
// PushNotificationsManager.swift
// OneWeather
//
// Created by Demid Merzlyakov on 18.01.2021.
// Copyright © 2021 OneLouder, Inc. All rights reserved.
//
import Foundation
import MoEngage
public class PushNotificationsManager: NSObject {
// MARK: - Private
private let log = Logger(componentName: "PushNotificationsManager")
// MARK: - Public
public override init() {
super.init()
UNUserNotificationCenter.current().delegate = self
}
public static let shared = PushNotificationsManager()
// TODO: forced re-register on timeout
public func registerForRemoteNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { [weak self]
(granted, error) in
guard let self = self else { return }
if let error = error {
self.log.error("Error: \(error)")
}
else {
self.log.info("Granted: \(granted)")
}
MoEngage.sharedInstance().registerForRemoteNotification(withCategories: nil, withUserNotificationCenterDelegate: self)
}
}
private var lastSetFIPSList = ""
public func updateNwsSubscriptions(for locations: [Location]) {
let fipsCodes = locations.compactMap { $0.fipsCode }
let newFipsList = fipsCodes.joined(separator: ",")
if newFipsList != lastSetFIPSList {
log.info("Set FIPS_LIST to '\(newFipsList)'")
lastSetFIPSList = newFipsList
}
MoEngage.sharedInstance().setUserAttribute(newFipsList, forKey: "FIPS_LIST")
}
public func set(pushToken: Data) {
let tokenString = pushToken.map { String(format: "%02.2hhx", $0) }.joined()
log.info("Got new APNS token: \(tokenString)")
MoEngage.sharedInstance().setPushToken(pushToken)
}
}
extension PushNotificationsManager: UNUserNotificationCenterDelegate {
private enum MoEngageScreenName: String {
case mainScreen = "com.handmark.expressweather.ui.activities.mainactivity"
case detailsScreen = "com.handmark.expressweather.ui.activities.weatherdetailsactivity"
case videosScreen = "com.handmark.expressweather.ui.activities.videodetailsactivity"
case alertsScreen = "com.handmark.expressweather.ui.activities.alertactivity"
}
private func makeMoEngageDeeplinkUrl(from response: UNNotificationResponse) -> URL? {
guard let appExtra = response.notification.request.content.userInfo["app_extra"] as? [String: Any] else {
return nil
}
if let urlString = appExtra["moe_deeplink"] as? String, let url = URL(string: urlString) {
return url
}
return nil
}
private func switchLocationIfNeeded(parsing screenData: [String: Any]?, using router: DeeplinksRouter) {
guard let screenData = screenData else {
return
}
let cityId = screenData["location"] as? String
let lat = screenData["lat"] as? String
let lon = screenData["lon"] as? String
let fipsCode = screenData["fipsCode"] as? String
guard (lat != nil && lon != nil) || cityId != nil || fipsCode != nil else {
log.debug("MoEngage push: no location data found")
return
}
let newLoc = GeoNamesPlace()
newLoc.latitude = lat
newLoc.longitude = lon
newLoc.optionalCityId = cityId
newLoc.fipsCode = fipsCode
LocationManager.shared.addIfNeeded(partialLocation: newLoc, selectLocation: true)
log.info("MoEngage push: location found: \(newLoc)")
}
private func handleMoEngageDeeplinks(for response: UNNotificationResponse) {
let userInfo = response.notification.request.content.userInfo
guard userInfo["moengage"] as? [String: Any] != nil else {
log.debug("No MoEngage data found.")
return
}
log.info("MoEngage push received: \(userInfo)")
let router = DeeplinksRouter()
if let moEngageUrl = makeMoEngageDeeplinkUrl(from: response) {
router.open(url: moEngageUrl)
}
else {
guard let appExtra = userInfo["app_extra"] as? [String: Any] else {
log.error("MoEngage push: no app_extra found.")
return
}
guard let screenNameStr = (appExtra["screenName"] as? String)?.trim().lowercased(),
let screenName = MoEngageScreenName(rawValue: screenNameStr) else {
log.error("MoEngage push: screenName not found or is not correct.")
return
}
let screenData = appExtra["screenData"] as? [String: Any]
switchLocationIfNeeded(parsing: screenData, using: router)
switch screenName {
case .detailsScreen:
router.openWeatherDetail()
case .videosScreen:
router.openVideo()
case .mainScreen:
guard let launchScreenId = screenData?["LaunchScreenID"] as? String else {
log.error("MoEngage push: LaunchScreenID not found for a MainActivity screen.")
return
}
switch launchScreenId {
case "0":
router.openToday()
case "1":
router.openForecast(timePeriod: nil)
case "2":
router.openPrecipitation()
case "3":
router.openRadar()
case "4":
router.openSunMoon()
default:
log.error("MoEngage push: Unknown launch screen id: \(launchScreenId)")
}
case .alertsScreen:
router.openAlerts()
}
}
}
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
MoEngage.sharedInstance().userNotificationCenter(center, didReceive: response) // not sure we should call it for PushPin notifications, too
handleMoEngageDeeplinks(for: response)
completionHandler()
}
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
if UIApplication.shared.applicationState == .active {
analytics(log: .ANALYTICS_PUSH_RECEIVED)
} else {
analytics(log: .ANALYTICS_PUSH_SELECTED, params: [.ANALYTICS_KEY_PUSH_NOTIFICATION_SOURCE: "background"])
}
log.debug("Got a push notification: \(notification.request.content.userInfo)");
completionHandler([.sound,.alert])
}
}
......@@ -7,16 +7,17 @@
import UIKit
private enum AppTab:Int, CaseIterable {
case today = 0
case forecast = 1
case radar = 2
case menu = 3
}
class AppTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
}
public enum AppTab:Int, CaseIterable {
case today = 0
case forecast = 1
case radar = 2
case menu = 3
}
public func setupTabBar() {
......
......@@ -67,13 +67,19 @@ class ForecastViewController: UIViewController {
}
}
public func switchTo(timePeriod: TimePeriod) {
if self.timePeriodControl.selectedSegmentIndex != timePeriod.rawValue {
self.timePeriodControl.selectedSegmentIndex = timePeriod.rawValue
}
forecastCellFactory.setTimePeriod(timePeriod: timePeriod)
self.tableView.reloadData()
}
@objc private func handleSegmentDidChange() {
guard let timePeriod = TimePeriod(rawValue: self.timePeriodControl.selectedSegmentIndex) else {
return
}
forecastCellFactory.setTimePeriod(timePeriod: timePeriod)
self.tableView.reloadData()
self.switchTo(timePeriod: timePeriod)
}
@objc private func handleCityButton() {
......
......@@ -43,7 +43,7 @@ class ForecastViewModel: ViewModelProtocol {
}
public func updateWeather() {
locationManager.updateAllWeatherIfNeeded()
locationManager.updateEverythingIfNeeded()
}
public func selectDailyWeatherAt(index:Int) {
......
......@@ -31,7 +31,7 @@ class TodayViewModel: ViewModelProtocol {
}
public func updateWeather() {
locationManager.updateAllWeatherIfNeeded()
locationManager.updateEverythingIfNeeded()
}
}
......
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