Skip to content

Commit ea3a174

Browse files
committed
Add tests and localisations
1 parent c34030d commit ea3a174

File tree

63 files changed

+1414
-115
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1414
-115
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import AVFoundation
6+
import Foundation
7+
8+
protocol CameraPermissionProviding {
9+
10+
var systemPermission: PermissionStore.Permission { get }
11+
12+
func requestPermission(_ completion: @escaping (Bool) -> Void)
13+
}
14+
15+
final class StreamCameraPermissionProvider: CameraPermissionProviding {
16+
var systemPermission: PermissionStore.Permission {
17+
switch AVCaptureDevice.authorizationStatus(for: .video) {
18+
case .notDetermined:
19+
return .unknown
20+
case .restricted:
21+
return .denied
22+
case .denied:
23+
return .denied
24+
case .authorized:
25+
return .granted
26+
@unknown default:
27+
return .unknown
28+
}
29+
}
30+
31+
func requestPermission(_ completion: @escaping (Bool) -> Void) {
32+
AVCaptureDevice
33+
.requestAccess(for: .video, completionHandler: completion)
34+
}
35+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import AVFoundation
6+
import Foundation
7+
8+
protocol MicrophonePermissionProviding {
9+
10+
var systemPermission: PermissionStore.Permission { get }
11+
12+
func requestPermission(_ completion: @escaping (Bool) -> Void)
13+
}
14+
15+
final class StreamMicrophonePermissionProvider: MicrophonePermissionProviding {
16+
var systemPermission: PermissionStore.Permission {
17+
if #available(iOS 17.0, *) {
18+
switch AVAudioApplication.shared.recordPermission {
19+
case .undetermined:
20+
return .unknown
21+
case .denied:
22+
return .denied
23+
case .granted:
24+
return .granted
25+
@unknown default:
26+
return .unknown
27+
}
28+
} else {
29+
switch AVAudioSession.sharedInstance().recordPermission {
30+
case .undetermined:
31+
return .unknown
32+
case .denied:
33+
return .denied
34+
case .granted:
35+
return .granted
36+
@unknown default:
37+
return .unknown
38+
}
39+
}
40+
}
41+
42+
func requestPermission(_ completion: @escaping (Bool) -> Void) {
43+
if #available(iOS 17.0, *) {
44+
AVAudioApplication
45+
.requestRecordPermission(completionHandler: completion)
46+
} else {
47+
return AVAudioSession
48+
.sharedInstance()
49+
.requestRecordPermission(completion)
50+
}
51+
}
52+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import Foundation
6+
import UserNotifications
7+
8+
protocol PushNotificationsPermissionProviding {
9+
10+
func systemPermission() async -> PermissionStore.Permission
11+
12+
func requestPermission(
13+
with options: UNAuthorizationOptions,
14+
_ completion: @escaping (Bool, Error?) -> Void
15+
)
16+
}
17+
18+
final class StreamPushNotificationsPermissionProvider: PushNotificationsPermissionProviding {
19+
func systemPermission() async -> PermissionStore.Permission {
20+
/// UNUserNotificationCenter cannot be initialised correctly during tests. For this reason
21+
/// we disable it.
22+
/// - Reference: The related crash looks like this
23+
/// __bundleProxyForCurrentProcess is nil: mainBundle.bundleURL__
24+
guard !SystemEnvironment.isTests else {
25+
return .unknown
26+
}
27+
let authorizationStatus = await UNUserNotificationCenter
28+
.current()
29+
.notificationSettings()
30+
.authorizationStatus
31+
32+
switch authorizationStatus {
33+
case .notDetermined:
34+
return .unknown
35+
case .provisional:
36+
return .granted
37+
case .denied:
38+
return .denied
39+
case .authorized:
40+
return .granted
41+
case .ephemeral:
42+
return .granted
43+
@unknown default:
44+
return .unknown
45+
}
46+
}
47+
48+
func requestPermission(
49+
with options: UNAuthorizationOptions,
50+
_ completion: @escaping (Bool, Error?) -> Void
51+
) {
52+
UNUserNotificationCenter
53+
.current()
54+
.requestAuthorization(
55+
options: options,
56+
completionHandler: completion
57+
)
58+
}
59+
}

Sources/StreamVideo/Utils/PermissionsStore/Namespace/Middleware/PermissionStore+CameraMiddleware.swift

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@ extension PermissionStore {
1010

1111
final class CameraMiddleware: Middleware<Namespace>, @unchecked Sendable {
1212

13+
private let permissionProvider: CameraPermissionProviding
1314
override var dispatcher: Store<PermissionStore.Namespace>.Dispatcher? {
1415
didSet { dispatcher?.dispatch(.setCameraPermission(systemPermission)) }
1516
}
1617

18+
init(
19+
permissionProvider: CameraPermissionProviding = StreamCameraPermissionProvider()
20+
) {
21+
self.permissionProvider = permissionProvider
22+
}
23+
1724
override func apply(
1825
state: PermissionStore.StoreState,
1926
action: PermissionStore.StoreAction,
@@ -33,23 +40,14 @@ extension PermissionStore {
3340
// MARK: - Private Helpers
3441

3542
private var systemPermission: Permission {
36-
switch AVCaptureDevice.authorizationStatus(for: .video) {
37-
case .notDetermined:
38-
return .unknown
39-
case .restricted:
40-
return .denied
41-
case .denied:
42-
return .denied
43-
case .authorized:
44-
return .granted
45-
@unknown default:
46-
return .unknown
47-
}
43+
permissionProvider.systemPermission
4844
}
4945

5046
private func requestPermission() {
51-
AVCaptureDevice.requestAccess(for: .video) { [weak self] in
52-
self?.dispatcher?.dispatch(.setCameraPermission($0 ? .granted : .denied))
47+
permissionProvider.requestPermission { [weak self] in
48+
self?
49+
.dispatcher?
50+
.dispatch(.setCameraPermission($0 ? .granted : .denied))
5351
}
5452
}
5553
}

Sources/StreamVideo/Utils/PermissionsStore/Namespace/Middleware/PermissionStore+MicrophoneMiddleware.swift

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@
22
// Copyright © 2025 Stream.io Inc. All rights reserved.
33
//
44

5-
import AVFoundation
65
import Foundation
76
import StreamWebRTC
87

98
extension PermissionStore {
109

1110
final class MicrophoneMiddleware: Middleware<Namespace>, @unchecked Sendable {
1211

12+
private let permissionProvider: MicrophonePermissionProviding
13+
1314
override var dispatcher: Store<PermissionStore.Namespace>.Dispatcher? {
1415
didSet { dispatcher?.dispatch(.setMicrophonePermission(systemPermission)) }
1516
}
1617

18+
init(
19+
permissionProvider: MicrophonePermissionProviding = StreamMicrophonePermissionProvider()
20+
) {
21+
self.permissionProvider = permissionProvider
22+
}
23+
1724
override func apply(
1825
state: PermissionStore.StoreState,
1926
action: PermissionStore.StoreAction,
@@ -33,40 +40,14 @@ extension PermissionStore {
3340
// MARK: - Private Helpers
3441

3542
private var systemPermission: Permission {
36-
if #available(iOS 17.0, *) {
37-
switch AVAudioApplication.shared.recordPermission {
38-
case .undetermined:
39-
return .unknown
40-
case .denied:
41-
return .denied
42-
case .granted:
43-
return .granted
44-
@unknown default:
45-
return .unknown
46-
}
47-
} else {
48-
switch AVAudioSession.sharedInstance().recordPermission {
49-
case .undetermined:
50-
return .unknown
51-
case .denied:
52-
return .denied
53-
case .granted:
54-
return .granted
55-
@unknown default:
56-
return .unknown
57-
}
58-
}
43+
permissionProvider.systemPermission
5944
}
6045

6146
private func requestPermission() {
62-
if #available(iOS 17.0, *) {
63-
AVAudioApplication.requestRecordPermission { [weak self] in
64-
self?.dispatcher?.dispatch(.setMicrophonePermission($0 ? .granted : .denied))
65-
}
66-
} else {
67-
return AVAudioSession.sharedInstance().requestRecordPermission { [weak self] in
68-
self?.dispatcher?.dispatch(.setMicrophonePermission($0 ? .granted : .denied))
69-
}
47+
permissionProvider.requestPermission { [weak self] in
48+
self?
49+
.dispatcher?
50+
.dispatch(.setMicrophonePermission($0 ? .granted : .denied))
7051
}
7152
}
7253
}

Sources/StreamVideo/Utils/PermissionsStore/Namespace/Middleware/PermissionStore+PushNotificationsMiddleware.swift

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ extension PermissionStore {
1010

1111
final class PushNotificationsMiddleware: Middleware<Namespace>, @unchecked Sendable {
1212

13+
private let permissionProvider: PushNotificationsPermissionProviding
14+
1315
override var dispatcher: Store<PermissionStore.Namespace>.Dispatcher? {
1416
didSet { didUpdate(dispatcher) }
1517
}
1618

19+
init(
20+
permissionProvider: PushNotificationsPermissionProviding = StreamPushNotificationsPermissionProvider()
21+
) {
22+
self.permissionProvider = permissionProvider
23+
}
24+
1725
override func apply(
1826
state: PermissionStore.StoreState,
1927
action: PermissionStore.StoreAction,
@@ -33,25 +41,7 @@ extension PermissionStore {
3341
// MARK: - Private Helpers
3442

3543
private func systemPermission() async -> Permission {
36-
let authorizationStatus = await UNUserNotificationCenter
37-
.current()
38-
.notificationSettings()
39-
.authorizationStatus
40-
41-
switch authorizationStatus {
42-
case .notDetermined:
43-
return .unknown
44-
case .provisional:
45-
return .granted
46-
case .denied:
47-
return .denied
48-
case .authorized:
49-
return .granted
50-
case .ephemeral:
51-
return .granted
52-
@unknown default:
53-
return .unknown
54-
}
44+
await permissionProvider.systemPermission()
5545
}
5646

5747
private func didUpdate(_ dispatcher: Store<Namespace>.Dispatcher?) {
@@ -68,11 +58,9 @@ extension PermissionStore {
6858
}
6959

7060
private func requestPermission(with options: UNAuthorizationOptions) {
71-
UNUserNotificationCenter
72-
.current()
73-
.requestAuthorization(options: options) { [weak self] in
74-
self?.didUpdateRequestAuthorization(granted: $0, error: $1)
75-
}
61+
permissionProvider.requestPermission(with: options) { [weak self] in
62+
self?.didUpdateRequestAuthorization(granted: $0, error: $1)
63+
}
7664
}
7765

7866
private func didUpdateRequestAuthorization(granted: Bool, error: Error?) {

Sources/StreamVideo/Utils/PermissionsStore/Namespace/PermissionStore+Action.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import UserNotifications
77

88
extension PermissionStore {
99

10-
public enum StoreAction: Sendable {
10+
public enum StoreAction: Sendable, Equatable {
1111
case setMicrophonePermission(Permission)
1212
case requestMicrophonePermission
1313

Sources/StreamVideo/Utils/PermissionsStore/PermissionsStore.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,31 @@ public final class PermissionStore: ObservableObject, @unchecked Sendable {
99

1010
@Injected(\.audioStore) private var audioStore
1111

12-
@Published public private(set) var hasMicrophonePermission: Bool = false
13-
@Published public private(set) var hasCameraPermission: Bool = false
12+
@Published public private(set) var hasMicrophonePermission: Bool
13+
@Published public private(set) var hasCameraPermission: Bool
1414

15-
private let store = Namespace.store(initialState: .initial)
15+
private let store: Store<Namespace>
1616
private let disposableBag = DisposableBag()
1717

18-
private static let shared = PermissionStore()
18+
static let shared = PermissionStore()
1919

20-
private init() {
20+
init(store: Store<Namespace> = Namespace.store(initialState: .initial)) {
21+
self.store = store
22+
hasMicrophonePermission = store.state.microphonePermission == .granted
23+
hasCameraPermission = store.state.cameraPermission == .granted
24+
2125
store
2226
.publisher(\.microphonePermission)
2327
.map { $0 == .granted }
2428
.receive(on: DispatchQueue.main)
25-
.assign(to: \.hasMicrophonePermission, on: self)
29+
.assign(to: \.hasMicrophonePermission, onWeak: self)
2630
.store(in: disposableBag)
2731

2832
store
2933
.publisher(\.cameraPermission)
3034
.map { $0 == .granted }
3135
.receive(on: DispatchQueue.main)
32-
.assign(to: \.hasCameraPermission, on: self)
36+
.assign(to: \.hasCameraPermission, onWeak: self)
3337
.store(in: disposableBag)
3438

3539
$hasMicrophonePermission

Sources/StreamVideo/Utils/SystemEnvironment.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ enum SystemEnvironment {
3737
#endif
3838
}
3939

40+
static var isTests: Bool {
41+
#if STREAM_TESTS
42+
return NSClassFromString("XCTest") != nil
43+
#else
44+
return false
45+
#endif
46+
}
47+
4048
private static var hasAppStoreReceipt: Bool {
4149
if let appStoreReceipt = Bundle.main.appStoreReceiptURL {
4250
return appStoreReceipt.lastPathComponent != "sandboxReceipt"

0 commit comments

Comments
 (0)