Lock the Link New Device flow to portrait and improve Cancel/Try Again handling. (#5094)

* Lock the LinkNewDeviceScreen to portrait.

* Add cancel buttons to the QR Code errors and handle cancel/startOver correctly.

* Use the shared QRCodeErrorView in the LinkNewDeviceScreen.

* Clarify the different QR Error/ErrorStates a bit more.

* Update snapshots.
This commit is contained in:
Doug
2026-02-12 11:21:24 +00:00
committed by GitHub
parent 6b93bbf558
commit 92a5fb395c
75 changed files with 303 additions and 193 deletions
@@ -308,13 +308,14 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
return return
} }
switch action { switch action {
case .startOver:
fatalError("QR code login shouldn't request to start over as it's handled within the screen.")
case .requestOIDCAuthorisation, .linkedDevice:
fatalError("QR code login shouldn't request an OIDC flow or link a device.")
case .signInManually: case .signInManually:
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.cancelledLoginWithQR) stateMachine.tryEvent(.cancelledLoginWithQR)
stateMachine.tryEvent(.confirmServer(.login)) stateMachine.tryEvent(.confirmServer(.login))
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.cancelledLoginWithQR)
case .signedIn(let userSession): case .signedIn(let userSession):
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
// Since the qr code login flow includes verification // Since the qr code login flow includes verification
@@ -322,8 +323,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
DispatchQueue.main.async { DispatchQueue.main.async {
self.stateMachine.tryEvent(.signedIn, userInfo: userSession) self.stateMachine.tryEvent(.signedIn, userInfo: userSession)
} }
case .requestOIDCAuthorisation, .linkedDevice: case .cancel:
fatalError("QR code login shouldn't request an OIDC flow or link a device.") navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.cancelledLoginWithQR)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -43,7 +43,8 @@ class LinkNewDeviceFlowCoordinator: FlowCoordinatorProtocol {
} }
private func presentLinkNewDeviceScreen() { private func presentLinkNewDeviceScreen() {
let coordinator = LinkNewDeviceScreenCoordinator(parameters: .init(clientProxy: flowParameters.userSession.clientProxy)) let coordinator = LinkNewDeviceScreenCoordinator(parameters: .init(clientProxy: flowParameters.userSession.clientProxy,
orientationManager: flowParameters.appMediator.windowManager))
coordinator.actionsPublisher coordinator.actionsPublisher
.sink { [weak self] action in .sink { [weak self] action in
guard let self else { return } guard let self else { return }
@@ -74,12 +75,14 @@ class LinkNewDeviceFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .signInManually, .signedIn: case .signInManually, .signedIn:
fatalError("QR linking shouldn't send sign-in actions.") fatalError("QR linking shouldn't send sign-in actions.")
case .dismiss: case .startOver:
navigationStackCoordinator.pop() navigationStackCoordinator.pop() // Pops back to the LinkNewDeviceScreen.
case .requestOIDCAuthorisation(let url, let continuation): case .requestOIDCAuthorisation(let url, let continuation):
actionsSubject.send(.requestOIDCAuthorisation(url, continuation)) actionsSubject.send(.requestOIDCAuthorisation(url, continuation))
case .linkedDevice: case .linkedDevice:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
case .cancel:
actionsSubject.send(.dismiss)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -16,10 +16,12 @@ enum LinkNewDeviceScreenCoordinatorAction {
struct LinkNewDeviceScreenCoordinatorParameters { struct LinkNewDeviceScreenCoordinatorParameters {
let clientProxy: ClientProxyProtocol let clientProxy: ClientProxyProtocol
let orientationManager: OrientationManagerProtocol
} }
final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol { final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol {
private let viewModel: LinkNewDeviceScreenViewModelProtocol private let viewModel: LinkNewDeviceScreenViewModelProtocol
private let orientationManager: OrientationManagerProtocol
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@@ -30,6 +32,7 @@ final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol {
init(parameters: LinkNewDeviceScreenCoordinatorParameters) { init(parameters: LinkNewDeviceScreenCoordinatorParameters) {
viewModel = LinkNewDeviceScreenViewModel(clientProxy: parameters.clientProxy) viewModel = LinkNewDeviceScreenViewModel(clientProxy: parameters.clientProxy)
orientationManager = parameters.orientationManager
} }
func start() { func start() {
@@ -47,6 +50,13 @@ final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol {
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
orientationManager.setOrientation(.portrait)
orientationManager.lockOrientation(.portrait)
}
func stop() {
orientationManager.lockOrientation(.all)
} }
func toPresentable() -> AnyView { func toPresentable() -> AnyView {
@@ -14,7 +14,12 @@ enum LinkNewDeviceScreenViewModelAction {
} }
struct LinkNewDeviceScreenViewState: BindableState { struct LinkNewDeviceScreenViewState: BindableState {
enum Mode: Equatable { case loading, readyToLink(isGeneratingCode: Bool), notSupported } enum Mode: Equatable {
case loading
case readyToLink(isGeneratingCode: Bool)
case error(QRCodeLoginState.ErrorState)
}
var mode: Mode = .loading var mode: Mode = .loading
let showLinkDesktopComputerButton: Bool let showLinkDesktopComputerButton: Bool
@@ -23,5 +28,6 @@ struct LinkNewDeviceScreenViewState: BindableState {
enum LinkNewDeviceScreenViewAction { enum LinkNewDeviceScreenViewAction {
case linkMobileDevice case linkMobileDevice
case linkDesktopComputer case linkDesktopComputer
case errorAction(QRCodeErrorView.Action)
case dismiss case dismiss
} }
@@ -39,6 +39,8 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
Task { await linkMobileDevice() } Task { await linkMobileDevice() }
case .linkDesktopComputer: case .linkDesktopComputer:
actionsSubject.send(.linkDesktopComputer) actionsSubject.send(.linkDesktopComputer)
case .errorAction(let action):
handleErrorAction(action)
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
} }
@@ -50,7 +52,7 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
if await clientProxy.isLoginWithQRCodeSupported { if await clientProxy.isLoginWithQRCodeSupported {
state.mode = .readyToLink(isGeneratingCode: false) state.mode = .readyToLink(isGeneratingCode: false)
} else { } else {
state.mode = .notSupported state.mode = .error(.notSupported)
} }
} }
@@ -73,8 +75,22 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
actionsSubject.send(.linkMobileDevice(progressPublisher)) actionsSubject.send(.linkMobileDevice(progressPublisher))
state.mode = .readyToLink(isGeneratingCode: false) state.mode = .readyToLink(isGeneratingCode: false)
} catch { } catch {
#warning("Needs some form of re-usable error handling, will handle with the next screen.") // This is hard to share a mapping from the QRCodeLoginError with the
state.mode = .notSupported // QRCodeLoginScreen given that some of those are scan errors…
state.mode = .error(.unknown)
}
}
private func handleErrorAction(_ action: QRCodeErrorView.Action) {
switch action {
case .startOver:
// Reset to ready state to allow trying again.
state.mode = .readyToLink(isGeneratingCode: false)
case .openSettings, .signInManually:
MXLog.error("Unexpected error action: \(action)")
actionsSubject.send(.dismiss)
case .cancel:
actionsSubject.send(.dismiss)
} }
} }
} }
@@ -5,6 +5,7 @@
// Please see LICENSE files in the repository root for full details. // Please see LICENSE files in the repository root for full details.
// //
import Combine
import Compound import Compound
import SwiftUI import SwiftUI
@@ -12,6 +13,13 @@ struct LinkNewDeviceScreen: View {
@Bindable var context: LinkNewDeviceScreenViewModel.Context @Bindable var context: LinkNewDeviceScreenViewModel.Context
var body: some View { var body: some View {
switch context.viewState.mode {
case .error(let errorState):
QRCodeErrorView(errorState: errorState, canSignInManually: false) { action in
context.send(viewAction: .errorAction(action))
}
.backgroundStyle(.compound.bgCanvasDefault)
default:
FullscreenDialog(topPadding: 24, horizontalPadding: 24) { FullscreenDialog(topPadding: 24, horizontalPadding: 24) {
mainContent mainContent
} bottomContent: { } bottomContent: {
@@ -23,6 +31,7 @@ struct LinkNewDeviceScreen: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar } .toolbar { toolbar }
} }
}
@ViewBuilder @ViewBuilder
var mainContent: some View { var mainContent: some View {
@@ -31,11 +40,8 @@ struct LinkNewDeviceScreen: View {
TitleAndIcon(title: L10n.screenLinkNewDeviceRootTitle, TitleAndIcon(title: L10n.screenLinkNewDeviceRootTitle,
icon: \.computer, icon: \.computer,
iconStyle: .default) iconStyle: .default)
case .notSupported: case .error:
TitleAndIcon(title: L10n.screenLinkNewDeviceErrorNotSupportedTitle, EmptyView() // Not reachable.
subtitle: L10n.screenLinkNewDeviceErrorNotSupportedSubtitle,
icon: \.errorSolid,
iconStyle: .alertSolid)
} }
} }
@@ -79,11 +85,8 @@ struct LinkNewDeviceScreen: View {
} }
} }
.disabled(isGeneratingCode) .disabled(isGeneratingCode)
case .notSupported: case .error:
Button(L10n.actionDismiss) { EmptyView() // Not reachable.
context.send(viewAction: .dismiss)
}
.buttonStyle(.compound(.primary))
} }
} }
@@ -103,7 +106,8 @@ struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = makeViewModel(mode: .readyToLink(isGeneratingCode: false)) static let viewModel = makeViewModel(mode: .readyToLink(isGeneratingCode: false))
static let generatingViewModel = makeViewModel(mode: .readyToLink(isGeneratingCode: true)) static let generatingViewModel = makeViewModel(mode: .readyToLink(isGeneratingCode: true))
static let loadingViewModel = makeViewModel(mode: .loading) static let loadingViewModel = makeViewModel(mode: .loading)
static let unsupportedViewModel = makeViewModel(mode: .notSupported) static let unsupportedViewModel = makeViewModel(mode: .error(.notSupported))
static let unknownErrorViewModel = makeViewModel(mode: .error(.unknown))
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {
@@ -127,7 +131,13 @@ struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview {
LinkNewDeviceScreen(context: unsupportedViewModel.context) LinkNewDeviceScreen(context: unsupportedViewModel.context)
} }
.previewDisplayName("Unsupported") .previewDisplayName("Unsupported")
.snapshotPreferences(expect: unsupportedViewModel.context.observe(\.viewState.mode).map { $0 == .notSupported }) .snapshotPreferences(expect: unsupportedViewModel.context.observe(\.viewState.mode).map { $0 == .error(.notSupported) })
NavigationStack {
LinkNewDeviceScreen(context: unknownErrorViewModel.context)
}
.previewDisplayName("Unknown error")
.snapshotPreferences(expect: unknownErrorViewModel.context.observe(\.viewState.mode).map { $0 == .error(.unknown) })
} }
static func makeViewModel(mode: LinkNewDeviceScreenViewState.Mode) -> LinkNewDeviceScreenViewModel { static func makeViewModel(mode: LinkNewDeviceScreenViewState.Mode) -> LinkNewDeviceScreenViewModel {
@@ -137,19 +147,25 @@ struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview {
case .loading: case .loading:
try? await Task.sleep(for: .seconds(20)) try? await Task.sleep(for: .seconds(20))
return false return false
case .readyToLink: case .error(.notSupported):
return true
case .notSupported:
return false return false
case .readyToLink, .error:
return true
} }
} }
let linkMobileProgressSubject = CurrentValueSubject<LinkNewDeviceService.LinkMobileProgress, QRCodeLoginError>(.starting)
clientProxy.linkNewDeviceServiceReturnValue = LinkNewDeviceServiceMock(.init(linkMobileProgressPublisher: linkMobileProgressSubject.asCurrentValuePublisher()))
let viewModel = LinkNewDeviceScreenViewModel(clientProxy: clientProxy) let viewModel = LinkNewDeviceScreenViewModel(clientProxy: clientProxy)
Task { Task {
try? await Task.sleep(for: .milliseconds(100)) try? await Task.sleep(for: .milliseconds(100))
if case .readyToLink(isGeneratingCode: true) = mode { if case .readyToLink(isGeneratingCode: true) = mode {
viewModel.context.send(viewAction: .linkMobileDevice) viewModel.context.send(viewAction: .linkMobileDevice)
} else if case .error = mode {
viewModel.context.send(viewAction: .linkMobileDevice)
linkMobileProgressSubject.send(completion: .failure(.unknown))
} }
} }
@@ -17,19 +17,26 @@ struct QRCodeLoginScreenCoordinatorParameters {
} }
enum QRCodeLoginScreenCoordinatorAction: CustomStringConvertible { enum QRCodeLoginScreenCoordinatorAction: CustomStringConvertible {
case dismiss /// Restart the flow.
///
/// This action should only be sent when linking a new device. When logging in it
/// is handled internally within the screen.
case startOver
case signInManually case signInManually
case signedIn(userSession: UserSessionProtocol) case signedIn(userSession: UserSessionProtocol)
case requestOIDCAuthorisation(URL, OIDCAccountSettingsPresenter.Continuation) case requestOIDCAuthorisation(URL, OIDCAccountSettingsPresenter.Continuation)
case linkedDevice case linkedDevice
/// Cancel the flow (dismiss the modal).
case cancel
var description: String { var description: String {
switch self { switch self {
case .dismiss: "dismiss" case .startOver: "startOver"
case .signInManually: "signInManually" case .signInManually: "signInManually"
case .signedIn: "signedIn" case .signedIn: "signedIn"
case .requestOIDCAuthorisation: "requestOIDCAuthorisation" case .requestOIDCAuthorisation: "requestOIDCAuthorisation"
case .linkedDevice: "linkedDevice" case .linkedDevice: "linkedDevice"
case .cancel: "cancel"
} }
} }
} }
@@ -60,14 +67,16 @@ final class QRCodeLoginScreenCoordinator: CoordinatorProtocol {
switch action { switch action {
case .signInManually: case .signInManually:
actionsSubject.send(.signInManually) actionsSubject.send(.signInManually)
case .dismiss: case .startOver:
actionsSubject.send(.dismiss) actionsSubject.send(.startOver)
case .signedIn(let userSession): case .signedIn(let userSession):
actionsSubject.send(.signedIn(userSession: userSession)) actionsSubject.send(.signedIn(userSession: userSession))
case .requestOIDCAuthorisation(let url, let continuation): case .requestOIDCAuthorisation(let url, let continuation):
actionsSubject.send(.requestOIDCAuthorisation(url, continuation)) actionsSubject.send(.requestOIDCAuthorisation(url, continuation))
case .linkedDevice: case .linkedDevice:
actionsSubject.send(.linkedDevice) actionsSubject.send(.linkedDevice)
case .cancel:
actionsSubject.send(.cancel)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -9,19 +9,26 @@
import SwiftUI import SwiftUI
enum QRCodeLoginScreenViewModelAction: CustomStringConvertible { enum QRCodeLoginScreenViewModelAction: CustomStringConvertible {
case dismiss /// Restart the flow.
///
/// This action should only be sent when linking a new device. When logging in it
/// is handled internally within the screen.
case startOver
case signInManually case signInManually
case signedIn(userSession: UserSessionProtocol) case signedIn(userSession: UserSessionProtocol)
case requestOIDCAuthorisation(URL, OIDCAccountSettingsPresenter.Continuation) case requestOIDCAuthorisation(URL, OIDCAccountSettingsPresenter.Continuation)
case linkedDevice case linkedDevice
/// Cancel the flow (dismiss the modal).
case cancel
var description: String { var description: String {
switch self { switch self {
case .dismiss: "dismiss" case .startOver: "startOver"
case .signInManually: "signInManually" case .signInManually: "signInManually"
case .signedIn: "signedIn" case .signedIn: "signedIn"
case .requestOIDCAuthorisation: "requestOIDCAuthorisation" case .requestOIDCAuthorisation: "requestOIDCAuthorisation"
case .linkedDevice: "linkedDevice" case .linkedDevice: "linkedDevice"
case .cancel: "cancel"
} }
} }
} }
@@ -49,13 +56,13 @@ struct QRCodeLoginScreenViewState: BindableState {
switch mode { switch mode {
case .login: case .login:
switch state { switch state {
case .loginInstructions, .scan, .error(.noCameraPermission): true case .loginInstructions, .scan: true
case .error: false case .error: false
case .linkDesktopInstructions, .displayCode, .displayQR, .confirmCode: false // Unreachable states. case .linkDesktopInstructions, .displayCode, .displayQR, .confirmCode: false // Unreachable states.
} }
case .linkDesktop, .linkMobile: case .linkDesktop, .linkMobile:
switch state { switch state {
case .displayCode, .confirmCode, .scan, .error(.noCameraPermission): true case .displayCode, .confirmCode, .scan: true
case .linkDesktopInstructions, .displayQR, .error: false case .linkDesktopInstructions, .displayQR, .error: false
case .loginInstructions: false // Unreachable state. case .loginInstructions: false // Unreachable state.
} }
@@ -81,7 +88,8 @@ struct QRCodeLoginScreenViewStateBindings {
} }
enum QRCodeLoginScreenViewAction { enum QRCodeLoginScreenViewAction {
case dismiss /// Cancel the entire flow (dismiss the modal).
case cancel
case startScan case startScan
case sendCheckCode case sendCheckCode
case errorAction(QRCodeErrorView.Action) case errorAction(QRCodeErrorView.Action)
@@ -107,13 +115,17 @@ enum QRCodeLoginState: Equatable {
case error(ErrorState) case error(ErrorState)
enum ErrorState: Equatable, CaseIterable { enum ErrorState: Equatable, CaseIterable {
/// The account provider doesn't support the use of QR codes.
case notSupported
case noCameraPermission case noCameraPermission
case connectionNotSecure case connectionNotSecure
case cancelled case cancelled
case declined case declined
case expired case expired
/// The other device does not support linking Element X by QR code.
case linkingNotSupported case linkingNotSupported
case deviceNotSupported /// Login cannot be continued due to a lack of Sliding Sync.
case slidingSyncNotAvailable
/// Expected a QR code for a new device, however the processed code belongs to a device that is already signed in. /// Expected a QR code for a new device, however the processed code belongs to a device that is already signed in.
case deviceAlreadySignedIn case deviceAlreadySignedIn
case unknown case unknown
@@ -49,8 +49,8 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
override func process(viewAction: QRCodeLoginScreenViewAction) { override func process(viewAction: QRCodeLoginScreenViewAction) {
switch viewAction { switch viewAction {
case .dismiss, .errorAction(.dismiss): case .cancel, .errorAction(.cancel):
actionsSubject.send(.dismiss) actionsSubject.send(.cancel)
case .startScan: case .startScan:
Task { await startScanIfPossible() } Task { await startScanIfPossible() }
case .sendCheckCode: case .sendCheckCode:
@@ -58,9 +58,12 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
case .errorAction(.startOver): case .errorAction(.startOver):
switch state.mode { switch state.mode {
case .login: case .login:
Task { await startScanIfPossible() } // Login restarts the flow on this screen.
state.bindings.qrResult = nil
state.state = .loginInstructions
case .linkDesktop, .linkMobile: case .linkDesktop, .linkMobile:
actionsSubject.send(.dismiss) // Linking a new device starts on the previous screen.
actionsSubject.send(.startOver)
} }
case .errorAction(.openSettings): case .errorAction(.openSettings):
appMediator.openAppSettings() appMediator.openAppSettings()
@@ -267,8 +270,8 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
case .success: case .success:
break // The state will be updated by the status publisher. break // The state will be updated by the status publisher.
case .failure(.userCancellation): case .failure(.userCancellation):
MXLog.info("User cancelled the WAS, dismissing.") MXLog.info("User cancelled the WAS, the flow must be restarted.")
actionsSubject.send(.dismiss) actionsSubject.send(.startOver)
case .failure: case .failure:
handleError(.unknown) handleError(.unknown)
} }
@@ -296,8 +299,8 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
state.state = .error(.linkingNotSupported) state.state = .error(.linkingNotSupported)
case .expired: case .expired:
state.state = .error(.expired) state.state = .error(.expired)
case .deviceNotSupported: case .slidingSyncNotAvailable:
state.state = .error(.deviceNotSupported) state.state = .error(.slidingSyncNotAvailable)
case .deviceAlreadySignedIn: case .deviceAlreadySignedIn:
state.state = .error(.deviceAlreadySignedIn) state.state = .error(.deviceAlreadySignedIn)
case .unknown: case .unknown:
@@ -12,11 +12,13 @@ struct QRCodeErrorView: View {
let errorState: QRCodeLoginState.ErrorState let errorState: QRCodeLoginState.ErrorState
let canSignInManually: Bool let canSignInManually: Bool
enum Action { case openSettings, startOver, signInManually, dismiss } enum Action { case openSettings, startOver, signInManually, cancel }
let action: (Action) -> Void let action: (Action) -> Void
var title: String { var title: String {
switch errorState { switch errorState {
case .notSupported:
L10n.screenLinkNewDeviceErrorNotSupportedTitle
case .noCameraPermission: case .noCameraPermission:
L10n.screenQrCodeLoginNoCameraPermissionStateTitle L10n.screenQrCodeLoginNoCameraPermissionStateTitle
case .connectionNotSecure: case .connectionNotSecure:
@@ -29,7 +31,7 @@ struct QRCodeErrorView: View {
L10n.screenQrCodeLoginErrorExpiredTitle L10n.screenQrCodeLoginErrorExpiredTitle
case .linkingNotSupported: case .linkingNotSupported:
L10n.screenQrCodeLoginErrorLinkingNotSuportedTitle L10n.screenQrCodeLoginErrorLinkingNotSuportedTitle
case .deviceNotSupported: case .slidingSyncNotAvailable:
L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedTitle(InfoPlistReader.main.bundleDisplayName) L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedTitle(InfoPlistReader.main.bundleDisplayName)
case .deviceAlreadySignedIn: case .deviceAlreadySignedIn:
L10n.screenQrCodeLoginErrorDeviceAlreadySignedInTitle L10n.screenQrCodeLoginErrorDeviceAlreadySignedInTitle
@@ -40,6 +42,8 @@ struct QRCodeErrorView: View {
var subtitle: String { var subtitle: String {
switch errorState { switch errorState {
case .notSupported:
L10n.screenLinkNewDeviceErrorNotSupportedSubtitle
case .noCameraPermission: case .noCameraPermission:
L10n.screenQrCodeLoginNoCameraPermissionStateDescription(InfoPlistReader.main.productionAppName) L10n.screenQrCodeLoginNoCameraPermissionStateDescription(InfoPlistReader.main.productionAppName)
case .connectionNotSecure: case .connectionNotSecure:
@@ -52,7 +56,7 @@ struct QRCodeErrorView: View {
L10n.screenQrCodeLoginErrorExpiredSubtitle L10n.screenQrCodeLoginErrorExpiredSubtitle
case .linkingNotSupported: case .linkingNotSupported:
L10n.screenQrCodeLoginErrorLinkingNotSuportedSubtitle(InfoPlistReader.main.bundleDisplayName) L10n.screenQrCodeLoginErrorLinkingNotSuportedSubtitle(InfoPlistReader.main.bundleDisplayName)
case .deviceNotSupported: case .slidingSyncNotAvailable:
L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedSubtitle(InfoPlistReader.main.bundleDisplayName) L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedSubtitle(InfoPlistReader.main.bundleDisplayName)
case .deviceAlreadySignedIn: case .deviceAlreadySignedIn:
L10n.screenQrCodeLoginErrorDeviceAlreadySignedInSubtitle L10n.screenQrCodeLoginErrorDeviceAlreadySignedInSubtitle
@@ -117,26 +121,30 @@ struct QRCodeErrorView: View {
iconStyle: iconStyle) iconStyle: iconStyle)
} }
@ViewBuilder
private var buttons: some View { private var buttons: some View {
VStack(spacing: 16) {
switch errorState { switch errorState {
case .noCameraPermission: case .noCameraPermission:
Button(L10n.screenQrCodeLoginNoCameraPermissionButton) { Button(L10n.screenQrCodeLoginNoCameraPermissionButton) {
action(.openSettings) action(.openSettings)
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
case .connectionNotSecure, .unknown, .expired, .declined, .deviceNotSupported:
Button(L10n.screenQrCodeLoginStartOverButton) { Button(L10n.actionCancel) {
action(.startOver) action(.cancel)
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.secondary))
case .cancelled: case .connectionNotSecure, .unknown, .expired, .declined, .slidingSyncNotAvailable, .cancelled:
Button(L10n.actionTryAgain) { Button(L10n.actionTryAgain) {
action(.startOver) action(.startOver)
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
case .linkingNotSupported:
VStack(spacing: 16) { Button(L10n.actionCancel) {
action(.cancel)
}
.buttonStyle(.compound(.secondary))
case .notSupported, .linkingNotSupported:
if canSignInManually { if canSignInManually {
Button(L10n.screenOnboardingSignInManually) { Button(L10n.screenOnboardingSignInManually) {
action(.signInManually) action(.signInManually)
@@ -144,18 +152,18 @@ struct QRCodeErrorView: View {
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
} }
Button(L10n.actionCancel) { Button(L10n.actionDismiss) {
action(.dismiss) action(.cancel)
}
.buttonStyle(.compound(.tertiary))
} }
.buttonStyle(.compound(canSignInManually ? .secondary : .primary))
case .deviceAlreadySignedIn: case .deviceAlreadySignedIn:
Button(L10n.actionContinue) { Button(L10n.actionContinue) {
action(.dismiss) action(.cancel)
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
} }
} }
}
} }
// MARK: - Previews // MARK: - Previews
@@ -181,13 +189,14 @@ struct QRCodeErrorView_Previews: PreviewProvider, TestablePreview {
private extension QRCodeLoginState.ErrorState { private extension QRCodeLoginState.ErrorState {
var previewDisplayName: String { var previewDisplayName: String {
switch self { switch self {
case .notSupported: "Not supported"
case .noCameraPermission: "No Camera Permission" case .noCameraPermission: "No Camera Permission"
case .connectionNotSecure: "Connection not secure" case .connectionNotSecure: "Connection not secure"
case .linkingNotSupported: "Linking unsupported" case .linkingNotSupported: "Linking unsupported"
case .cancelled: "Cancelled" case .cancelled: "Cancelled"
case .declined: "Declined" case .declined: "Declined"
case .expired: "Expired" case .expired: "Expired"
case .deviceNotSupported: "Device not supported" case .slidingSyncNotAvailable: "Sliding sync not available"
case .deviceAlreadySignedIn: "Device already signed in" case .deviceAlreadySignedIn: "Device already signed in"
case .unknown: "Unknown error" case .unknown: "Unknown error"
} }
@@ -116,7 +116,7 @@ struct QRCodeLoginScreen: View {
} }
} bottomContent: { } bottomContent: {
Button(L10n.actionCancel) { Button(L10n.actionCancel) {
context.send(viewAction: .dismiss) context.send(viewAction: .cancel)
} }
.buttonStyle(.compound(.secondary)) .buttonStyle(.compound(.secondary))
} }
@@ -281,7 +281,7 @@ struct QRCodeLoginScreen: View {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
if context.viewState.shouldDisplayCancelButton { if context.viewState.shouldDisplayCancelButton {
Button(L10n.actionCancel) { Button(L10n.actionCancel) {
context.send(viewAction: .dismiss) context.send(viewAction: .cancel)
} }
} }
} }
@@ -262,7 +262,7 @@ private extension HumanQrLoginError {
case .Expired, .NotFound: // The most likely cause of a .NotFound is that the rendezvous session expired on the server side case .Expired, .NotFound: // The most likely cause of a .NotFound is that the rendezvous session expired on the server side
.qrCodeError(.expired) .qrCodeError(.expired)
case .SlidingSyncNotAvailable: case .SlidingSyncNotAvailable:
.qrCodeError(.deviceNotSupported) .qrCodeError(.slidingSyncNotAvailable)
case .OtherDeviceNotSignedIn: case .OtherDeviceNotSignedIn:
.qrCodeError(.deviceNotSignedIn) .qrCodeError(.deviceNotSignedIn)
case .Unknown, .OidcMetadataInvalid, .CheckCodeAlreadySent, .CheckCodeCannotBeSent: case .Unknown, .OidcMetadataInvalid, .CheckCodeAlreadySent, .CheckCodeCannotBeSent:
@@ -103,7 +103,7 @@ enum QRCodeLoginError: Error, Equatable {
case declined case declined
case linkingNotSupported case linkingNotSupported
case expired case expired
case deviceNotSupported case slidingSyncNotAvailable
case deviceNotSignedIn case deviceNotSignedIn
case deviceAlreadySignedIn case deviceAlreadySignedIn
case unknown case unknown
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3ccc74bdd99363168ab860d90be717836a98e21b2011305582b34959cb7dd13a oid sha256:592b59289bed364dd2adc7728f3e12347ac94424a624be85bdce69c8e128aae6
size 111866 size 112157
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8e5f2cafc52726da840b03245f0cb66d48d8af7be99f55e4e09bfa192887d4ce oid sha256:38ff2a2eb43eceb91f9aabfa7a35d928cc6bf39b7a4bce26ef3c81cdd3e36150
size 125605 size 124522
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:45a70d51380f1bb93b25b0f789b0a437fdd5e28450b6680dc96c40a1153b2f3d oid sha256:9f35e13949126aa35bbc49b9e5dbf0845ca60be7063ee7fc3a0384164f96e856
size 101315 size 102469
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7ecc2829a7b715265cbe825c0fddb7cfb707c29380eb92b3cae23d3f977e4e3d oid sha256:842cbb773b3cb49647582e37d8a55043e83d3be1dbb0699e301049503b3f3d52
size 111986 size 113151
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:58fafcba5ce430c2b9db2d04d5f08b7fb49f657e7ea766d019aceaba938f5fa4 oid sha256:d8241c970619e687e62815ce3f7abb9fbfdea419aa7253bb72583aca0463b3a3
size 112156 size 112495
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1f1a820798a46a32b2747ea2efd5af67daf883e5cfd566409eb7e4911437afee oid sha256:3c904990b0d618a09545732264ae620e994259148474dccbf535edf694cc5b7a
size 125621 size 124221
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:865ad5f990f5afef41e924c286aebb8c99d9629577f4d75b237aa9972aa4a167
size 96939
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9619af1b33240507268b5b8ff1934e025758aaf23ad8b6d5a42a5e3baaed20d0
size 106049
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d59e49988fd995a3af8ec3ed54a951a6f7e6675d135cc8c0ffba780f64535365
size 53470
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84dcb8268c7f3f878ffd5d32beb9991a6e7212894e799b1df509a0e4a314516c
size 66759
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5ee9d2b8142ec642ea6f8d606769aaf2b4c1742855e5d67370f8d1b270a6e15a oid sha256:865ad5f990f5afef41e924c286aebb8c99d9629577f4d75b237aa9972aa4a167
size 108670 size 96939
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2bda2afc3ec1c2a1668a24b60021cf0045a8d6a8da870241e1f6c005f1b1a460 oid sha256:9619af1b33240507268b5b8ff1934e025758aaf23ad8b6d5a42a5e3baaed20d0
size 123442 size 106049
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:df24040677476878fd5ac4a22873177105c26094f74732fedb6af07642b6d44d oid sha256:d59e49988fd995a3af8ec3ed54a951a6f7e6675d135cc8c0ffba780f64535365
size 62919 size 53470
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:84932548b4b69ad3e0a1ab732cd895863e2a036903429f1febd973dda7cf54b1 oid sha256:84dcb8268c7f3f878ffd5d32beb9991a6e7212894e799b1df509a0e4a314516c
size 80320 size 66759
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1f8cf845823645d2e588ba8a428ea3593202278610a2c80f6d7e1c748fa268c7 oid sha256:99039cfa29c1ad171ccb4f79ead377ad8df08d157832d3d8d95f196d1965ac6a
size 90008 size 96338
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4695ac841ffb2d4e18503526d93ad8830324e39bb7d0066001a3986d339e8ee1 oid sha256:aeca0e8f518801efe79a78796a2cb87e8cb8f910b673882472ee021be2543779
size 96486 size 104068
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:79bdf4106793f8f3d8fb80feac0e0964d709190b9bfeede34cb3bc23493ef001 oid sha256:3bbf39094d65d397119568ff8a6bea9c313bd11e6aa4b2ededd718143b15f480
size 47530 size 53140
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3794b5a8528c3f5548788488df8248d60448aba81d9c3a9e89882ccac6b1e000 oid sha256:07dfb1ddbf0f9db98c5f28d01e5700ece886ea38060b512919ef9c0b8cda0ece
size 59265 size 65950
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0ab5f1cee96b039b902b3907bc714c842eaf5627f61ba8c86bac0b15ae9b1124 oid sha256:4da7776836f54807cfb1d032409cb3e5547fac036ca8ff8fd12d39962a048abd
size 142040 size 148049
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f6bf4551fca5b548bdbd46dc435d24599fb120545f3c38dc301f85d3591a60b8 oid sha256:b45f5822a671edf93fb06b55a446f2b45fb29547ea5e7edd5dd15a01976951dd
size 187585 size 194475
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:becdb1165a086f0330eec535042723adbb7fc6f54c5dbd5ceec89e3c47026996 oid sha256:8af042953fdd7d2dad2aaa7615acfc03e53ecfba021da7b38133e8185acfe91f
size 100875 size 106023
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5de8ec9e0a53a9c96576d4870063b4d7758b23d79bde721e858833ceea741454 oid sha256:eebe6b00f270ff92a737bc669452414fe849405385e4189b98cc01ecf26a3823
size 156932 size 157922
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bfaacefcc921a093540aac44cce94220e00eeb5b8177182add57ec84f3a0686a oid sha256:0df156666150ef572576f07a015accc9096251b902be50f5833ff9f86c2e94e1
size 87657 size 93666
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:eb5fb9dc291ba1f184e9267a8e0abf9abd2b4730b93e3fb62868df0b1658e643 oid sha256:a17d386ccc05cfd597178e48a66e8ae7caed4991862f7db5ce9b28d84b72b521
size 93414 size 100469
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d6c978a5fcbde7333ac77c8c3dc63b275206f8427e24f4bca54dac26eb44c970 oid sha256:f57c645139b41a6405c285cc23d54fc9ea0ec01c166b46096e11545918706951
size 45537 size 50889
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1f8cc9c945184f57b3b9ffb14862738dc82854de36bdcbe77fbcbbee7014df51 oid sha256:16b1c87494e516deefdfab2e71517f8c9e37b795b76294fcfc493bf66004a897
size 52829 size 59081
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc51ca496c5f50060a8750613f9454cf9280ad22276cc42cb92603100937383a
size 90595
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f2c18ea2b2b409ce38ea3c52d93daf1ffdf2f9ace3e8fbe24b1a7e6766a5a9f
size 98690
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3fa9554b1ad1e3e848a983cfac396114f9ef6d9408aaab83e41538242273ecd
size 48875
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:052b428d2fd84511082b5801122a5b5d5ddc985b48fb073c21f175fc7ded0545
size 62110
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:46530158add2765258605e3ae20c8a69aa7338f7190eab13df38993d5de59aca oid sha256:3e9f46a2e060cd117a567faff96f56223a5082772c7bd983685371500afc9f76
size 90983 size 96992
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1b9ad00a9444f377f069870058a9cce484200ecf80ddf0993374ba6603f916c1 oid sha256:f1a27690d4564af5e1d5bff2638555a98be4147c6af0434b7cbb641289f5686e
size 99958 size 107013
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ef52548b808b59f966b999650d45fce4b98fbbc6bc4becd11759b84a24bf3e5c oid sha256:6ef62bd49ad2b7bfba16ce5acb5297d034fc6ee44b248714f31e7b6de0cb3306
size 49878 size 55230
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:abd85913c2264d6276dfa36081826f501edfaef8a669bc9cae0b73e4d7c45052 oid sha256:d6d472aa4f4b88ff4116e8ca78f6401c2182ba563b148f0c53d32cb256d23b09
size 63407 size 69659
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1bb7ee19252c156dbe87f074885f68ca6fc2d58b9a628abc9ae34267fb4bd6a4 oid sha256:1a1956eb383999db5a1a0d49eb2e1f1dcbd6066739463c3e3e1b9afcee68cdbf
size 107644 size 111070
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6fe13470f847b819eebef43f10fc5d670c3d0820f217f48c72f08141da008073 oid sha256:2812be22f47f22db567773722833b96494518ffa260668fcfe9e5c1ed0fb9bfb
size 133115 size 136223
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:939a9c7d6a38eacd8b5636f258e2de8d65ea6a75abb563d2065988557c937d79 oid sha256:596d6d97bc0b59420fb41e3f5d377dea44b87c66584fc8d89b4893b2c541b52a
size 67578 size 70428
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:34ae92445cb12df66949b874ecdb718ef2099261cfa0c3e435051643a395198f oid sha256:e6825fc0ab0c340f5b78f8d1d8a3f62b38be46ec548385cc12ca271d5afe76a7
size 94076 size 96598
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c59427ee9b1963e79e162fa3e2e666293ed7c557b95f160997d1e9c2437d75f3 oid sha256:f53530dad8ac0e18025303bd5e9f39d9a2c1d4f797b1ab575d5765aec9804902
size 99906 size 102733
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d7adf94a154d8969516a4044a6adb7a77b8d936b2dff32572ee4476d9113f222 oid sha256:6284e9b1981fcd873bf2e460ddc837d3a9501724c0e76b85887b71ff8c81d501
size 123512 size 125964
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fda12a17466c9a0ac0b6853f6ec7920b113fc4256bef2661db2c18ea2a304a65 oid sha256:a9a79b94000ef8792fedfd95aea03c8ed40b955f90857acf9eedad1dc7a76413
size 60872 size 63166
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:648db4e91f5b828f25c3a6b121374bc957d2dbae8ad87914e5766a54dfc16e76 oid sha256:a2055ba432f09bf921d04f852e5635bf22b5311affb358cc68b175fee3ccf295
size 85578 size 87594
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a3c4204abe4bf3f79d53baa128e23ab0e64dc92044aa573dd5875e932d5c4c77 oid sha256:cd6aff7c2429ded2e44c7dc7bdac66fbad5f81f502ec5e5f5829bd03dd602bdc
size 100003 size 106290
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:25958be6f832b80dc4d941187bc9b73a04ea65a7f08cbe85b389120a02bb5937 oid sha256:6614cb1f9dcbd51b13e126935bf5c33ccfbfc5b4b7d1086f3a5dc0727ddca0f5
size 120131 size 127732
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0dde38bb16a2bf867a1246e183531b8e0e19ed22a26cfb33ef833746f0b58a06 oid sha256:53c64633e033c97b1ed56887ed77589c89c395bfbb876d5678fd6da27a158b38
size 60173 size 65737
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:61469ce1934f2f8adc28dcb232e703b653ae69480e03a46dfa1ae21e76a37136 oid sha256:209af483bc80d71ca347c4ff84e5b55afc37de4c192363026ea0363dd66591e4
size 87525 size 94066
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ffc09f736bbc07cecad5e93fbbcaa6eb4bdf90020b88446562b497e095d3a494
size 101929
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3e582bae4560bb35730a54b55919550a44e7a645bd3a79139d0beefab19fb75
size 116499
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:714c7b55b386f686b943edd992bf855a3d678d6a3ab83c44c03e88de3cea9f49
size 60429
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08706c72bfb0f679d634887cc081a539814d0f2c0461e079985a94bc7c0d96d3
size 79243
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d877a090876dd54ca18bf5ec98f21b8d28ecd00ef9a0ff86a2b0b0544bebf89d
size 96604
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:74650d5dd76c0e9cd1e20034f3e9eb9dd664078e7057f2eeed621828ee2be32f
size 105745
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:35c19a7f7b187a4fd4a0e7af53e7da80f72166c603ad022ec7c182d48bc23cff
size 54227
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4eefb6bf9ac16f888fad1988d9b9f05e6f02a3358010a618e8471dcb07845b54
size 68362
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:01e8cd4bdedb6a3ade9e4ce142418ea13c6aa7419bc8f756244cb1da9b38d7b3 oid sha256:61956bae4a87d1073ef462e0eeb1bc1ba43b155e99bb24bc428419666902b204
size 90233 size 96242
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cbfd78285154e0a1ffbe6d427a0c90448051c89ed62c28afd1abd09e5d96964f oid sha256:287eb47327afda08b0fcc6356aed7b49a49bad707d1a063a9286f0c3c0309810
size 96352 size 103407
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:68b5025c6c900caa7af12f0bc6d77c63799efc496452414bba1948a6441e7daf oid sha256:936c35ac1c5a06a8efe4a94587208f6ff118eba5c83412fb552835cf769103fa
size 47933 size 53285
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:25fca7ccc104f7df8e44df9341a25025dc1cb9900ea698660267eb8ab512690e oid sha256:9277a9f9be3ee8eb2b36b4738e38810168279ba963aa2504e3dbe1383f9c9166
size 60663 size 66915
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2e66a10d0251fd8d46b38e75a12729d2f37dea6c251ecd1d65c05005d2cdeab5 oid sha256:19b3c9c7130960cadbd4ef8c86e48af9c9129172f4befa195e69bf9942380228
size 88854 size 94696
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:36e4f3b626c67f30d7c5146c8ef62c1377fac2ae79afcb975064d484ac3bd170 oid sha256:f9a007a9348d053ae95005c9b9a37f50e2ea7537abf0f33cbeaf5e3473c8dd5f
size 94985 size 101164
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d6c978a5fcbde7333ac77c8c3dc63b275206f8427e24f4bca54dac26eb44c970 oid sha256:f57c645139b41a6405c285cc23d54fc9ea0ec01c166b46096e11545918706951
size 45537 size 50889
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1f8cc9c945184f57b3b9ffb14862738dc82854de36bdcbe77fbcbbee7014df51 oid sha256:16b1c87494e516deefdfab2e71517f8c9e37b795b76294fcfc493bf66004a897
size 52829 size 59081