import LChatCore import SwiftUI // 🤖 The iOS receiver's confirm card (web→app handoff — the Mac // AddFromLinkSheet's sibling): an invite link that arrived from OUTSIDE the // app (the join page's "Open in the app" scheme, or a universal link on an // instance subdomain) pre-fills this sheet — it never auto-joins; the Add // tap is the confirming act for any URL that came from elsewhere. // Deliberately NO editable field: iOS has no paste-a-link (Paul's call — // joining lives on the web), so only an externally-arrived, already-validated // link can reach this card. SpaceClaim handles all three link shapes // (member/setup invites, DM invites, add-device links) with no webview; the // secret in the #fragment never reaches any server. struct AddFromLinkConfirm: View { @EnvironmentObject var app: AppState @Environment(\.dismiss) private var dismiss let link: String /// Called after the claim persisted (app.add ran) — RootView selects the /// new space and drops the pending link. let onAdded: (SavedInstance) -> Void @State private var busy = false @State private var error: String? private var host: String { parseJoinLink(link).flatMap { URL(string: $0.origin)?.host } ?? "" } var body: some View { VStack(spacing: 14) { Image(systemName: "person.crop.circle.badge.plus") .font(.system(size: 44)) .foregroundStyle(.tint) .accessibilityHidden(true) Text("Add this space?") .font(.title3.bold()) Text("You opened an invite for \(host). It joins this \(DeviceName.current) and syncs to your Eleven account.") .font(.callout) .foregroundStyle(.secondary) .multilineTextAlignment(.center) if let error { Text(error) .font(.footnote) .foregroundStyle(.red) .multilineTextAlignment(.center) } Button(action: add) { HStack(spacing: 6) { if busy { ProgressView().controlSize(.small) } Text(busy ? "Joining…" : "Add Space") } .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) .disabled(busy) Button("Cancel") { dismiss() } .disabled(busy) } .padding(24) .presentationDetents([.medium]) } private func add() { guard !busy else { return } busy = true error = nil Task { @MainActor in do { // Carry the name you already go by; an add-device link's own // "~name" (or "Me") wins inside the claim when this is empty. let screenName = app.instances.first(where: { !$0.isArchived })?.screenName ?? app.instances.first?.screenName ?? "" let inst = try await SpaceClaim.claim(setupLink: link, screenName: screenName) app.add(inst) // Keychain + list + vault push onAdded(inst) // RootView: select + dismiss the pending link } catch { self.error = (error as? LocalizedError)?.errorDescription ?? "Couldn't add that space." busy = false } } } }