import Foundation enum AppStep: Equatable { case splash case main case userCreatorChatRoom(roomId: Int) case contentDetail(contentId: Int) case seriesDetail(seriesId: Int) case creatorCommunityAll(creatorId: Int) case creatorChannelCommunityPostDetail(postId: Int) case message case audition case giftDetail(applicationNo: String) case eventDetail(eventId: Int) } enum UserDefaultsKey { case userId } enum UserDefaults { static var userId = 0 static func int(forKey: UserDefaultsKey) -> Int { userId } } final class AppState { static let shared = AppState() var rootStep: AppStep = .main var pendingDeepLinkAction: AppDeepLinkAction? var appliedStep: AppStep? var appStep: AppStep { appliedStep ?? rootStep } var isPushRoomFromDeepLink = false var pushRoomId = 0 var pendingCommentPostId: Int? func setAppStep( step: AppStep, ifCurrentStep matches: ((AppStep) -> Bool)? = nil, completion: @escaping () -> Void = {} ) { defer { completion() } guard matches?(appStep) ?? true else { return } appliedStep = step } func requestExternalNavigation(navigationAction: @escaping () -> Void, cancelAction: @escaping () -> Void) { navigationAction() } func setPendingCommunityCommentDeepLink(creatorId: Int, postId: Int) { pendingCommentPostId = postId } func clearPendingCommunityCommentDeepLink() { pendingCommentPostId = nil } } let APPSCHEME = "soda" @main enum GiftDetailDeepLinkCheck { static var assertions = 0 static func check(_ condition: @autoclosure () -> Bool, _ message: String) { assertions += 1 guard condition() else { fatalError("FAIL: \(message)") } } static func settle() { RunLoop.current.run(until: Date().addingTimeInterval(0.05)) } static func main() { let valid = "voiceon://?DeepLinkValue=gift_detail&DEEPLINKID=GIFT-2026-%30%30%31" check(AppDeepLinkHandler.handle(urlString: valid), "valid gift detail accepted") settle() check(AppState.shared.appliedStep == .giftDetail(applicationNo: "GIFT-2026-001"), "decoded identifier routes to gift detail") for url in [ "voiceon://gift-detail/A-1002609300001", "voiceon-test://gift-detail/A-1002609300001" ] { AppState.shared.appliedStep = nil check(AppDeepLinkHandler.handle(urlString: url), "path gift detail accepted for \(url)") settle() check(AppState.shared.appliedStep == .giftDetail(applicationNo: "A-1002609300001"), "path identifier routes to gift detail") } AppState.shared.appliedStep = nil check(AppDeepLinkHandler.handle(urlString: "voiceon://?deepLinkValue=%20GIFT_DETAIL%20&deepLinkId=%20GIFT-2026-002%20"), "trimmed semantic values accepted") settle() check(AppState.shared.appliedStep == .giftDetail(applicationNo: "GIFT-2026-002"), "identifier whitespace trimmed") for url in [ "voiceon://?deepLinkValue=GIFT_DETAIL", "voiceon://?deepLinkId=GIFT-2026-003", "voiceon://?deepLinkValue=GIFT_DETAIL&deepLinkId=%20%20", "voiceon://?deepLinkValue=OTHER&deepLinkId=GIFT-2026-004", "voiceon://gift-detail/", "http://?deepLinkValue=GIFT_DETAIL&deepLinkId=GIFT-2026-005" ] { check(!AppDeepLinkHandler.handle(urlString: url), "rejects invalid or unsupported \(url)") } AppState.shared.rootStep = .splash AppState.shared.pendingDeepLinkAction = nil AppState.shared.appliedStep = nil check(AppDeepLinkHandler.handle(urlString: "voiceon://?deepLinkValue=GIFT_DETAIL&deepLinkId=GIFT-2026-006"), "valid splash link accepted") settle() if case .giftDetail(let applicationNo)? = AppState.shared.pendingDeepLinkAction { check(applicationNo == "GIFT-2026-006", "splash stores gift action") } else { check(false, "splash stores gift action") } check(AppState.shared.appliedStep == nil, "splash does not apply gift action") AppState.shared.pendingDeepLinkAction = nil check(AppDeepLinkHandler.handle(urlString: "voiceon-test://gift-detail/A-1002609300001"), "splash accepts path gift detail") settle() if case .giftDetail(let applicationNo)? = AppState.shared.pendingDeepLinkAction { check(applicationNo == "A-1002609300001", "splash stores path gift action") } else { check(false, "splash stores path gift action") } check(AppState.shared.appliedStep == nil, "splash does not apply path gift action") AppState.shared.rootStep = .main AppState.shared.appliedStep = nil AppState.shared.pendingDeepLinkAction = nil check(AppDeepLinkHandler.handle(urlString: "voiceon://?content_id=42&deepLinkValue=GIFT_DETAIL&deepLinkId=GIFT-2026-007"), "existing content route accepted") settle() check(AppState.shared.appliedStep == .contentDetail(contentId: 42), "existing content route still applies") checkCommunityLinks() print("PASS AppDeepLinkGiftDetailCheck assertions=\(assertions)") } static func checkCommunityLinks() { for userId in [0, 42] { UserDefaults.userId = userId for source in [AppDeepLinkSource.external, .notificationList] { for url in [ "voiceon://community?postId=12345", "voiceon-test://community?post_id=12345", "voiceon:///community?POSTID=12345", "voiceon://community/77?postId=12345", "voiceon://?community_id=77&postId=12345" ] { AppState.shared.appliedStep = nil AppState.shared.pendingCommentPostId = nil check(AppDeepLinkHandler.handle(urlString: url, source: source), "community link accepted: \(url), userId=\(userId)") settle() check(AppState.shared.appliedStep == .creatorChannelCommunityPostDetail(postId: 12345), "community link opens requested post detail: \(url)") check(AppState.shared.pendingCommentPostId == nil, "detail link does not queue legacy comment sheet") } } } for rootStep in [AppStep.main, .splash] { AppState.shared.rootStep = rootStep for route in ["voiceon://community", "voiceon:///community", "voiceon://community/77", "voiceon://?community_id=77"] { for parameter in ["", "postId=", "postId=abc", "postId=0", "postId=-1", "postId=1.5", "postId=999999999999999999999999", "post_id=0"] { let separator = route.contains("?") ? "&" : "?" let url = route + separator + parameter AppState.shared.appliedStep = .contentDetail(contentId: 42) AppState.shared.pendingDeepLinkAction = .content(contentId: 99) check(!AppDeepLinkHandler.handle(urlString: url), "invalid community link rejected: \(url)") settle() check(AppState.shared.appliedStep == .contentDetail(contentId: 42), "invalid postId leaves current route unchanged") if case .content(let contentId)? = AppState.shared.pendingDeepLinkAction { check(contentId == 99, "invalid postId preserves existing deferred action") } else { check(false, "invalid postId preserves existing deferred action") } } } } check(!AppDeepLinkHandler.handle(urlString: "voiceon://community?postId=bad&content_id=42"), "invalid community route does not fall through to content") check(!AppDeepLinkHandler.handle(urlString: "voiceon:///community?message_id=42"), "missing postId does not fall through to message") AppState.shared.rootStep = .splash AppState.shared.appliedStep = nil AppState.shared.pendingDeepLinkAction = nil check(AppDeepLinkHandler.handle(urlString: "voiceon://community?postId=12345"), "splash accepts community detail") settle() check(AppState.shared.appliedStep == nil, "splash defers detail navigation") guard let action = AppState.shared.pendingDeepLinkAction else { check(false, "splash stores community detail action") return } AppState.shared.rootStep = .main AppState.shared.pendingDeepLinkAction = nil AppDeepLinkHandler.apply(action: action) check(AppState.shared.appliedStep == .creatorChannelCommunityPostDetail(postId: 12345), "deferred community link opens requested post detail") } }