SendBirdDesk 1.1.6

SendBirdDesk 1.1.6

由以下人员维护:Tez ParkMinhyuk KimJed GyeongJaesung LeeKai



  • 作者:
  • Jed Gyeong,Tez Park,Minhyuk Kim,Sendbird,Celine Moon,Damon Park,Young Hwang 和 Kai Lee

Sendbird Desk SDK for iOS

目录

  1. 简介
  2. 开始之前
  3. 入门
  4. 创建您的第一个工单
  5. 实现指南

简介

Sendbird Desk 通过实时应用内支持,使客户参与度更强。Desk SDK 允许您轻松初始化、配置并将客户支持相关功能构建到您的 iOS 应用程序中。

如何工作

Sendbird Desk 是 Sendbird Chat 平台的插件,用于管理工单,所有 Desk 事件都通过 聊天 SDK 处理。

每个工单都会分配适当的客服代表,并将被定向到聊天组的频道,该频道通过 Sendbird Chat SDK 实现实时消息。

概念

以下是 Desk SDK 的一些主要组件。

  • 频道:支持请求的多种方式,例如来自不同操作系统平台的内联聊天或社交媒体(如 Facebook 和 Instagram)。
  • 工单:当客服代表和客户开始对话时创建工单,它是客户咨询的单元。工单有五种类型。
  • 客服代表:客服代表接收请求并在 Sendbird 控制台 中处理对话。
  • 管理员:管理员是具有管理整体控制台设置和工单额外权限的客服代表。
  • 消息:Desk 有两种主要消息类型,并进一步分为子类型。以下表格显示了消息的层次结构。
发送者 子类型
用户消息 客服或客户 富文本消息
管理员消息 由 Desk 服务器发送,没有特定发送者 通知消息和系统消息

注意:富文本消息进一步分为 URL 预览请求关闭工单的确认反馈请求 消息。

关于Sendbird Desk SDK for iOS的更多信息

了解更多关于Sendbird Desk SDK for iOS的信息,请访问Desk SDK for iOS 文档。如果您对错误和功能请求有任何评论或问题,请访问Sendbird 社区


开始前的准备

本节将展示使用Sendbird Desk SDK for iOS所需检查的前提条件。

要求

Sendbird Desk SDK for iOS的最低要求如下

  • iOS 11+
  • Sendbird Chat SDK for iOS 4.6.7+

开始使用

本节将为您提供开始使用Sendbird Desk SDK for iOS所需的信息。

尝试示例应用

我们的示例应用展示了Sendbird Desk SDK的核心功能。从我们的GitHub存储库下载应用程序,了解您可以使用实际SDK做什么,并开始构建您自己的项目。

步骤1:从仪表板创建Sendbird应用程序

一个Sendbird应用程序包含了聊天服务所需的一切,包括用户、消息和频道。要创建应用程序

  1. 访问Sendbird仪表板,输入您的电子邮件和密码,创建新账户。您也可以使用Google账户进行注册。
  2. 当设置向导提示时,输入您的组织信息以管理Sendbird应用程序。
  3. 最后,完成设置后在仪表板主页面出现时,点击右上角的创建+

无论平台如何,每个应用程序只能集成一个Sendbird应用程序;然而,应用程序支持Sendbird提供的所有平台之间的通信,无需任何额外设置。

注意:所有数据都限于单个应用程序的范围,因此不同Sendbird应用程序中的用户无法互相聊天。

步骤2:下载和安装Desk SDK

如果您熟悉在项目中使用外部库或SDK,安装Desk SDK很简单。您可以使用以下两种方法之一安装Desk SDK

  1. 创建或编辑您的Podfile文件。
# Uncomment the next line to define a global platform for your project
platform :ios, '11.0'

target 'YourTarget' do
    # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
    use_frameworks!
    
    # Pods for YourTarget
    pod 'SendBirdDesk'

end
  1. 运行pod install
  2. 打开 YOUR_PROJECT.xcworkspace

或者,您可以下载最新的 Desk SDK for iOS。将可在 Github 仓库中找到的 Desk SDK 复制到您的项目目录中,并确保在您的工文件中也包含了该库。


创建您的第一个工单

安装完成后,可以为代理商和客户之间的通讯创建一个工单。按照以下分步说明来创建您的第一个工单。

步骤 1:初始化 Desk SDK

启动客户端应用程序时必须初始化“SBDSKMain”实例。在应用完成启动的“applicationDidFinishLaunching”方法中调用“SendbirdChat.initialize(params:)”和“SBDSKMain.initializeDesk()”方法。首先由仪表板中的“APP_ID”调用“initialize(params:)”,该“APP_ID”属于您的 Sendbird 应用程序。

class AppDelegate: UIApplicationDelegate {
    func applicationDidFinishLaunching(_ application: UIApplication) {
        let initParams = InitParams(applicationId: APP_ID)
        SendbirdChat.initialize(params: initParams)
        SBDSKMain.initializeDesk()
    }
}

注意:使用 Sendbird Chat SDK 构建,Sendbird Desk SDK 是一个插件,用于通过工单处理客户询问。工单中的消息由 Sendbird Chat 平台处理,每个工单都与 Sendbird Chat 中的组频道对应。由于这种交互,Desk 和 Chat SDK 都应使用相同的 APP_ID

步骤 2:验证客户身份

客户可以通过多种类型的渠道请求支持:应用内聊天或社交媒体,如 Facebook、Instagram 和 Twitter。要使用 Desk SDK 的这些支持功能, SendbirdDesk 实例应根据请求的渠道与 Sendbird 服务器连接。

  • Sendbird Chat 平台:使用 SendbirdChat.connect()SBDSKMain.authenticate() 方法以及他们的用户标识进行认证。
  • 社交媒体平台:无需认证,因为顾客会自动使用社交媒体账户在 仪表板 登记。

认证后,顾客可以根据 Sendbird Chat 平台与客服进行实时聊天。

SendbirdChat.connect(userId: USER_ID, authToken: ACCESS_TOKEN) { (user, error) in
    guard error == nil else {
        // Handle error
        return
    }
    
    // Use the same user ID and access token(`authToken`) used in the SendbirdChat.connect()
    SBDSKMain.authenticate(withUserId: USER_ID, accessToken: ACCESS_TOKEN) { (error) in
        guard error == nil else {   
            // Handle error
            return
        }
        
        // SendBirdDesk is now initialized, and the customer is authenticated.
    }
}

备注来自 Sendbird Chat 平台的客户 表示已通过 Chat SDK 认证的用戶。如果您同时实现 Chat SDK 和 Desk SDK,首先使用用户标识和访问令牌 将用户连接到 Sendbird 服务器

第3步:创建工单

使用 SBDSKTicket.createTicket() 方法在客户初始消息之前或之后创建新的工单。返回的工单将包含可以通过 ticket.channel 访问的会话实例。因此,您可以使用 SendbirdChat SDK 向该会话发送消息。

SBDSKTicket.createTicket(with: TITLE, userName: USER_NAME) { (ticket, error) in
    guard error == nil else {   
        // Handle error.
        return
    }
    
    // The ticket is created. Agents and customers can chat with each other by sending a message through the ticket.channel.sendUserMessage() or sendFileMessage().
}

在 Sendbird 服务器上成功创建工单后,您可以通过服务器的回调访问该工单及其会话。

在客户发送第一条消息之前,客服无法在仪表板上看到工单,且不会发生工单分配。一旦开始对话,Desk Dashboard 会在发送和接收消息的同时将工单分配给可用的客服。

在创建工单时,您可以使用以下参数传递到 SBDSKTicket.createTicket() 的参数。

参数 类型 描述
TICKET_TITLE 字符串 指定工单的标题。
USER_NAME 字符串 指定提交或接收工单的用户姓名。
GROUP_KEY 字符串 指定特定团队的标识符。
CUSTOM_FIELDS [String: String] 指定由 键值 形式的自定义项目组成的工单附加信息。只有仪表板中按 设置 > 工单 字段注册的自定义字段可以作为键使用。
PRIORITY SBDSKTicketPriority 指定工单的优先级值。数值越大,优先级越高。有效值为 .low.medium.high.urgent
RELATED_CHANNEL_URLS [String] 指定与该工单相关的 Sendbird Chat 平台的群组会话,由会话 URL 和会话名称组成。最多可以添加3个相关会话。
var customFields = [String: String]()
customFields["product"] = "desk"
customFields["line"] = "14"
customFields["select"] = "option2"

SBDSKTicket.createTicket(with: TICKET_TITLE, userName: USER_NAME, groupKey: GROUP_KEY customFields: customFields, priority: PRIORITY, relatedChannels: RELATED_CHANNEL_URLS) { (ticket, error) in
    guard error == nil else {   
        // Handle error.
        return 
    }
    
    // The ticket is created with parameters.
}

实施指南

本部分详细说明了如何从您的客户端应用程序处理和关闭工单。

为工单添加自定义信息

使用 ticket.setCustomFields() 方法为工单添加附加信息。

var customFields = [String: String]()
customFields["product"] = "Desk"
customFields["line"] = "\(30)"

ticket.setCustomFields(customFields) { (error) in
    guard error == nil else {   
        // Handle error.
        return 
    }
    
    // Custom fields for the ticket are set.
    // Some fields can be ignored if their keys aren't registered in the dashboard.
}

注意:只能使用在您的仪表板 Desk > 设置 > 票务字段 中注册的自定义字段作为键。

为客户添加自定义信息

使用 SBDSKMainsetCustomerCustomFields() 方法让您的客户添加关于他们的附加信息。

var customFields = [String: String]()
customFields["gender"] = "Female"
customFields["age"] = "\(30)"

SBDSKMain.setCustomerCustomFields(customFields) { (error) in
    guard error == nil else {   
        // Handle error.
        return 
    }
}

// Custom fields for the customer are set.
// Some fields can be ignored if their keys aren't registered in the dashboard.

注意:只能使用在您的仪表板 Desk > 设置 > 客户字段 中注册的自定义字段作为键。

添加相关频道

相关频道表示Sendbird Chat平台上与工单相关的组频道。在创建工单时,将相关组频道的channel_url作为参数传递给SBDSKTicket.createTicket()方法中的relatedChannels。要更新相关频道,请使用SBDSKTicket.setRelatedChannels()。回调中的SBDSKTicket.relatedChannels属性指示相关频道的组频道对象,并包含频道名称和它们的URL。

SBDSKTicket.createTicket(withTitle: TITLE, userName: USER_NAME) { (ticket, error) in
    guard error == nil else {   
        // Handle error.
        return
    }
    
    // The ticket is created. Agents and customers can chat with each other by sending a message through the ticket.channel.sendUserMessage() or sendFileMessage().
   ...

   ticket.setRelatedChannels(RELATED_CHANNEL_URLS) { (error) in
        guard error == nil else {   
            // Handle error.
            return 
       }
            
    // The ticket.relatedChannels property has been updated.
   }
}

注意:每个工单最多可以添加3个相关频道。

添加URL预览

通过URL预览,您的应用程序用户在打开链接之前可以了解他们将会得到什么。

要使用URL预览,每个文本消息都应该检查是否包含任何URL。其次,分析URL的HTML源代码以查找OpenGraph元数据。然后,将提取的OG元数据设置为JSON对象,并将其字符串化以作为参数传递给UserMessageUpdateParams类的data属性,该类用于GroupChannel updateUserMessage(messageId:params:data:completionHandler:)方法的参数。带有URL预览的更新消息将通过BaseChannelDelegatechannel(_:didUpdate:)代理方法发送到客户端应用。

ticket.channel?.sendUserMessage(TEXT, completionHandler: { userMessage, error in
    guard let userMessage = userMessage, error == nil else {    
        // Handle error.
        return 
    }
            
    let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
    let match = detector?.firstMatch(in: userMessage.message, options: [], range: NSMakeRange(0, userMessage.message.count))
    guard let url = match?.url else {   // No matching URL 
        return 
    }
         
    URLSession().dataTask(with: url) { (data, response, error) in
        guard error == nil, let data = data else {  
            // Handle error.
            return 
        }
                
        guard let httpResponse = response as? HTTPURLResponse,
        let contentType = httpResponse.allHeaderFields["Content-Type"] as? String,
            contentType.contains("text/html") else { return }
        let htmlBody = String(data: data, encoding: .utf8)

        // Extract ogTitle, ogImage, ogUrl, ogSiteName, ogDescription from htmlBody.
        // Refer to Open Graph Protocol(https://ogp.me/), and other open source implementations of OG tag parsing for further details. 

        let urlPreview = ["type": "SENDBIRD_DESK_URL_PREVIEW",
            "body": [
                "title": ogTitle,
                "image": ogImage,
                "url": ogUrl,
                "site_name": ogSiteName,
                "description": ogDescription
            ]
        ] as [String: Any]
        // Stringified JSON object let jsonData = try? JSONSerialization.data(withJSONObject: urlPreview, options: [])

        let params = UserMessageUpdateParams()
        params.message = userMessage.message
        params.data = jsonData?.base64EncodedString()
        params.customType =  "SENDBIRD_DESK_RICH_MESSAGE"
        ticket.channel?.update(messageId: userMessage.messageId, params: params, completionHandler: { (userMessage, error) in
            guard error == nil else {   
                // Handle error.
                return 
            }
            ...
            // Pass data to update(messageId:params:completionHandler:) method in the GroupChannel. 
        })
    }
})

有许多从HTML主体提取OG元数据的方法。您还可以参考我们的GitHub存储库以了解我们使用的方法。

BaseChannelDelegatechannel(_:didUpdate:)代理方法中,您可以在下面的UserMessage data属性中找到URL预览的数据。

{
    "type": "SENDBIRD_DESK_URL_PREVIEW",
    "body": {
        "url": "https://sendbird.com/",
        "site_name": "Sendbird",
        "title": "A Complete Chat Platform, Messaging and Chat SDK and API",
        "description": "Sendbird's chat, voice and video APIs and SDKs connect users through immersive, modern communication solutions...",
        "image": "https://6cro14eml0v2yuvyx3v5j11j-wpengine.netdna-ssl.com/wp-content/uploads/sendbird_thumbnail.png"
    }
}

接收系统消息

管理员消息是系统发送的可定制消息,分为两种类型。.通知是发送并显示给客户和坐席的消息,例如欢迎消息或延迟消息。.系统消息是在票据详细信息视图中,当票据状态或指派人有变化时发送并显示给坐席的消息。

注意:您可以在 Desk > 设置 > 触发器 中自定义通知,并在仪表板中在 Desk > 设置 > 系统消息 中自定义系统消息。

当客户端应用程序通过 BaseChannelDelegatechannel(_:didReceive:) 方法收到消息时,通过 custom_type 的值区分系统消息和通知消息,其子类型在 data 中如下指定。

{
    "message_id": 40620745,
    "type": "ADMM",
    "custom_type": "SENDBIRD_DESK_ADMIN_MESSAGE_CUSTOM_TYPE",
    "data": "{\"type\": \"SYSTEM_MESSAGE_TICKET_ASSIGNED_BY_SYSTEM\", \"ticket\": <Ticket Object>}",
    "message": "The ticket is automatically assigned to Cindy."
}

注意:当 data 中有 SYSTEM_MESSAGE_TICKET_TRANSFERRED_BY_AGENT 时,transfer 才出现。

系统消息仅供代理显示。请参阅以下示例代码,以避免向客户显示系统消息。

public static func isVisible(message: BaseMessage) -> Bool {
    if let message = message as? AdminMessage, let data = Data(base64Encoded: message.data), data.isEmpty == false {
        let isSystemMessage = (message.customType == "ADMIN_MESSAGE_CUSTOM_TYPE")
            
        let dataObject = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
        let type = dataObject?["type"] as? String
        return !isSystemMessage && 
            type != EVENT_TYPE_ASSIGN &&
            type != EVENT_TYPE_TRANSFER &&
            type != EVENT_TYPE_CLOSE
    }
    
    return true
}

请求确认关闭票据

虽然管理员有权限直接关闭票据,但根据代理权限设置,代理人可以像管理员一样关闭票据或请求客户是否关闭票据。确认请求消息可以有3种状态如下。

状态 描述
WAITING 当代理发送确认请求消息时设置。
CONFIRMED 当客户同意关闭票据时设置。(默认:true
DECLINED 当客户拒绝关闭工单时设置该标志。(默认: 未选中)

当客户回复消息时,将通过调用 SBDSKTicket.confirmEndOfChat() 方法,将响应 (同意)或 (拒绝)发送到 Desk 服务器,分别以 CONFIRMEDDECLINED 形式。

SBDSKTicket.confirmEndOfChat(with: USER_MESSAGE, confirm: true|false) { (ticket, error) in
    guard error == nil else {   
        // Handle error.
        return 
    }
            
    // You can update the UI of the message. For example, you can hide YES and NO buttons. 
}

Sendbird 服务器通过 BaseChannelDelegatechannel(_:didUpdate:) 方法通知客户客户端应用程序更新。

func channel(_ channel: BaseChannel, didUpdate message: BaseMessage) {
    SBDSKTicket.getByChannelUrl(channel.channelURL) { (ticket, error) in
        guard error == nil else {   
            // Handle error
            return 
        }
        
        if let data = Data(base64Encoded: message.data), data.isEmpty == false {
            let dataObject = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
            let type = dataObject?["type"] as? String
            
            let isClosureInquired = (type == "SENDBIRD_DESK_INQUIRE_TICKET_CLOSURE")
            if isClosureInquired {
                let closureInquiry = dataObject?["body"] as? [String: Any]
                let state = closureInquiry?["state"] as? String
                
                switch state {
                    case "CONFIRMED":
                    // TODO: Implement your code for the UI when the customer confirms to close the ticket.
                    case "DECLINED":
                    // TODO: Implement your code for the UI when the customer declines to close the ticket.
                    case "WAITING":
                    // TODO: Implement your code for the UI when there is no response from the customer.
                    default: break
                }
            }
        }
    }
}

注意:您可以在 BaseChannelDelegatechannel(_:didUpdate:) 代理方法中的 message.data 属性中找到以下字符串化的 JSON 对象。

{
    "type": "SENDBIRD_DESK_INQUIRE_TICKET_CLOSURE",
    "body": {
        "state": "CONFIRMED"
    }
}

请求客户反馈

您可以在关闭工单后立即向客户发送消息,询问他们对通过工单提供的支持是否满意。当在仪表板中开启 客户满意度评分 功能后,客户将收到一条请求评分并留下评论作为反馈的消息。该消息具有以下两种状态。

状态 描述
WAITING 当代理发送客户反馈请求消息时设置。
CONFIRMED 当客户发送响应时设置。

当客户回复消息时,通过调用 ticket.submitFeedback() 方法将评分和评论发送到 Desk 服务器。然后,确认请求消息的状态将更改为 CONFIRMED

ticket.submitFeedback(with: USER_MESSAGE, score: SCORE, comment: COMMENT) { (ticket, error) in
    guard error == nil else {   
        // Handle error.
        return 
    }
    ...
}

Sendbird Desk 服务器通过 BaseChannelDelegatechannel(_:didUpdate:) 代理方法通知客户的客户端应用程序更新。

func channel(_ channel: BaseChannel, didUpdate message: BaseMessage) {
    SBDSKTicket.getByChannelUrl(channel.channelURL) { (ticket, error) in
        guard error == nil else {   
            // Handle error.
            return 
        }
                
        if let data = Data(base64Encoded: message.data), data.isEmpty == false {
            let dataObject = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
            let type = dataObject?["type"] as? String
                    
            let isFeedbackMessage = (type == "SENDBIRD_DESK_CUSTOMER_SATISFACTION")
            if isFeedbackMessage {
                let closureInquiry = dataObject?["body"] as? [String: Any]
                let state = closureInquiry?["state"] as? String
                        
                switch state {
                    case "CONFIRMED":
                    // TODO: Implement your code for the UI when there is a response from the customer.
                    case "WAITING":
                    // TODO: Implement your code for the UI when there is no response from the customer.
                    default: break
                }
            }
        }
    }
}

注意:您可以在 BaseChannelDelegatechannel(_:didUpdate:) 代理方法中的 message.data 属性中找到以下字符串化的 JSON 对象。

{
    "type": "SENDBIRD_DESK_CUSTOMER_SATISFACTION",
    "body": {
        "state": "CONFIRMED",
        "customerSatisfactionScore": 3,
        "customerSatisfactionComment": "It was really helpful :)"
    }
}

重新打开已关闭的工单

已关闭的工单可以通过使用 ticket.reopen() 方法重新打开。

ticket.reopen { (ticket, error) in
    guard error == nil else {   
        // Handle error.
        return 
    }
    ...

}

检索工单列表

您可以使用 SBDSKTicket.getOpenedList()SBDSKTicket.getClosedList() 方法检索当前客户的所有开放和关闭工单。

注意:每次请求最多只能检索10个工单,按创建时间降序排列。

// getOpenedList()
SBDSKTicket.getOpenedList(withOffset: OFFSET) { (tickets, hasNext, error) in
    guard error == nil else {   
        // Handle error. 
        return 
    }
            
    // offset += tickets.size(); for the next tickets.
    // TODO: Implement your code to display the ticket list.
}
// getClosedList() 
SBDSKTicket.getClosedList(withOffset: OFFSET) { (tickets, hasNext, error) in
    guard error == nil else {
        // Handle error. 
        return 
    }
            
    // offset += tickets.size(); for the next tickets.
    // TODO: Implement your code to display the ticket list.
}

对于设置了自定义字段的工单,您可以在 getOpenList()getClosedList() 中添加过滤器,按自定义字段的键和值对工单进行排序。

let customFieldFilter = ["subject": "doggy_doggy"]

SBDSKTicket.getOpenedList(withOffset: OFFSET, customFieldFilter: CUSTOM_FIELD_FILTER) { (tickets, hasNext, error) in
    guard error == nil else {   
        // Handle error.
        return
    }

    // offset += tickets.length; for the next tickets.
    // TODO: Implement your code to display the ticket list.
}

检索特定工单

您可以通过工单的渠道URL检索特定的工单。

SBDSKTicket.getByChannelUrl(channel.channelURL) { (ticket, error) in
    guard error == nil else {   
        // Handle error.
        return
    }
    ...
}

显示打开工单数量

您可以使用 SBDSKTicket.getOpenCount() 方法在您的客户端应用程序中显示打开工单的数量。

SBDSKTicket.getOpenCount { (count, error) in
    guard error == nil else {   
        // Handle rror.
        return 
    }
            
    // TODO: Implement your code with the result vale.
}