Skip to content

Commit

Permalink
Add custom region via URL
Browse files Browse the repository at this point in the history
  • Loading branch information
hilmyveradin committed Apr 5, 2024
1 parent 47b6a80 commit cd2f73f
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 19 deletions.
60 changes: 54 additions & 6 deletions OBAKit/Orchestration/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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: "[email protected]", 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() {
Expand Down
2 changes: 2 additions & 0 deletions OBAKit/Strings/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Binary file modified OBAKit/Strings/es.lproj/Localizable.strings
Binary file not shown.
Binary file modified OBAKit/Strings/it.lproj/Localizable.strings
Binary file not shown.
Binary file modified OBAKit/Strings/pl.lproj/Localizable.strings
Binary file not shown.
Binary file modified OBAKit/Strings/zh-Hans.lproj/Localizable.strings
Binary file not shown.
73 changes: 63 additions & 10 deletions OBAKitCore/DeepLinks/URLSchemeRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand All @@ -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
Expand All @@ -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))
}
}
7 changes: 5 additions & 2 deletions OBAKitCore/Models/Region.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion TodayView/TodayViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit cd2f73f

Please sign in to comment.