diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 463dea13b..2d8608ca8 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -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 { diff --git a/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift b/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift index 41f6d6dfe..c2bdd3b65 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift @@ -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 } } } diff --git a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift index 514f8213d..e2887b651 100644 --- a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift @@ -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)) } diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenCoordinator.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenCoordinator.swift index ed67a5ed8..0d98e85ad 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenCoordinator.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenCoordinator.swift @@ -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 } diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenModels.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenModels.swift index 53c187de7..f08c08226 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenModels.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenModels.swift @@ -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 { diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift index 8b9f6997b..ca751ce53 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift @@ -22,14 +22,17 @@ typealias QRCodeLoginScreenViewModelType = StateStoreViewModel = .init() var actionsPublisher: AnyPublisher { 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)) } } diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift index c7523acb1..138526389 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift @@ -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") } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift index 9524aeb9c..20d2136cc 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift @@ -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 { diff --git a/ElementX/Sources/Services/Background/ApplicationProtocol.swift b/ElementX/Sources/Services/Background/ApplicationProtocol.swift index 1a098fbb8..2e9034717 100644 --- a/ElementX/Sources/Services/Background/ApplicationProtocol.swift +++ b/ElementX/Sources/Services/Background/ApplicationProtocol.swift @@ -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) + } } diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Connection-not-secure.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Connection-not-secure.png new file mode 100644 index 000000000..7096bd819 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Connection-not-secure.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81808a0d38699a96abea059176bf065956bbbe2f457cb58bd4f8ec008e043807 +size 240316 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.No-Camera-Permission.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.No-Camera-Permission.png new file mode 100644 index 000000000..ed6d8807a --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.No-Camera-Permission.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51b58490668adc1c908ec4ecc3e8a95f2c2c47a246f293d559c5e6a9a7f2d8b3 +size 154291 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Unknown-error.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Unknown-error.png new file mode 100644 index 000000000..b40b1b6fc --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Unknown-error.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19143f0cb869a917d7bff9d6c510ad5c589dad58f2f6a1f96c43402c8cb7eaca +size 130219 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Connection-not-secure.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Connection-not-secure.png new file mode 100644 index 000000000..258c2fa52 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Connection-not-secure.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e532918bbc3dd6f19b1c6f595bf4e78328777f67ba168ca73820acf36c4b5fb3 +size 364040 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.No-Camera-Permission.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.No-Camera-Permission.png new file mode 100644 index 000000000..e6b6647db --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.No-Camera-Permission.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45919d107396913c55c1bc4eb9a2cc3aeca0778399d3cc3fad55bf03fe710f80 +size 221041 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Unknown-error.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Unknown-error.png new file mode 100644 index 000000000..39877da5a --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Unknown-error.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2fc9f0ebae6cf4c1521ffbde06fbdad3bc15071db2a9461abf1b7d1415f113e +size 164912 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Connection-not-secure.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Connection-not-secure.png new file mode 100644 index 000000000..92f18204a --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Connection-not-secure.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d2b9794ad3b7d038113d0b87bd0f694194471adb29357e963f8143d955dc4ac +size 164424 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.No-Camera-Permission.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.No-Camera-Permission.png new file mode 100644 index 000000000..7e1e3b21e --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.No-Camera-Permission.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d19862c113e8168a9d05d81cb03c5f18fd29cfa22eed9fdc4ddef654c920c0c0 +size 98538 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Unknown-error.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Unknown-error.png new file mode 100644 index 000000000..99997a1ab --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Unknown-error.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a70dec75d62f4ebb3d00f613c3314c741bef3cc757d5c9b78358ca8b9535d52 +size 78473 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Connection-not-secure.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Connection-not-secure.png new file mode 100644 index 000000000..ab8b67cdd --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Connection-not-secure.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54b0bc2bacf2504febf374970d5afdbf8f8f03b139005c5c81d04761faf66132 +size 268685 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.No-Camera-Permission.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.No-Camera-Permission.png new file mode 100644 index 000000000..1638bfad7 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.No-Camera-Permission.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0e59c61d996514d1f3698856e8ad26cd405a036e2e57dcd6cb616ced0c27b6d +size 154608 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Unknown-error.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Unknown-error.png new file mode 100644 index 000000000..f8d1c750a --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Unknown-error.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d7c1eb55621a0c337ecc6c7882fdd6c48d55ca6ba9bceaaa7e334dc123b0b1e +size 111028