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

366 lines
12 KiB
Swift

import Foundation
import AppsFlyerLib
enum AppDeepLinkAction {
case live(roomId: Int)
case userCreatorChat(roomId: Int)
case content(contentId: Int)
case series(seriesId: Int)
case giftDetail(applicationNo: String)
case community(postId: Int)
case eventDetail(eventId: Int)
case message
case audition
}
enum AppDeepLinkSource {
case external
case notificationList
}
enum AppDeepLinkHandler {
private static var navigatingEventId: Int?
static func handle(url: URL, source: AppDeepLinkSource = .external) -> Bool {
if isOneLink(url) {
AppsFlyerLib.shared().performOnAppAttribution(with: url)
return true
}
guard isSupportedScheme(url) else {
return false
}
guard let action = parseAction(url: url) else {
return false
}
dispatch(action: action, source: source)
return true
}
static func handleEventDeepLink(result: DeepLinkResult) -> Bool {
guard result.status == .found,
let clickEvent = result.deepLink?.clickEvent,
clickEvent["deep_link_value"] as? String == "event-detail",
let id = eventId(clickEvent["deep_link_sub5"] as? String) else {
return false
}
dispatch(action: .eventDetail(eventId: id))
return true
}
static func handlePushDeepLink(userInfo: [AnyHashable: Any]) -> Bool {
if userInfo["notifly_message_type"] as? String == "push-notification" {
if let url = userInfo["url"] as? URL, !url.absoluteString.isEmpty {
return true
}
if let urlString = userInfo["url"] as? String, !urlString.isEmpty {
return true
}
}
let deepLinkString = (userInfo["deep_link"] as? String ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !deepLinkString.isEmpty else { return false }
if let url = URL(string: deepLinkString), isOneLink(url) {
AppsFlyerLib.shared().handlePushNotification(userInfo)
} else {
_ = handle(urlString: deepLinkString)
}
return true
}
private static func dispatch(action: AppDeepLinkAction, source: AppDeepLinkSource = .external) {
DispatchQueue.main.async {
if case .splash = AppState.shared.rootStep {
if case .eventDetail(let eventId) = action,
case .eventDetail(let pendingId)? = AppState.shared.pendingDeepLinkAction,
pendingId == eventId {
return
}
AppState.shared.pendingDeepLinkAction = action
return
}
apply(action: action, source: source)
}
}
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 .userCreatorChat(let roomId):
guard roomId > 0 else { return }
AppState.shared.setAppStep(step: .userCreatorChatRoom(roomId: 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 .giftDetail(let applicationNo):
AppState.shared.setAppStep(step: .giftDetail(applicationNo: applicationNo))
case .community(let postId):
guard postId > 0 else { return }
AppState.shared.setAppStep(step: .creatorChannelCommunityPostDetail(postId: postId))
case .eventDetail(let eventId):
guard eventId > 0, navigatingEventId != eventId else { return }
navigatingEventId = eventId
AppState.shared.requestExternalNavigation(
navigationAction: {
guard navigatingEventId == eventId else { return }
AppState.shared.setAppStep(
step: .eventDetail(eventId: eventId),
ifCurrentStep: { currentStep in
guard navigatingEventId == eventId else { return false }
if case .eventDetail(let currentId) = currentStep {
return currentId != eventId
}
return true
},
completion: {
if navigatingEventId == eventId {
navigatingEventId = nil
}
}
)
},
cancelAction: {
if navigatingEventId == eventId {
navigatingEventId = nil
}
}
)
case .message:
AppState.shared.setAppStep(step: .message)
case .audition:
AppState.shared.setAppStep(step: .audition)
}
}
private static func isOneLink(_ url: URL) -> Bool {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return false
}
return components.scheme?.lowercased() == "https"
&& components.host?.lowercased() == "voiceon.onelink.me"
&& components.user == nil && components.password == nil
&& (components.port == nil || components.port == 443)
}
private static func eventId(_ identifier: String?) -> Int? {
guard let identifier = identifier, !identifier.isEmpty,
identifier.utf8.allSatisfy({ $0 >= 48 && $0 <= 57 }),
let value = Int64(identifier), value > 0 else {
return nil
}
return Int(exactly: value)
}
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)
let host = components?.host?.lowercased() ?? ""
let pathRoute = url.pathComponents.first { $0 != "/" && !$0.isEmpty }?.lowercased()
if host == "event-detail" || (host.isEmpty && pathRoute == "event-detail") {
let scheme = url.scheme?.lowercased()
guard scheme == "voiceon" || scheme == "voiceon-test" else { return nil }
let path = url.pathComponents.filter { $0 != "/" && !$0.isEmpty }
guard path.count == (host.isEmpty ? 2 : 1), let id = eventId(path.last) else {
return nil
}
return .eventDetail(eventId: id)
}
if host == "community" || (host.isEmpty && pathRoute == "community") {
return parsePathStyle(url: url, components: components)
}
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 value = queryMap["deeplinkvalue"]?.trimmingCharacters(in: .whitespacesAndNewlines),
value.caseInsensitiveCompare("GIFT_DETAIL") == .orderedSame,
let applicationNo = queryMap["deeplinkid"]?.trimmingCharacters(in: .whitespacesAndNewlines),
!applicationNo.isEmpty {
return .giftDetail(applicationNo: applicationNo)
}
if let communityId = queryMap["community_id"], let value = Int(communityId), value > 0 {
guard let postId = communityPostId(queryMap: queryMap) else {
return nil
}
return .community(postId: postId)
}
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 "chat":
guard let identifier = identifier, let roomId = Int(identifier), roomId > 0 else {
return nil
}
return .userCreatorChat(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 "gift-detail":
guard let applicationNo = identifier?.trimmingCharacters(in: .whitespacesAndNewlines),
!applicationNo.isEmpty else {
return nil
}
return .giftDetail(applicationNo: applicationNo)
case "community":
guard let postId = communityPostId(queryItems: components?.queryItems) else {
return nil
}
return .community(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
}
}