SDKドキュメント

iOS SDK(GTNContactSDK)

GTN Contact の顧客向けチャットをネイティブiOSアプリに組み込むための SwiftUI SDK です。会話一覧・チャット・添付・CSATアンケートまでを含む画面一式を提供します。

概要

項目内容
最小OSiOS 17+
言語Swift 6(strict concurrency 対応、@MainActor / Sendable 準拠)
UIOS標準SwiftUI(SF Symbols・システムカラー)。文言は日本語
依存ライブラリゼロ(URLSession / URLSessionWebSocketTask / Codable のみ)
配布形態Swift Package(sdks/ios/GTNContactSDK)

インストール(Swift Package Manager)

Xcodeの File > Add Package Dependencies… で sdks/ios/GTNContactSDK ディレクトリをローカルパッケージとして追加するか、Package.swift に依存を記述します。

dependencies: [
    .package(path: "../GTNContactSDK")
]

クイックスタート

1. SDKを初期化(configure)

アプリ起動後・サポートUI表示前に一度だけ GTNContact.configure を呼び出します。

import GTNContactSDK

GTNContact.configure(
    baseURL: URL(string: "https://contact.example.com")!,
    tokenProvider: {
        // ホストアプリの認証バックエンドから顧客トークンを取得して返す
        try await MyAuthBackend.fetchSupportToken()
    },
    language: "ja"
)

2. トークン供給(tokenProvider)について

  • SDKは絶対に自分でトークンを発行しません。tokenProvider(@Sendable () async throws -> String)でホストアプリが顧客トークンを供給します。
  • 取得したトークンはSDK内部でキャッシュされ、RESTとWebSocketの両方に使われます。
  • devモードのトークン形式は dev:<customerId> です(サンプルアプリでは POST /v1/guest で自前取得)。

3. サポート画面を表示

SwiftUIから(推奨):

.sheet(isPresented: $showSupport) {
    SupportHomeView(
        sourceScreen: "請求明細画面",          // 起動元画面名 → サポートフローが自動解決される
        context: ["契約プラン": "プレミアム"], // フローに渡す追加コンテキスト
        showsCloseButton: true
    )
}

UIKitなどからモーダル起動:

GTNContact.presentSupport(sourceScreen: "ホーム画面", context: ["契約プラン": "プレミアム"])

認証

tokenProvider と 401 リトライ

  • 全リクエストに Authorization: Bearer <token> が付与されます。
  • APIが 401 を返した場合、SDKはキャッシュを破棄して tokenProvider を再呼び出しし、1回だけリトライします。再取得後も401なら GTNContactError.unauthorized を投げます。
  • トークン失効時の再取得(リフレッシュ)はホストアプリの責務です。tokenProvider は常に「現在有効なトークン」を返すよう実装してください。

ゲスト→ログイン統合(claimGuest)

ゲスト時代の会話をログイン後の顧客に統合します(POST /v1/identities/claim)。tokenProvider がログイン後のトークンを返すようになった後に呼び出してください。

let mergedCount = try await GTNContact.claimGuest(guestCustomerId: guestId) // 統合された会話数

プッシュトークン登録

// APNsデバイストークン(16進文字列)の登録(POST /v1/push-tokens、platform: "ios")
try await GTNContact.registerPushToken(deviceTokenHex)
// ログアウト時などに解除(DELETE /v1/push-tokens)
try await GTNContact.unregisterPushToken(deviceTokenHex)

画面の埋め込み方

エントリポイント用途
SupportHomeView(sourceScreen:context:showsCloseButton:)会話一覧+新規問い合わせ。SwiftUIの sheet や NavigationStack に直接埋め込む
GTNContact.presentSupport(sourceScreen:context:)UIKit等からの最短導線。keyWindowの最前面VCに pageSheet でモーダル表示
ChatView(conversationId:initialMessages:conversation:)特定の会話のチャット画面を直接開く(プッシュ通知タップからの遷移など)
// 特定の会話を直接開く
ChatView(conversationId: conversationId)

機能リファレンス

会話

  • 会話一覧(GET /v1/conversations): 新しい順表示、プルリフレッシュ
  • 会話開始(POST /v1/conversations): sourceScreen でフローが自動解決され、チケットも自動作成されます
  • ステータスは5種類で、日本語バッジ表示されます
status表示
botボット対応中
queued接続待ち
agent対応中
waiting_customer返信待ち
closed終了

構造化メッセージ(7種)

MessageBubbleView が contentType に応じてレンダリングします。

contentType内容挙動
textテキスト通常バブル
buttons選択肢ボタンタップで value をtext送信。最新メッセージのみ有効、selected は押下不可
form入力フォーム最新のみ編集可、過去分は読み取り専用
carouselカルーセル横スクロールカード
fileファイルタップでダウンロードSAS取得→表示
link_cardリンクカード外部ブラウザで開く
systemシステム通知中央寄せグレー小文字

差分同期とページング(tail / before / after)

メッセージの seq は会話内で単調増加するカーソルです。GET /v1/conversations/{id}/messages は3つのクエリを受け付けます。

パラメータ用途レスポンス
tail=1初回表示: 最新50件(limit 指定可)を取得prevCursor(次の before に渡すseq)と hasMoreBefore が返る
before=<prevCursor>上スクロール: 過去メッセージを遡って取得同上(さらに古い分があれば hasMoreBefore: true)
after=<maxSeq>差分同期: 保持している最大seqより新しい分だけ取得nextCursor(次の after に渡すseq)と hasMore

推奨パターン: 初回は tail=1 で最新50件 → 上スクロールで before=prevCursor を繰り返して遡り → 新着・再接続時は after=maxSeq で差分同期。

内蔵の ChatView は3つのカーソルをすべて使い分けます: 初回表示は getLatestMessages(tail=1)で最新50件のみを取得して最下部を表示し、上スクロールで先頭に到達すると getMessagesBefore(before=prevCursor)で古いページを追加読み込みします(スクロール位置は維持)。新着・再接続時の取りこぼし回復は従来どおり after=maxSeq の差分同期+id による重複排除です。REST APIを直接呼ぶ場合も同じ3カーソルが利用できます。

WebSocket(リアルタイム)

SocketClient が会話単位で GET /v1/ws?conversationId=<id>&token=<bearer token> に接続し、AsyncStream<SocketEvent> でイベントを配信します。

イベント内容
connected接続確立(再接続含む)。受信側は after=maxSeq で差分同期し、readを再送すること
disconnected切断(自動再接続が走る)
messageCreated新着メッセージ。自分の送信もエコーされるため id で重複排除する
conversationUpdatedstatus変更・エージェント参加など
typing入力中通知(SDKは送信側500msデバウンス、受信時「入力中…」表示)
read既読通知(senderType: agent なら自分のメッセージに既読表示)
presence閲覧者一覧(顧客UIでは未使用)
reactionUpdated絵文字リアクションの追加/削除。対象メッセージの全リアクションのスナップショットが届くため丸ごと置き換える
  • 自動再接続: 指数バックオフ 1s → 最大10s。再接続時は差分取得+既読再送を行います。
  • 送信系: sendTyping(isTyping:) / sendRead(lastSeq:)。画面破棄時は必ず close() を呼びます。

送達・既読

  • 送信は clientMessageId でべき等。ネットワークエラー時は同一IDで最大3回自動リトライし、失敗時はタップで再送できます。
  • 表示は 送信中(時計)→ 送達(チェック)→ 既読(「既読」表示)の3段階。既読は会話詳細の reads とWSの read イベントで更新されます。

絵文字リアクション

  • バブルの長押しでクイックリアクション(👍 🙏 ❤️ 😂 🎉 😢)を付けられます。バブル下のチップをタップすると自分のリアクションをトグルできます。
  • メニューの「その他の絵文字…」から全絵文字ピッカー(MCEmojiPicker。SDKの依存としてSPMで自動解決)を開き、任意の絵文字を選べます(カテゴリ・肌色バリアント対応)。
  • API: addReaction(conversationId:messageId:emoji:) / removeReaction(conversationId:messageId:emoji:)(POST / DELETE /v1/conversations/{id}/messages/{messageId}/reactions)。追加は同一絵文字の再送に対してべき等です。
  • どちらも対象メッセージの全リアクション(スナップショット)を返します。他の参加者の変更はWSの reactionUpdated で届きます。
  • ViewModelでは toggleReaction(for:emoji:) / hasMyReaction(_:emoji:) を利用できます(会話は単一顧客のため reactorType == .customer が自分の判定)。

添付(SASフロー)

  1. createAttachmentUpload(conversationId:fileName:contentType:) — POST /v1/conversations/{id}/attachments でアップロード用SASを発行(15分有効)
  2. uploadAttachment(sas:data:) — 返却された uploadUrl へ headers(x-ms-blob-type 等)付きで Azure Blobへ直接PUT
  3. sendMessage(contentType: .file) — blobName / fileName / fileSize を含む file メッセージを送信
  4. 閲覧時は attachmentDownloadURL(conversationId:blobName:) でダウンロード用SAS(60分有効)を取得

内蔵の AttachmentPicker はPhotosPicker+fileImporterでこのフローを自動実行します。

CSAT(満足度アンケート)

  • 会話クローズ後、CsatFormView がアンケートを表示します(1会話1回。回答済みはローカルにも記録)。
  • 設問は csatQuestions()(GET /v1/csat/questions、有効設問のみorder順)で取得。設問タイプは star5 / binary / nps / text の4種で、必須バリデーション付き。
  • 回答は submitCsat(conversationId:score:comment:answers:)(POST /v1/conversations/{id}/csat)で送信します(クローズ後のみ)。

エラーハンドリング

エラーは GTNContactError(LocalizedError 準拠)で投げられ、errorDescription はそのままユーザーに表示できる日本語です。

ケース発生条件日本語メッセージ
notConfiguredconfigure 未呼び出しSDKが初期化されていません。GTNContact.configure を呼び出してください。
network(underlying:)ネットワーク到達不可・タイムアウト通信に失敗しました。ネットワーク接続を確認して、もう一度お試しください。
unauthorizedトークン再取得後も401認証に失敗しました。しばらくしてからもう一度お試しください。
conversationClosed409 conversation_closedこの会話は終了しているため、メッセージを送信できません。
csatAlreadySubmitted409 already_submittedこのアンケートにはすでに回答済みです。
conversationNotClosed409 conversation_not_closed会話が終了していないため、アンケートに回答できません。
server(status:code:detail:)その他のサーバーエラーエラーが発生しました(code): detail
decoding(underlying:)レスポンスの解析失敗サーバー応答の解析に失敗しました。

409 コード一覧

HTTPerror コード意味
409conversation_closedクローズ済み会話へのメッセージ送信
409already_submittedCSATにすでに回答済み
409conversation_not_closed未クローズ会話へのCSAT回答

サンプルアプリの起動方法

  1. APIサーバーをローカルで起動(http://localhost:3010)
  2. sdks/ios/SampleApp を開いてビルド:
xcodebuild -project SampleApp.xcodeproj -scheme SampleApp
  1. アプリ内の「デモログイン」は POST /v1/guest でトークンを自前取得してSDKに供給します(=ホストアプリの認証バックエンドの代役)。
  2. UIテスト SampleAppUITests がログイン→会話開始→ボタン選択→フォーム送信まで自動実行します。

FAQ

ローカルAPI(localhost)に接続できない

http://localhost:3010 など平文HTTPへ接続する場合、ホストアプリの Info.plist に App Transport Security の例外が必要です。

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsLocalNetworking</key>
    <true/>
</dict>

Auth0 接続時に変更は必要?

SDK側のコード変更は不要で、tokenProvider の実装だけが変わります。

  • devモード(サーバーの ALLOW_DEV_AUTH が有効)では dev:<customerId> 形式のトークンをそのまま渡せます。
  • サーバーに AUTH0_DOMAIN が設定されている場合、tokenProvider は Auth0が発行した顧客用JWT(AUTH0_CUSTOMER_AUDIENCE のaudience)を返すようにします。エージェント用audienceとは分離されています。
  • トークン失効時はSDKの401リトライで tokenProvider が再呼び出しされるので、そこでリフレッシュ済みトークンを返してください。
  • ゲスト運用から移行する場合は、ログイン完了後に GTNContact.claimGuest(guestCustomerId:) でゲスト時代の会話を統合できます。

横断検索

チケット・会話・記事・フローを検索