Skip to content

Commit

Permalink
QR Code error views (#2678)
Browse files Browse the repository at this point in the history
Co-authored-by: Doug <douglase@element.io>
  • Loading branch information
Velin92 and pixlwave authored Apr 11, 2024
1 parent eda7d59 commit 9bc24e2
Show file tree
Hide file tree
Showing 21 changed files with 202 additions and 25 deletions.
12 changes: 12 additions & 0 deletions ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
5 changes: 5 additions & 0 deletions ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ struct HeroImage: View {
case normal
case positive
case subtle
case critical

var foregroundColor: Color {
switch self {
Expand All @@ -34,6 +35,8 @@ struct HeroImage: View {
return .compound.iconSuccessPrimary
case .subtle:
return .compound.iconSecondary
case .critical:
return .compound.iconCriticalPrimary
}
}

Expand All @@ -45,6 +48,8 @@ struct HeroImage: View {
return .compound.bgSuccessSubtle
case .subtle:
return .compound.bgSubtlePrimary
case .critical:
return .compound.bgCanvasDefault
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -66,6 +71,8 @@ enum QRCodeLoginState: Equatable {

enum QRCodeLoginErrorState: Equatable {
case noCameraPermission
case connectionNotSecure
case unknown
}

enum QRCodeLoginScanningState: Equatable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand All @@ -41,6 +44,8 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
actionsSubject.send(.cancel)
case .startScan:
Task { await startScanIfPossible() }
case .openSettings:
application.openAppSettings()
}
}

Expand All @@ -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))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ struct QRCodeLoginScreen: View {
case .scan:
qrScanContent
case .error:
// TODO: Handle error states
EmptyView()
errorContent
}
}

Expand All @@ -58,7 +57,7 @@ struct QRCodeLoginScreen: View {
}
.padding(.horizontal, 24)

SFNumberedListView(items: context.viewState.listItems)
SFNumberedListView(items: context.viewState.initialStateListItems)
}
} bottomContent: {
Button(L10n.actionContinue) {
Expand Down Expand Up @@ -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))

Expand All @@ -125,7 +124,7 @@ struct QRCodeLoginScreen: View {
}
}
}

private var qrScanner: some View {
QRCodeScannerView()
.aspectRatio(1.0, contentMode: .fill)
Expand All @@ -136,7 +135,7 @@ struct QRCodeLoginScreen: View {
QRScannerViewOverlay(length: qrFrame.height)
)
}

@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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")
Expand All @@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ protocol ApplicationProtocol {
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier)

func open(_ url: URL)

func openAppSettings()

var backgroundTimeRemaining: TimeInterval { get }

Expand All @@ -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)
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 9bc24e2

Please sign in to comment.