From cd2f73f294e95f282fea77f58e1dc3a3d71363bb Mon Sep 17 00:00:00 2001 From: hilmyveradin Date: Fri, 5 Apr 2024 16:56:36 +0700 Subject: [PATCH] Add custom region via URL --- OBAKit/Orchestration/Application.swift | 60 ++++++++++++-- OBAKit/Strings/en.lproj/Localizable.strings | 2 + OBAKit/Strings/es.lproj/Localizable.strings | Bin 78222 -> 78670 bytes OBAKit/Strings/it.lproj/Localizable.strings | Bin 78636 -> 79070 bytes OBAKit/Strings/pl.lproj/Localizable.strings | Bin 77762 -> 78212 bytes .../Strings/zh-Hans.lproj/Localizable.strings | Bin 68946 -> 69270 bytes OBAKitCore/DeepLinks/URLSchemeRouter.swift | 73 +++++++++++++++--- OBAKitCore/Models/Region.swift | 7 +- TodayView/TodayViewController.swift | 2 +- 9 files changed, 125 insertions(+), 19 deletions(-) diff --git a/OBAKit/Orchestration/Application.swift b/OBAKit/Orchestration/Application.swift index 8ab7c2113..2af72e3b4 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: Strings.error, + message: OBALoc("region_url.error_messsage", value: "The provided region URL is invalid or does not point to a functional OBA server.", comment: "Error message of Custom Region URL if it's invalid or does not point to a functional OBA server"), + preferredStyle: .alert + ) + alertController.addAction(UIAlertAction(title: Strings.ok, style: .default)) + topViewController.present(alertController, animated: true) + presentAddRegionAlertOnActive = false + } } @objc public func applicationWillResignActive(_ application: UIApplication) { @@ -429,15 +442,50 @@ 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 { + guard let regionData else { + presentAddRegionAlertOnActive = true + return + } + + 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) + } catch { + presentAddRegionAlertOnActive = true + return + } + } + return true + } } override public func apiServicesRefreshed() { diff --git a/OBAKit/Strings/en.lproj/Localizable.strings b/OBAKit/Strings/en.lproj/Localizable.strings index 2c3964564..097a00282 100644 --- a/OBAKit/Strings/en.lproj/Localizable.strings +++ b/OBAKit/Strings/en.lproj/Localizable.strings @@ -770,3 +770,5 @@ /* Format string with placeholders for distance from stop, walking time to stop, and predicted arrival time. e.g. 1.2 miles, 17m: arriving at 09:41 A.M. */ "walk_time_view.distance_time_fmt" = "%1$@, %2$@: arriving at %3$@"; +/* Error message of Custom Region URL if it's invalid or does not point to a functional OBA server */ +"region_url.error_messsage" = "The provided region URL is invalid or does not point to a functional OBA server."; diff --git a/OBAKit/Strings/es.lproj/Localizable.strings b/OBAKit/Strings/es.lproj/Localizable.strings index 83af42a757ffe1a9c0285c308af28fa517eb514d..4f81cd237e487f33865cb6b3cb228fa3eeb3b209 100644 GIT binary patch delta 339 zcmYL^&k6xi6vjVMO0kn9xhW}XWNjhJPARh1vs(ae~g@&q<_VQpt;<2k&;q`ZI^ z@ZF21Teow*@BI0l(=+vWrH=Q+shw`20fB*va;2n;mAc%@O@S%K7*gM0W&Qwl_$HDF zVZ))bVD6p|*+ikcgG)gopcF6`ED>zoQ=3!SP?M%jV1zz;bbT!0aTtGPUyH>(GY6JY zJ=S}eVav={l}WxfG!y?=3+jT^dW*++%?)oz)rm@>)}&!zFXIr=4_7;y?d#gu8NR?w Y(VnJ_x%7dWA}0Rd6JKMYiViY-AH1PNPyhe` delta 11 ScmX^2jHT}}OT!k%K0^Q_Vg(`q diff --git a/OBAKit/Strings/it.lproj/Localizable.strings b/OBAKit/Strings/it.lproj/Localizable.strings index 1cc98f47b53f581ad5b70654abead3d85139aa5d..442cd1e96bacbf2e40a7188e97e699bd8ef2db54 100644 GIT binary patch delta 316 zcmZ4UjOE@-mWC~iI>zk!3|b5d46c*Ut4lNIGo(#^zFt;afgzM3h`|Rap2?5~CQBI9 z8H&NIJccrcM1~xoObU>nKfRuhQGD`;J7St?45bWt49N^7K&gBn3#3Sa!Jom2!4XIo zGo&&U0ks0j$qUy=vX=sRIShJ}^Y4jHcJdRd_W@b}vor;$DhCJ?f%=Mo>~yG!sX%@j z(Ci|h>6t)#K=OHDyA&8+0NDtKfn@TLWDCGCQTq0=2VDBs^H$p16q~J NpvR!ZV9mhA000E&LR|m= delta 11 Scmccjl4Z>^mWC~iI>rDba|HGP diff --git a/OBAKit/Strings/pl.lproj/Localizable.strings b/OBAKit/Strings/pl.lproj/Localizable.strings index 78af3a0e765c53fed4a023f4a5acd968a15c8b13..70ff92e2fd35727aea19e2ec6b6d72c24ba85185 100644 GIT binary patch delta 320 zcmX?fpQYt7OT!k%L;BP77}*$IC;wNK_ReQWV^CmlW+-JSW+-9EXUGNef*4X6(it*= zygVQ;6v+1h(wShDnLt(QK(!!#9zz*JB0~<4l>+4FPmX;lK6%4!G0ilfX?YCEK$Q?P zKxz~i{281W9D#H((DWjpP9QlsafKv%DUg@Lpf_3nuIS_tKOxQlpuH(TbMq$a%8BzU z0A&%bDV@wHC|p(m6iZ|%2ixn!m=9!C0?o|<+L{EU^MK;1K)M`gQ#Mdt6;M12ERqN0 pDFA%|a$PpieOX{1f@E14lfh;{+*S^98&D<0#d-`%4Au->3;<6LL>K@7 delta 11 ScmZp9%yQ^HOT!k%L;3(B00lPy diff --git a/OBAKit/Strings/zh-Hans.lproj/Localizable.strings b/OBAKit/Strings/zh-Hans.lproj/Localizable.strings index be9273dff48bcf3ba8a1843a35dbf55107e24d94..d340dfaf790d6d5785699bb1c53df746a479495d 100644 GIT binary patch delta 228 zcmcaKi)GqemWC~iLA>nx3|b5d46c)PRiwT18Pb4kXNFRSVuliie1=>gFNh(PA)O%; z$jbxrLV@#3xTWD5jYPG%b%I8K@Fs z21tzpgFk~4gCmeG2AW<3)CnXfJ5HBmF9q^)81yC!9u}Rfa#+a9A?ZT$Z2y)rtKj?L r5W63wdZdaaX{EC#3x>=O`rDA4Z4uW}1~HE}ePK{Wm=c3E0~Z4TO5r$F delta 11 ScmbO>m*vtdmWC~iLA(GRr35?x diff --git a/OBAKitCore/DeepLinks/URLSchemeRouter.swift b/OBAKitCore/DeepLinks/URLSchemeRouter.swift index 54c275d33..7d051676d 100644 --- a/OBAKitCore/DeepLinks/URLSchemeRouter.swift +++ b/OBAKitCore/DeepLinks/URLSchemeRouter.swift @@ -15,6 +15,33 @@ public struct StopURLData { public let regionID: Int } +/// `AddRegionURLData` is a data structure that encapsulates the information needed to add a new region +/// through a deep link. It contains the name of the region, the URL to the OneBusAway (OBA) server, and an optional +/// URL to the OpenTripPlanner (OTP) server. +/// +/// - Parameters: +/// - name: The name of the region to be added. This is a human-readable string that identifies the region. +/// - obaURL: The URL to the OneBusAway (OBA) server for the region. This URL is used to access transit data. +/// - otpURL: An optional URL to the OpenTripPlanner (OTP) server. If provided, it can be used for trip planning. +/// If nil, it indicates that the region does not support OTP or that the URL was not provided. +public struct AddRegionURLData { + public let name: String + public let obaURL: URL + public let otpURL: URL? +} + +/// `URLType` represents the types of URLs that the `URLSchemeRouter` can handle. +/// It distinguishes between viewing a stop and adding a region through deep linking. +/// +/// - viewStop: A URL type for viewing details of a specific stop. +/// Contains a `StopURLData` object with the stop ID and region ID. +/// - addRegion: A URL type for adding a new region. +/// Contains an optional `AddRegionURLData` object with the necessary information for adding the region. +/// If the data is nil, it indicates that the URL didn't contain valid or complete data for adding a region. +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 +52,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 +93,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/OBAKitCore/Models/Region.swift b/OBAKitCore/Models/Region.swift index a04e426d2..698d44e09 100644 --- a/OBAKitCore/Models/Region.swift +++ b/OBAKitCore/Models/Region.swift @@ -194,7 +194,8 @@ public class Region: NSObject, Identifiable, Codable { /// - Parameter coordinateRegion: The coordinate region that circumscribes this region. /// - Parameter contactEmail: The contact email address for this region. /// - Parameter regionIdentifier: The identifier for this region. If unassigned, it will be given a random value. - public required init(name: String, OBABaseURL: URL, coordinateRegion: MKCoordinateRegion, contactEmail: String, regionIdentifier: Int? = nil) { + /// - Parameter regionIdentifier: The identifier for this region. If unassigned, it will be given a random value. + public required init(name: String, OBABaseURL: URL, coordinateRegion: MKCoordinateRegion, contactEmail: String, regionIdentifier: Int? = nil, openTripPlannerURL: URL? = nil) { self.name = name self.regionIdentifier = regionIdentifier ?? 1000 + Int.random(in: 0...999) isActive = true @@ -207,12 +208,14 @@ public class Region: NSObject, Identifiable, Codable { regionBounds = [bound] self.contactEmail = contactEmail + self.openTripPlannerURL = openTripPlannerURL + // Uninitialized properties facebookURL = nil language = "en_US" open311Servers = [] openTripPlannerContactEmail = nil - openTripPlannerURL = nil + paymentAndroidAppID = nil paymentWarningBody = nil paymentWarningTitle = nil 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) }