From e999276c9ac7ccdac4cf922c12401940aed89744 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Tue, 15 Oct 2024 17:21:08 +0300 Subject: [PATCH] [Testing]Allow push notification sending/handling --- DemoApp/Sources/AppDelegate.swift | 104 +++++++------ .../Sources/Components/AppEnvironment.swift | 43 +++++- .../Deeplinks/DeeplinkAdapter.swift | 22 ++- .../DemoPushNotificationAdapter.swift | 142 ++++++++++++++++++ DemoApp/Sources/Components/Router.swift | 34 ++++- Scripts/sendPushNotification.sh | 46 ++++++ StreamVideo.xcodeproj/project.pbxproj | 14 ++ fastlane/Fastfile | 5 + 8 files changed, 354 insertions(+), 56 deletions(-) create mode 100644 DemoApp/Sources/Components/PushNotifications/DemoPushNotificationAdapter.swift create mode 100755 Scripts/sendPushNotification.sh diff --git a/DemoApp/Sources/AppDelegate.swift b/DemoApp/Sources/AppDelegate.swift index ca5c2ff89..d28f0b14c 100644 --- a/DemoApp/Sources/AppDelegate.swift +++ b/DemoApp/Sources/AppDelegate.swift @@ -10,13 +10,13 @@ import UIKit class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { @Injected(\.streamVideo) var streamVideo + @Injected(\.pushNotificationAdapter) var pushNotificationAdapter func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { - UNUserNotificationCenter.current().delegate = self - setUpRemoteNotifications() + _ = pushNotificationAdapter setUpPerformanceTracking() // Setup a dummy video file to loop when working with from the simulator @@ -58,53 +58,65 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele AppState.shared.pushToken = deviceToken } - func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void - ) { - let userInfo = response.notification.request.content.userInfo - log.debug("push notification received \(userInfo)") - guard let stream = userInfo["stream"] as? [String: Any], - let callCid = stream["call_cid"] as? String else { - return - } - let components = callCid.components(separatedBy: ":") - if components.count >= 2 { - let callType = components[0] - let callId = components[1] - let call = streamVideo.call(callType: callType, callId: callId) - AppState.shared.activeCall = call - Task { - do { - try Task.checkCancellation() - try await streamVideo.connect() - - try Task.checkCancellation() - try await call.accept() - - try Task.checkCancellation() - try await call.join() - } catch { - log.error(error) - } - } - } - } +// +// func userNotificationCenter( +// _ center: UNUserNotificationCenter, +// didReceive response: UNNotificationResponse +// ) async { +// do { +// try await pushNotificationAdapter.handleNotification(response) +// } catch { +// log.error(error) +// } + //// let userInfo = response.notification.request.content.userInfo + //// log.debug("push notification received \(userInfo)") + //// guard + //// let stream = userInfo["stream"] as? [String: Any], + //// let callCid = stream["call_cid"] as? String + //// else { return } + //// + //// let components = callCid.components(separatedBy: ":") + //// if components.count >= 2 { + //// let callType = components[0] + //// let callId = components[1] + //// let call = streamVideo.call(callType: callType, callId: callId) + //// AppState.shared.activeCall = call + //// do { + //// try Task.checkCancellation() + //// try await streamVideo.connect() + //// + //// try Task.checkCancellation() + //// try await call.accept() + //// + //// try Task.checkCancellation() + //// try await call.join() + //// } catch { + //// log.error(error) + //// } + //// } +// } +// +// func userNotificationCenter( +// _ center: UNUserNotificationCenter, +// willPresent notification: UNNotification +// ) async -> UNNotificationPresentationOptions { +// log.debug("Will present received push notification: \(notification).") +// return [.banner, .sound] +// } // MARK: - Private Helpers - private func setUpRemoteNotifications() { - UNUserNotificationCenter - .current() - .requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in - if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - } - } +// private func setUpRemoteNotifications() { +// UNUserNotificationCenter +// .current() +// .requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in +// if granted { +// Task { @MainActor in +// UIApplication.shared.registerForRemoteNotifications() +// } +// } +// } +// } private func setUpPerformanceTracking() { guard AppEnvironment.performanceTrackerVisibility == .visible else { return } diff --git a/DemoApp/Sources/Components/AppEnvironment.swift b/DemoApp/Sources/Components/AppEnvironment.swift index 69532c9df..05a2961f6 100644 --- a/DemoApp/Sources/Components/AppEnvironment.swift +++ b/DemoApp/Sources/Components/AppEnvironment.swift @@ -76,7 +76,30 @@ extension AppEnvironment { } } - func joinLink(_ callId: String, callType: String = .default) -> URL { + var identifier: String { + switch self { + case .pronto: + return "pronto" + case .prontoStaging: + return "pronto-staging" + case .staging: + return "staging" + case .legacy: + return "legacy" + case .demo: + return "demo" + case let .custom: + return "custom" + } + } + + func joinLink( + _ callId: String, + callType: String = .default, + apiKey: String? = nil, + userId: String? = nil, + token: String? = nil + ) -> URL { switch self { case .demo: return url @@ -85,17 +108,26 @@ extension AppEnvironment { .appendingPathComponent("join") .appendingPathComponent(callId) .addQueryParameter("type", value: callType) + .addQueryParameter("api_key", value: apiKey) + .addQueryParameter("user_id", value: userId) + .addQueryParameter("token", value: token) case let .custom(baseURL, _, _): return baseURL .url .appendingPathComponent("join") .appendingPathComponent(callId) .addQueryParameter("type", value: callType) + .addQueryParameter("api_key", value: apiKey) + .addQueryParameter("user_id", value: userId) + .addQueryParameter("token", value: token) default: return url .appendingPathComponent("join") .appendingPathComponent(callId) .addQueryParameter("type", value: callType) + .addQueryParameter("api_key", value: apiKey) + .addQueryParameter("user_id", value: userId) + .addQueryParameter("token", value: token) } } @@ -252,6 +284,7 @@ extension AppEnvironment { enum SupportedDeeplink: Debuggable, CaseIterable { case pronto + case prontoStaging case staging case demo case legacy @@ -260,6 +293,8 @@ extension AppEnvironment { switch self { case .pronto: return BaseURL.pronto.url + case .prontoStaging: + return BaseURL.prontoStaging.url case .staging: return BaseURL.staging.url case .demo: @@ -273,6 +308,8 @@ extension AppEnvironment { switch self { case .pronto: return "Pronto" + case .prontoStaging: + return "Pronto Staging" case .staging: return "Staging" case .demo: @@ -286,9 +323,9 @@ extension AppEnvironment { static var supportedDeeplinks: [SupportedDeeplink] = { switch configuration { case .debug: - return [.pronto, .demo, .staging, .legacy] + return [.pronto, .prontoStaging, .demo, .staging, .legacy] case .test: - return [.pronto, .demo, .staging, .legacy] + return [.pronto, .prontoStaging, .demo, .staging, .legacy] case .release: return [.demo] } diff --git a/DemoApp/Sources/Components/Deeplinks/DeeplinkAdapter.swift b/DemoApp/Sources/Components/Deeplinks/DeeplinkAdapter.swift index 2a6d169e3..29394e534 100644 --- a/DemoApp/Sources/Components/Deeplinks/DeeplinkAdapter.swift +++ b/DemoApp/Sources/Components/Deeplinks/DeeplinkAdapter.swift @@ -10,12 +10,18 @@ struct DeeplinkInfo: Equatable { var callId: String var callType: String var baseURL: AppEnvironment.BaseURL + var apiKey: String? + var token: String? + var userId: String? static let empty = DeeplinkInfo( url: nil, callId: "", callType: "", - baseURL: AppEnvironment.baseURL + baseURL: AppEnvironment.baseURL, + apiKey: nil, + token: nil, + userId: nil ) } @@ -25,15 +31,20 @@ struct DeeplinkAdapter { return true } - let result = AppEnvironment + let supported = AppEnvironment .supportedDeeplinks .compactMap(\.deeplinkURL.host) + + let result = supported .first { url.host == $0 } != nil return result } - func handle(url: URL) -> (deeplinkInfo: DeeplinkInfo, user: User?) { + func handle(url: URL) -> ( + deeplinkInfo: DeeplinkInfo, + user: User? + ) { guard canHandle(url: url) else { return (.empty, nil) } @@ -79,7 +90,10 @@ struct DeeplinkAdapter { url: url, callId: callId, callType: callType, - baseURL: baseURL + baseURL: baseURL, + apiKey: url.queryParameters["api_key"], + token: url.queryParameters["token"], + userId: url.queryParameters["user_id"] ), nil ) diff --git a/DemoApp/Sources/Components/PushNotifications/DemoPushNotificationAdapter.swift b/DemoApp/Sources/Components/PushNotifications/DemoPushNotificationAdapter.swift new file mode 100644 index 000000000..0530b84b1 --- /dev/null +++ b/DemoApp/Sources/Components/PushNotifications/DemoPushNotificationAdapter.swift @@ -0,0 +1,142 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation +import StreamVideo +import UIKit +import UserNotifications + +final class DemoPushNotificationAdapter: NSObject, UNUserNotificationCenterDelegate { + + override init() { + super.init() + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.delegate = self + notificationCenter + .requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in + if granted { + Task { @MainActor in + UIApplication.shared.registerForRemoteNotifications() + } + } + } + } + + // MARK: - UNUserNotificationCenterDelegate + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification + ) async -> UNNotificationPresentationOptions { + log.debug("Will present received push notification: \(notification).") + return [.banner, .sound] + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse + ) async { + do { + try await handleNotification(response) + } catch { + log.error(error) + } + } + + // MARK: - Private Helpers + + private func handleNotification( + _ response: UNNotificationResponse + ) async throws { + guard + let userInfo = response.notification.request.content.userInfo["aps"] as? [String: Any] + else { + throw ClientError("Invalid push notification format.") + } + + if let request = JoinCallRequestPayload(userInfo) { + log.debug("Received join call push notification.") + fireDeeplink(for: request) + } else { + log.debug("Unhandled push notification received.") + } + } + + private func fireDeeplink(for request: JoinCallRequestPayload) { + let joinCallURL = request + .environment + .joinLink( + request.id, + callType: request.type, + apiKey: request.apiKey, + userId: request.userId, + token: request.token + ) + + Task { @MainActor in + #if targetEnvironment(simulator) + Router.shared.handle(url: joinCallURL) + #else + UIApplication.shared.open(joinCallURL) + #endif + } + } +} + +extension DemoPushNotificationAdapter: InjectionKey { + static var currentValue: DemoPushNotificationAdapter = .init() +} + +extension InjectedValues { + var pushNotificationAdapter: DemoPushNotificationAdapter { + get { Self[DemoPushNotificationAdapter.self] } + set { _ = newValue } + } +} + +struct JoinCallRequestPayload { + var environment: AppEnvironment.BaseURL + var type: String + var id: String + var apiKey: String? + var userId: String? + var token: String? + + init?(_ container: [AnyHashable: Any]) { + guard + let root = container["stream"] as? [String: Any], + let cId = (root["call_cid"] as? String) ?? (root["cid"] as? String) + else { + return nil + } + + let components = cId.components(separatedBy: ":") + + guard components.count == 2 else { + return nil + } + + type = components[0] + id = components[1] + + if let appEnvironment = root["environment"] as? String { + switch appEnvironment { + case AppEnvironment.BaseURL.pronto.identifier: + environment = .pronto + case AppEnvironment.BaseURL.prontoStaging.identifier: + environment = .prontoStaging + case AppEnvironment.BaseURL.demo.identifier: + environment = .demo + default: + environment = AppEnvironment.baseURL + } + } else { + environment = AppEnvironment.baseURL + } + + apiKey = root["api_key"] as? String + userId = root["user_id"] as? String + token = root["token"] as? String + } +} diff --git a/DemoApp/Sources/Components/Router.swift b/DemoApp/Sources/Components/Router.swift index b98606939..cfab4898a 100644 --- a/DemoApp/Sources/Components/Router.swift +++ b/DemoApp/Sources/Components/Router.swift @@ -72,13 +72,41 @@ final class Router: ObservableObject { } if - deeplinkInfo.baseURL != AppEnvironment.baseURL, - let currentUser = appState.currentUser { + let apiKey = deeplinkInfo.apiKey, + let token = deeplinkInfo.token, + let userId = deeplinkInfo.userId, + appState.currentUser?.id != userId { + Task { + do { + await appState.logout() + AppEnvironment.baseURL = .custom( + baseURL: deeplinkInfo.baseURL, + apiKey: apiKey, + token: token + ) + try await handleLoggedInUserCredentials( + .init( + userInfo: .init(id: userId), + token: .init(rawValue: token) + ), + deeplinkInfo: deeplinkInfo + ) + } catch { + log.error(error) + } + } + } else if deeplinkInfo.baseURL != AppEnvironment.baseURL, let currentUser = appState.currentUser { Task { do { await appState.logout() AppEnvironment.baseURL = deeplinkInfo.baseURL - try await handleLoggedInUserCredentials(.init(userInfo: currentUser, token: .empty), deeplinkInfo: deeplinkInfo) + try await handleLoggedInUserCredentials( + .init( + userInfo: currentUser, + token: .empty + ), + deeplinkInfo: deeplinkInfo + ) } catch { log.error(error) } diff --git a/Scripts/sendPushNotification.sh b/Scripts/sendPushNotification.sh new file mode 100755 index 000000000..69631bce9 --- /dev/null +++ b/Scripts/sendPushNotification.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# Check if a payload file is provided as an argument +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Get the file path from the argument +PAYLOAD_FILE=$1 + +# Check if the payload file exists +if [ ! -f "$PAYLOAD_FILE" ]; then + echo "Error: File $PAYLOAD_FILE not found!" + exit 1 +fi + +# Get the bundle identifier of the app (you should replace this with your app's actual bundle identifier) +BUNDLE_IDENTIFIER="io.getstream.iOS.VideoDemoApp" + +# Get a list of booted simulators +BOOTED_SIMULATORS=$(xcrun simctl list devices | grep '(Booted)' | awk -F '[()]' '{print $2}') + +# Check if there are any booted simulators +if [ -z "$BOOTED_SIMULATORS" ]; then + echo "No booted simulators found." + exit 0 +fi + +# Send the push notification to each booted simulator +for SIMULATOR_ID in $BOOTED_SIMULATORS; do + echo "Sending push notification to simulator with ID: $SIMULATOR_ID" + COMMAND="xcrun simctl push $SIMULATOR_ID $BUNDLE_IDENTIFIER $PAYLOAD_FILE" + echo "Will execute" + echo "$COMMAND" + + ${COMMAND} + + if [ $? -eq 0 ]; then + echo "Push notification sent to $SIMULATOR_ID successfully." + else + echo "Failed to send push notification to $SIMULATOR_ID." + fi +done + +echo "Push notification process completed." \ No newline at end of file diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 35348c82e..68824c2d3 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -161,6 +161,8 @@ 40429D5F2C779B3D00AC7FFF /* ScreenShareSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40429D5E2C779B3D00AC7FFF /* ScreenShareSession.swift */; }; 40429D602C779B5A00AC7FFF /* TrackType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40BBC4872C623C6E002AEF92 /* TrackType.swift */; }; 40429D612C779B7000AC7FFF /* SFUSignalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40C689192C64F74F0054528A /* SFUSignalService.swift */; }; + 40460DB12CBE9A2D003BBB3C /* DemoPushNotificationAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40460DB02CBE9A2D003BBB3C /* DemoPushNotificationAdapter.swift */; }; + 40460DB22CBE9A2D003BBB3C /* DemoPushNotificationAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40460DB02CBE9A2D003BBB3C /* DemoPushNotificationAdapter.swift */; }; 4046DEE92A9E381F00CA6D2F /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4046DEE82A9E381F00CA6D2F /* AppIntentVocabulary.plist */; }; 4046DEF02A9F469100CA6D2F /* GDPerformanceView-Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 4046DEEF2A9F469100CA6D2F /* GDPerformanceView-Swift */; settings = {ATTRIBUTES = (Required, ); }; }; 4046DEF22A9F510C00CA6D2F /* DebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4046DEF12A9F510C00CA6D2F /* DebugMenu.swift */; }; @@ -1498,6 +1500,7 @@ 403FF3F12BA1DC480092CE8A /* YpCbCrPixelRange+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "YpCbCrPixelRange+Default.swift"; sourceTree = ""; }; 40429D5C2C779AED00AC7FFF /* WebRTCMigrationStatusObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebRTCMigrationStatusObserver.swift; sourceTree = ""; }; 40429D5E2C779B3D00AC7FFF /* ScreenShareSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenShareSession.swift; sourceTree = ""; }; + 40460DB02CBE9A2D003BBB3C /* DemoPushNotificationAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoPushNotificationAdapter.swift; sourceTree = ""; }; 4046DEE82A9E381F00CA6D2F /* AppIntentVocabulary.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = AppIntentVocabulary.plist; sourceTree = ""; }; 4046DEEA2A9E38DC00CA6D2F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4046DEF12A9F510C00CA6D2F /* DebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugMenu.swift; sourceTree = ""; }; @@ -2812,6 +2815,7 @@ 4030E5962A9DF48C003E8CBA /* Components */ = { isa = PBXGroup; children = ( + 40460DAF2CBE9A22003BBB3C /* PushNotifications */, 845C098F2C0E0B6B00F725B3 /* SessionTimer */, 403EFC9D2BDBFDEE0057C248 /* Feedback */, 40CB9FA52B7FB1B2006BED93 /* Snapshot */, @@ -2973,6 +2977,14 @@ path = StreamPixelBufferRepository; sourceTree = ""; }; + 40460DAF2CBE9A22003BBB3C /* PushNotifications */ = { + isa = PBXGroup; + children = ( + 40460DB02CBE9A2D003BBB3C /* DemoPushNotificationAdapter.swift */, + ); + path = PushNotifications; + sourceTree = ""; + }; 4049CE802BBBF73A003D07D2 /* AsyncImage */ = { isa = PBXGroup; children = ( @@ -5987,6 +5999,7 @@ 408D29AD2B6D680D00885473 /* SnapshotTrigger.swift in Sources */, 4046DEF22A9F510C00CA6D2F /* DebugMenu.swift in Sources */, 40F445C22A9E1449004BE3DA /* JoinCallView.swift in Sources */, + 40460DB22CBE9A2D003BBB3C /* DemoPushNotificationAdapter.swift in Sources */, 409146102B6913BA007F3C17 /* DemoStatsView.swift in Sources */, 40BBC47C2C6227F1002AEF92 /* View+PresentDemoMoreMenu.swift in Sources */, 402EE1302AA8861B00312632 /* DemoChatViewModel.swift in Sources */, @@ -6042,6 +6055,7 @@ 406303472AD943B60091AE77 /* GoogleHelper.swift in Sources */, 4029E94B2CB8086000E1D571 /* DemoManualQualitySelectionButtonView.swift in Sources */, 40AB35592B738C7700E465CC /* DemoQRCodeScannerButton.swift in Sources */, + 40460DB12CBE9A2D003BBB3C /* DemoPushNotificationAdapter.swift in Sources */, 40AB355F2B738C7D00E465CC /* ReactionsViewModifier.swift in Sources */, 406A8EA22AA1D7EF001F598A /* TokenResponse.swift in Sources */, 40AB354F2B738C5D00E465CC /* ChangeEnvironmentViewModifier.swift in Sources */, diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a516d49b2..693cdf93c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -338,6 +338,11 @@ lane :restart_video_buddy do start_video_buddy end +desc 'Send push notifications to all running simulators.' +lane :send_push_notification do |options| +sh("../Scripts/sendPushNotification.sh", options[:payload]) +end + lane :build_xcframeworks do match_me output_directory = File.absolute_path("#{Dir.pwd}/../Products")