mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Final tweaks to the existing QR code screens to match the designs. (#4874)
* Simplify QR errors even more and use the correct error state for deviceAlreadySignedIn. * Remove the pre-defined padding on TitleAndIcon and use it in the QRCodeLoginScreen too. * Update FullscreenDialog to allow different horizontal padding in each section.
This commit is contained in:
@@ -1956,6 +1956,10 @@ internal enum L10n {
|
||||
internal static var screenLinkNewDeviceRootMobileDevice: String { return L10n.tr("Localizable", "screen_link_new_device_root_mobile_device") }
|
||||
/// What type of device do you want to link?
|
||||
internal static var screenLinkNewDeviceRootTitle: String { return L10n.tr("Localizable", "screen_link_new_device_root_title") }
|
||||
/// Please try again and make sure that you’ve entered the 2-digit code correctly. If the numbers still don’t match then contact your account provider.
|
||||
internal static var screenLinkNewDeviceWrongNumberSubtitle: String { return L10n.tr("Localizable", "screen_link_new_device_wrong_number_subtitle") }
|
||||
/// The numbers don’t match
|
||||
internal static var screenLinkNewDeviceWrongNumberTitle: String { return L10n.tr("Localizable", "screen_link_new_device_wrong_number_title") }
|
||||
/// This account has been deactivated.
|
||||
internal static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") }
|
||||
/// Incorrect username and/or password
|
||||
@@ -2188,6 +2192,10 @@ internal enum L10n {
|
||||
internal static var screenQrCodeLoginErrorDeclinedSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_declined_subtitle") }
|
||||
/// Sign in declined
|
||||
internal static var screenQrCodeLoginErrorDeclinedTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_declined_title") }
|
||||
/// You don’t need to do anything else.
|
||||
internal static var screenQrCodeLoginErrorDeviceAlreadySignedInSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_device_already_signed_in_subtitle") }
|
||||
/// Your other device is already signed in
|
||||
internal static var screenQrCodeLoginErrorDeviceAlreadySignedInTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_device_already_signed_in_title") }
|
||||
/// Sign in expired. Please try again.
|
||||
internal static var screenQrCodeLoginErrorExpiredSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_expired_subtitle") }
|
||||
/// The sign in was not completed in time
|
||||
|
||||
@@ -21,8 +21,10 @@ struct FullscreenDialog<Content: View, BottomContent: View>: View {
|
||||
|
||||
/// Padding applied to the top of the content automatically. Use `UIConstants` for preset values.
|
||||
var topPadding: CGFloat = UIConstants.titleTopPaddingToNavigationBar
|
||||
/// Padding applied to the content and buttons automatically
|
||||
/// Padding applied to the content automatically
|
||||
var horizontalPadding: CGFloat = 16
|
||||
/// Padding applied to the buttons automatically
|
||||
var bottomHorizontalPadding: CGFloat = 16
|
||||
/// The spacing between the content and the buttons.
|
||||
var spacing: CGFloat = 16
|
||||
|
||||
@@ -83,7 +85,7 @@ struct FullscreenDialog<Content: View, BottomContent: View>: View {
|
||||
VStack(spacing: 0) {
|
||||
bottomContent()
|
||||
.readableFrame()
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.horizontal, bottomHorizontalPadding)
|
||||
.padding(.top, spacing)
|
||||
.padding(.bottom, UIConstants.actionButtonBottomPadding)
|
||||
|
||||
@@ -113,7 +115,7 @@ struct FullscreenDialog<Content: View, BottomContent: View>: View {
|
||||
|
||||
bottomContent()
|
||||
.readableFrame()
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.horizontal, bottomHorizontalPadding)
|
||||
.padding(.bottom, UIConstants.actionButtonBottomPadding)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -12,7 +12,7 @@ struct LinkNewDeviceScreen: View {
|
||||
@Bindable var context: LinkNewDeviceScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: 0) {
|
||||
FullscreenDialog(topPadding: 24, horizontalPadding: 24) {
|
||||
mainContent
|
||||
} bottomContent: {
|
||||
buttons
|
||||
@@ -31,7 +31,6 @@ struct LinkNewDeviceScreen: View {
|
||||
TitleAndIcon(title: L10n.screenLinkNewDeviceRootTitle,
|
||||
icon: \.computer,
|
||||
iconStyle: .default)
|
||||
.padding(.horizontal, -16) // Remove the FullscreenDialog's default padding in this state.
|
||||
case .notSupported:
|
||||
TitleAndIcon(title: L10n.screenLinkNewDeviceErrorNotSupportedTitle,
|
||||
subtitle: L10n.screenLinkNewDeviceErrorNotSupportedSubtitle,
|
||||
|
||||
@@ -65,7 +65,7 @@ enum QRCodeLoginState: Equatable {
|
||||
/// Any full screen error state
|
||||
case error(ErrorState)
|
||||
|
||||
enum ErrorState: Equatable {
|
||||
enum ErrorState: Equatable, CaseIterable {
|
||||
case noCameraPermission
|
||||
case connectionNotSecure
|
||||
case cancelled
|
||||
@@ -73,6 +73,8 @@ enum QRCodeLoginState: Equatable {
|
||||
case expired
|
||||
case linkingNotSupported
|
||||
case deviceNotSupported
|
||||
/// Expected a QR code for a new device, however the processed code belongs to a device that is already signed in.
|
||||
case deviceAlreadySignedIn
|
||||
case unknown
|
||||
}
|
||||
|
||||
@@ -91,8 +93,6 @@ enum QRCodeLoginState: Equatable {
|
||||
case notAllowed(scannedProvider: String, allowedProviders: [String])
|
||||
/// The QR code has been processed but it belongs to a device not signed in.
|
||||
case deviceNotSignedIn
|
||||
/// Expected a QR code for a new device, however the processed code belongs to a device that is already signed in.
|
||||
case deviceAlreadySignedIn
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
@@ -102,8 +102,6 @@ enum QRCodeLoginState: Equatable {
|
||||
L10n.screenChangeServerErrorUnauthorizedHomeserverTitle(scannedProvider)
|
||||
case .deviceNotSignedIn:
|
||||
L10n.screenQrCodeLoginDeviceNotSignedInScanStateSubtitle
|
||||
case .deviceAlreadySignedIn:
|
||||
L10n.screenQrCodeLoginDeviceNotSignedInScanStateSubtitle // FIXME: Update string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,8 +113,6 @@ enum QRCodeLoginState: Equatable {
|
||||
L10n.screenChangeServerErrorUnauthorizedHomeserverContent(allowedProviders.formatted(.list(type: .and)))
|
||||
case .deviceNotSignedIn:
|
||||
L10n.screenQrCodeLoginDeviceNotSignedInScanStateDescription
|
||||
case .deviceAlreadySignedIn:
|
||||
L10n.screenQrCodeLoginDeviceNotSignedInScanStateDescription // FIXME: Update string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
|
||||
case .deviceNotSignedIn:
|
||||
state.state = .scan(.scanFailed(.deviceNotSignedIn))
|
||||
case .deviceAlreadySignedIn:
|
||||
state.state = .scan(.scanFailed(.deviceAlreadySignedIn))
|
||||
state.state = .error(.deviceAlreadySignedIn)
|
||||
case .cancelled:
|
||||
state.state = .error(.cancelled)
|
||||
case .connectionInsecure:
|
||||
|
||||
@@ -31,6 +31,8 @@ struct QRCodeErrorView: View {
|
||||
L10n.screenQrCodeLoginErrorLinkingNotSuportedTitle
|
||||
case .deviceNotSupported:
|
||||
L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedTitle(InfoPlistReader.main.bundleDisplayName)
|
||||
case .deviceAlreadySignedIn:
|
||||
L10n.screenQrCodeLoginErrorDeviceAlreadySignedInTitle
|
||||
case .unknown:
|
||||
L10n.commonSomethingWentWrong
|
||||
}
|
||||
@@ -52,56 +54,43 @@ struct QRCodeErrorView: View {
|
||||
L10n.screenQrCodeLoginErrorLinkingNotSuportedSubtitle(InfoPlistReader.main.bundleDisplayName)
|
||||
case .deviceNotSupported:
|
||||
L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedSubtitle(InfoPlistReader.main.bundleDisplayName)
|
||||
case .deviceAlreadySignedIn:
|
||||
L10n.screenQrCodeLoginErrorDeviceAlreadySignedInSubtitle
|
||||
case .unknown:
|
||||
L10n.screenQrCodeLoginUnknownErrorDescription
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog {
|
||||
header
|
||||
} bottomContent: {
|
||||
footer
|
||||
var icon: KeyPath<CompoundIcons, Image> {
|
||||
switch errorState {
|
||||
case .noCameraPermission: \.takePhotoSolid
|
||||
case .deviceAlreadySignedIn: \.checkCircleSolid
|
||||
default: \.errorSolid
|
||||
}
|
||||
}
|
||||
|
||||
var iconStyle: BigIcon.Style {
|
||||
switch errorState {
|
||||
case .noCameraPermission: .defaultSolid
|
||||
case .deviceAlreadySignedIn: .successSolid
|
||||
default: .alertSolid
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: 24, horizontalPadding: 24) {
|
||||
mainContent
|
||||
} bottomContent: {
|
||||
buttons
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var header: some View {
|
||||
private var mainContent: some View {
|
||||
switch errorState {
|
||||
case .noCameraPermission:
|
||||
VStack(spacing: 16) {
|
||||
BigIcon(icon: \.takePhotoSolid, style: .defaultSolid)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(title)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(subtitle)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
case .connectionNotSecure:
|
||||
VStack(spacing: 40) {
|
||||
VStack(spacing: 16) {
|
||||
BigIcon(icon: \.errorSolid, style: .alertSolid)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(title)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(subtitle)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
header
|
||||
|
||||
VStack(spacing: 24) {
|
||||
Text(L10n.screenQrCodeLoginConnectionNoteSecureStateListHeader)
|
||||
@@ -117,31 +106,19 @@ struct QRCodeErrorView: View {
|
||||
}
|
||||
}
|
||||
default:
|
||||
simpleErrorStack
|
||||
header
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var simpleErrorStack: some View {
|
||||
VStack(spacing: 16) {
|
||||
BigIcon(icon: \.errorSolid, style: .alertSolid)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(title)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(subtitle)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
var header: some View {
|
||||
TitleAndIcon(title: title,
|
||||
subtitle: subtitle,
|
||||
icon: icon,
|
||||
iconStyle: iconStyle)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var footer: some View {
|
||||
private var buttons: some View {
|
||||
switch errorState {
|
||||
case .noCameraPermission:
|
||||
Button(L10n.screenQrCodeLoginNoCameraPermissionButton) {
|
||||
@@ -172,6 +149,11 @@ struct QRCodeErrorView: View {
|
||||
}
|
||||
.buttonStyle(.compound(.tertiary))
|
||||
}
|
||||
case .deviceAlreadySignedIn:
|
||||
Button(L10n.actionContinue) {
|
||||
action(.cancel)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,42 +162,34 @@ struct QRCodeErrorView: View {
|
||||
|
||||
struct QRCodeErrorView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
QRCodeErrorViewSnapshot(errorState: .noCameraPermission, canSignInManually: false)
|
||||
.previewDisplayName("No Camera Permission")
|
||||
ForEach(QRCodeLoginState.ErrorState.allCases, id: \.self) { errorState in
|
||||
NavigationStack {
|
||||
QRCodeErrorView(errorState: errorState, canSignInManually: true) { _ in }
|
||||
.toolbar(.visible, for: .navigationBar)
|
||||
}
|
||||
.previewDisplayName(errorState.previewDisplayName)
|
||||
}
|
||||
|
||||
QRCodeErrorViewSnapshot(errorState: .connectionNotSecure, canSignInManually: false)
|
||||
.previewDisplayName("Connection not secure")
|
||||
|
||||
QRCodeErrorViewSnapshot(errorState: .linkingNotSupported, canSignInManually: true)
|
||||
.previewDisplayName("Linking unsupported")
|
||||
QRCodeErrorViewSnapshot(errorState: .linkingNotSupported, canSignInManually: false)
|
||||
.previewDisplayName("Linking unsupported restricted flow")
|
||||
|
||||
QRCodeErrorViewSnapshot(errorState: .cancelled, canSignInManually: false)
|
||||
.previewDisplayName("Cancelled")
|
||||
|
||||
QRCodeErrorViewSnapshot(errorState: .declined, canSignInManually: false)
|
||||
.previewDisplayName("Declined")
|
||||
|
||||
QRCodeErrorViewSnapshot(errorState: .expired, canSignInManually: false)
|
||||
.previewDisplayName("Expired")
|
||||
|
||||
QRCodeErrorViewSnapshot(errorState: .deviceNotSupported, canSignInManually: false)
|
||||
.previewDisplayName("Device not supported")
|
||||
|
||||
QRCodeErrorViewSnapshot(errorState: .unknown, canSignInManually: false)
|
||||
.previewDisplayName("Unknown error")
|
||||
NavigationStack {
|
||||
QRCodeErrorView(errorState: .linkingNotSupported, canSignInManually: false) { _ in }
|
||||
.toolbar(.visible, for: .navigationBar)
|
||||
}
|
||||
.previewDisplayName("Linking unsupported restricted flow")
|
||||
}
|
||||
}
|
||||
|
||||
private struct QRCodeErrorViewSnapshot: View {
|
||||
let errorState: QRCodeLoginState.ErrorState
|
||||
let canSignInManually: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
QRCodeErrorView(errorState: errorState, canSignInManually: canSignInManually) { _ in }
|
||||
.toolbar(.visible, for: .navigationBar)
|
||||
private extension QRCodeLoginState.ErrorState {
|
||||
var previewDisplayName: String {
|
||||
switch self {
|
||||
case .noCameraPermission: "No Camera Permission"
|
||||
case .connectionNotSecure: "Connection not secure"
|
||||
case .linkingNotSupported: "Linking unsupported"
|
||||
case .cancelled: "Cancelled"
|
||||
case .declined: "Declined"
|
||||
case .expired: "Expired"
|
||||
case .deviceNotSupported: "Device not supported"
|
||||
case .deviceAlreadySignedIn: "Device already signed in"
|
||||
case .unknown: "Unknown error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,24 +49,12 @@ struct QRCodeLoginScreen: View {
|
||||
}
|
||||
|
||||
private var initialContent: some View {
|
||||
FullscreenDialog {
|
||||
FullscreenDialog(topPadding: 24, horizontalPadding: 24) {
|
||||
VStack(alignment: .leading, spacing: 40) {
|
||||
VStack(spacing: 16) {
|
||||
BigIcon(icon: \.computer, style: .default)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.screenQrCodeLoginInitialStateTitle(InfoPlistReader.main.productionAppName))
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(L10n.screenQrCodeLoginInitialStateSubtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
TitleAndIcon(title: L10n.screenQrCodeLoginInitialStateTitle(InfoPlistReader.main.productionAppName),
|
||||
subtitle: L10n.screenQrCodeLoginInitialStateSubtitle,
|
||||
icon: \.computer,
|
||||
iconStyle: .default)
|
||||
|
||||
SFNumberedListView(items: context.viewState.initialStateListItems)
|
||||
}
|
||||
@@ -81,17 +69,21 @@ struct QRCodeLoginScreen: View {
|
||||
@ViewBuilder
|
||||
private var displayCodeContent: some View {
|
||||
if case let .displayCode(displayCodeState) = context.viewState.state {
|
||||
FullscreenDialog {
|
||||
FullscreenDialog(topPadding: 24) {
|
||||
VStack(spacing: 32) {
|
||||
VStack(spacing: 40) {
|
||||
VStack(spacing: 24) {
|
||||
displayCodeHeader(state: displayCodeState)
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
PINTextField(pinCode: .constant(displayCodeState.code),
|
||||
maxLength: displayCodeState.code.count,
|
||||
size: .small)
|
||||
.disabled(true)
|
||||
}
|
||||
|
||||
VStack(spacing: 4) {
|
||||
ProgressView()
|
||||
|
||||
Text(L10n.screenQrCodeLoginVerifyCodeLoading)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodySM)
|
||||
@@ -104,63 +96,36 @@ struct QRCodeLoginScreen: View {
|
||||
}
|
||||
.buttonStyle(.compound(.secondary))
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
|
||||
private func displayCodeHeader(state: QRCodeLoginState.QRCodeLoginDisplayCodeState) -> some View {
|
||||
VStack(spacing: 16) {
|
||||
switch state {
|
||||
case .deviceCode:
|
||||
BigIcon(icon: \.computer, style: .default)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.screenQrCodeLoginDeviceCodeTitle)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(L10n.screenQrCodeLoginDeviceCodeSubtitle)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
case .verificationCode:
|
||||
BigIcon(icon: \.lock, style: .default)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.screenQrCodeLoginVerifyCodeTitle)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(L10n.screenQrCodeLoginVerifyCodeSubtitle)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
switch state {
|
||||
case .deviceCode:
|
||||
TitleAndIcon(title: L10n.screenQrCodeLoginDeviceCodeTitle,
|
||||
subtitle: L10n.screenQrCodeLoginDeviceCodeSubtitle,
|
||||
icon: \.computer,
|
||||
iconStyle: .default)
|
||||
case .verificationCode:
|
||||
TitleAndIcon(title: L10n.screenQrCodeLoginVerifyCodeTitle,
|
||||
subtitle: L10n.screenQrCodeLoginVerifyCodeSubtitle,
|
||||
icon: \.lock,
|
||||
iconStyle: .default)
|
||||
}
|
||||
}
|
||||
|
||||
private var qrScanContent: some View {
|
||||
FullscreenDialog {
|
||||
FullscreenDialog(topPadding: 24) {
|
||||
VStack(spacing: 40) {
|
||||
VStack(spacing: 16) {
|
||||
BigIcon(icon: \.takePhotoSolid, style: .default)
|
||||
|
||||
Text(L10n.screenQrCodeLoginScanningStateTitle)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
TitleAndIcon(title: L10n.screenQrCodeLoginScanningStateTitle,
|
||||
icon: \.takePhotoSolid,
|
||||
iconStyle: .default)
|
||||
|
||||
qrScanner
|
||||
}
|
||||
} bottomContent: {
|
||||
qrScanFooter
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
Reference in New Issue
Block a user