feat(apple): contacts screen and consent prompts

Device list, detail, and block list under Settings, plus the two sheets:
an incoming offer and a device asking to be remembered. Both are
answer-only, since swiping away would leave the sender waiting.

Accepting an offer routes the released ticket into the ordinary receive
path, so the platform still chooses the destination. The prompt does not
ask a second time; it only falls back to the review sheet when the
destination is unusable.
This commit is contained in:
2026-08-06 17:57:54 +02:00
parent 256ff89423
commit 1369df4578
7 changed files with 535 additions and 1 deletions

View File

@@ -6,6 +6,9 @@ enum ReceiveMethod {
case invitationFile
case qrCode
case nfc
/// Pushed by a remembered device and already accepted by the user, so no
/// invitation was acquired by hand.
case offer
}
enum ReceiveHistoryDeleteTarget: Equatable {
@@ -154,6 +157,40 @@ final class ReceiveModel: ObservableObject {
}
}
/// Receive a transfer the user has already accepted in the offer prompt.
///
/// The consent happened in that prompt, so this does not ask again: it
/// inspects the ticket and starts, falling back to the ordinary review sheet
/// only when the destination is not usable and the user has to fix it.
func receiveOffered(ticket: String) {
let trimmed = ticket.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return messages.error(.resource(L10n.Error.invitationEmpty)) }
state.ticket = trimmed
state.method = .offer
state.inspection = nil
state.isInspecting = true
Task {
switch await repository.inspectTicket(trimmed) {
case .success(let inspection):
state.inspection = inspection
state.isInspecting = false
if state.canReceive(coreInitialized: coreState.isInitialized) {
receive()
} else {
// Usually a missing or unwritable destination: show the review
// sheet so the user can point it somewhere valid.
state.isAcquisitionOpen = true
}
case .failure(let error):
state.ticket = ""
state.method = nil
state.inspection = nil
state.isInspecting = false
messages.error(error)
}
}
}
func receive() {
let current = state
guard let folder = current.receiveFolder else { return }