From 2ee1c89c432e923e0b46c1de36e210f949f230dd Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Tue, 3 Dec 2024 01:54:47 +0200 Subject: [PATCH] [Feature]Introduce CallKit availability policies (#611) --- CHANGELOG.md | 3 + .../06-advanced/03-callkit-integration.swift | 24 +++++ .../GloballyUsedVariables.swift | 7 ++ .../CallKitAlwaysAvailabilityPolicy.swift | 13 +++ .../CallKitAvailabilityPolicy.swift | 49 ++++++++++ .../CallKitAvailabilityPolicyProtocol.swift | 11 +++ ...CallKitRegionBasedAvailabilityPolicy.swift | 38 ++++++++ .../StreamVideo/CallKit/CallKitAdapter.swift | 13 +++ .../LocaleProvider/StreamLocaleProvider.swift | 58 ++++++++++++ StreamVideo.xcodeproj/project.pbxproj | 48 ++++++++++ ...CallKitRegionBasedAvailabilityPolicy.swift | 90 +++++++++++++++++++ 11 files changed, 354 insertions(+) create mode 100644 Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAlwaysAvailabilityPolicy.swift create mode 100644 Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAvailabilityPolicy.swift create mode 100644 Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAvailabilityPolicyProtocol.swift create mode 100644 Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitRegionBasedAvailabilityPolicy.swift create mode 100644 Sources/StreamVideo/Utils/LocaleProvider/StreamLocaleProvider.swift create mode 100644 StreamVideoTests/CallKit/AvailabilityPolicy/CallKitRegionBasedAvailabilityPolicy.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 5029a5e77..3b1efee10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### ✅ Added +- CallKit availability policies allows you to control wether `Callkit` should be enabled/disabled based on different rules [#611](https://github.com/GetStream/stream-video-swift/pull/611) + ### 🐞 Fixed - By observing the `CallKitPushNotificationAdapter.deviceToken` you will be notified with an empty `deviceToken` value, once the object unregister push notifications. [#608](https://github.com/GetStream/stream-video-swift/pull/608) - When a call you receive a ringing while the app isn't running (and the screen is locked), websocket connection wasn't recovered. [#600](https://github.com/GetStream/stream-video-swift/pull/600) diff --git a/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift b/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift index 807dbeb3c..92685bf59 100644 --- a/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift +++ b/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift @@ -13,6 +13,30 @@ import Intents @MainActor fileprivate func content() { + container { + @Injected(\.callKitAdapter) var callKitAdapter + + callKitAdapter.availabilityPolicy = .always + } + + container { + @Injected(\.callKitAdapter) var callKitAdapter + + callKitAdapter.availabilityPolicy = .regionBased + } + + container { + struct MyCustomAvailabilityPolicy: CallKitAvailabilityPolicyProtocol { + var isAvailable: Bool { + // Example: Enable CallKit only for premium users + return UserManager.currentUser?.isPremium == true + } + } + + @Injected(\.callKitAdapter) var callKitAdapter + callKitAdapter.availabilityPolicy = .custom(MyCustomAvailabilityPolicy()) + } + container { @Injected(\.callKitAdapter) var callKitAdapter diff --git a/DocumentationTests/DocumentationTests/DocumentationTests/GloballyUsedVariables.swift b/DocumentationTests/DocumentationTests/DocumentationTests/GloballyUsedVariables.swift index 8cf205b95..ff68fa6cf 100644 --- a/DocumentationTests/DocumentationTests/DocumentationTests/GloballyUsedVariables.swift +++ b/DocumentationTests/DocumentationTests/DocumentationTests/GloballyUsedVariables.swift @@ -422,3 +422,10 @@ var otherParticipant = CallParticipant( audioLevels: [], pin: nil ) + +final class UserManager { + struct AppUser { + var isPremium: Bool + } + static var currentUser: AppUser? +} diff --git a/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAlwaysAvailabilityPolicy.swift b/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAlwaysAvailabilityPolicy.swift new file mode 100644 index 000000000..8c524f842 --- /dev/null +++ b/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAlwaysAvailabilityPolicy.swift @@ -0,0 +1,13 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// A policy implementation where CallKit is always available. +/// +/// This policy ignores regional or other constraints. +struct CallKitAlwaysAvailabilityPolicy: CallKitAvailabilityPolicyProtocol { + /// CallKit is always available with this policy. + var isAvailable: Bool { true } +} diff --git a/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAvailabilityPolicy.swift b/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAvailabilityPolicy.swift new file mode 100644 index 000000000..b0f335ded --- /dev/null +++ b/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAvailabilityPolicy.swift @@ -0,0 +1,49 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// A policy that defines when CallKit is available. +/// It can be configured to always enable CallKit, enable it based on the user's +/// region, or use a custom implementation. +public enum CallKitAvailabilityPolicy: CustomStringConvertible { + + /// CallKit is always available, regardless of conditions. + case always + + /// CallKit availability is determined based on the user's region. + case regionBased + + /// CallKit availability is determined by a custom policy. + /// - Parameter policy: A custom policy implementing `CallKitAvailabilityPolicyProtocol`. + case custom(CallKitAvailabilityPolicyProtocol) + + /// A textual description of the availability policy. + /// + /// - Returns: A string representation of the policy. + public var description: String { + switch self { + case .always: + return ".always" + case .regionBased: + return ".regionBased" + case let .custom(policy): + return ".custom(\(policy))" + } + } + + /// The underlying policy implementation based on the selected availability. + /// + /// - Returns: An instance conforming to `CallKitAvailabilityPolicyProtocol`. + var policy: CallKitAvailabilityPolicyProtocol { + switch self { + case .always: + return CallKitAlwaysAvailabilityPolicy() + case .regionBased: + return CallKitRegionBasedAvailabilityPolicy() + case let .custom(policy): + return policy + } + } +} diff --git a/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAvailabilityPolicyProtocol.swift b/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAvailabilityPolicyProtocol.swift new file mode 100644 index 000000000..4a1e934f4 --- /dev/null +++ b/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitAvailabilityPolicyProtocol.swift @@ -0,0 +1,11 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// A protocol defining the requirements for CallKit availability policies. +public protocol CallKitAvailabilityPolicyProtocol { + /// Indicates whether CallKit is available under the policy. + var isAvailable: Bool { get } +} diff --git a/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitRegionBasedAvailabilityPolicy.swift b/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitRegionBasedAvailabilityPolicy.swift new file mode 100644 index 000000000..fd3e3845c --- /dev/null +++ b/Sources/StreamVideo/CallKit/AvailabilityPolicy/CallKitRegionBasedAvailabilityPolicy.swift @@ -0,0 +1,38 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// A policy implementation where CallKit availability depends on the region. +/// +/// This policy disables CallKit in specific regions, identified by their region +/// codes, to comply with regional regulations or restrictions. It utilizes the +/// injected `StreamLocaleProvider` to retrieve the current locale information. +struct CallKitRegionBasedAvailabilityPolicy: CallKitAvailabilityPolicyProtocol { + + /// A provider for locale information. + @Injected(\.localeProvider) private var localeProvider + + /// A set of region identifiers where CallKit is unavailable. + /// + /// This includes both two-letter and three-letter region codes. + private var unavailableRegions: Set = [ + "CN", // China (two-letter code) + "CHN" // China (three-letter code) + ] + + /// Determines if CallKit is available based on the current region. + /// + /// - Returns: `true` if CallKit is available; otherwise, `false`. + /// - Note: If the region cannot be determined, CallKit is considered unavailable. + var isAvailable: Bool { + // Retrieve the current region identifier from the locale provider. + guard let identifier = localeProvider.identifier else { + return false + } + + // CallKit is unavailable if the region is part of the restricted set. + return !unavailableRegions.contains(identifier) + } +} diff --git a/Sources/StreamVideo/CallKit/CallKitAdapter.swift b/Sources/StreamVideo/CallKit/CallKitAdapter.swift index 6e9137f45..e0dac2708 100644 --- a/Sources/StreamVideo/CallKit/CallKitAdapter.swift +++ b/Sources/StreamVideo/CallKit/CallKitAdapter.swift @@ -26,6 +26,11 @@ open class CallKitAdapter { didSet { callKitService.callSettings = callSettings } } + /// The policy defining the availability of CallKit services. + /// + /// - Default: `.regionBased` + public var availabilityPolicy: CallKitAvailabilityPolicy = .regionBased + /// The currently active StreamVideo client. /// - Important: We need to update it whenever a user logins. public var streamVideo: StreamVideo? { @@ -46,6 +51,14 @@ open class CallKitAdapter { } private func didUpdate(_ streamVideo: StreamVideo?) { + guard availabilityPolicy.policy.isAvailable else { + log + .warning( + "CallKitAdapter cannot be activated because the current availability policy (\(availabilityPolicy.policy)) doesn't allow it." + ) + return + } + callKitService.streamVideo = streamVideo guard streamVideo != nil else { diff --git a/Sources/StreamVideo/Utils/LocaleProvider/StreamLocaleProvider.swift b/Sources/StreamVideo/Utils/LocaleProvider/StreamLocaleProvider.swift new file mode 100644 index 000000000..765f50b9f --- /dev/null +++ b/Sources/StreamVideo/Utils/LocaleProvider/StreamLocaleProvider.swift @@ -0,0 +1,58 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// A protocol that defines the requirements for a locale provider. +/// +/// This protocol abstracts the retrieval of a region identifier, allowing +/// flexibility and testability in components that depend on locale information. +protocol LocaleProviding { + + /// The region identifier of the current locale. + /// + /// - Returns: A string representing the region identifier (e.g., "US" or "GB"), + /// or `nil` if the region cannot be determined. + var identifier: String? { get } +} + +/// A provider for accessing the current locale's region identifier. +/// +/// This class abstracts locale information, offering compatibility for different +/// iOS versions. +final class StreamLocaleProvider: LocaleProviding { + + /// Retrieves the region identifier for the current locale. + /// + /// - For iOS 16 and later, it uses the `region` property. + /// - For earlier versions, it falls back to `regionCode`. + /// + /// - Returns: A string representing the region identifier, or `nil` if unavailable. + var identifier: String? { + if #available(iOS 16, *) { + // Retrieve the region identifier for iOS 16 and later. + return NSLocale.current.region?.identifier + } else { + // Retrieve the region code for earlier iOS versions. + return NSLocale.current.regionCode + } + } +} + +enum LocaleProvidingKey: InjectionKey { + /// The current value of the `StreamLocaleProvider` used for dependency injection. + static var currentValue: LocaleProviding = StreamLocaleProvider() +} + +/// Extension of `InjectedValues` to provide access to the `StreamLocaleProvider`. +extension InjectedValues { + + /// The locale provider, used to access region information within the app. + /// + /// This value can be overridden for testing or specific use cases. + var localeProvider: LocaleProviding { + get { Self[LocaleProvidingKey.self] } + set { Self[LocaleProvidingKey.self] = newValue } + } +} diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 355d8467f..c03c8cd5e 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -8,6 +8,12 @@ /* Begin PBXBuildFile section */ 40013DDC2B87AA2300915453 /* SerialActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40013DDB2B87AA2300915453 /* SerialActor.swift */; }; + 40034C262CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C252CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift */; }; + 40034C282CFE156800A318B1 /* CallKitAvailabilityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C272CFE156800A318B1 /* CallKitAvailabilityPolicy.swift */; }; + 40034C2A2CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C292CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */; }; + 40034C2C2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C2B2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift */; }; + 40034C2E2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C2D2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */; }; + 40034C312CFE168D00A318B1 /* StreamLocaleProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C302CFE168D00A318B1 /* StreamLocaleProvider.swift */; }; 40073B6F2C456CB4006A2867 /* StreamPictureInPictureVideoRendererTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40073B6E2C456CB4006A2867 /* StreamPictureInPictureVideoRendererTests.swift */; }; 40073B752C456E06006A2867 /* StreamPictureInPictureAdaptiveWindowSizePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40073B732C456DFC006A2867 /* StreamPictureInPictureAdaptiveWindowSizePolicy.swift */; }; 40073B762C456E0E006A2867 /* StreamPictureInPictureWindowSizePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40073B682C456250006A2867 /* StreamPictureInPictureWindowSizePolicy.swift */; }; @@ -1431,6 +1437,12 @@ /* Begin PBXFileReference section */ 40013DDB2B87AA2300915453 /* SerialActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialActor.swift; sourceTree = ""; }; + 40034C252CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitAvailabilityPolicyProtocol.swift; sourceTree = ""; }; + 40034C272CFE156800A318B1 /* CallKitAvailabilityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitAvailabilityPolicy.swift; sourceTree = ""; }; + 40034C292CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitRegionBasedAvailabilityPolicy.swift; sourceTree = ""; }; + 40034C2B2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitAlwaysAvailabilityPolicy.swift; sourceTree = ""; }; + 40034C2D2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitRegionBasedAvailabilityPolicy.swift; sourceTree = ""; }; + 40034C302CFE168D00A318B1 /* StreamLocaleProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamLocaleProvider.swift; sourceTree = ""; }; 40073B682C456250006A2867 /* StreamPictureInPictureWindowSizePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamPictureInPictureWindowSizePolicy.swift; sourceTree = ""; }; 40073B6E2C456CB4006A2867 /* StreamPictureInPictureVideoRendererTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamPictureInPictureVideoRendererTests.swift; sourceTree = ""; }; 40073B712C456DF6006A2867 /* StreamPictureInPictureFixedWindowSizePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamPictureInPictureFixedWindowSizePolicy.swift; sourceTree = ""; }; @@ -2601,6 +2613,33 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 40034C212CFE116200A318B1 /* AvailabilityPolicy */ = { + isa = PBXGroup; + children = ( + 40034C2B2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift */, + 40034C292CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */, + 40034C272CFE156800A318B1 /* CallKitAvailabilityPolicy.swift */, + 40034C252CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift */, + ); + path = AvailabilityPolicy; + sourceTree = ""; + }; + 40034C242CFE154F00A318B1 /* AvailabilityPolicy */ = { + isa = PBXGroup; + children = ( + 40034C2D2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */, + ); + path = AvailabilityPolicy; + sourceTree = ""; + }; + 40034C2F2CFE168900A318B1 /* LocaleProvider */ = { + isa = PBXGroup; + children = ( + 40034C302CFE168D00A318B1 /* StreamLocaleProvider.swift */, + ); + path = LocaleProvider; + sourceTree = ""; + }; 40073B702C456DE0006A2867 /* WindowSizePolicy */ = { isa = PBXGroup; children = ( @@ -3898,6 +3937,7 @@ 40DE867A2BBEAA6900E88D8A /* CallKit */ = { isa = PBXGroup; children = ( + 40034C242CFE154F00A318B1 /* AvailabilityPolicy */, 40DE867C2BBEAA8600E88D8A /* CallKitPushNotificationAdapterTests.swift */, 40F017412BBEC81C00E89FD1 /* CallKitServiceTests.swift */, 40F0173A2BBEB1A900E89FD1 /* CallKitAdapterTests.swift */, @@ -4147,6 +4187,7 @@ 40FB01FF2BAC8A4000A1C206 /* CallKit */ = { isa = PBXGroup; children = ( + 40034C212CFE116200A318B1 /* AvailabilityPolicy */, 40FB02022BAC93A800A1C206 /* CallKitAdapter.swift */, 40FB02042BAC94FB00A1C206 /* CallKitPushNotificationAdapter.swift */, 40FB02002BAC8A4A00A1C206 /* CallKitService.swift */, @@ -4919,6 +4960,7 @@ 84AF64D3287C79220012A503 /* Utils */ = { isa = PBXGroup; children = ( + 40034C2F2CFE168900A318B1 /* LocaleProvider */, 4067F3062CDA32F0002E28BD /* AudioSession */, 408CF9C42CAEC24500F56833 /* ScreenPropertiesAdapter */, 40C9E44F2C9880D300802B28 /* Unwrap */, @@ -6285,12 +6327,14 @@ 8490DD21298D4ADF007E53D2 /* StreamJsonDecoder.swift in Sources */, 40382F2E2C88B87D00C2D00F /* ReflectiveStringConvertible.swift in Sources */, 40BBC48F2C623C6E002AEF92 /* StreamRTCPeerConnection+Events.swift in Sources */, + 40034C2C2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift in Sources */, 84C4004229E3F446007B69C2 /* ConnectedEvent.swift in Sources */, 84DC389C29ADFCFD00946713 /* GetOrCreateCallResponse.swift in Sources */, 406B3BD92C8F337000FC93A1 /* MediaAdapting.swift in Sources */, 4065839B2B877ADA00B4F979 /* CIImage+Sendable.swift in Sources */, 84DCA2242A3A0F0D000C3411 /* HTTPClient.swift in Sources */, 84A737CE28F4716E001A6769 /* signal.pb.swift in Sources */, + 40034C2A2CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift in Sources */, 84D6494029E94C14002CA428 /* CallsQuery.swift in Sources */, 8490032329D308A000AD9BB4 /* BackstageSettingsRequest.swift in Sources */, 40C6891C2C657F280054528A /* Publisher+AsyncStream.swift in Sources */, @@ -6328,6 +6372,7 @@ 84BAD77E2A6BFFB200733156 /* BroadcastSampleHandler.swift in Sources */, 40C2B5BB2C2C41DA00EC2C2D /* RejectCallRequest+Reason.swift in Sources */, 40C9E4482C94743800802B28 /* Stream_Video_Sfu_Signal_TrackSubscriptionDetails+Convenience.swift in Sources */, + 40034C282CFE156800A318B1 /* CallKitAvailabilityPolicy.swift in Sources */, 840042C92A6FF9A200917B30 /* BroadcastConstants.swift in Sources */, 84F73854287C1A2D00A363F4 /* InjectedValuesExtensions.swift in Sources */, 40C9E44A2C94744E00802B28 /* Stream_Video_Sfu_Models_VideoDimension+Convenience.swift in Sources */, @@ -6354,6 +6399,7 @@ 40FB15192BF77EE700D5E580 /* StreamCallStateMachine+IdleStage.swift in Sources */, 40382F2B2C88B84800C2D00F /* Stream_Video_Sfu_Event_SfuEvent.OneOf_EventPayload+Payload.swift in Sources */, 84BAD7842A6C01AF00733156 /* BroadcastBufferReader.swift in Sources */, + 40034C312CFE168D00A318B1 /* StreamLocaleProvider.swift in Sources */, 84D91E9C2C7CB0AA00B163A0 /* CallSessionParticipantCountsUpdatedEvent.swift in Sources */, 846E4AF529CDEA66003733AB /* ConnectUserDetailsRequest.swift in Sources */, 846D16262A52CE8C0036CE4C /* SpeakerManager.swift in Sources */, @@ -6677,6 +6723,7 @@ 40429D612C779B7000AC7FFF /* SFUSignalService.swift in Sources */, 435F01B32A501148009CD0BD /* OwnCapability+Identifiable.swift in Sources */, 40BBC4B32C6276C4002AEF92 /* LocalNoOpMediaAdapter.swift in Sources */, + 40034C262CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift in Sources */, 40FB150F2BF77CEC00D5E580 /* StreamStateMachine.swift in Sources */, 40CB9FA42B7F8EA4006BED93 /* AVCaptureSession+ActiveCaptureDevice.swift in Sources */, 4159F1762C86FA41002B94D3 /* RTMPSettingsResponse.swift in Sources */, @@ -6764,6 +6811,7 @@ 40C9E4572C98B06E00802B28 /* WebRTCConfiguration_Tests.swift in Sources */, 40C9E4592C98B1A900802B28 /* WebRTCStateAdapter_Tests.swift in Sources */, 40F017612BBEF15E00E89FD1 /* CallParticipantResponse+Dummy.swift in Sources */, + 40034C2E2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift in Sources */, 406B3C552C92031000FC93A1 /* WebRTCCoordinatorStateMachine_JoiningStageTests.swift in Sources */, 40C9E4642C99886900802B28 /* WebRTCCoorindator_Tests.swift in Sources */, 40F017772BBEF43B00E89FD1 /* CallSessionParticipantLeftEvent+Dummy.swift in Sources */, diff --git a/StreamVideoTests/CallKit/AvailabilityPolicy/CallKitRegionBasedAvailabilityPolicy.swift b/StreamVideoTests/CallKit/AvailabilityPolicy/CallKitRegionBasedAvailabilityPolicy.swift new file mode 100644 index 000000000..3d73182f8 --- /dev/null +++ b/StreamVideoTests/CallKit/AvailabilityPolicy/CallKitRegionBasedAvailabilityPolicy.swift @@ -0,0 +1,90 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +@testable import StreamVideo +import XCTest + +final class CallKitRegionBasedAvailabilityPolicy_Tests: XCTestCase { + + private lazy var mockLocaleProvider: MockLocaleProvider! = .init() + private lazy var subject: CallKitRegionBasedAvailabilityPolicy! = .init() + + // MARK: - Lifecycle + + override func setUp() { + super.setUp() + InjectedValues[\.localeProvider] = mockLocaleProvider + } + + override func tearDown() { + mockLocaleProvider = nil + subject = nil + // Ensure no lingering dependency overrides. + InjectedValues[\.localeProvider] = StreamLocaleProvider() + super.tearDown() + } + + // MARK: - isAvailable + + func test_isAvailable_whenRegionIsUnavailable_returnsFalse() { + // Given + mockLocaleProvider.stubIdentifier = "CN" + + // When + let result = subject.isAvailable + + // Then + XCTAssertFalse(result, "CallKit should not be available in CN.") + } + + func test_isAvailable_whenRegionIsAvailable_returnsTrue() { + // Given + mockLocaleProvider.stubIdentifier = "US" + + // When + let result = subject.isAvailable + + // Then + XCTAssertTrue(result, "CallKit should be available in US.") + } + + func test_isAvailable_whenRegionIsNil_returnsFalse() { + // Given + mockLocaleProvider.stubIdentifier = nil + + // When + let result = subject.isAvailable + + // Then + XCTAssertFalse(result, "CallKit should not be available when the region is nil.") + } + + func test_isAvailable_whenRegionIsThreeLetterUnavailable_returnsFalse() { + // Given + mockLocaleProvider.stubIdentifier = "CHN" + + // When + let result = subject.isAvailable + + // Then + XCTAssertFalse(result, "CallKit should not be available in CHN.") + } + + func test_isAvailable_whenRegionIsThreeLetterAvailable_returnsTrue() { + // Given + mockLocaleProvider.stubIdentifier = "GBR" + + // When + let result = subject.isAvailable + + // Then + XCTAssertTrue(result, "CallKit should be available in GBR.") + } +} + +final class MockLocaleProvider: LocaleProviding { + var stubIdentifier: String? + + var identifier: String? { stubIdentifier } +}