import SwiftUI /// "Join a call as a guest": paste the invite link you were sent, and /// that meeting's own browser sign-in runs for it, on whatever server the /// link names. Nothing about it is kept. /// /// The clipboard is read only through the Paste control, so iPhone never /// asks whether Calls may paste, and nothing is read that the person did /// not hand over. @MainActor public struct GuestJoinView: View { @ObservedObject private var model: CallsModel private let done: () -> Void @State private var link: String @State private var problem: String = "" /// `problem` shows the link's validation at once — for snapshots of /// the inline error. public init( model: CallsModel, link: String = "", problem: Bool = false, done: @escaping () -> Void ) { self.model = model self.done = done _link = State(initialValue: link) var message = "" if problem, case .failure(let failure) = MeetingLink.validate(link) { message = failure.message } _problem = State(initialValue: message) } public var body: some View { NavigationStack { Form { Section { TextField("Invite link", text: $link, prompt: Text("https://…/j/…")) .accessibilityIdentifier("inviteLink") #if os(iOS) .keyboardType(.URL).textInputAutocapitalization(.never).autocorrectionDisabled() #endif .onSubmit(join) .onChange(of: link) { problem = "" } PasteButton(payloadType: String.self) { strings in guard let text = strings.first else { return } Task { @MainActor in paste(text) } } .accessibilityIdentifier("pasteInvite") if !problem.isEmpty { Label(problem, systemImage: "exclamationmark.circle.fill") .foregroundStyle(.red).fixedSize(horizontal: false, vertical: true) .accessibilityLabel("Error: \(problem)") } } footer: { Text( "Paste the link someone sent you. Your browser opens to confirm who you are for this meeting; nothing is saved on this device, and your Calls server stays \(model.serverHost)." ) } } .formStyle(.grouped) #if os(iOS) .navigationTitle("Join as a guest") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel", action: done) } ToolbarItem(placement: .confirmationAction) { joinButton } } #else // A Mac sheet has no title bar to carry these, so they sit under // the form where a Mac dialog keeps its buttons. .safeAreaInset(edge: .top) { Text("Join as a guest").font(.title2.bold()) .frame(maxWidth: .infinity, alignment: .leading).padding([.horizontal, .top], 20) } .safeAreaInset(edge: .bottom) { HStack { Spacer() Button("Cancel", action: done).keyboardShortcut(.cancelAction) joinButton.keyboardShortcut(.defaultAction) }.padding([.horizontal, .bottom], 20) } #endif } #if os(macOS) .frame(width: 460) .fixedSize(horizontal: false, vertical: true) #endif .tint(CallsAppearance.accent) } private var joinButton: some View { Button("Join", action: join).accessibilityIdentifier("guestJoin") .disabled(link.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } /// Fills the field only with a Calls link; anything else on the /// clipboard is left there, and said so. private func paste(_ text: String) { if case .success(let url) = MeetingLink.validate(text) { link = url.absoluteString problem = "" } else { problem = "The clipboard doesn’t hold a Calls invite link." } } private func join() { if let failure = model.joinAsGuest(link) { problem = failure.message } else { done() } } }