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 } }