diff --git a/Package.swift b/Package.swift index 4995ed953d..dd63492d04 100644 --- a/Package.swift +++ b/Package.swift @@ -533,6 +533,7 @@ package.addModules([ .feature( name: "NewConnectionFeature", dependencies: [ + "CameraPermissionClient", .product(name: "CodeScanner", package: "CodeScanner", condition: .when(platforms: [.iOS])), converse, "Common", @@ -672,21 +673,11 @@ package.addModules([ ) ), .client( - name: "P2PConnectivityClient", + name: "CameraPermissionClient", dependencies: [ - asyncExtensions, - "Common", - converse, dependencies, - engineToolkit, // Model: SignTX contains Manifest - "JSON", - profile, // Account - "ProfileClient", - "SharedModels", ], - tests: .yes(dependencies: [ - "TestUtils", - ]) + tests: .no ), .client( name: "Data", @@ -776,6 +767,23 @@ package.addModules([ dependencies: ["TestUtils"] ) ), + .client( + name: "P2PConnectivityClient", + dependencies: [ + asyncExtensions, + "Common", + converse, + dependencies, + engineToolkit, // Model: SignTX contains Manifest + "JSON", + profile, // Account + "ProfileClient", + "SharedModels", + ], + tests: .yes(dependencies: [ + "TestUtils", + ]) + ), .client( name: "PasteboardClient", dependencies: [dependencies], diff --git a/Sources/Clients/CameraPermissionClient/CameraPermissionClient+Interface.swift b/Sources/Clients/CameraPermissionClient/CameraPermissionClient+Interface.swift new file mode 100644 index 0000000000..f138b3cfee --- /dev/null +++ b/Sources/Clients/CameraPermissionClient/CameraPermissionClient+Interface.swift @@ -0,0 +1,6 @@ +import Foundation + +// MARK: - UserDefaultsClient +public struct CameraPermissionClient: Sendable { + public var getCameraAccess: @Sendable () async -> Bool +} diff --git a/Sources/Clients/CameraPermissionClient/CameraPermissionClient+Live.swift b/Sources/Clients/CameraPermissionClient/CameraPermissionClient+Live.swift new file mode 100644 index 0000000000..ba59187a69 --- /dev/null +++ b/Sources/Clients/CameraPermissionClient/CameraPermissionClient+Live.swift @@ -0,0 +1,16 @@ +import AVKit +import Dependencies +import Foundation + +// MARK: - UserDefaultsClient + DependencyKey +extension CameraPermissionClient: DependencyKey { + public static let liveValue = Self( + getCameraAccess: { + await withCheckedContinuation { continuation in + AVCaptureDevice.requestAccess(for: .video) { access in + continuation.resume(returning: access) + } + } + } + ) +} diff --git a/Sources/Clients/CameraPermissionClient/CameraPermissionClient+Test.swift b/Sources/Clients/CameraPermissionClient/CameraPermissionClient+Test.swift new file mode 100644 index 0000000000..a69e9b984a --- /dev/null +++ b/Sources/Clients/CameraPermissionClient/CameraPermissionClient+Test.swift @@ -0,0 +1,25 @@ +import Dependencies +import Foundation +import XCTestDynamicOverlay + +public extension DependencyValues { + var cameraPermissionClient: CameraPermissionClient { + get { self[CameraPermissionClient.self] } + set { self[CameraPermissionClient.self] = newValue } + } +} + +// MARK: - CameraPermissionClient + TestDependencyKey +extension CameraPermissionClient: TestDependencyKey { + public static let previewValue = Self.noop + + public static let testValue = Self( + getCameraAccess: unimplemented("\(Self.self).getCameraAccess") + ) +} + +public extension CameraPermissionClient { + static let noop = Self( + getCameraAccess: { false } + ) +} diff --git a/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Interface.swift b/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Interface.swift index c73832197d..3db093d788 100644 --- a/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Interface.swift +++ b/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Interface.swift @@ -19,7 +19,7 @@ public extension DependencyValues { // MARK: - P2PConnectivityClient public struct P2PConnectivityClient: DependencyKey, Sendable { - public var getLocalNetworkAuthorization: GetLocalNetworkAuthorization + public var getLocalNetworkAccess: GetLocalNetworkAccess public var getP2PClients: GetP2PClients public var addP2PClientWithConnection: AddP2PClientWithConnection public var deleteP2PClientByID: DeleteP2PClientByID @@ -31,7 +31,7 @@ public struct P2PConnectivityClient: DependencyKey, Sendable { } public extension P2PConnectivityClient { - typealias GetLocalNetworkAuthorization = @Sendable () async -> Bool + typealias GetLocalNetworkAccess = @Sendable () async -> Bool typealias GetP2PClients = @Sendable () async throws -> AnyAsyncSequence<[P2P.ClientWithConnectionStatus]> typealias AddP2PClientWithConnection = @Sendable (P2P.ConnectionForClient, AlsoConnect) async throws -> Void; typealias AlsoConnect = Bool typealias DeleteP2PClientByID = @Sendable (P2PClient.ID) async throws -> Void diff --git a/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Live.swift b/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Live.swift index dd9be79a48..537ca6e9b3 100644 --- a/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Live.swift +++ b/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Live.swift @@ -77,7 +77,7 @@ public extension P2PConnectivityClient { let localNetworkAuthorization = LocalNetworkAuthorization() return Self( - getLocalNetworkAuthorization: { + getLocalNetworkAccess: { await localNetworkAuthorization.requestAuthorization() }, getP2PClients: { diff --git a/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Test.swift b/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Test.swift index c130311fa0..0599d55464 100644 --- a/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Test.swift +++ b/Sources/Clients/P2PConnectivityClient/P2PConnectivityClient+Test.swift @@ -9,7 +9,7 @@ import XCTestDynamicOverlay extension P2PConnectivityClient: TestDependencyKey { public static let previewValue = Self.noop public static let testValue = Self( - getLocalNetworkAuthorization: unimplemented("\(Self.self).getLocalNetworkAuthorization"), + getLocalNetworkAccess: unimplemented("\(Self.self).getLocalNetworkAccess"), getP2PClients: unimplemented("\(Self.self).getP2PClients"), addP2PClientWithConnection: unimplemented("\(Self.self).addP2PClientWithConnection"), deleteP2PClientByID: unimplemented("\(Self.self).deleteP2PClientByID"), @@ -22,7 +22,7 @@ extension P2PConnectivityClient: TestDependencyKey { extension P2PConnectivityClient { static let noop = Self( - getLocalNetworkAuthorization: { false }, + getLocalNetworkAccess: { false }, getP2PClients: { [].async.eraseToAnyAsyncSequence() }, addP2PClientWithConnection: { _, _ in }, deleteP2PClientByID: { _ in }, diff --git a/Sources/Core/Resources/Generated/L10n.generated.swift b/Sources/Core/Resources/Generated/L10n.generated.swift index 11f115b9c2..aac3675203 100644 --- a/Sources/Core/Resources/Generated/L10n.generated.swift +++ b/Sources/Core/Resources/Generated/L10n.generated.swift @@ -215,16 +215,28 @@ public enum L10n { public static let textFieldPlaceholder = L10n.tr("Localizable", "newConnection.textFieldPlaceholder", fallback: "Name of Connector") /// Link to Connector public static let title = L10n.tr("Localizable", "newConnection.title", fallback: "Link to Connector") - public enum LocalNetworkAuthorization { + public enum CameraPermission { public enum DeniedAlert { /// Cancel - public static let cancelButtonTitle = L10n.tr("Localizable", "newConnection.LocalNetworkAuthorization.DeniedAlert.cancelButtonTitle", fallback: "Cancel") + public static let cancelButtonTitle = L10n.tr("Localizable", "newConnection.cameraPermission.deniedAlert.cancelButtonTitle", fallback: "Cancel") + /// Camera access is required to link to connector. + public static let message = L10n.tr("Localizable", "newConnection.cameraPermission.deniedAlert.message", fallback: "Camera access is required to link to connector.") + /// Settings + public static let settingsButtonTitle = L10n.tr("Localizable", "newConnection.cameraPermission.deniedAlert.settingsButtonTitle", fallback: "Settings") + /// Access Required + public static let title = L10n.tr("Localizable", "newConnection.cameraPermission.deniedAlert.title", fallback: "Access Required") + } + } + public enum LocalNetworkPermission { + public enum DeniedAlert { + /// Cancel + public static let cancelButtonTitle = L10n.tr("Localizable", "newConnection.localNetworkPermission.deniedAlert.cancelButtonTitle", fallback: "Cancel") /// Local Network access is required to link to connector. - public static let message = L10n.tr("Localizable", "newConnection.LocalNetworkAuthorization.DeniedAlert.message", fallback: "Local Network access is required to link to connector.") + public static let message = L10n.tr("Localizable", "newConnection.localNetworkPermission.deniedAlert.message", fallback: "Local Network access is required to link to connector.") /// Settings - public static let settingsButtonTitle = L10n.tr("Localizable", "newConnection.LocalNetworkAuthorization.DeniedAlert.settingsButtonTitle", fallback: "Settings") - /// Permission Required - public static let title = L10n.tr("Localizable", "newConnection.LocalNetworkAuthorization.DeniedAlert.title", fallback: "Permission Required") + public static let settingsButtonTitle = L10n.tr("Localizable", "newConnection.localNetworkPermission.deniedAlert.settingsButtonTitle", fallback: "Settings") + /// Access Required + public static let title = L10n.tr("Localizable", "newConnection.localNetworkPermission.deniedAlert.title", fallback: "Access Required") } } } diff --git a/Sources/Core/Resources/Resources/en.lproj/Localizable.strings b/Sources/Core/Resources/Resources/en.lproj/Localizable.strings index b931829a53..c4c63cb1cf 100644 --- a/Sources/Core/Resources/Resources/en.lproj/Localizable.strings +++ b/Sources/Core/Resources/Resources/en.lproj/Localizable.strings @@ -77,10 +77,14 @@ "newConnection.textFieldHint" = "Name this Connector, e.g. \"Chrome on Macbook Pro\""; "newConnection.textFieldPlaceholder" = "Name of Connector"; "newConnection.title" = "Link to Connector"; -"newConnection.LocalNetworkAuthorization.DeniedAlert.title" = "Permission Required"; -"newConnection.LocalNetworkAuthorization.DeniedAlert.message" = "Local Network access is required to link to connector."; -"newConnection.LocalNetworkAuthorization.DeniedAlert.cancelButtonTitle" = "Cancel"; -"newConnection.LocalNetworkAuthorization.DeniedAlert.settingsButtonTitle" = "Settings"; +"newConnection.localNetworkPermission.deniedAlert.title" = "Access Required"; +"newConnection.localNetworkPermission.deniedAlert.message" = "Local Network access is required to link to connector."; +"newConnection.localNetworkPermission.deniedAlert.cancelButtonTitle" = "Cancel"; +"newConnection.localNetworkPermission.deniedAlert.settingsButtonTitle" = "Settings"; +"newConnection.cameraPermission.deniedAlert.title" = "Access Required"; +"newConnection.cameraPermission.deniedAlert.message" = "Camera access is required to link to connector."; +"newConnection.cameraPermission.deniedAlert.cancelButtonTitle" = "Cancel"; +"newConnection.cameraPermission.deniedAlert.settingsButtonTitle" = "Settings"; "manageP2PClients.P2PConnectionsTitle" = "Linked Connector"; "manageP2PClients.P2PConnectionsSubtitle" = "Your Radix Wallet is linked to the following desktop browser using the Connector browser extension."; diff --git a/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+Action.swift b/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+Action.swift new file mode 100644 index 0000000000..126d1c399e --- /dev/null +++ b/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+Action.swift @@ -0,0 +1,48 @@ +import ComposableArchitecture +import ConverseCommon +import Foundation + +// MARK: - CameraPermission.Action +public extension CameraPermission { + enum Action: Sendable, Equatable { + static func view(_ action: ViewAction) -> Self { .internal(.view(action)) } + case `internal`(InternalAction) + case delegate(DelegateAction) + } +} + +// MARK: - CameraPermission.Action.ViewAction +public extension CameraPermission.Action { + enum ViewAction: Sendable, Equatable { + public enum PermissionDeniedAlertAction: Sendable, Equatable { + case dismissed + case cancelButtonTapped + case openSettingsButtonTapped + } + + case appeared + case permissionDeniedAlert(PermissionDeniedAlertAction) + } +} + +// MARK: - CameraPermission.Action.InternalAction +public extension CameraPermission.Action { + enum InternalAction: Sendable, Equatable { + case view(ViewAction) + case system(SystemAction) + } +} + +// MARK: - CameraPermission.Action.SystemAction +public extension CameraPermission.Action { + enum SystemAction: Sendable, Equatable { + case displayPermissionDeniedAlert + } +} + +// MARK: - CameraPermission.Action.DelegateAction +public extension CameraPermission.Action { + enum DelegateAction: Sendable, Equatable { + case permissionResponse(Bool) + } +} diff --git a/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+Reducer.swift b/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+Reducer.swift new file mode 100644 index 0000000000..7d4a8f0152 --- /dev/null +++ b/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+Reducer.swift @@ -0,0 +1,68 @@ +import CameraPermissionClient +import Common +import ComposableArchitecture +#if os(iOS) +import class UIKit.UIApplication +#endif + +// MARK: - CameraPermission +public struct CameraPermission: Sendable, ReducerProtocol { + @Dependency(\.cameraPermissionClient) var cameraPermissionClient + @Dependency(\.openURL) var openURL + + public init() {} +} + +public extension CameraPermission { + func reduce(into state: inout State, action: Action) -> EffectTask { + switch action { + case .internal(.view(.appeared)): + return .run { send in + let allowed = await cameraPermissionClient.getCameraAccess() + if allowed { + await send(.delegate(.permissionResponse(true))) + } else { + await send(.internal(.system(.displayPermissionDeniedAlert))) + } + } + + case .internal(.system(.displayPermissionDeniedAlert)): + state.permissionDeniedAlert = .init( + title: { TextState(L10n.NewConnection.CameraPermission.DeniedAlert.title) }, + actions: { + ButtonState( + role: .cancel, + action: .send(.cancelButtonTapped), + label: { TextState(L10n.NewConnection.CameraPermission.DeniedAlert.cancelButtonTitle) } + ) + ButtonState( + role: .none, + action: .send(.openSettingsButtonTapped), + label: { TextState(L10n.NewConnection.CameraPermission.DeniedAlert.settingsButtonTitle) } + ) + }, + message: { TextState(L10n.NewConnection.CameraPermission.DeniedAlert.message) } + ) + return .none + + case let .internal(.view(.permissionDeniedAlert(action))): + state.permissionDeniedAlert = nil + switch action { + case .dismissed, .cancelButtonTapped: + return .run { send in + await send(.delegate(.permissionResponse(false))) + } + case .openSettingsButtonTapped: + return .run { send in + await send(.delegate(.permissionResponse(false))) + #if os(iOS) + await openURL(URL(string: UIApplication.openSettingsURLString)!) + #endif + } + } + + case .delegate: + return .none + } + } +} diff --git a/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+State.swift b/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+State.swift new file mode 100644 index 0000000000..022ac5da4c --- /dev/null +++ b/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+State.swift @@ -0,0 +1,19 @@ +import ComposableArchitecture +import Foundation + +// MARK: - CameraPermission.State +public extension CameraPermission { + struct State: Sendable, Equatable { + var permissionDeniedAlert: AlertState? + + init() { + self.permissionDeniedAlert = nil + } + } +} + +#if DEBUG +public extension CameraPermission.State { + static let previewValue: Self = .init() +} +#endif diff --git a/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+View.swift b/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+View.swift new file mode 100644 index 0000000000..56cff855f1 --- /dev/null +++ b/Sources/Features/NewConnectionFeature/Children/CameraPermission/CameraPermission+View.swift @@ -0,0 +1,57 @@ +import ComposableArchitecture +import Resources +import SwiftUI + +// MARK: - CameraPermission.View +public extension CameraPermission { + @MainActor + struct View: SwiftUI.View { + private let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + } +} + +public extension CameraPermission.View { + var body: some View { + WithViewStore( + store, + observe: ViewState.init(state:), + send: { .view($0) } + ) { viewStore in + ZStack {} + .alert( + store.scope( + state: \.permissionDeniedAlert, + action: { .view(.permissionDeniedAlert($0)) } + ), + dismiss: .dismissed + ) + .onAppear { viewStore.send(.appeared) } + } + } +} + +// MARK: - CameraPermission.View.ViewState +extension CameraPermission.View { + struct ViewState: Equatable { + init(state: CameraPermission.State) {} + } +} + +#if DEBUG + +// MARK: - ScanQR_Preview +struct CameraPermission_Preview: PreviewProvider { + static var previews: some View { + CameraPermission.View( + store: .init( + initialState: .previewValue, + reducer: CameraPermission() + ) + ) + } +} +#endif diff --git a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+Action.swift b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+Action.swift deleted file mode 100644 index ee121b8714..0000000000 --- a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+Action.swift +++ /dev/null @@ -1,48 +0,0 @@ -import ComposableArchitecture -import ConverseCommon -import Foundation - -// MARK: - LocalNetworkAuthorization.Action -public extension LocalNetworkAuthorization { - enum Action: Sendable, Equatable { - static func view(_ action: ViewAction) -> Self { .internal(.view(action)) } - case `internal`(InternalAction) - case delegate(DelegateAction) - } -} - -// MARK: - LocalNetworkAuthorization.Action.ViewAction -public extension LocalNetworkAuthorization.Action { - enum ViewAction: Sendable, Equatable { - public enum AuthorizationDeniedAlertAction: Sendable, Equatable { - case dismissed - case cancelButtonTapped - case openSettingsButtonTapped - } - - case appeared - case authorizationDeniedAlert(AuthorizationDeniedAlertAction) - } -} - -// MARK: - LocalNetworkAuthorization.Action.InternalAction -public extension LocalNetworkAuthorization.Action { - enum InternalAction: Sendable, Equatable { - case view(ViewAction) - case system(SystemAction) - } -} - -// MARK: - LocalNetworkAuthorization.Action.SystemAction -public extension LocalNetworkAuthorization.Action { - enum SystemAction: Sendable, Equatable { - case displayAuthorizationDeniedAlert - } -} - -// MARK: - LocalNetworkAuthorization.Action.DelegateAction -public extension LocalNetworkAuthorization.Action { - enum DelegateAction: Sendable, Equatable { - case localNetworkAuthorizationResponse(Bool) - } -} diff --git a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+Reducer.swift b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+Reducer.swift deleted file mode 100644 index 1c3b1b170f..0000000000 --- a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+Reducer.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Common -import ComposableArchitecture -import P2PConnectivityClient -#if os(iOS) -import class UIKit.UIApplication -#endif - -// MARK: - LocalNetworkAuthorization -public struct LocalNetworkAuthorization: Sendable, ReducerProtocol { - @Dependency(\.p2pConnectivityClient) var p2pConnectivityClient - @Dependency(\.openURL) var openURL - - public init() {} -} - -public extension LocalNetworkAuthorization { - func reduce(into state: inout State, action: Action) -> EffectTask { - switch action { - case .internal(.view(.appeared)): - return .run { send in - let isLocalNetworkAuthorized = await p2pConnectivityClient.getLocalNetworkAuthorization() - if isLocalNetworkAuthorized { - await send(.delegate(.localNetworkAuthorizationResponse(true))) - } else { - await send(.internal(.system(.displayAuthorizationDeniedAlert))) - } - } - - case .internal(.system(.displayAuthorizationDeniedAlert)): - state.authorizationDeniedAlert = .init( - title: { TextState(L10n.NewConnection.LocalNetworkAuthorization.DeniedAlert.title) }, - actions: { - ButtonState( - role: .cancel, - action: .send(.cancelButtonTapped), - label: { TextState(L10n.NewConnection.LocalNetworkAuthorization.DeniedAlert.cancelButtonTitle) } - ) - ButtonState( - role: .none, - action: .send(.openSettingsButtonTapped), - label: { TextState(L10n.NewConnection.LocalNetworkAuthorization.DeniedAlert.settingsButtonTitle) } - ) - }, - message: { TextState(L10n.NewConnection.LocalNetworkAuthorization.DeniedAlert.message) } - ) - return .none - - case let .internal(.view(.authorizationDeniedAlert(action))): - state.authorizationDeniedAlert = nil - switch action { - case .dismissed: - return .none - case .cancelButtonTapped: - return .run { send in - await send(.delegate(.localNetworkAuthorizationResponse(false))) - } - case .openSettingsButtonTapped: - return .run { send in - await send(.delegate(.localNetworkAuthorizationResponse(false))) - #if os(iOS) - await openURL(URL(string: UIApplication.openSettingsURLString)!) - #endif - } - } - - case .delegate: - return .none - } - } -} diff --git a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+State.swift b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+State.swift deleted file mode 100644 index e4c7f0c809..0000000000 --- a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+State.swift +++ /dev/null @@ -1,19 +0,0 @@ -import ComposableArchitecture -import Foundation - -// MARK: - LocalNetworkAuthorization.State -public extension LocalNetworkAuthorization { - struct State: Sendable, Equatable { - var authorizationDeniedAlert: AlertState? - - init() { - self.authorizationDeniedAlert = nil - } - } -} - -#if DEBUG -public extension LocalNetworkAuthorization.State { - static let previewValue: Self = .init() -} -#endif diff --git a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+View.swift b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+View.swift deleted file mode 100644 index 42ffeaaee2..0000000000 --- a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkAuthorization+View.swift +++ /dev/null @@ -1,57 +0,0 @@ -import ComposableArchitecture -import Resources -import SwiftUI - -// MARK: - LocalNetworkAuthorization.View -public extension LocalNetworkAuthorization { - @MainActor - struct View: SwiftUI.View { - private let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - } -} - -public extension LocalNetworkAuthorization.View { - var body: some View { - WithViewStore( - store, - observe: ViewState.init(state:), - send: { .view($0) } - ) { viewStore in - Color.clear - .alert( - store.scope( - state: \.authorizationDeniedAlert, - action: { .view(.authorizationDeniedAlert($0)) } - ), - dismiss: .dismissed - ) - .onAppear { viewStore.send(.appeared) } - } - } -} - -// MARK: - LocalNetworkAuthorization.View.ViewState -extension LocalNetworkAuthorization.View { - struct ViewState: Equatable { - init(state: LocalNetworkAuthorization.State) {} - } -} - -#if DEBUG - -// MARK: - ScanQR_Preview -struct LocalNetworkAuthorization_Preview: PreviewProvider { - static var previews: some View { - LocalNetworkAuthorization.View( - store: .init( - initialState: .previewValue, - reducer: LocalNetworkAuthorization() - ) - ) - } -} -#endif diff --git a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+Action.swift b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+Action.swift new file mode 100644 index 0000000000..fe9c105394 --- /dev/null +++ b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+Action.swift @@ -0,0 +1,48 @@ +import ComposableArchitecture +import ConverseCommon +import Foundation + +// MARK: - LocalNetworkPermission.Action +public extension LocalNetworkPermission { + enum Action: Sendable, Equatable { + static func view(_ action: ViewAction) -> Self { .internal(.view(action)) } + case `internal`(InternalAction) + case delegate(DelegateAction) + } +} + +// MARK: - LocalNetworkPermission.Action.ViewAction +public extension LocalNetworkPermission.Action { + enum ViewAction: Sendable, Equatable { + public enum PermissionDeniedAlertAction: Sendable, Equatable { + case dismissed + case cancelButtonTapped + case openSettingsButtonTapped + } + + case appeared + case permissionDeniedAlert(PermissionDeniedAlertAction) + } +} + +// MARK: - LocalNetworkPermission.Action.InternalAction +public extension LocalNetworkPermission.Action { + enum InternalAction: Sendable, Equatable { + case view(ViewAction) + case system(SystemAction) + } +} + +// MARK: - LocalNetworkPermission.Action.SystemAction +public extension LocalNetworkPermission.Action { + enum SystemAction: Sendable, Equatable { + case displayPermissionDeniedAlert + } +} + +// MARK: - LocalNetworkPermission.Action.DelegateAction +public extension LocalNetworkPermission.Action { + enum DelegateAction: Sendable, Equatable { + case permissionResponse(Bool) + } +} diff --git a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+Reducer.swift b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+Reducer.swift new file mode 100644 index 0000000000..28b68c2817 --- /dev/null +++ b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+Reducer.swift @@ -0,0 +1,68 @@ +import Common +import ComposableArchitecture +import P2PConnectivityClient +#if os(iOS) +import class UIKit.UIApplication +#endif + +// MARK: - LocalNetworkPermission +public struct LocalNetworkPermission: Sendable, ReducerProtocol { + @Dependency(\.p2pConnectivityClient) var p2pConnectivityClient + @Dependency(\.openURL) var openURL + + public init() {} +} + +public extension LocalNetworkPermission { + func reduce(into state: inout State, action: Action) -> EffectTask { + switch action { + case .internal(.view(.appeared)): + return .run { send in + let allowed = await p2pConnectivityClient.getLocalNetworkAccess() + if allowed { + await send(.delegate(.permissionResponse(true))) + } else { + await send(.internal(.system(.displayPermissionDeniedAlert))) + } + } + + case .internal(.system(.displayPermissionDeniedAlert)): + state.permissionDeniedAlert = .init( + title: { TextState(L10n.NewConnection.LocalNetworkPermission.DeniedAlert.title) }, + actions: { + ButtonState( + role: .cancel, + action: .send(.cancelButtonTapped), + label: { TextState(L10n.NewConnection.LocalNetworkPermission.DeniedAlert.cancelButtonTitle) } + ) + ButtonState( + role: .none, + action: .send(.openSettingsButtonTapped), + label: { TextState(L10n.NewConnection.LocalNetworkPermission.DeniedAlert.settingsButtonTitle) } + ) + }, + message: { TextState(L10n.NewConnection.LocalNetworkPermission.DeniedAlert.message) } + ) + return .none + + case let .internal(.view(.permissionDeniedAlert(action))): + state.permissionDeniedAlert = nil + switch action { + case .dismissed, .cancelButtonTapped: + return .run { send in + await send(.delegate(.permissionResponse(false))) + } + case .openSettingsButtonTapped: + return .run { send in + await send(.delegate(.permissionResponse(false))) + #if os(iOS) + await openURL(URL(string: UIApplication.openSettingsURLString)!) + #endif + } + } + + case .delegate: + return .none + } + } +} diff --git a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+State.swift b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+State.swift new file mode 100644 index 0000000000..771026d006 --- /dev/null +++ b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+State.swift @@ -0,0 +1,19 @@ +import ComposableArchitecture +import Foundation + +// MARK: - LocalNetworkPermission.State +public extension LocalNetworkPermission { + struct State: Sendable, Equatable { + var permissionDeniedAlert: AlertState? + + init() { + self.permissionDeniedAlert = nil + } + } +} + +#if DEBUG +public extension LocalNetworkPermission.State { + static let previewValue: Self = .init() +} +#endif diff --git a/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+View.swift b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+View.swift new file mode 100644 index 0000000000..33284ba3ee --- /dev/null +++ b/Sources/Features/NewConnectionFeature/Children/LocalNetworkAuthorization/LocalNetworkPermission+View.swift @@ -0,0 +1,59 @@ +import ComposableArchitecture +import Resources +import SwiftUI + +// MARK: - LocalNetworkPermission.View +public extension LocalNetworkPermission { + @MainActor + struct View: SwiftUI.View { + private let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + } +} + +public extension LocalNetworkPermission.View { + var body: some View { + WithViewStore( + store, + observe: ViewState.init(state:), + send: { .view($0) } + ) { viewStore in + ZStack {} + .alert( + store.scope( + state: \.permissionDeniedAlert, + action: { .view(.permissionDeniedAlert($0)) } + ), + dismiss: .dismissed + ) + .onAppear { viewStore.send(.appeared) } + } + } +} + +// MARK: - LocalNetworkPermission.View.ViewState +extension LocalNetworkPermission.View { + struct ViewState: Equatable { + init(state: LocalNetworkPermission.State) {} + } +} + +#if DEBUG + +// MARK: - ScanQR_Preview +extension LocalNetworkPermission { + struct Preview: PreviewProvider { + static var previews: some SwiftUI.View { + LocalNetworkPermission.View( + store: .init( + initialState: .previewValue, + reducer: LocalNetworkPermission() + ) + ) + } + } +} +#endif diff --git a/Sources/Features/NewConnectionFeature/Children/ScanQR/ScanQR+Reducer.swift b/Sources/Features/NewConnectionFeature/Children/ScanQR/ScanQR+Reducer.swift index 8292070a52..12d7def5e4 100644 --- a/Sources/Features/NewConnectionFeature/Children/ScanQR/ScanQR+Reducer.swift +++ b/Sources/Features/NewConnectionFeature/Children/ScanQR/ScanQR+Reducer.swift @@ -1,3 +1,4 @@ +import CameraPermissionClient import Common import ComposableArchitecture import ConverseCommon diff --git a/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+Action.swift b/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+Action.swift index 0f602921e8..6f0e652455 100644 --- a/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+Action.swift +++ b/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+Action.swift @@ -15,7 +15,8 @@ public extension NewConnection { // MARK: - NewConnection.Action.ChildAction public extension NewConnection.Action { enum ChildAction: Sendable, Equatable { - case localNetworkAuthorization(LocalNetworkAuthorization.Action) + case cameraPermission(CameraPermission.Action) + case localNetworkPermission(LocalNetworkPermission.Action) case scanQR(ScanQR.Action) case connectUsingSecrets(ConnectUsingSecrets.Action) } diff --git a/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+Reducer.swift b/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+Reducer.swift index 9b60bbae3d..84a69b8a3a 100644 --- a/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+Reducer.swift +++ b/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+Reducer.swift @@ -13,8 +13,11 @@ public extension NewConnection { Reduce(core) EmptyReducer() - .ifCaseLet(/State.localNetworkAuthorization, action: /Action.child .. Action.ChildAction.localNetworkAuthorization) { - LocalNetworkAuthorization() + .ifCaseLet(/State.localNetworkPermission, action: /Action.child .. Action.ChildAction.localNetworkPermission) { + LocalNetworkPermission() + } + .ifCaseLet(/State.cameraPermission, action: /Action.child .. Action.ChildAction.cameraPermission) { + CameraPermission() } .ifCaseLet(/State.scanQR, action: /Action.child .. Action.ChildAction.scanQR) { ScanQR() @@ -28,7 +31,7 @@ public extension NewConnection { switch action { case .internal(.view(.dismissButtonTapped)): switch state { - case .localNetworkAuthorization, .scanQR: + case .localNetworkPermission, .cameraPermission, .scanQR: return .run { send in await send(.delegate(.dismiss)) } @@ -52,8 +55,20 @@ public extension NewConnection { ) } - case let .child(.localNetworkAuthorization(.delegate(.localNetworkAuthorizationResponse(isAuthorized)))): - if isAuthorized { + case let .child(.localNetworkPermission(.delegate(.permissionResponse(allowed)))): + if allowed { + #if os(iOS) + state = .cameraPermission(.init()) + #elseif os(macOS) + state = .scanQR(.init()) + #endif + return .none + } else { + return .run { send in await send(.delegate(.dismiss)) } + } + + case let .child(.cameraPermission(.delegate(.permissionResponse(allowed)))): + if allowed { state = .scanQR(.init()) return .none } else { diff --git a/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+State.swift b/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+State.swift index 4abd3b1d30..fc24522577 100644 --- a/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+State.swift +++ b/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+State.swift @@ -5,12 +5,13 @@ import SwiftUI // MARK: - NewConnection.State public extension NewConnection { enum State: Equatable { - case localNetworkAuthorization(LocalNetworkAuthorization.State) + case localNetworkPermission(LocalNetworkPermission.State) + case cameraPermission(CameraPermission.State) case scanQR(ScanQR.State) case connectUsingSecrets(ConnectUsingSecrets.State) public init() { - self = .localNetworkAuthorization(.init()) + self = .localNetworkPermission(.init()) } } } diff --git a/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+View.swift b/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+View.swift index 3533817bd0..34f1f630d3 100644 --- a/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+View.swift +++ b/Sources/Features/NewConnectionFeature/Coordinator/NewConnectionFeature+View.swift @@ -37,9 +37,14 @@ public extension NewConnection.View { ForceFullScreen { SwitchStore(store) { CaseLet( - state: /NewConnection.State.localNetworkAuthorization, - action: { NewConnection.Action.child(.localNetworkAuthorization($0)) }, - then: { LocalNetworkAuthorization.View(store: $0) } + state: /NewConnection.State.localNetworkPermission, + action: { NewConnection.Action.child(.localNetworkPermission($0)) }, + then: { LocalNetworkPermission.View(store: $0) } + ) + CaseLet( + state: /NewConnection.State.cameraPermission, + action: { NewConnection.Action.child(.cameraPermission($0)) }, + then: { CameraPermission.View(store: $0) } ) CaseLet( state: /NewConnection.State.scanQR,