import SwiftUI #if os(iOS) import SafariServices #endif struct SetupStep: Identifiable { enum Status { case done, active, todo } let name: String var status: Status var detail: String? var signIn = false var signInURL: URL? var id: String { name } } func signInStep(_ name: String, hostname: String, state: String, authURL: URL?) -> SetupStep { if state == "Running" { return SetupStep(name: name, status: .done) } if state == "NeedsMachineAuth" { return SetupStep(name: name, status: .active, detail: "\(hostname) is waiting for a tailnet admin to approve it") } if state == "NeedsLogin" { return SetupStep(name: name, status: .active, detail: authURL == nil ? "Waiting for Tailscale's sign-in link…" : "\(hostname) is waiting for you", signIn: true, signInURL: authURL) } return SetupStep(name: name, status: .active, detail: "Waiting for Tailscale (\(state))…") } struct StepsCard: View { let title: String let detail: String let steps: [SetupStep] var body: some View { VStack(alignment: .leading, spacing: 0) { Text(title) .font(Fonts.font(17, weight: 700)) Text(detail) .foregroundStyle(Palette.muted) .fixedSize(horizontal: false, vertical: true) .padding(.top, 4) .padding(.bottom, 10) ForEach(Array(steps.enumerated()), id: \.element.id) { i, step in if i > 0 { Rectangle().fill(Palette.border).frame(height: 1) } StepRow(step: step) } } .padding(20) .frame(maxWidth: .infinity, alignment: .leading) .background(Palette.raised, in: RoundedRectangle(cornerRadius: 10)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(Palette.border)) .frame(maxWidth: 460) .padding(16) .frame(maxWidth: .infinity, maxHeight: .infinity) #if os(macOS) .onAppear { Tailnet.shared.serviceWatchers += 1 } .onDisappear { Tailnet.shared.serviceWatchers -= 1 } #endif } } private struct StepRow: View { let step: SetupStep @State private var signingIn = false var body: some View { HStack(alignment: .center, spacing: 10) { icon .frame(width: 18, height: 18) VStack(alignment: .leading, spacing: 2) { Text(step.name) .foregroundStyle(step.status == .todo ? Palette.muted : Palette.text) if let detail = step.detail { Text(detail) .font(Fonts.font(12)) .foregroundStyle(Palette.muted) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) } } .frame(maxWidth: .infinity, alignment: .leading) if step.signIn { Button("Sign in") { #if os(macOS) if let url = step.signInURL { openExternal(url) } #else signingIn = true #endif } .buttonStyle(WebButtonStyle(primary: true)) .disabled(step.signInURL == nil) .fixedSize() } } .padding(.vertical, 8) #if os(iOS) .sheet(isPresented: $signingIn) { if let url = step.signInURL { SafariView(url: url) .ignoresSafeArea() } } .onChange(of: step.signIn) { if !step.signIn { signingIn = false } } #endif } @ViewBuilder private var icon: some View { switch step.status { case .done: Image(systemName: "checkmark") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(Palette.ok) case .active: ProgressView() .controlSize(.small) case .todo: Image(systemName: "circle") .font(.system(size: 8)) .foregroundStyle(Palette.muted) } } } #if os(iOS) private struct SafariView: UIViewControllerRepresentable { let url: URL func makeUIViewController(context: Context) -> SFSafariViewController { SFSafariViewController(url: url) } func updateUIViewController(_ controller: SFSafariViewController, context: Context) {} } #endif