Files
sodalive-ios/SodaLive/Sources/App/AppDeepLinkHandler.swift

235 lines
7.2 KiB
Swift

import Foundation
enum AppDeepLinkAction {
case live(roomId: Int)
case content(contentId: Int)
case series(seriesId: Int)
case community(creatorId: Int, postId: Int?)
case message
case audition
}
enum AppDeepLinkSource {
case external
case notificationList
}
enum AppDeepLinkHandler {
static func handle(url: URL, source: AppDeepLinkSource = .external) -> Bool {
guard isSupportedScheme(url) else {
return false
}
guard let action = parseAction(url: url) else {
return false
}
DispatchQueue.main.async {
if case .splash = AppState.shared.rootStep {
AppState.shared.pendingDeepLinkAction = action
return
}
apply(action: action, source: source)
}
return true
}
static func handle(urlString: String, source: AppDeepLinkSource = .external) -> Bool {
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
guard let url = URL(string: trimmed) else {
return false
}
return handle(url: url, source: source)
}
static func apply(action: AppDeepLinkAction) {
apply(action: action, source: .external)
}
private static func apply(action: AppDeepLinkAction, source: AppDeepLinkSource) {
switch action {
case .live(let roomId):
guard roomId > 0 else { return }
AppState.shared.isPushRoomFromDeepLink = source == .external
AppState.shared.pushRoomId = 0
AppState.shared.pushRoomId = roomId
case .content(let contentId):
guard contentId > 0 else { return }
AppState.shared.setAppStep(step: .contentDetail(contentId: contentId))
case .series(let seriesId):
guard seriesId > 0 else { return }
AppState.shared.setAppStep(step: .seriesDetail(seriesId: seriesId))
case .community(let creatorId, let postId):
guard creatorId > 0 else { return }
if let postId = postId, postId > 0 {
AppState.shared.setPendingCommunityCommentDeepLink(creatorId: creatorId, postId: postId)
} else {
AppState.shared.clearPendingCommunityCommentDeepLink()
}
AppState.shared.setAppStep(step: .creatorCommunityAll(creatorId: creatorId))
case .message:
AppState.shared.setAppStep(step: .message)
case .audition:
AppState.shared.setAppStep(step: .audition)
}
}
private static func isSupportedScheme(_ url: URL) -> Bool {
guard let scheme = url.scheme?.lowercased() else {
return false
}
let appScheme = APPSCHEME.lowercased()
return scheme == appScheme || scheme == "voiceon" || scheme == "voiceon-test"
}
private static func parseAction(url: URL) -> AppDeepLinkAction? {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if let routeAction = parsePathStyle(url: url, components: components) {
return routeAction
}
return parseQueryStyle(components: components)
}
private static func parsePathStyle(url: URL, components: URLComponents?) -> AppDeepLinkAction? {
let pathComponents = url.pathComponents.filter { $0 != "/" && !$0.isEmpty }
let host = components?.host?.lowercased() ?? ""
if !host.isEmpty {
let identifier = pathComponents.first
return makeAction(route: host, identifier: identifier, components: components)
}
guard !pathComponents.isEmpty else {
return nil
}
let route = pathComponents[0].lowercased()
let identifier = pathComponents.count > 1 ? pathComponents[1] : nil
return makeAction(route: route, identifier: identifier, components: components)
}
private static func parseQueryStyle(components: URLComponents?) -> AppDeepLinkAction? {
guard let queryItems = components?.queryItems else {
return nil
}
var queryMap: [String: String] = [:]
for item in queryItems {
queryMap[item.name.lowercased()] = item.value
}
if let roomId = queryMap["room_id"], let value = Int(roomId), value > 0 {
return .live(roomId: value)
}
if let contentId = queryMap["content_id"], let value = Int(contentId), value > 0 {
return .content(contentId: value)
}
if let seriesId = queryMap["series_id"], let value = Int(seriesId), value > 0 {
return .series(seriesId: value)
}
if let communityId = queryMap["community_id"], let value = Int(communityId), value > 0 {
return .community(creatorId: value, postId: communityPostId(queryMap: queryMap))
}
if queryMap["message_id"] != nil {
return .message
}
if queryMap["audition_id"] != nil {
return .audition
}
return nil
}
private static func makeAction(route: String, identifier: String?, components: URLComponents?) -> AppDeepLinkAction? {
switch route {
case "live":
guard let identifier = identifier, let roomId = Int(identifier), roomId > 0 else {
return nil
}
return .live(roomId: roomId)
case "content":
guard let identifier = identifier, let contentId = Int(identifier), contentId > 0 else {
return nil
}
return .content(contentId: contentId)
case "series":
guard let identifier = identifier, let seriesId = Int(identifier), seriesId > 0 else {
return nil
}
return .series(seriesId: seriesId)
case "community":
let postId = communityPostId(queryItems: components?.queryItems)
if let identifier = identifier, let creatorId = Int(identifier), creatorId > 0 {
return .community(creatorId: creatorId, postId: postId)
}
guard let creatorId = fallbackCommunityCreatorId() else {
return nil
}
return .community(creatorId: creatorId, postId: postId)
case "message":
return .message
case "audition":
return .audition
default:
return nil
}
}
private static func communityPostId(queryItems: [URLQueryItem]?) -> Int? {
guard let queryItems = queryItems else {
return nil
}
var queryMap: [String: String] = [:]
for item in queryItems {
queryMap[item.name.lowercased()] = item.value
}
return communityPostId(queryMap: queryMap)
}
private static func communityPostId(queryMap: [String: String]) -> Int? {
if let postId = queryMap["postid"], let value = Int(postId), value > 0 {
return value
}
if let postId = queryMap["post_id"], let value = Int(postId), value > 0 {
return value
}
return nil
}
private static func fallbackCommunityCreatorId() -> Int? {
let userId = UserDefaults.int(forKey: .userId)
return userId > 0 ? userId : nil
}
}