Files
simplex-chat/apps/ios/product/flows/connection.md
2026-04-01 14:17:27 +00:00

10 KiB

Connection Flow

Related spec: spec/api.md | spec/architecture.md

Overview

Establishing contact between two SimpleX Chat users. SimpleX uses no user identifiers; connections are formed through one-time invitation links or permanent SimpleX addresses. Each connection creates unique unidirectional SMP queues, ensuring no server can correlate sender and receiver. Supports incognito mode for per-contact random profile generation.

Prerequisites

  • User profile created and chat engine running
  • Network connectivity to SMP relay servers
  • For QR code scanning: camera permission granted

Step-by-Step Processes

  1. User taps "+" button in ChatListView -> NewChatMenuButton -> "Add contact".
  2. NewChatView is presented.
  3. Calls apiAddContact(incognito:):
    func apiAddContact(incognito: Bool) async
        -> ((CreatedConnLink, PendingContactConnection)?, Alert?)
    
  4. Internally sends ChatCommand.apiAddContact(userId:incognito:) to core.
  5. Core creates SMP queues and returns ChatResponse1.invitation(user, connLinkInv, connection).
  6. Returns (CreatedConnLink, PendingContactConnection).
  7. CreatedConnLink contains the invitation URI (both full and short link forms).
  8. UI displays:
    • QR code rendered by QRCode view (scannable by peer)
    • Share button to send link via system share sheet
    • Copy button for clipboard
  9. A PendingContactConnection appears in the chat list while awaiting peer.
  1. User receives a SimpleX link (pasted, scanned, or opened via URL scheme).
  2. If opened via deep link: SimpleXApp.onOpenURL sets chatModel.appOpenUrl.
  3. For manual entry: User pastes link in NewChatView.
  4. First, apiConnectPlan(connLink:inProgress:) is called to validate:
    func apiConnectPlan(connLink: String, inProgress: BoxedValue<Bool>) async
        -> ((CreatedConnLink, ConnectionPlan)?, Alert?)
    
  5. Returns ConnectionPlan indicating whether it is an invitation, contact address, or group link, and whether connection is already established.
  6. If valid, calls apiConnect(incognito:connLink:):
    func apiConnect(incognito: Bool, connLink: CreatedConnLink) async
        -> (ConnReqType, PendingContactConnection)?
    
  7. Core creates the connection and returns one of:
    • ChatResponse1.sentConfirmation(user, connection) -- for invitation links (type: .invitation)
    • ChatResponse1.sentInvitation(user, connection) -- for contact address links (type: .contact)
    • ChatResponse1.contactAlreadyExists(user, contact) -- duplicate
  8. PendingContactConnection appears in chat list while awaiting peer confirmation.
  1. For short links with embedded profile data, the app uses a two-phase flow.
  2. apiPrepareContact(connLink:contactShortLinkData:) or apiPrepareGroup(connLink:directLink:groupShortLinkData:) creates a local prepared chat. directLink is true for standard group links, false for channel relay links.
  3. Returns ChatData with the prepared contact/group shown in UI before connecting.
  4. User can switch profiles or set incognito before committing.
  5. apiConnectPreparedContact(contactId:incognito:msg:) finalizes the connection.
  6. Returns ChatResponse1.startedConnectionToContact(user, contact).

4. Accept Contact Request

  1. When a peer connects via the user's SimpleX address, core generates a ChatEvent.receivedContactRequest.
  2. processReceivedMsg handles the event, adding a UserContactRequest to ChatModel.
  3. Contact request appears in ChatListView as a special ContactRequestView row.
  4. User taps "Accept":
    func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact?
    
  5. Sends ChatCommand.apiAcceptContact(incognito:contactReqId:).
  6. Core returns ChatResponse1.acceptingContactRequest(user, contact).
  7. Connection handshake proceeds asynchronously.
  8. User can also reject: apiRejectContactRequest(contactReqId:) -> ChatResponse1.contactRequestRejected.

5. Connection Established

  1. Both sides complete the SMP handshake asynchronously.
  2. Core sends ChatEvent.contactConnected(user, contact, userCustomProfile).
  3. processReceivedMsg updates ChatModel:
    • Contact status transitions from pending to active.
    • Chat becomes available for messaging.
  4. NtfManager may post a notification: "Contact connected".
  5. The PendingContactConnection in the chat list is replaced by the full contact chat.

6. Create SimpleX Address

  1. User navigates to Settings or taps "Create SimpleX address" during onboarding.
  2. Calls apiCreateUserAddress():
    func apiCreateUserAddress() async throws -> CreatedConnLink?
    
  3. Core creates a permanent address (unlike one-time invitations).
  4. Address is stored in ChatModel.shared.userAddress.
  5. Can be shared publicly; multiple contacts can connect via the same address.
  6. User must accept each incoming contact request individually.
  7. To delete: apiDeleteUserAddress() removes the address and associated SMP queues.
  1. User scans, pastes, or opens a relay address link (URL path /r or SimplexLinkType.relay).
  2. In ContentView.connectViaUrl_(): early return with alert "Relay address" / "This is a chat relay address, it cannot be used to connect."
  3. In NewChatView.planAndConnect(): .simplexLink(_, .relay, _, _) pattern triggers the same alert.
  4. The link is NOT processed further. No connection is attempted.

7b. Channel Prepared Group Flow

  1. When connecting to a channel link (GroupShortLinkInfo.direct == false):
  2. apiPrepareGroup(connLink:directLink:groupShortLinkData:) is called with directLink: false, preparing the channel locally.
  3. groupShortLinkInfo.groupRelays (hostnames) stored in ChatModel.shared.channelRelayHostnames[groupId].
  4. Pre-join UI shows channel icon and "Open new channel" (not "Open new group").
  5. apiConnectPreparedGroup(groupId:incognito:msg:) returns (GroupInfo, [RelayConnectionResult]).
  6. RelayConnectionResult contains relayMember: GroupMember and optional relayError: ChatError? per relay.
  7. Relay members are upserted to chatModel.groupMembers; channelRelayHostnames entry is cleared.

7. Incognito Connection

  1. Before connecting, user toggles "Incognito" in the connection UI.
  2. incognito: true is passed to apiAddContact, apiConnect, or apiAcceptContactRequest.
  3. Core generates a random display name for this connection only.
  4. The random profile is stored per-connection; the user's real profile is never shared.
  5. Incognito status is shown with a mask icon in the chat.
  6. Can also be toggled for pending connections via apiSetConnectionIncognito(connId:incognito:).

Data Structures

Type Location Description
CreatedConnLink SimpleXChat/APITypes.swift Contains connFullLink (URI) and optional connShortLink
PendingContactConnection SimpleXChat/ChatTypes.swift Represents an in-progress connection before contact is established
ConnectionPlan Shared/Model/AppAPITypes.swift Enum describing what a link will do: connect contact, join group, already connected, etc.
ConnReqType Shared/Views/NewChat/NewChatView.swift .invitation, .contact, or .groupLink -- type of connection request
Contact SimpleXChat/ChatTypes.swift Full contact model with profile, connection status, preferences
UserContactRequest SimpleXChat/ChatTypes.swift Incoming contact request awaiting acceptance
ChatType SimpleXChat/ChatTypes.swift .direct, .group, .local, .contactRequest, .contactConnection
GroupShortLinkInfo Shared/Model/AppAPITypes.swift Contains direct: Bool, groupRelays: [String], publicGroupId: String?; transient data returned by prepare
RelayConnectionResult Shared/Model/AppAPITypes.swift Contains relayMember: GroupMember, relayError: ChatError?; per-relay join outcome

Error Cases

Error Cause Handling
ChatError.invalidConnReq Malformed or expired link Alert: "Invalid connection link"
ChatError.unsupportedConnReq Link requires newer app version Alert: "Unsupported connection link"
ChatError.errorAgent(.SMP(_, .AUTH)) Link already used or deleted Alert: "Connection error (AUTH)"
ChatError.errorAgent(.SMP(_, .BLOCKED(info))) Server operator blocked connection Alert: "Connection blocked" with reason
ChatError.errorAgent(.SMP(_, .QUOTA)) Too many undelivered messages Alert: "Undelivered messages"
ChatError.errorAgent(.INTERNAL("SEUniqueID")) Duplicate connection attempt Alert: "Already connected?"
ChatError.errorAgent(.BROKER(_, .TIMEOUT)) Server timeout Retryable via chatApiSendCmdWithRetry
ChatError.errorAgent(.BROKER(_, .NETWORK)) Network failure Retryable via chatApiSendCmdWithRetry
contactAlreadyExists Connecting to existing contact Alert: "Contact already exists" with contact name
errorAgent(.SMP(_, .AUTH)) on accept Sender deleted request Alert: "Sender may have deleted the connection request"

Key Files

File Purpose
Shared/Views/NewChat/NewChatView.swift Main connection UI: create link, paste link, QR scan
Shared/Views/NewChat/NewChatMenuButton.swift "+" button menu in chat list
Shared/Views/NewChat/QRCode.swift QR code rendering for invitation links
Shared/Views/NewChat/AddContactLearnMore.swift Help text explaining connection process
Shared/Views/ChatList/ContactRequestView.swift Incoming contact request display
Shared/Views/ChatList/ContactConnectionView.swift Pending connection display
Shared/Views/ChatList/ContactConnectionInfo.swift Connection details sheet
Shared/Model/SimpleXAPI.swift API functions: apiAddContact, apiConnect, apiConnectPlan, apiAcceptContactRequest, apiCreateUserAddress
Shared/Model/AppAPITypes.swift ConnectionPlan enum, GroupLink struct
SimpleXChat/APITypes.swift CreatedConnLink, ComposedMessage, command/response types
SimpleXChat/ChatTypes.swift Contact, PendingContactConnection, UserContactRequest
  • apps/ios/product/README.md -- Product overview: Contacts capability map
  • apps/ios/product/flows/messaging.md -- Messaging after connection is established