Tap Customer Service Development Guide
This article describes how to access the customer service system provided by TDS in the game.
SDK initialization
Get TapSDK at download page and introduce TapSupport
module:
- Unity
- Android
- iOS
- UE4
The SDK can be imported either through Unity Package Manager or manually**. Please choose according to your project needs.
Method 1: Use Unity Package Manager
NPMJS Installation
As of version 3.25.0, TapSDK supports NPMJS installation, with the advantage that only the version number needs to be configured and nested dependencies are supported.
Add the following dependencies to your project's Packages/manifest.json
file:
"dependencies":{
"com.taptap.tds.support":"3.29.4",
}
However, it should be noted that scopedRegistries
must be declared under the same level of dependencies in Packages/manifest.json
:
"scopedRegistries": [
{
"name": "NPMJS",
"url": "https://registry.npmjs.org/",
"scopes": ["com.tapsdk", "com.taptap", "com.leancloud"]
}
]
GitHub Installation
Add the following dependencies to your project's Packages/manifest.json
file:
"dependencies":{
"com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#3.29.4",
"com.taptap.tds.support":"https://github.com/TapTap/TapSupport-Unity.git#3.29.4",
"com.leancloud.storage":"https://github.com/leancloud/csharp-sdk-upm.git#storage-2.3.0",
}
Select Window > Package Manager in the top menu of Unity to see the packages already installed in your project.
Method 2: Import Manually
Find the download addresses of TapSDK Unity and LeanCloud C# SDK on the download page , and download
TapSDK-UnityPackage.zip
andLeanCloud-SDK-Realtime-Unity.zip
respectively.In the Unity project, go to Assets > Import Packages > Custom Packages, and from the unzipped
TapSDK-UnityPackage.zip
, select the TapSDK packages that you want to use in the game, and import them:TapTap_Common.unitypackage
TapSDK Basic library, mandatory。TapTap_Support.unitypackage
TapTap Customer Service Library, a must。
The decompresses
LeanCloud-SDK-Realtime-Unity.zip
is in the Plugins folder, drag and drop it to Unity.
The TapSupport SDK relies on the TapCommon module:
repositories{
flatDir {
dirs 'libs'
}
}
dependencies {
...
implementation name:'TapCommon_3.29.4', ext:'aar'
implementation (name:'TapSupport_3.29.4', ext:'aar')
}
Verify that network permissions have been added to AndroidManifest.xml
:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
The TapSupport SDK relies on the TapCommon module:
TapCommonSDK.framework
TapSupportSDK.framework
The TapSupport SDK relies on the TapCommon module:
PublicDependencyModuleNames.AddRange(
new string[]
{
"TapCommon",
"TapSupport"
// ... add other public dependencies that you statically link with here ...
}
);
Before initializing the SDK, there are a few preparations to complete:
- Ask the vendor administrator or customer service administrator to invite you to become a customer service administrator.
- Each vendor will be assigned a unique domain name, which you need to write down and use. You can find the available domain names in Settings > Developer Information in the Customer Service Workbench. If you want to use your own domain name, you can contact the vendor administrator to bind it in the Game Customer Service module of the Developer Center.
- In the Customer Service Workbench, create a product for your game (Settings > Administration > Products & Categories) and write down the ID of this product.
Then use the following code to initialize the TapSupport module:
- Unity
- Android
- iOS
- UE4
using TapTap.Support;
TapSupport.Init("https://please-replace-with-your-customized.domain.com", "Product ID");
import com.tds.tapsupport.TapSupport;
import com.tds.tapsupport.TapSupportCallback;
import com.tds.tapsupport.TapSupportConfig;
TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "Product ID", new TapSupportCallback() {
@Override
public void onUnreadStatusChanged(boolean hasUnread) {
// We'll discuss the use of callbacks in § Unread message notifications.
}
})
TapSupport.setConfig(this, config);
#import <TapSupportSDK/TapSupportSDK.h>
TapSupportConfig *config = [TapSupportConfig new];
config.server = @"https://please-replace-with-your-customized.domain.com";
config.productID = @"Product ID";
config.callback = self;
[TapSupport shareInstance].config = config;
#include "TapUESupport.h"
FTapSupportConfig Config;
Config.ServerUrl = TEXT("https://please-replace-with-your-customized.domain.com");
Config.ProductID = TEXT("Product ID");
TapUESupport::Init(Config);
In the above code example, please-replace-with-your-customized.domain.com
is the domain name obtained or bound in Preparation 2. The product ID
is the ID of the new product created in Preparation 3.
Login
In order to ensure that the information submitted by a player, such as forms, is only accessible to that player, the customer service system requires a login in order to be used. We offer three different login options:
- TDSUser (TDS Built-in Account) Login
- TDSUser login
- Anonymous Login
Logging in here refers to the process of authentication when a gamer uses the customer service system's features within the game. This step is the interaction between the game side and the customer service system, and is usually not perceived by the player, as opposed to logging in to the game itself (e.g., the anonymous login here has nothing to do with the game's guest login).
TDS Built-in Account Login
If your game uses the TDS built-in account service, you can log in to the customer service system directly on the client side using the logged-in TDSUser authorization.
The TDS Built-in Account Service is a user system provided by TDS that supports multiple login methods. If your game is already using TapTap Login, Friends, Achievements, Leaderboards and other services, you are probably already using TDS Built-in Accounts. For more information, please refer to "Introduction to Built-in Account Features".
To use TDSUser authorization to log in to the customer service system, you first request an authorization token using a logged-in TDS user account, and then use that token to call the TDS login interface of the customer service module:
- Unity
- Android
- iOS
- UE4
try {
string token = await TDSUser.RetrieveShortToken();
await TapSupport.LoginWithTDSCredential(token);
Debug.Log("Log in to TDSUser JWT Completion");
} catch (TapException e) {
Debug.LogError($"{e.Code} : {e.Message}");
} catch (Exception e) {
Debug.Log(e);
}
TDSUser.retrieveShortTokenInBackground(tdsUser.getSessionToken()).subscribe(new Observer<JSONObject>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(JSONObject jsonObject) {
String credential = jsonObject.getString("identityToken");
TapSupport.loginWithTDSCredential(credential, new TapSupport.LoginCallback() {
@Override
public void onComplete(boolean success, Throwable error) {
if (success) {
// Login Successful
} else {
// Login Failure
Log.e("TapSupportActivity", "login:error:" + error.toString());
}
}
});
}
@Override
public void onError(Throwable error) {
// Failed to request authorization token
}
@Override
public void onComplete() {
}
});
[TDSUser retrieveShortTokenWithCallback:^(NSString *_Nullable token, NSError *_Nullable error) {
if (error) {
// Failed to request authorization token
} else {
[TapSupport loginWithTDSCredential:token
handler:^(BOOL succcess, NSError *_Nullable error) {
if (succcess) {
// Successful login
} else {
// Login Failure
}
}];
}
}];
// Todo 1
if (TSharedPtr<FTDSUser> User = FTDSUser::GetCurrentUser())
{
User->RetrieveShortToken(
FStringSignature::CreateLambda([](const FString& Credential)
{
/** Getting Credential Success */
TapUESupport::LoginWithTDSCredential(
Credential,
FSimpleDelegate::CreateLambda([]()
{
/*** Login Successful */
}),
FTUError::FDelegate::CreateLambda([](const FTUError& Error)
{
/*** Login Failure */
}));
}),
FLCError::FDelegate::CreateLambda([](const FLCError& Error)
{
/** Failed to get Credential */
}));
}
Exception Handling
If the SDK throws an exception during login and subsequent processes, developers can be notified via a callback.
Among these exceptions, we need to handle the token expiration (EXPIRED_CREDENTIAL
) exception in particular, and re-execute the above request authorization token, call login interface
process to log in the customer service again.
For other kinds of exceptions, it is recommended to show them to players directly. Since customer service exceptions usually don't affect the player's playing experience, we can consider only prompting the player that customer service is not available at the customer service portal, and provide the interaction of manually triggering retries.
- Unity
- Android
- iOS
- UE4
if (e is TapException ex && ex.Code == 9006) {
// Login expired
} else {
// Other exceptions
}
if (e instanceof ServerException) {
try {
org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody);
if(errorResponse.getInt("numCode")==9006){
// Login expired
}
} catch (JSONException ex) {
// ignore
}
}
if (error) {
if (error.code == 9006) {
// log back in
} else {
// Other exceptions
using TapTap.Support;
TapSupport.Init("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback
{
OnGetUnreadStatusError = (exception) => {
if (e is TapException ex && ex.Code == 9006) {
// Login expired, re-execute TDSUser.RetrieveShortToken and TapSupport.LoginWithTDSCredential
} else {
// Other exceptions
}
}
});
import com.tds.tapsupport.TapSupport;
import com.tds.tapsupport.TapSupportCallback;
import com.tds.tapsupport.TapSupportConfig;
TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback() {
@Override
public void onGetUnreadStatusError(Throwable e) {
if (e instanceof ServerException) {
try {
org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody);
if(errorResponse.getInt("numCode")==9006){
// Login expired, re-execute TDSUser.RetrieveShortToken and TapSupport.LoginWithTDSCredential
} else {
// Other exceptions
}
} catch (JSONException ex) {
// ignore
}
}
}
});
TapSupport.setConfig(this, config);
Associated login for game's own account system
If your game uses another user system, the customer service system also supports logging in with signed user information.
Signing user information requires the use of a secret, so a server is required to use this method.
The developer first needs to generate an auth secret in Settings - Developer Settings - Player Authentication in the Customer Service Workbench, and then Use this secret on the server to JWT-sign the player information using the HS256 algorithm. The player information (payload) should contain the user's unique identifier and a name for display, and should be structured as follows:
{
"sub": "U1234567", // unique identification
"name": "Dash" // Displayed in the name of the work order, customer service back office
}
JWT Example of a signature
- Algorithm: HS256
- payload:
{"sub": "U1234567", "name": "Dash"}
- secret:
44a23a3701955756301768bbb5dd1e1ea51500b556fb73201de76d5365150653
exports JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJVMTIzNDU2NyIsIm5hbWUiOiJEYXNoIn0.OV2LCP-7cLVTfjJlx21q9O1Tj_LlM0LFyW3OOu7CeNk
Finally, the obtained JWT is returned to the client, and the client calls the third-party account login interface of the SDK to complete the login:
- Unity
- Android
- iOS
- UE4
TapSupport.LoginWithCustomCredential(jwt);
TapSupport.loginWithCustomCredential("User JWT");
[TapSupport loginWithCustomeCredential:@"User JWT"];
// Todo 3
TapUESupport::LoginWithCustomCredential(TEXT("User JWT"));
For security reasons, we usually add an exp
field to the JWT payload to specify its expiration time, and the customer service system will honor the JWT's expiration time setting:
{
"sub": "U1234567",
"name": "Dash",
"exp": 1676546560 // Unix epoch
}
Considering both security and performance, we recommend setting the JWT expiration time to 1-3 days from the current time.
Exception Handling
If an exception occurs in the SDK during login and subsequent processes, the developer can be notified via a callback.
Among these exceptions, we need to handle the JWT expiration (EXPIRED_CREDENTIAL
) exception in particular, and re-execute the above request JWT, call login interface
process to log in the customer service again.
For other kinds of exceptions, it is recommended to show them to the player directly. Since customer service exceptions usually do not affect the player's playing experience, we can consider only prompting the player that the customer service is unavailable at the customer service portal, and provide the interaction of manually triggering the retry.
- Unity
- Android
- iOS
- UE4
if (e is TapException ex && ex.Code == 9006) {
// Login expired
} else {
// Other exceptions
}
if (e instanceof ServerException) {
try {
org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody);
if(errorResponse.getInt("numCode")==9006){
// Login expired
}
} catch (JSONException ex) {
// ignore
}
}
if (error) {
if (error.code == 9006) {
// log back in
} else {
// Other exceptions
using TapTap.Support;
TapSupport.Init("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback
{
OnGetUnreadStatusError = (exception) => {
if (e is TapException ex && ex.Code == 9006) {
// Login expires, refetches the new JWT and executes TapSupport.LoginWithCustomCredential(jwt)
} else {
// Other exceptions
}
}
});
import com.tds.tapsupport.TapSupport;
import com.tds.tapsupport.TapSupportCallback;
import com.tds.tapsupport.TapSupportConfig;
TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback() {
@Override
public void onGetUnreadStatusError(Throwable e) {
if (e instanceof ServerException) {
try {
org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody);
if(errorResponse.getInt("numCode")==9006){
// Login expires, re-fetches the new JWT and executes TapSupport.loginWithCustomCredential("new JWT");
} else {
// Other exceptions
}
} catch (JSONException ex) {
// ignore
}
}
}
});
TapSupport.setConfig(this, config);
Anonymous Login
Anonymous login means that the game uses a string that only the current player can get as an anonymous identification (ID) to log into the customer service system.
- Unity
- Android
- iOS
- UE4
TapSupport.LoginAnonymously("uuid");
TapSupport.loginAnonymously("uuid");
[TapSupport loginAnonymously:@"uuid"];
TapUESupport::LoginAnonymously("uuid");
Anonymous login can be used to differentiate players by device in scenarios where the game does not have an account system or the player is not logged in, or to differentiate players by account after they have logged into their game account.
Associating with Devices
The game client generates and persists a UUID for the device, and then uses this ID to call the anonymous login interface to associate the player with the device.
At this point, the anonymous ID on the device is the only identity credentials. If the ID is lost due to deletion of the application or clearing of local data, the player will not be able to access data such as historical work orders.
associated with a game account
Similarly, to be associated with a game account, the anonymous login interface needs to be called using an ID that has a one-to-one relationship with the account.
The anonymous login mechanism is very flexible. For example, if you want a player to have a separate customer service account for each game, you can assign a separate anonymous ID to each game; if you want to track the player across games, you can use an anonymous ID at the pass level, in which case the customer service system doesn't know what the ID means, which is where the name "anonymous" comes from. This is where the name "anonymous" comes from.
The price of flexibility is security. If not used correctly, anonymous login can lead to data leakage. The customer service system doesn't care where the anonymous ID came from, and can't do any verification, which means that the historical work order data of the player who leaked the ID will also be leaked. Therefore, "only the current player can get it" needs to be guaranteed by the game side. More specifically, the anonymous ID needs to meet the following conditions:
- Unique and unchanging to the player
- Cannot be guessed or deduced, cannot be enumerated.
- It is not public, and will not be disclosed by the player through sharing or screenshots.
Example of a correct anonymous ID
- ✅
b3a59993-c659-49f9-9f51-f2c808a472a0
:The game user system generates a UUID when a new player is created that serves as the player's anonymous ID for the customer service system, and the player logs in to log in to the customer service system with that ID. - ✅
sha1('2436234209' + secret)
:Append a fixed secret to the player's UID on the server side and then hash it. Only after the player logs in can he get the ID to log into the customer service system.
Example of an insecure anonymous ID
Simply put, no client-only solution is secure:
- ❌
'2436234209'
Player UID: This is usually public information, visible and potentially shared by the player in the game, and a purely numeric ID that is easy to enumerate. - ❌
sha1('2436234209')
:Hashing alone is still easy to enumerate when the algorithm is guessed.
To reduce the risk of misuse, the anonymous login interface restricts the minimum length of the anonymous ID to 32.
Clear Login Status
- Unity
- Android
- iOS
- UE4
TapSupport.Logout();
TapSupport.logout();
[TapSupport logout];
TapUESupport::Logout();
Open the customer service page
- Unity
- Android
- iOS
- UE4
TapSupport.OpenSupportView();
TapSupport.openSupportView();
[TapSupport openSupportView];
// TODO 4: Check if you can omit the parameter
TapUESupport::OpenSupportView("/", nullptr, nullptr);
TapUESupport::OpenSupportView();
If the opened page has no content, you can first configure some subcategories for that game category in the Customer Service Workbench.
Scenario-based portals
In addition to landing pages, the SDK also supports opening specific pages directly in specific scenarios.
- Unity
- Android
- iOS
- UE4
TapSupport.OpenSupportView();
TapSupport.openSupportView("/path");
[TapSupport openSupportViewWithPath:@"/path"];
// TODO 5: Check if you can omit the parameter
TapUESupport::OpenSupportView("/path", nullptr, nullptr);
TapUESupport::OpenSupportView(TEXT("/path"));
Different pages are distinguished by different path parameters. The currently supported pages are:
path | clarification |
---|---|
/tickets/new?category_id={id} | To submit a new work order, specify the id of the category. |
/tickets | Player work order list page to see all historical work orders of the player. |
/articles/{id} | Knowledge base article page. |
Reporting Information
The work order module supports developers to collect additional information such as device, player, etc. through custom fields. Developers can bring in additional information when opening a support page (openSupportView), and the SDK will record this information in the work order fields when submitting a work order. This information can be viewed and analyzed in the customer service workbench.
Before reporting data, you first need to define fields in the Customer Service Workbench (Settings > Administration > Work Order Fields). These fields need to be set to "Customer Service Only" access. Once created, you need to write down the ID of the field, which we will use in the SDK.
- Unity
- Android
- iOS
- UE4
Dictionary<string, object> fields = new Dictionary<string, object>();
fields.Add("243", "iOS 15.1"); // 243 is the ID of the "OS" field created in the background.
fields.Add("244", "Dash"); // 244 is the ID of the Role Name field created in the backend
TapSupport.OpenSupportView("/", fields);
Map<String, Object> fields = new HashMap<>();
fields.put("243", "iOS 15.1"); // 243 is the ID of the "OS" field created in the background.
fields.put("244", "Dash"); // 244 is the ID of the Role Name field created in the backend
TapSupport.openSupportView("/", fields);
NSDictionary *fields = @{@"243":@"iOS 15.1", @"244":@"WiFi"}; // 243 is the ID in the "OS" field created in the backend, 244 is the ID in the "Role Name" field
[TapSupport openSupportViewWithPath:@"/" fieldsData:fields];
// TODO 6: Check if you can omit the parameter
TSharedPtr<FJsonObject> Fields = MakeShareable(new FJsonObject);
Fields->SetStringField("243", "iOS 15.1");
Fields->SetStringField("244", "Dash");
TapUESupport::OpenSupportView("/", Fields);
In addition to setting it when opening the customer service page, developers can also set the global default field information through the following interface:
- Unity
- Android
- iOS
- UE4
TapSupport.SetDefaultFieldsData(fields: fields);
TapSupport.setDefaultFieldsData(fields);
[TapSupport shareInstance].defaultFieldsData = fields;
TapUESupport::SetDefaultFieldsData(Fields);
Global fields can be updated after setup:
- Unity
- Android
- iOS
- UE4
TapSupport.DefaultFields = new Dictionary<string, object> {
{ "key", "value" }
};
TapSupport.updateDefaultField("key", "value");
[TapSupport updateDefaultFieldWithValue:@"value" forKey:@"key"];
// TODO 7
TSharedPtr<FJsonValue> Value = MakeShared<FJsonValueString>(TEXT("Value"));
TapUESupport::UpdateDefaultField(TEXT("key"), Value);
Globally set fields are merged with the incoming fields
parameter at OpenSupportView time. Fields passed in during the OpenSupportView
method have a higher priority than global fields, meaning that global fields will not take effect if they are the same.
Closing the support page
Players can click the close button within the customer service page to exit. However, in certain scenarios, the game may need to actively close the customer service page:
- Unity
- Android
- iOS
- UE4
TapSupport.CloseSupportView();
TapSupport.closeSupportView();
[TapSupport closeSupportView];
TapUESupport::CloseSupportView();
Unread Message Notification
Unread messages are generated when there are new developments in a submitted work order (e.g. a new customer service response). Usually in-game, players are notified of unread messages in the customer service portal using red dots, etc. The SDK automatically polls for dimensional messages, and when the status of an unread message changes - from none to yes or yes to no - the SDK notifies the developer via a callback to notify the developer:
- Unity
- Android
- iOS
- UE4
using TapTap.Support;
TapSupport.Init("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback
{
UnReadStatusChanged = (hasUnRead, exception) =>
{
Debug.Log($"hasUnRead:{hasUnRead} exception:{exception}");
}
});
import com.tds.tapsupport.TapSupport;
import com.tds.tapsupport.TapSupportCallback;
import com.tds.tapsupport.TapSupportConfig;
TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback() {
@Override
public void onUnreadStatusChanged(boolean hasUnread) {}
});
TapSupport.setConfig(this, config);
#import <TapSupportSDK/TapSupportSDK.h>
// callback need to be realized TapSupportDelegate
TapSupportConfig *config = [TapSupportConfig new];
config.server = @"https://please-replace-with-your-customized.domain.com";
config.productID = @"categorization ID";
config.callback = self;
[TapSupport shareInstance].config = config;
TapUESupport::OnUnreadStatusChanged.BindUObject(this, &YourUObject::OnUnreadStatusChanged);
Developers do not need to additionally clear the local unread notification status (small red dot) when tapping the customer service portal. This is because clicking on the customer service portal does not mean that the player has viewed all unread work orders. If the player has viewed all unread work orders, the SDK will get the latest status and notify the developer via the callback mentioned above.
Pause Polling
The SDK's built-in polling mechanism intelligently adjusts the frequency of getting the status of unread messages. However, there are some scenarios where these requests are still unnecessary overhead, such as when a player wants to pause all unnecessary background requests during a game match (when there is no red dot displayed on the interface), and the SDK provides a pair of APIs for controlling polling for this purpose:
Pause
: pauses pollingResume
: resumes polling.
- Unity
- Android
- iOS
- UE4
TapSupport.Resume();
TapSupport.Pause();
TapSupport.resume();
TapSupport.pause();
[TapSupport resume];
[TapSupport pause];
TapUESupport::Resume();
TapUESupport::Pause();
SDK Polling Policy
- Initially, a request is initiated immediately, and the next request interval is set to be 10s.
- If there is no change in the status of an unread message compared to the current status, increase the interval by 10s until the maximum interval is 300s, and reset the interval to 10s if there is a change.
- If the player never opens the WebView, the interval is increased to a constant 3600s.
- Calling the
Resume
method resets the polling to its initial state.