<!-- AI agents: this is the Markdown version of a Reception documentation page. Index: /llms.txt -->

# Open & close the chat

Place support wherever it fits your app: in settings, on a home screen, in a toolbar or menu, behind a floating button, or in your navigation. ReceptionKit supplies the chat screen; you write the entry-point code and control its position, label, style, and unread badge. The four examples below are starting points for your own design.

Settings rows and buttons call `Reception.shared.openChat()` to let ReceptionKit present its sheet over the current screen. A dedicated tab embeds `ReceptionChatView()` and uses your app’s tab selection. The examples below show both patterns, followed by [dismissal options](#dismiss-the-chat).

## Choose an entry point {#entry-examples}

These mockups show four common placements. Choose the one that fits your app, then use the matching [SwiftUI example](#entry-implementations) below.

![Minimal settings mockup highlighting a Support row with a red unread dot](/docs/images/entry-placement-settings.webp "Settings row · Part of your settings")

![Minimal home-screen mockup highlighting a Support button within the page content](/docs/images/entry-placement-home.webp "Home-screen button · In the page content")

![Minimal app mockup highlighting a floating Support button at the bottom right, above the navigation](/docs/images/entry-placement-floating.webp "Floating button · Above the page content")

![Minimal app mockup highlighting a Support tab with a red unread dot in the bottom navigation](/docs/images/entry-placement-tabs.webp "Dedicated tab · In your app’s navigation")

### Or anywhere else in your app {#suggested-placements}

A toolbar action, a help menu, your own custom button: the placement, label, icon, style, and unread indicator are yours to build. Keep the entry point discoverable, give it a meaningful accessible label, and show a dot or count when `Reception.shared.unreadCount` is greater than zero. See [Unread messages](/docs/unread-messages) for badge behavior.

## Open the chat {#opening-the-chat}

Call `Reception.shared.openChat()` from your own settings row, toolbar, help menu, or floating button. ReceptionKit has no public ready-made settings row or floating entry button. Use your app's controls, styling, and localized labels.

This action belongs in a SwiftUI button on the main actor:

```swift
Button("Contact support") {
    Reception.shared.openChat()
}
```

`Reception.shared.openChat()` requests the SDK chat sheet over the current screen and returns immediately. It works in SwiftUI and UIKit without changing your root view or navigation.

ReceptionKit UI and state APIs are `@MainActor` isolated. For an action arriving outside the main actor, hop to it before opening. This belongs in the host callback that requests presentation:

```swift
Task { @MainActor in
    Reception.shared.openChat()
}
```

Avoid opening Support while another host sheet is being presented. Route the action through your app's presentation coordinator when you need to dismiss an existing modal first.

## Entry-point code examples {#entry-implementations}

Adapt these controls to your app's design. Each example reads the live `Reception.shared.unreadCount` and hides its badge at zero.

Configure ReceptionKit before creating these views. Import `SwiftUI` and `ReceptionKit` in the host file, and localize the labels and unread-message plurals through your app's normal resources.

### Settings row {#settings-row}

Put `GuideSupportSettingsRow()` inside an existing settings `List` or `Section`. The row opens the SDK sheet and hides its badge at zero.

The row uses the reusable `GuideUnreadBadge` defined just below it:

```swift
@MainActor
struct GuideSupportSettingsRow: View {
    var body: some View {
        Button { Reception.shared.openChat() } label: {
            HStack {
                Label("Support", systemImage: "bubble.left.and.bubble.right")
                Spacer()
                if Reception.shared.unreadCount > 0 {
                    GuideUnreadBadge(count: Reception.shared.unreadCount)
                }
                Image(systemName: "chevron.right")
                    .font(.footnote.weight(.semibold))
                    .foregroundStyle(.tertiary)
            }
        }
        .accessibilityLabel("Support")
        .accessibilityValue(Text("\(Reception.shared.unreadCount) unread messages"))
    }
}

struct GuideUnreadBadge: View {
    let count: Int
    var body: some View {
        Text(count, format: .number)
            .font(.subheadline.weight(.semibold))
            .monospacedDigit()
            .foregroundStyle(.white)
            .frame(minWidth: 26, minHeight: 26)
            .background(.indigo, in: Capsule())
    }
}
```

### Home-screen entry {#home-screen-entry}

Place `GlassSupportHomeButton()` in your home screen. This host-owned button uses native Liquid Glass on iOS 26 and regular system material on earlier versions. Adapt its colors and spacing to your app.

The wrapper observes the SDK count; the control hides the badge at zero and opens the SDK sheet. Its glass styling uses [SwiftUI’s glassEffect](https://developer.apple.com/documentation/swiftui/view/glasseffect(_:in:)), independently of `Reception.shared.appearance`:

```swift
import SwiftUI
import ReceptionKit

// Place in the host home screen, after configuring ReceptionKit.
@MainActor
struct GlassSupportHomeButton: View {
    var body: some View {
        GlassSupportHomeControl(count: Reception.shared.unreadCount)
    }
}

@MainActor
struct GlassSupportHomeControl: View {
    let count: Int

    var body: some View {
        Group {
            if #available(iOS 26.0, *) {
                button
                    .glassEffect(.regular.interactive(),
                                 in: .rect(cornerRadius: 24))
            } else {
                button
                    .background(.regularMaterial,
                                in: RoundedRectangle(cornerRadius: 24))
            }
        }
        .accessibilityLabel("Support")
        .accessibilityValue(Text("\(count) unread messages"))
    }

    private var button: some View {
        Button { Reception.shared.openChat() } label: {
            HStack(spacing: 12) {
                Image(systemName: "bubble.left.and.bubble.right.fill")
                    .font(.title2).foregroundStyle(.indigo)
                VStack(alignment: .leading, spacing: 4) {
                    Text("Support").font(.headline).foregroundStyle(.primary)
                    Text("Continue the conversation")
                        .font(.subheadline).foregroundStyle(.secondary)
                }
                Spacer(minLength: 0)
                if count > 0 {
                    Text(count, format: .number)
                        .font(.subheadline.weight(.semibold))
                        .monospacedDigit()
                        .foregroundStyle(.white)
                        .frame(minWidth: 26, minHeight: 26)
                        .background(.indigo, in: Capsule())
                }
                Image(systemName: "chevron.right")
                    .font(.footnote.weight(.semibold)).foregroundStyle(.secondary)
            }
            .padding(20)
            .frame(maxWidth: .infinity, alignment: .leading)
            .contentShape(RoundedRectangle(cornerRadius: 24))
        }
        .buttonStyle(.plain)
    }
}
```

### Floating button {#floating-button}

Keep support within reach on a main screen with a host-owned floating button. Place it above your safe area, clear of navigation and primary actions. This example uses native Liquid Glass on iOS 26 and regular material on earlier versions; it opens the SDK sheet through `Reception.shared.openChat()`.

After configuration, add this overlay to your screen content:

```swift
.overlay(alignment: .bottomTrailing) {
    FloatingSupportButton()
        .padding(.trailing, 24)
        .padding(.bottom, 30)
}
```

The wrapper observes the live count; its reusable control supplies the native button:

```swift
import SwiftUI
import ReceptionKit

@MainActor
struct FloatingSupportButton: View {
    var body: some View {
        FloatingSupportControl(count: Reception.shared.unreadCount)
    }
}

@MainActor
struct FloatingSupportControl: View {
    let count: Int

    var body: some View {
        Group {
            if #available(iOS 26.0, *) {
                button.glassEffect(.regular.interactive(), in: .capsule)
            } else {
                button.background(.regularMaterial, in: Capsule())
            }
        }
        .accessibilityLabel("Support")
        .accessibilityValue(Text("\(count) unread messages"))
    }

    private var button: some View {
        Button { Reception.shared.openChat() } label: {
            HStack(spacing: 10) {
                Label("Support", systemImage: "bubble.left.and.bubble.right.fill")
                    .font(.headline)
                if count > 0 {
                    Text(count, format: .number)
                        .font(.subheadline.weight(.semibold))
                        .monospacedDigit()
                        .foregroundStyle(.white)
                        .frame(minWidth: 26, minHeight: 26)
                        .background(.indigo, in: Capsule())
                }
            }
            .padding(.horizontal, 20)
            .frame(minHeight: 56)
            .contentShape(Capsule())
        }
        .buttonStyle(.plain)
    }
}
```

### Dedicated Support tab {#support-tab}

A dedicated tab can host `ReceptionChatView()` directly. Add the badge to the tab's destination, using the integer overload: a zero value hides it. Reading the SDK value inside `body` lets SwiftUI update the badge while another tab is selected.

Merge the Support destination into your existing `TabView`; the other destinations below are placeholders for your real screens:

```swift
@MainActor
struct GuideSupportTabs: View {
    var body: some View {
        TabView {
            Text("Today")
                .tabItem { Label("Today", systemImage: "house") }
            Text("Habits")
                .tabItem { Label("Habits", systemImage: "checkmark.circle") }
            NavigationStack { ReceptionChatView() }
                .tabItem { Label("Support", systemImage: "bubble.left.and.bubble.right") }
                .badge(Reception.shared.unreadCount)
            Text("Settings")
                .tabItem { Label("Settings", systemImage: "gearshape") }
        }
        .tint(.indigo)
    }
}
```

Unread monitoring begins at configuration, before the Support tab is first visited. Your app owns tab selection. `Reception.shared.openChat()` and notification taps forwarded with `openChat: true` present the SDK sheet; they do not select this tab. If your app routes a support notification into the tab, forward it with `openChat: false` and select the Support tab through your own navigation state. Do not also call `openChat()` for the same action. See [Entry point](/docs/entry-point#custom-navigation).

Choosing the tab does not itself clear the count. It updates after the visible chat successfully marks loaded replies read. Newer replies can remain unread. Network failure can leave a badge in place; do not overwrite the read-only property or keep a separate counter to force it to zero.

## Use custom navigation {#custom-navigation}

Use `ReceptionChatView()` when your app needs to own the sheet binding or navigation path. Configure ReceptionKit first and display only one chat at a time.

### Present a custom sheet

Place this view in your host app. Its Boolean controls presentation and can also be set to `false` by host actions that must dismiss the sheet:

```swift
import SwiftUI
import ReceptionKit

@MainActor
struct HelpView: View {
    @State private var showsSupport = false

    var body: some View {
        Button("Support") {
            showsSupport = true
        }
        .sheet(isPresented: $showsSupport) {
            ReceptionChatView()
        }
    }
}
```

When presented as a sheet, `ReceptionChatView()` supplies its own navigation stack and a native drag indicator. Do not add an extra `NavigationStack` around it. Swipe-to-dismiss works unless your host disables interactive dismissal. The optional SDK close button also dismisses a custom sheet through SwiftUI's dismiss environment.

### Push onto your navigation stack

Place this view in the host app for a navigation-based entry point:

```swift
import SwiftUI
import ReceptionKit

@MainActor
struct HelpNavigationView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Support") {
                    ReceptionChatView()
                }
            }
            .navigationTitle("Help")
        }
    }
}
```

In an ordinary navigation stack, the chat uses your navigation controls instead of adding the sheet close button. The user goes back with the host's Back button or navigation gesture. For programmatic dismissal, update the host's navigation path or destination binding.

If your navigation stack is inside a sheet or another modal, test the Back button and dismissal in that hierarchy.

### Route open requests deliberately

`Reception.shared.openChat()` and forwarded notification taps with `openChat: true` request the SDK sheet; they do not toggle `showsSupport` or push your custom destination. A request does nothing while chat is already visible. When an action opens a host-owned `ReceptionChatView()`, do not also call `openChat()` for that action.

For an app that routes all support UI through its own navigation, forward a support notification with `openChat: false` and use the returned Boolean to decide whether to open your own destination. Keep foreground receipt separate from notification taps: receiving a notification should not automatically navigate. See [Push notifications](/docs/push-notifications).

Unread monitoring starts with `Reception.configure(appId:)`, before the first custom chat appearance. Reading `unreadCount` in a SwiftUI view observes updates.

## Dismiss the chat {#dismiss-the-chat}

`Reception.shared.closeChat()` closes the SDK sheet programmatically, for example before navigating to a deep link target or showing a logout screen. Choose dismissal according to who owns the presentation:

| Presentation | Dismissal |
| --- | --- |
| SDK sheet from `openChat()` or a push tap | Swipe down, use the optional SDK close button, or call `Reception.shared.closeChat()`. |
| Host sheet containing `ReceptionChatView()` | Set your sheet binding to `false`, use the sheet gesture, or enable the SDK close button. |
| Dedicated Support tab | Select another tab through your host tab state. |
| Host navigation destination | Use Back, the navigation gesture, or update your navigation state. |

A host-owned `ReceptionChatView` presentation is dismissed by the host. Do not call `Reception.shared.logout()` merely to close a chat: it resets the support session and local data. Logout, switching to a different App ID, and successful data deletion dismiss the SDK-owned sheet, but they do not change your custom sheet binding or navigation path. Coordinate those host states during [login and logout](/docs/accounts#sign-out) and [data deletion](/docs/accounts#delete-data).

### Show the close button {#shows-close-button}

`showsCloseButton` defaults to `false`. Configure these local appearance settings during app initialization on the main actor:

```swift
Reception.shared.appearance.showsCloseButton = true
Reception.shared.appearance.closeIcon = .chevronDown
```

`closeIcon` defaults to `.xmark`; supported values are `.xmark`, `.chevronDown`, and `.arrowDown`. Changing the icon alone does not make the button visible. See [showsCloseButton and closeIcon](/docs/appearance) for their appearance reference.

Published [remote appearance](/docs/remote-appearance) can override local values. Set `Reception.shared.usesRemoteAppearance = false` during initialization if your app must use only its local appearance. If the host disables interactive dismissal, provide an available dismissal control; do not rely on a remote-controlled close button as the only exit.

![Native chat with an xmark close button](/docs/images/ios-close-xmark.webp "closeIcon = .xmark")

![Native chat with a downward chevron close button](/docs/images/ios-close-chevron.webp "closeIcon = .chevronDown")

![Native chat with a downward arrow close button](/docs/images/ios-close-arrow.webp "closeIcon = .arrowDown")

## Observe chat visibility {#is-chat-open}

`Reception.shared.isChatOpen` is an observable, read-only Boolean. It reflects whether the shared chat content has appeared, including custom `ReceptionChatView()` presentations. It becomes false when that content disappears.

Use it in your host's main-actor presentation logic, for example to avoid requesting another chat:

```swift
if !Reception.shared.isChatOpen {
    Reception.shared.openChat()
}
```

It is visibility state, not a presentation binding or a network status. It may still be false immediately after `Reception.shared.openChat()` while the sheet is being presented, and it can remain true while a visible chat's app is backgrounded. Do not treat it as proof of an active connection or use it as your custom sheet's writable state. A host presentation coordinator must also account for transitions and pending requests.

For analytics, `.chatOpened` is available through [events](/docs/events). There is no matching public chat-closed event; observe visibility when your host needs that transition.

## When ReceptionKit uses the network {#network-activity}

### Before the first valid send

For an unused support session, configuration stores settings and reads cached appearance without starting authenticated chat requests. Opening the chat, drafting text, choosing photos, or forwarding a push does not register a device or load a conversation. Unread checks stay inactive.

Public appearance is the exception: the configured SDK or a visible chat can fetch project appearance with the App ID while active, without a session. Disable remote appearance to use local appearance only.

Registration begins with the first valid send attempt, even if delivery later fails.

### After activation

A visible, active chat loads messages, reports read state, and keeps the conversation updated. Dismissal or backgrounding pauses live updates; an already-running request may still finish.

Unread updates continue while the chat is closed and your app is active. Closing the chat also refreshes the count. See [Unread refresh timing](/docs/unread-messages#refresh-timing) for the automatic checks and push-related pauses.

### Last active in chat

The dashboard’s Last active in chat records recent chat activity. App launches and unread checks do not count. An unused chat cannot report activity; In chat is a separate live-connection signal. See [Dashboard](/docs/dashboard).

## Show unread replies {#unread-badge}

Read `Reception.shared.unreadCount` in your SwiftUI entry point and hide its badge when the count is zero. The [Settings row](#settings-row), [home-screen entry](#home-screen-entry), [floating button](#floating-button), and [dedicated tab](#support-tab) examples above each show that behavior.

After support has been used, unread updates also work while chat is closed. For automatic updates, push handling, and app-icon badge ownership, follow [Unread messages](/docs/unread-messages).

## Verify your integration

1. Open Support from your entry point, then dismiss it and open it again.
2. Navigate through login and your main screens; confirm the SDK sheet opens over the current screen.
3. Test the sheet gesture, optional close button, and navigation Back behavior in the presentation you chose.
4. Confirm your custom sheet or path also dismisses during host account transitions.
5. Forward a notification tap and confirm it opens exactly one intended chat. Test foreground receipt separately.
6. Send a message, leave the chat, and follow the [unread verification flow](/docs/unread-messages#verify-unread-behavior).

If no sheet opens, check that configuration ran and whether another modal is occupying the presentation location. If the close button does not match your setting, check the active remote appearance and whether the chat is actually in a presented context.
