Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 719: Add Custom Region URL #723

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
hilmyveradin marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
hilmyveradin marked this conversation as resolved.
Show resolved Hide resolved
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 {
hilmyveradin marked this conversation as resolved.
Show resolved Hide resolved
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
Loading