Skip to content

Commit

Permalink
[ABW-737] Improve camera permissions UX (#191)
Browse files Browse the repository at this point in the history
Co-authored-by: David Roman <2538074+davdroman@users.noreply.github.com>
  • Loading branch information
davdroman-rdx and davdroman committed Dec 20, 2022
1 parent 963add6 commit 1251159
Show file tree
Hide file tree
Showing 26 changed files with 518 additions and 232 deletions.
32 changes: 20 additions & 12 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ package.addModules([
.feature(
name: "NewConnectionFeature",
dependencies: [
"CameraPermissionClient",
.product(name: "CodeScanner", package: "CodeScanner", condition: .when(platforms: [.iOS])),
converse,
"Common",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

// MARK: - UserDefaultsClient
public struct CameraPermissionClient: Sendable {
public var getCameraAccess: @Sendable () async -> Bool
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
)
}
Original file line number Diff line number Diff line change
@@ -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 }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public extension P2PConnectivityClient {
let localNetworkAuthorization = LocalNetworkAuthorization()

return Self(
getLocalNetworkAuthorization: {
getLocalNetworkAccess: {
await localNetworkAuthorization.requestAuthorization()
},
getP2PClients: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -22,7 +22,7 @@ extension P2PConnectivityClient: TestDependencyKey {

extension P2PConnectivityClient {
static let noop = Self(
getLocalNetworkAuthorization: { false },
getLocalNetworkAccess: { false },
getP2PClients: { [].async.eraseToAnyAsyncSequence() },
addP2PClientWithConnection: { _, _ in },
deleteP2PClientByID: { _ in },
Expand Down
24 changes: 18 additions & 6 deletions Sources/Core/Resources/Generated/L10n.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
Expand Down
12 changes: 8 additions & 4 deletions Sources/Core/Resources/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<Action> {
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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import ComposableArchitecture
import Foundation

// MARK: - CameraPermission.State
public extension CameraPermission {
struct State: Sendable, Equatable {
var permissionDeniedAlert: AlertState<Action.ViewAction.PermissionDeniedAlertAction>?

init() {
self.permissionDeniedAlert = nil
}
}
}

#if DEBUG
public extension CameraPermission.State {
static let previewValue: Self = .init()
}
#endif
Loading

0 comments on commit 1251159

Please sign in to comment.