diff --git a/Apps/OneBusAway/AppDelegate.m b/Apps/OneBusAway/AppDelegate.m index e008c14aa..f7779a032 100644 --- a/Apps/OneBusAway/AppDelegate.m +++ b/Apps/OneBusAway/AppDelegate.m @@ -85,8 +85,7 @@ - (void)applicationWillResignActive:(UIApplication *)application { #pragma mark - OBAApplicationDelegate - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - [CustomRegionDeepLink parseURL:url application:self.app]; - return YES; + return [self.app application:app open:url options:options]; } - (UIApplication*)uiApplication { diff --git a/OBAKit/DeepLinks/CustomRegionDeepLink.swift b/OBAKit/DeepLinks/CustomRegionDeepLink.swift deleted file mode 100644 index 5326ab5cd..000000000 --- a/OBAKit/DeepLinks/CustomRegionDeepLink.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// CustomRegionDeepLink.swift -// OBAKit -// -// Created by Hilmy Veradin on 27/03/24. -// - -import Foundation -import UIKit -import OBAKitCore -import SwiftUI -import MapKit - -@objc public class CustomRegionDeepLink: NSObject { - - @objc(parseURL:application:) public static func parseURL(_ url: URL, application: Application) { - // Check if the add-region URL is null then return the default URL - guard url.host == "add-region" else { - return - } - - // Proceed with parsing the URL - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true), - let queryItems = components.queryItems, - let name = queryItems.first(where: { $0.name == "name" })?.value, - let obaUrlString = queryItems.first(where: { $0.name == "oba-url" })?.value, - let obaUrl = URL(string: obaUrlString) else { - - // Show an error message when the custom URL is invalid - displayErrorMessage(application: application) - return - } - - guard let apiService = application.apiService else { - return - } - - Task { @MainActor in - do { - application.viewRouter.rootNavigateTo(page: .map) - - guard var regionCoordinate = try await apiService.getAgenciesWithCoverage().list.first?.region ?? nil else { return } - - // Manually set the span of latitude and longitude delta because the value of latitude and longitude region is very small - regionCoordinate.span.latitudeDelta = 2 - regionCoordinate.span.longitudeDelta = 2 - - // Attempt to extract otp-url if present - let otpUrlString = queryItems.first(where: { $0.name == "otp-url" })?.value - let otpUrl = otpUrlString != nil ? URL(string: otpUrlString!) : nil - - // Create region provider - let regionProvider = RegionPickerCoordinator(regionsService: application.regionsService) - - // Set current region based on given URL - let currentRegion = Region(name: name, OBABaseURL: obaUrl, coordinateRegion: regionCoordinate, contactEmail: "example@example.com", openTripPlannerURL: otpUrl) - - // Add and set current region - try await regionProvider.add(customRegion: currentRegion) - try await regionProvider.setCurrentRegion(to: currentRegion) - } - } - - } - - static func displayErrorMessage(application: Application) { - DispatchQueue.main.async { - // Obtain the current window and key window - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } - guard let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) else { return } - - let alertController = UIAlertController( - title: "Error", - message: "The provided region URL is invalid or does not point to a functional OBA server.", - preferredStyle: .alert - ) - alertController.addAction(UIAlertAction(title: "OK", style: .default)) - - // Get the root view controller of the key window and present the alert controller - if let rootViewController = keyWindow.rootViewController { - presentOnTopViewController(rootViewController: rootViewController, viewControllerToPresent: alertController) - } - } - } - - // Ensure presentOnTopViewController is updated to work with the current setup - static func presentOnTopViewController(rootViewController: UIViewController, viewControllerToPresent: UIViewController) { - var currentViewController = rootViewController - while let presentedViewController = currentViewController.presentedViewController { - currentViewController = presentedViewController - } - currentViewController.present(viewControllerToPresent, animated: true, completion: nil) - } - -} diff --git a/OBAKit/Orchestration/Application.swift b/OBAKit/Orchestration/Application.swift index 8ab7c2113..8eced9a85 100644 --- a/OBAKit/Orchestration/Application.swift +++ b/OBAKit/Orchestration/Application.swift @@ -278,6 +278,7 @@ public class Application: CoreApplication, PushServiceDelegate { } private var presentDonationUIOnActive = false + private var presentAddRegionAlertOnActive = false private var donationPromptID: String? public func pushService(_ pushService: PushService, receivedDonationPrompt id: String?) { @@ -363,6 +364,18 @@ public class Application: CoreApplication, PushServiceDelegate { presentDonationUIOnActive = false donationPromptID = nil } + + if presentAddRegionAlertOnActive, let topViewController { + // Show alert for nil addRegion data + let alertController = UIAlertController( + title: "Error", + message: "The provided region URL is invalid or does not point to a functional OBA server.", + preferredStyle: .alert + ) + alertController.addAction(UIAlertAction(title: "OK", style: .default)) + topViewController.present(alertController, animated: true) + presentAddRegionAlertOnActive = false + } } @objc public func applicationWillResignActive(_ application: UIApplication) { @@ -429,15 +442,48 @@ public class Application: CoreApplication, PushServiceDelegate { } let router = URLSchemeRouter(scheme: scheme) - guard - let stopData = router.decode(url: url), - let topViewController = topViewController - else { + + guard let urlType = router.decodeURLType(from: url) else { return false } - viewRouter.navigateTo(stopID: stopData.stopID, from: topViewController) - return true + switch urlType { + case .viewStop(let stopData): + guard let topViewController = self.topViewController else { return false } + viewRouter.navigateTo(stopID: stopData.stopID, from: topViewController) + return true + case .addRegion(let regionData): + viewRouter.rootNavigateTo(page: .map) + Task { @MainActor in + do { + if let regionData = regionData { + guard let regionCoordinate = try await self.apiService?.getAgenciesWithCoverage().list.first?.region else { return } + + // Adjustments for coordinate span + var adjustedRegionCoordinate = regionCoordinate + adjustedRegionCoordinate.span.latitudeDelta = 2 + adjustedRegionCoordinate.span.longitudeDelta = 2 + + // Create region provider + let regionProvider = RegionPickerCoordinator(regionsService: self.regionsService) + + // Construct Region from URL data + let currentRegion = Region(name: regionData.name, OBABaseURL: regionData.obaURL, coordinateRegion: adjustedRegionCoordinate, contactEmail: "example@example.com", openTripPlannerURL: regionData.otpURL) + + // Add and set current region + try await regionProvider.add(customRegion: currentRegion) + try await regionProvider.setCurrentRegion(to: currentRegion) + } else { + presentAddRegionAlertOnActive = true + return + } + } catch { + presentAddRegionAlertOnActive = true + return + } + } + return true + } } override public func apiServicesRefreshed() { diff --git a/OBAKitCore/DeepLinks/URLSchemeRouter.swift b/OBAKitCore/DeepLinks/URLSchemeRouter.swift index 54c275d33..fe567553e 100644 --- a/OBAKitCore/DeepLinks/URLSchemeRouter.swift +++ b/OBAKitCore/DeepLinks/URLSchemeRouter.swift @@ -15,6 +15,17 @@ public struct StopURLData { public let regionID: Int } +public struct AddRegionURLData { + public let name: String + public let obaURL: URL + public let otpURL: URL? +} + +// Enum to distinguish between URL types +public enum URLType { + case viewStop(StopURLData) + case addRegion(AddRegionURLData?) +} /// Provides support for deep linking into the app by way of a custom URL scheme. /// /// Custom URL scheme deep linking (e.g. `onebusaway://view-stop?region_id=1&stop_id=12345`) @@ -25,21 +36,37 @@ public class URLSchemeRouter: NSObject { /// The app bundle's URL scheme for extensions. private let scheme: String + private let viewStopHost = "view-stop" + private let addRegionHost = "add-region" + /// Creates a new URL Scheme Router. /// - Parameter scheme: The app bundle's `extensionURLScheme` value. public init(scheme: String) { self.scheme = scheme } - // MARK: - Stop URLs + /// Decode URL Types based on host + public func decodeURLType(from url: URL) -> URLType? { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return nil + } - private let viewStopHost = "view-stop" + switch components.host { + case viewStopHost: + return decodeViewStop(from: components) + case addRegionHost: + return decodeAddRegion(from: components) + default: + return nil + } + } + // MARK: - Stop URLs /// Encodes the ID for a Stop along with its Region ID into an URL with the scheme `extensionURLScheme`. /// - Parameters: /// - stopID: The ID for the Stop. /// - regionID: The ID for the Region that hosts the Stop. - public func encode(stopID: StopID, regionID: Int) -> URL { + public func encodeViewStop(stopID: StopID, regionID: Int) -> URL { var components = URLComponents() components.scheme = scheme components.host = viewStopHost @@ -50,17 +77,27 @@ public class URLSchemeRouter: NSObject { /// Decodes a `StopURLData` struct from `url`, which can be used to display a `StopViewController`. /// - Parameter url: An URL created from calling `URLSchemeRouter.encode()` - public func decode(url: URL) -> StopURLData? { + private func decodeViewStop(from components: URLComponents) -> URLType? { guard - let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - components.host == viewStopHost, let stopID = components.queryItem(named: "stopID")?.value, let regionIDString = components.queryItem(named: "regionID")?.value, - let regionID = Int(regionIDString) - else { - return nil + let regionID = Int(regionIDString) else { + return nil } + return .viewStop(StopURLData(stopID: stopID, regionID: regionID)) + } - return StopURLData(stopID: stopID, regionID: regionID) + // MARK: - Add Region URLs + /// Encodes the OBA URL for adding custom region along with its Name into an URL with the scheme `extensionURLScheme`. It also has optional OTP URL + private func decodeAddRegion(from components: URLComponents) -> URLType? { + guard + let name = components.queryItem(named: "name")?.value, + let obaUrlString = components.queryItem(named: "oba-url")?.value, + let obaURL = URL(string: obaUrlString) else { + return .addRegion(nil) + } + let otpUrlString = components.queryItem(named: "otp-url")?.value + let otpURL = otpUrlString != nil ? URL(string: otpUrlString!) : nil + return .addRegion(AddRegionURLData(name: name, obaURL: obaURL, otpURL: otpURL)) } } diff --git a/TodayView/TodayViewController.swift b/TodayView/TodayViewController.swift index 354dfc00a..74efac4cc 100644 --- a/TodayView/TodayViewController.swift +++ b/TodayView/TodayViewController.swift @@ -183,7 +183,7 @@ class TodayViewController: UIViewController, BookmarkDataDelegate, NCWidgetProvi } let router = URLSchemeRouter(scheme: Bundle.main.extensionURLScheme!) - let url = router.encode(stopID: bookmark.stopID, regionID: bookmark.regionIdentifier) + let url = router.encodeViewStop(stopID: bookmark.stopID, regionID: bookmark.regionIdentifier) extensionContext?.open(url, completionHandler: nil) }