Why the chat belongs inside the app

When something breaks in an iOS app, most users don't go looking for a support email. They close the app, or they open the App Store and leave a review. A chat one tap away from the problem catches them first, and it can attach what you'd otherwise have to ask for: the device, the iOS version, the app version and anything your app knows, like their plan.

DevReply's iOS SDK is written in Swift and SwiftUI, with no dependencies. It needs iOS 17 and Swift 6 / Xcode 16 or later. Your users get a home screen with start buttons (bug, billing, idea, question), their conversations, photos and files, and the chat in their phone's language (34 languages, Hebrew and Arabic right to left). There's no web view in it.

1. Add the Swift package

In Xcode, add the package https://github.com/Code-Axolot-Team/devreply-ios with the product DevReply, “Up to Next Major” from 0.5.0. In a Package.swift:

// Package.swift
.package(url: "https://github.com/Code-Axolot-Team/devreply-ios", from: "0.5.0")

2. Configure it once, at launch

Create an app in the DevReply dashboard and copy its iOS public key. It starts with pk_ and is safe to ship in your app. A secret key (sk_…) never goes in an app.

import SwiftUI
import DevReply

@main
struct MyApp: App {
    init() {
        // Once, at launch (your iOS public key from the dashboard; it's safe to ship)
        DevReply.configure("pk_…")
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

3. Add a “Message the developer” button

Call present() from any button. It opens the chat over whatever is on screen:

Button("Message the developer") {
    DevReply.present()                // or present(category: .bug)
}

If you'd rather drive it from SwiftUI state, use the view modifier:

struct SettingsView: View {
    @State private var showChat = false

    var body: some View {
        List {
            if DevReply.isAvailable {
                Button("Message the developer") { showChat = true }
            }
        }
        .devReplyMessenger(isPresented: $showChat)
    }
}

Your team can switch the chat off in the dashboard, say for a holiday, without a release. Then present returns false and shows nothing, and DevReply.isAvailable lets you hide your own button.

4. Tell it who the user is

All of this is optional. A name and email save the user typing them, and attributes show up next to every conversation in your inbox:

DevReply.setUser(name: "Ana", email: "[email protected]")
DevReply.setAttributes(["plan": "pro", "trial": false])

If your app has accounts, mirror sign-in, sign-out and account deletion:

DevReply.login(userID: account.id)     // after sign-in: your own id for the user, never an email or a secret
DevReply.logout()                      // on every sign-out and account switch
let deleted = await DevReply.deleteUser()   // in your delete-account flow; false = queued, retried until done

logout makes the device forget the chat, so the next person on a shared phone starts empty. deleteUser deletes the user's name, email, attributes, conversations and files, which covers Apple's rule that account deletion happens in the app.

5. Open it from the paywall, with context

The most expensive messages come from a failed purchase. Open the chat right there, with a draft in the composer and context for your team. The user sees the draft and sends it; nothing is sent on its own. askName: false skips the name form, so it's one tap from the error to the message:

DevReply.present(
    category: .billing,
    message: "My purchase didn't go through",
    attributes: ["source": "paywall", "rc_error_code": "PURCHASE_NOT_ALLOWED"],
    askName: false
)

The attributes go with that one conversation and show in the inbox as “Opened with”, so the message arrives already diagnosed.

6. Replies that reach them: push and email

While the chat is open, replies arrive live. When it isn't, DevReply shows an unread bubble over your app, sends a push, and on Pro emails users who left an email and haven't read the reply. For push, pass the device token from your app delegate:

final class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        DevReply.registerPush(deviceToken)
    }
}

// In your App: @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

Then upload your APNs key in the dashboard. DevReply never takes over your notification handling: it sets its own delegate only when your app has none. If you have a UNUserNotificationCenterDelegate, forward with DevReply.presentationOptions(for:) and DevReply.handleNotificationResponse(_:); if a push library hands you a tapped notification's data, pass it to DevReply.handleNotificationOpened(userInfo:). The dashboard's push card shows “✓ Taps open the chat” once it works.

The email has a “Reply in the app” button that opens yourapp://devreply?devreply=<conversation>. Add a URL scheme, set yourapp://devreply as the deep link in the dashboard, and let DevReply look at links first:

.onOpenURL { url in
    if DevReply.handle(url) { return }
    // your own links
}

What you see on your side

app.devreply.com
The DevReply inbox: a message tagged Billing, with the user’s iPhone, iOS and app version, and what the app passed with it
The inbox for Tenball, our golf app. The golfer is a demo account.

The message lands in your inbox in seconds, tagged with the start button the user picked, and conversations waiting on you sit in red at the top. Next to it: their name, device, iOS and app version, language, your attributes and the “Opened with” context. You reply from the dashboard with your own name and photo, in Markdown (bold, lists, links). When it's sorted, resolve it; the user sees it's done and can reopen it by replying.

Let your coding agent install it

Every app in DevReply has a setup guide written for coding agents, with your own keys in it: the dashboard, the app → Settings → “Add DevReply to your app”. Hand it to Claude Code, Cursor or Codex in your repository and it adds the package, the configure call, the button, push and the deep link, then checks the install with the guide's status URL. You can also connect the agent over MCP; the setup tools are free on every plan:

claude mcp add --transport http devreply https://api.devreply.com/mcp

On Pro, the same connection lets your agent read and sort the inbox and draft replies you approve. The full list for agents is in llms.txt.

More guides

Fix it before it's a review.

Free to start, no card: unlimited apps, users and teammates, 50 conversations a month. Pro is $49 a month when your agent takes over.