// // Copyright 2025 Element Creations Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Combine import Compound import SwiftUI struct LinkNewDeviceScreen: View { @Bindable var context: LinkNewDeviceScreenViewModel.Context 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) { mainContent } bottomContent: { buttons } .background() .backgroundStyle(.compound.bgSubtleSecondary) .navigationTitle(L10n.commonLinkNewDevice) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } } } @ViewBuilder var mainContent: some View { switch context.viewState.mode { case .loading, .readyToLink: TitleAndIcon(title: L10n.screenLinkNewDeviceRootTitle, icon: \.computer, iconStyle: .default) case .error: EmptyView() // Not reachable. } } @ViewBuilder var buttons: some View { switch context.viewState.mode { case .loading: Button { } label: { Label { Text(L10n.commonLoading) } icon: { ProgressView() .tint(.compound.iconOnSolidPrimary) } } .buttonStyle(.compound(.primary)) .disabled(true) case .readyToLink(let linkState): let isGeneratingCode = linkState == .generatingCode VStack(spacing: 16) { Button { context.send(viewAction: .linkMobileDevice) } label: { Label { Text(isGeneratingCode ? L10n.screenLinkNewDeviceRootLoadingQrCode : L10n.screenLinkNewDeviceRootMobileDevice) } icon: { if isGeneratingCode { ProgressView() .tint(.compound.iconOnSolidPrimary) } else { CompoundIcon(\.mobile) } } } .buttonStyle(.compound(.primary)) .accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.mobileDevice) if context.viewState.showLinkDesktopComputerButton { Button { context.send(viewAction: .linkDesktopComputer) } label: { Label(L10n.screenLinkNewDeviceRootDesktopComputer, icon: \.computer) } .buttonStyle(.compound(.primary)) .accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.desktopComputer) } } .disabled(linkState != .idle) case .error: EmptyView() // Not reachable. } } var toolbar: some ToolbarContent { ToolbarItem(placement: .cancellationAction) { Button(L10n.actionCancel) { context.send(viewAction: .dismiss) } .accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.cancel) } } } // MARK: - Previews @available(iOS 26.0, *) struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = makeViewModel(mode: .readyToLink(.idle)) static let verifyingViewModel = makeViewModel(mode: .readyToLink(.verifyingDeviceOwner)) static let generatingViewModel = makeViewModel(mode: .readyToLink(.generatingCode)) static let loadingViewModel = makeViewModel(mode: .loading) static let unsupportedViewModel = makeViewModel(mode: .error(.notSupported)) static let unknownErrorViewModel = makeViewModel(mode: .error(.unknown)) static var previews: some View { ElementNavigationStack { LinkNewDeviceScreen(context: viewModel.context) } .previewDisplayName("Ready") .snapshotPreferences(expect: viewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(.idle) }) ElementNavigationStack { LinkNewDeviceScreen(context: verifyingViewModel.context) } .previewDisplayName("Verifying") .snapshotPreferences(expect: verifyingViewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(.verifyingDeviceOwner) }) ElementNavigationStack { LinkNewDeviceScreen(context: generatingViewModel.context) } .previewDisplayName("Generating") .snapshotPreferences(expect: generatingViewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(.generatingCode) }) ElementNavigationStack { LinkNewDeviceScreen(context: loadingViewModel.context) } .previewDisplayName("Loading") ElementNavigationStack { LinkNewDeviceScreen(context: unsupportedViewModel.context) } .previewDisplayName("Unsupported") .snapshotPreferences(expect: unsupportedViewModel.context.observe(\.viewState.mode).map { $0 == .error(.notSupported) }) ElementNavigationStack { 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 { LinkNewDeviceScreenViewModel(clientProxy: ClientProxyMock(.init()), appLockService: AppLockServiceMock.mock(), initialState: .init(mode: mode, showLinkDesktopComputerButton: true)) } }