QR Code error views (#2678)

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Mauro 2024-04-11 10:32:56 +02:00 committed by GitHub
parent eda7d59518
commit 9bc24e2038
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 202 additions and 25 deletions

View File

@ -901,6 +901,18 @@ class ApplicationMock: ApplicationProtocol {
openReceivedInvocations.append(url)
openClosure?(url)
}
//MARK: - openAppSettings
var openAppSettingsCallsCount = 0
var openAppSettingsCalled: Bool {
return openAppSettingsCallsCount > 0
}
var openAppSettingsClosure: (() -> Void)?
func openAppSettings() {
openAppSettingsCallsCount += 1
openAppSettingsClosure?()
}
}
class AudioConverterMock: AudioConverterProtocol {

View File

@ -25,6 +25,7 @@ struct HeroImage: View {
case normal
case positive
case subtle
case critical
var foregroundColor: Color {
switch self {
@ -34,6 +35,8 @@ struct HeroImage: View {
return .compound.iconSuccessPrimary
case .subtle:
return .compound.iconSecondary
case .critical:
return .compound.iconCriticalPrimary
}
}
@ -45,6 +48,8 @@ struct HeroImage: View {
return .compound.bgSuccessSubtle
case .subtle:
return .compound.bgSubtlePrimary
case .critical:
return .compound.bgCanvasDefault
}
}
}

View File

@ -49,11 +49,7 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
case .close:
actionsSubject.send(.close)
case .openSystemSettings:
guard let url = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(url) else {
return
}
UIApplication.shared.open(url)
UIApplication.shared.openAppSettings()
case .sendLocation(let geoURI, let isUserLocation):
actionsSubject.send(.selectedLocation(geoURI, isUserLocation: isUserLocation))
}

View File

@ -40,7 +40,8 @@ final class QRCodeLoginScreenCoordinator: CoordinatorProtocol {
}
init(parameters: QRCodeLoginScreenCoordinatorParameters) {
viewModel = QRCodeLoginScreenViewModel(qrCodeLoginService: parameters.qrCodeLoginService)
viewModel = QRCodeLoginScreenViewModel(qrCodeLoginService: parameters.qrCodeLoginService,
application: UIApplication.shared)
orientationManager = parameters.orientationManager
}

View File

@ -23,7 +23,7 @@ enum QRCodeLoginScreenViewModelAction {
struct QRCodeLoginScreenViewState: BindableState {
var state: QRCodeLoginState = .initial
private let listItem3AttributedText = {
private static let initialStateListItem3AttributedText = {
let boldPlaceholder = "{bold}"
var finalString = AttributedString(L10n.screenQrCodeLoginInitialStateItem3(boldPlaceholder))
var boldString = AttributedString(L10n.screenQrCodeLoginInitialStateItem3Action)
@ -32,7 +32,7 @@ struct QRCodeLoginScreenViewState: BindableState {
return finalString
}()
private let listItem4AttributedText = {
private static let initialStateListItem4AttributedText = {
let boldPlaceholder = "{bold}"
var finalString = AttributedString(L10n.screenQrCodeLoginInitialStateItem4(boldPlaceholder))
var boldString = AttributedString(L10n.screenQrCodeLoginInitialStateItem4Action)
@ -41,19 +41,24 @@ struct QRCodeLoginScreenViewState: BindableState {
return finalString
}()
var listItems: [AttributedString] {
[
AttributedString(L10n.screenQrCodeLoginInitialStateItem1(InfoPlistReader.main.productionAppName)),
AttributedString(L10n.screenQrCodeLoginInitialStateItem2),
listItem3AttributedText,
listItem4AttributedText
]
}
let initialStateListItems = [
AttributedString(L10n.screenQrCodeLoginInitialStateItem1(InfoPlistReader.main.productionAppName)),
AttributedString(L10n.screenQrCodeLoginInitialStateItem2),
initialStateListItem3AttributedText,
initialStateListItem4AttributedText
]
let connectionNotSecureListItems = [
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem1),
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem2),
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem3)
]
}
enum QRCodeLoginScreenViewAction {
case cancel
case startScan
case openSettings
}
enum QRCodeLoginState: Equatable {
@ -66,6 +71,8 @@ enum QRCodeLoginState: Equatable {
enum QRCodeLoginErrorState: Equatable {
case noCameraPermission
case connectionNotSecure
case unknown
}
enum QRCodeLoginScanningState: Equatable {

View File

@ -22,14 +22,17 @@ typealias QRCodeLoginScreenViewModelType = StateStoreViewModel<QRCodeLoginScreen
class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScreenViewModelProtocol {
private let qrCodeLoginService: QRCodeLoginServiceProtocol
private let application: ApplicationProtocol
private let actionsSubject: PassthroughSubject<QRCodeLoginScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<QRCodeLoginScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(qrCodeLoginService: QRCodeLoginServiceProtocol) {
init(qrCodeLoginService: QRCodeLoginServiceProtocol,
application: ApplicationProtocol) {
self.qrCodeLoginService = qrCodeLoginService
self.application = application
super.init(initialViewState: QRCodeLoginScreenViewState())
}
@ -41,6 +44,8 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
actionsSubject.send(.cancel)
case .startScan:
Task { await startScanIfPossible() }
case .openSettings:
application.openAppSettings()
}
}
@ -51,6 +56,7 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
/// Only for mocking initial states
fileprivate init(state: QRCodeLoginState) {
qrCodeLoginService = QRCodeLoginServiceMock(configuration: .init())
application = ApplicationMock()
super.init(initialViewState: .init(state: state))
}
}

View File

@ -40,8 +40,7 @@ struct QRCodeLoginScreen: View {
case .scan:
qrScanContent
case .error:
// TODO: Handle error states
EmptyView()
errorContent
}
}
@ -58,7 +57,7 @@ struct QRCodeLoginScreen: View {
}
.padding(.horizontal, 24)
SFNumberedListView(items: context.viewState.listItems)
SFNumberedListView(items: context.viewState.initialStateListItems)
}
} bottomContent: {
Button(L10n.actionContinue) {
@ -107,7 +106,7 @@ struct QRCodeLoginScreen: View {
case .invalid:
VStack(spacing: 16) {
Button(L10n.screenQrCodeLoginInvalidScanStateRetryButton) {
// TODO: Implement try again
context.send(viewAction: .startScan)
}
.buttonStyle(.compound(.primary))
@ -125,7 +124,7 @@ struct QRCodeLoginScreen: View {
}
}
}
private var qrScanner: some View {
QRCodeScannerView()
.aspectRatio(1.0, contentMode: .fill)
@ -136,7 +135,7 @@ struct QRCodeLoginScreen: View {
QRScannerViewOverlay(length: qrFrame.height)
)
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
@ -145,6 +144,98 @@ struct QRCodeLoginScreen: View {
}
}
}
@ViewBuilder
private var errorContent: some View {
if case let .error(errorState) = context.viewState.state {
FullscreenDialog {
errorContentHeader(errorState: errorState)
} bottomContent: {
errorContentFooter(errorState: errorState)
}
.padding(.horizontal, 24)
}
}
@ViewBuilder
private func errorContentHeader(errorState: QRCodeLoginState.QRCodeLoginErrorState) -> some View {
switch errorState {
case .noCameraPermission:
VStack(spacing: 16) {
HeroImage(icon: \.takePhotoSolid, style: .subtle)
VStack(spacing: 8) {
Text(L10n.screenQrCodeLoginNoCameraPermissionStateTitle)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
Text(L10n.screenQrCodeLoginNoCameraPermissionStateDescription)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
}
}
case .connectionNotSecure:
VStack(spacing: 40) {
VStack(spacing: 16) {
HeroImage(icon: \.error, style: .critical)
VStack(spacing: 8) {
Text(L10n.screenQrCodeLoginConnectionNoteSecureStateTitle)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
Text(L10n.screenQrCodeLoginConnectionNoteSecureStateDescription)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
}
}
VStack(spacing: 24) {
Text(L10n.screenQrCodeLoginConnectionNoteSecureStateListHeader)
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyLGSemibold)
.multilineTextAlignment(.center)
SFNumberedListView(items: context.viewState.connectionNotSecureListItems)
}
}
case .unknown:
VStack(spacing: 16) {
HeroImage(icon: \.error, style: .critical)
VStack(spacing: 8) {
Text(L10n.commonSomethingWentWrong)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
Text(L10n.screenQrCodeLoginUnknownErrorDescription)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
}
}
}
}
private func errorContentFooter(errorState: QRCodeLoginState.QRCodeLoginErrorState) -> some View {
switch errorState {
case .noCameraPermission:
Button(L10n.screenQrCodeLoginNoCameraPermissionButton) {
context.send(viewAction: .openSettings)
}
.buttonStyle(.compound(.primary))
case .connectionNotSecure, .unknown:
Button(L10n.screenQrCodeLoginStartOverButton) {
context.send(viewAction: .startScan)
}
.buttonStyle(.compound(.primary))
}
}
}
private struct QRScannerViewOverlay: View {
@ -183,6 +274,12 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
static let invalidStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.invalid))
static let noCameraPermissionStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.noCameraPermission))
static let connectionNotSecureStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.connectionNotSecure))
static let unknownErrorStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.unknown))
static var previews: some View {
QRCodeLoginScreen(context: initialStateViewModel.context)
.previewDisplayName("Initial")
@ -195,5 +292,14 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
QRCodeLoginScreen(context: invalidStateViewModel.context)
.previewDisplayName("Invalid")
QRCodeLoginScreen(context: noCameraPermissionStateViewModel.context)
.previewDisplayName("No Camera Permission")
QRCodeLoginScreen(context: connectionNotSecureStateViewModel.context)
.previewDisplayName("Connection not secure")
QRCodeLoginScreen(context: unknownErrorStateViewModel.context)
.previewDisplayName("Unknown error")
}
}

View File

@ -619,8 +619,7 @@ class RoomScreenInteractionHandler {
}
private func openSystemSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
application.open(url)
application.openAppSettings()
}
private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> RoomTimelineControllerAction {

View File

@ -24,6 +24,8 @@ protocol ApplicationProtocol {
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier)
func open(_ url: URL)
func openAppSettings()
var backgroundTimeRemaining: TimeInterval { get }
@ -34,4 +36,11 @@ extension UIApplication: ApplicationProtocol {
func open(_ url: URL) {
open(url, options: [:], completionHandler: nil)
}
func openAppSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString) else {
return
}
open(url)
}
}