跳到主要内容

平台能力集成

Host SDK 支持接入方覆写部分开放给小游戏开发者的 JS 接口以集成平台方登录 SDK、广告 SDK、支付 SDK 等功能。 接入 Host SDK 的开发者可以通过 CustomCommand 接口 接入定制功能。

1 登录 SDK​

1.1 登录组件接口列表​

Host SDK 给小游戏开发者提供了的登录 JS 接口如下。

  • tj.login

    • 输入参数: Object args
    属性类型必填说明
    timeoutnumberN超时时间,单位ms
    successfunctionN接口调用成功的回调函数
    failfunctionN接口调用失败的回调函数
    completefunctionN接口调用结束的回调函数(调用成功、失败都会执行)
    • args.success 回调函数参数: Object res
    属性类型说明
    codestring调用接口获取登录凭证(code)

登录能力可通过覆写 tj.login 接口实现。

tj.login = function (obj) {
tj.customCommand({
success: function(res) {
let resObj = JSON.parse(res);
obj.success(resObj);
},
fail: function(res) {
let apiRes = {
errMsg: res.errorMsg,
errno: res.errorCode
};
obj.fail(apiRes);

},
complete: function() {
obj.complete();
}
}, "loginUnity");
};

代码中同样使用 CustomCommand 执行 Java 中的 Native Func loginUnity(),loginUnity() 的主要功能是通过平台自身的登录 SDK 获取 Code 并返回给宿主,宿主将其传递给游戏后,游戏可通过 Code2Session() 换取用户信息。

在多进程游戏的场景下,如果用户在主进程已经登录,则可以使用 AIDL 或其他进程间通信方式从游戏进程向主进程发起获取 Code 的请求,并由主进程统一返回 Code。

hostHandle.setCustomCommandListener("loginUnity", ((customCommandHandle, bundle) -> {
try {
loginService.requesetLogin(new ILoginCallback.Stub() {
@Override
public void loginEnd(String code) {
JSONObject loginRes = new JSONObject();
try {
loginRes.put("code", code);
} catch (Exception e) {
customCommandHandle.fail(1002, "Failed to serialize login result" + e.getMessage());
}

customCommandHandle.pushResult(loginRes.toString());
customCommandHandle.success();
}
});
} catch (RemoteException e) {
customCommandHandle.fail(1001, "Failed to connect Login Service: " + e.getMessage());
}
}));

1.2 登录全流程​

登录全流程示意图如下:

游戏客户端通过调用tj.login(流程图中的第3步)获取code后,需要通过JS code2session获取用户的登录信息,实现用户登录。

jscode2session​

提供平台方的登录能力,宿主后台会将用户的登录凭证(code)传递给平台方,平台方需要根据该凭证返回用户的登录信息。

宿主后台会结合请求的上下文,将平台方返回的 uid 映射为游戏侧可用的 openid 和 unionid,同时转发 session_key 给游戏侧做为登录凭证。

请求示例:

GET /jscode2session?js_code={code} HTTP/1.1
Authorization: Bearer {service_token}

请求参数说明:

参数说明
js_code平台方登录凭证
Authorization服务端令牌,格式为 Bearer {service_token}

响应示例:

HTTP/1.1 200 OK
Content-Type: application/json

{
"uid": "123456"
"session_key": "abcdefg",
"errcode": 0,
"errmsg": "ok"
}

响应字段说明:

字段说明
uid平台方返回的用户唯一标识
session_key平台方返回的 session_key
errcode错误码,0 表示成功
errmsg错误信息

1.3 登录接口集成示例​

接入方 App 可在工程内提供自定义 JS 文件用于覆写 JS 接口,并在设置游戏启动参数时指定自定义 JS 文件的路径。

auth.js 示例

// auth.js - Custom authentication implementation

// Override tj.login
tj.login = function(obj) {
tj.customCommand('platformLogin', obj);
};

// Override tj.checkSession
tj.checkSession = function(obj) {
tj.customCommand('checkSession', obj);
};

// Override tj.pluginLogin
tj.pluginLogin = function(obj) {
tj.customCommand('pluginLogin', obj);
};

在宿主中注入登录 JS 文件

const { TJConstants } = require('webglhost-runtime-pc');
const path = require('path');

const options = {};
options[TJConstants.LAUNCH_KEY] = 'launch-key';
options[TJConstants.USER_ID] = 'user-123';

// Inject custom auth script
options[TJConstants.CUSTOM_SCRIPT_FILES] = [
path.join(__dirname, 'custom-scripts', 'auth.js')
];

await gameHandle.setGameStartOptions(options);

在宿主中实现登录逻辑

// Setup custom command listeners for auth
gameHandle.setCustomCommandListener('platformLogin', (request, handle) => {
// Call your platform's login SDK
yourPlatformSDK.login()
.then(result => {
handle.success({
code: result.authCode,
userId: result.userId
});
})
.catch(error => {
handle.fail(20001, error.message);
});
});

gameHandle.setCustomCommandListener('checkSession', (request, handle) => {
// Check session validity
yourPlatformSDK.checkSession()
.then(() => {
handle.success({});
})
.catch(error => {
handle.fail(20002, 'Session expired');
});
});

gameHandle.setCustomCommandListener('pluginLogin', (request, handle) => {
// Handle plugin login
yourPlatformSDK.pluginLogin()
.then(result => {
handle.success({
code: result.authCode
});
})
.catch(error => {
handle.fail(20003, error.message);
});
});

2 广告 SDK​

2.1 广告组件接口列表​

Host SDK 给小游戏开发者提供了的广告 JS 接口如下。

tj.createRewardedVideoAd()​

返回值为RewardedVideoAd,暴露的方法有:

函数名函数说明返回值
RewardedVideoAd.load()加载激励视频广告Promise
  • 激励视频广告加载数据的结果
RewardedVideoAd.show()显示激励视频广告Promise
  • 激励视频广告显示操作的结果
RewardedVideoAd.destroy()销毁激励视频广告实例
RewardedVideoAd.onLoad(function listener)监听激励视频广告加载事件。参数:function listener
  • 激励视频广告加载事件的监听函数
RewardedVideoAd.offLoad(function listener)监听激励视频广告加载事件。参数:function listener
  • onLoad 传入的监听函数。不传此参数则移除所有监听函数。
RewardedVideoAd.onError(function listener)监听激励视频错误事件。参数:function listener
  • 激励视频错误事件的监听函数
RewardedVideoAd.offError(function listener)移除激励视频错误事件的监听函数参数:function listener
  • onError 传入的监听函数。不传此参数则移除所有监听函数。
RewardedVideoAd.onClose(function listener)监听用户点击 关闭广告 按钮的事件。参数:function listener
  • 用户点击 关闭广告 按钮的事件的监听函数
RewardedVideoAd.offClose(function listener)移除用户点击 关闭广告 按钮的事件的监听函数参数:function listener
  • onClose 传入的监听函数。不传此参数则移除所有监听函数。

下面以 "创建激励视频广告" 为例子,介绍怎样覆盖广告接口,接入广告逻辑。

2.2 广告组件接口示例​

tj.createRewardedVideoAd()

此接口用于创建激励视频广告组件,并返回 RewardedVideoAd 实例,实例暴露的部分方法有:

  • RewardedVideoAd.load() 加载激励广告,返回一个 Promise

  • RewardedVideoAd.onLoad(callback) 接收函数作为参数,监听广告加载事件

  • RewardedVideoAd.offLoad(callback) 移除广告加载事件的监听函数,不传参数则移除所有

Host SDK 并不内置广告功能,仅提供了 tj.createRewardedVideoAd() 的一个默认实现,此实现返回的 RewardedVideoAd 实例暴露的方法均为空实现,小游戏开发者调用 RewardedVideoAd.load() 并不能真实加载激励广告。

因此接入方 App 需要覆写此 tj.createRewardedVideoAd(),使得 RewardedVideoAd.load() 内调用 App 内接入的第三方广告 SDK 的加载接口,同理实现 RewardedVideoAd 暴露的其他接口以实现广告功能的集成。

2.3 接入第三方广告 SDK​

以集成激励广告 API 为例,可参考以下 ad.js 实现:

ad.js 示例

// ad.js - Custom ad implementation

let rewardedVideoAdOnLoadCallbacks = [];
let rewardedVideoAdOnErrorCallbacks = [];
let rewardedVideoAdOnCloseCallbacks = [];

tj.createRewardedVideoAd = (obj) => {
const adUnitId = obj.adUnitId;

return {
onLoad: (callback) => {
console.info("[Host] invoke RewardedVideoAd.onLoad");
if (callback && typeof callback === "function") {
if (!rewardedVideoAdOnLoadCallbacks.includes(callback)) {
rewardedVideoAdOnLoadCallbacks.push(callback);
}
}
},

load: () => {
console.info("[Host] invoke RewardedVideoAd.load");

return new Promise((resolve, reject) => {
tj.customCommand("loadRewardAd", {
adUnitId: adUnitId,
success: function (res) {
console.info("[Host] RewardedVideoAd.load success");
resolve(res);
},
fail: function (obj) {
console.info("[Host] RewardedVideoAd.load fail");
reject(obj);
},
complete: function () {
console.info("[Host] RewardedVideoAd.load complete");
},
});
});
},

show: () => {
console.info("[Host] invoke RewardedVideoAd.show");

return new Promise((resolve, reject) => {
tj.customCommand("showRewardAd", {
adUnitId: adUnitId,
success: function (res) {
console.info("[Host] RewardedVideoAd.show success");
resolve(res);
},
fail: function (obj) {
console.info("[Host] RewardedVideoAd.show fail");
reject(obj);
},
});
});
},

offLoad: (callback) => {
console.info("[Host] invoke RewardedVideoAd.offLoad");
if (callback === undefined) {
rewardedVideoAdOnLoadCallbacks = [];
} else if (callback && typeof callback === "function") {
rewardedVideoAdOnLoadCallbacks = rewardedVideoAdOnLoadCallbacks.filter(
(cb) => cb !== callback
);
}
},

onError: (callback) => {
console.info("[Host] invoke RewardedVideoAd.onError");
if (callback && typeof callback === "function") {
if (!rewardedVideoAdOnErrorCallbacks.includes(callback)) {
rewardedVideoAdOnErrorCallbacks.push(callback);
}
}
},

offError: (callback) => {
console.info("[Host] invoke RewardedVideoAd.offError");
if (callback === undefined) {
rewardedVideoAdOnErrorCallbacks = [];
} else if (callback && typeof callback === "function") {
rewardedVideoAdOnErrorCallbacks = rewardedVideoAdOnErrorCallbacks.filter(
(cb) => cb !== callback
);
}
},

onClose: (callback) => {
console.info("[Host] invoke RewardedVideoAd.onClose");
if (callback && typeof callback === "function") {
if (!rewardedVideoAdOnCloseCallbacks.includes(callback)) {
rewardedVideoAdOnCloseCallbacks.push(callback);
}
}
},

offClose: (callback) => {
console.info("[Host] invoke RewardedVideoAd.offClose");
if (callback === undefined) {
rewardedVideoAdOnCloseCallbacks = [];
} else if (callback && typeof callback === "function") {
rewardedVideoAdOnCloseCallbacks = rewardedVideoAdOnCloseCallbacks.filter(
(cb) => cb !== callback
);
}
},

destroy: () => {
console.info("[Host] invoke RewardedVideoAd.destroy");
rewardedVideoAdOnLoadCallbacks = [];
rewardedVideoAdOnErrorCallbacks = [];
rewardedVideoAdOnCloseCallbacks = [];
}
};
};

// Callback functions to be called from host
rewardedVideoLoadCallback = () => {
try {
rewardedVideoAdOnLoadCallbacks.forEach((callback) => callback());
} catch (error) {
console.error("[Host] Error in rewardedVideoLoadCallback:", error);
}
};

rewardedVideoErrorCallback = (error) => {
try {
rewardedVideoAdOnErrorCallbacks.forEach((callback) => callback(error));
} catch (error) {
console.error("[Host] Error in rewardedVideoErrorCallback:", error);
}
};

rewardedVideoCloseCallback = (result) => {
try {
rewardedVideoAdOnCloseCallbacks.forEach((callback) => callback(result));
} catch (error) {
console.error("[Host] Error in rewardedVideoCloseCallback:", error);
}
};

在宿主中注入广告 JS 文件

const { TJConstants } = require('webglhost-runtime-pc');
const path = require('path');

const options = {};
options[TJConstants.LAUNCH_KEY] = 'launch-key';
options[TJConstants.USER_ID] = 'user-123';

// Inject custom ad script
options[TJConstants.CUSTOM_SCRIPT_FILES] = [
path.join(__dirname, 'custom-scripts', 'ad.js')
];

await gameHandle.setGameStartOptions(options);

在宿主中实现广告逻辑

// Setup custom command listeners for ads
gameHandle.setCustomCommandListener('loadRewardAd', (request, handle) => {
const adUnitId = request.adUnitId;

console.log('Loading reward ad:', adUnitId);

// Call your ad SDK to load the ad
yourAdSDK.loadRewardedVideo(adUnitId)
.then(() => {
handle.success({});

// Notify JS that ad loaded successfully
gameHandle.runCustomScript(`
rewardedVideoLoadCallback();
`, (result, error) => {
if (error) {
console.error('Error calling load callback:', error);
}
});
})
.catch(error => {
handle.fail(30001, error.message);

// Notify JS that ad load failed
gameHandle.runCustomScript(`
rewardedVideoErrorCallback({ errCode: 30001, errMsg: '${error.message}' });
`, (result, error) => {
if (error) {
console.error('Error calling error callback:', error);
}
});
});
});

gameHandle.setCustomCommandListener('showRewardAd', (request, handle) => {
const adUnitId = request.adUnitId;

console.log('Showing reward ad:', adUnitId);

// Call your ad SDK to show the ad
yourAdSDK.showRewardedVideo(adUnitId)
.then(result => {
handle.success({});

// Notify JS when ad is closed
const isAdWatched = result.isAdWatched; // User watched ad completely
gameHandle.runCustomScript(`
rewardedVideoCloseCallback({ isEnded: ${isAdWatched} });
`, (result, error) => {
if (error) {
console.error('Error calling close callback:', error);
}
});
})
.catch(error => {
handle.fail(30002, error.message);
});
});

2.4 完整广告集成示例​

const { TJHostHandle, TJConstants } = require('webglhost-runtime-pc');
const path = require('path');

class AdManager {
constructor(gameHandle) {
this.gameHandle = gameHandle;
this.setupAdListeners();
}

setupAdListeners() {
// Load ad listener
this.gameHandle.setCustomCommandListener('loadRewardAd', (request, handle) => {
this.handleLoadRewardAd(request, handle);
});

// Show ad listener
this.gameHandle.setCustomCommandListener('showRewardAd', (request, handle) => {
this.handleShowRewardAd(request, handle);
});
}

async handleLoadRewardAd(request, handle) {
const adUnitId = request.adUnitId;

try {
console.log(`Loading rewarded video ad: ${adUnitId}`);

// Simulate ad loading (replace with actual ad SDK call)
await this.simulateAdLoad(adUnitId);

// Success callback to host
handle.success({});

// Notify JS layer that ad loaded
this.notifyAdLoaded();

} catch (error) {
handle.fail(30001, error.message);
this.notifyAdError(error);
}
}

async handleShowRewardAd(request, handle) {
const adUnitId = request.adUnitId;

try {
console.log(`Showing rewarded video ad: ${adUnitId}`);

// Simulate ad showing (replace with actual ad SDK call)
const result = await this.simulateAdShow(adUnitId);

// Success callback to host
handle.success({});

// Notify JS layer that ad closed
this.notifyAdClosed(result.isEnded);

} catch (error) {
handle.fail(30002, error.message);
this.notifyAdError(error);
}
}

notifyAdLoaded() {
this.gameHandle.runCustomScript(`
if (typeof rewardedVideoLoadCallback === 'function') {
rewardedVideoLoadCallback();
}
`, (result, error) => {
if (error) {
console.error('Error notifying ad loaded:', error);
}
});
}

notifyAdError(error) {
this.gameHandle.runCustomScript(`
if (typeof rewardedVideoErrorCallback === 'function') {
rewardedVideoErrorCallback({
errCode: ${error.code || 30000},
errMsg: '${error.message}'
});
}
`, (result, err) => {
if (err) {
console.error('Error notifying ad error:', err);
}
});
}

notifyAdClosed(isEnded) {
this.gameHandle.runCustomScript(`
if (typeof rewardedVideoCloseCallback === 'function') {
rewardedVideoCloseCallback({ isEnded: ${isEnded} });
}
`, (result, error) => {
if (error) {
console.error('Error notifying ad closed:', error);
}
});
}

// Simulate ad loading (replace with actual ad SDK integration)
async simulateAdLoad(adUnitId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Ad ${adUnitId} loaded successfully`);
resolve();
}, 1000);
});
}

// Simulate ad showing (replace with actual ad SDK integration)
async simulateAdShow(adUnitId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Ad ${adUnitId} shown successfully`);
// Simulate user watching ad completely
resolve({ isEnded: true });
}, 2000);
});
}
}

// Usage
async function initializeWithAds() {
const hostHandle = await new Promise((resolve, reject) => {
TJHostHandle.initialize('sdk-key', 'sdk-secret', (handle, error) => {
if (error) reject(error);
else resolve(handle);
});
});

const gameHandle = await hostHandle.createGameHandle();

// Initialize ad manager
const adManager = new AdManager(gameHandle);

// Setup game options with custom ad script
const options = {
[TJConstants.LAUNCH_KEY]: 'launch-key',
[TJConstants.USER_ID]: 'user-123',
[TJConstants.CUSTOM_SCRIPT_FILES]: [
path.join(__dirname, 'custom-scripts', 'ad.js')
]
};

await gameHandle.setGameStartOptions(options);
await gameHandle.start();
await gameHandle.play();
}

initializeWithAds().catch(console.error);

3 支付 SDK​

3.1 流程图​

3.1.1 小游戏支付​

3.1.2 主动查询订单​

3.2 客户端接口​

tj.requestPayment(Object obj)​

参数说明

属性名类型默认值必填说明
modestringgame是支付的类型,枚举值
  • game -目前仅支持该类型
envnumber0是环境配置,枚举值
  • 0 - 表示支付正式环境,目前仅支持该类型
currencyTypestringCNY是币种,枚举值
  • CNY - 表示人民币
  • VND - 表示越南盾
  • EUR - 表示欧元
  • ...
platformstring是申请接入时的平台,android或者ios
buyQuantitynumber否所购买的数量
zoneIdstring1否游戏服务区 id,开发者自定义。游戏不分大区则默认填写"1"
customIdstring是游戏开发者自定义的唯一订单号,订单支付成功后通过服务端支付结果回调回传。必须具有唯一性,如果不传或重复传相同值,则会报错。
extraInfostring否游戏开发者自定义的其他信息,订单支付成功后通过服务端支付结果回调回传。字符串长度最大不能超过 256。
goodTypenumber2否支付场景,枚举值
  • 2 - 表示道具直购场景,目前仅支持该类型
orderAmountnumber是代表道具现金价格,单位为【人民币分】
goodNamestring是goodType为道具直购场景时,代表道具名称,长度限制小于等于10个字符,用于区分道具类型
optionsmap否扩展参数,可以根据平台需求定制
successfunction否接口调用成功的回调函数
failfunction否接口调用失败的回调函数
completefunction否接口调用结束的回调函数(调用成功、失败都会执行)

3.2.1 回调成功参数​

Object类型,属性如下

属性名类型说明
errMsgstringrequestPayment:ok

3.2.2 回调失败参数​

Object类型,属性如下

属性名类型说明
errMsgstring"requestPayment:fail " + 错误详情
errCodenumber错误码,对应信息可参考错误码说明

3.2.3 回调结束参数​

Object类型,属性如下

属性名类型说明
errMsgstringrequestPayment:ok

3.2.4 错误码说明​

根据实际支持情况映射

errCodeerrMsg说明
-1requestPayment:fail 支付失败用户支付失败
-1requestPayment:fail internal error小游戏框架内部错误
-2requestPayment:fail 支付取消用户取消支付
3requestPayment:fail error_tip_start_cash_register_fail拉起收银台失败,如遇小游戏框架内部错误
4requestPayment:fail 网络异常用户网络异常
-15002requestPayment:fail 请求参数不合法请求参数不合法
-15006requestPayment:fail app 没有支付权限请确认是否设置游戏币汇率
-15098requestPayment:fail 当前用户未通过实名认证当前用户未通过实名认证
-15099requestPayment:fail 当前用户累计支付金额超过限制当前用户累计支付金额超过限制
-15101requestPayment:fail customId 为空或者不唯一customId 为空或者不唯一
-16000requestPayment:fail 用户未登录用户未登录
-20002requestPayment:fail 交易存在风险交易存在风险,被风控策略拦截
21113requestPayment:fail face authentication failed检测到疑似未成年下单,拉起人脸核验,人脸验证不通过

3.2.5 示例代码​

tj.requestPayment({
mode: 'game',
env: 0,
currencyType: 'CNY',
platform: 'ios',
zoneId: '1',
customId: 'order_' + Date.now(),
extraInfo: JSON.stringify({
orderId: 'test_order_' + Date.now(),
userId: '10001'
}),
goodType: 2,
orderAmount: 100,
goodName: '去广告',
options: {
'PRODUCT_CODE': '1001'
},
success: function success(res) {
console.log('[游戏JS端] 支付成功:', res);
tj.showToast({
title: '支付成功,再来一局!',
duration: 2000
});
},
fail: function fail(err) {
if (err.errCode && err.errCode == -2) {
console.log('[游戏JS端] 用户取消支付');
tj.showToast({
title: '支付取消,请重新支付!',
duration: 2000
});
} else {
console.error('[游戏JS端] 支付失败:', err);
tj.showToast({
title: '支付失败,请重试!',
duration: 2000
});
}
},
complete: function complete(res) {
console.log('[游戏JS端] 支付流程结束:', res);
}
})

3.3 服务端接口​

3.3.1 服务端回调接口​

支付成功后,平台方服务端向宿主服务端发送支付成功回调。宿主服务端在验签后将回调发送给游戏服务端。

3.3.1.1 回调地址​

POST https://minihost.tuanjie.cn/game-api/v1/payment/callback

3.3.1.2 请求参数​
参数名类型说明
timestampstring时间戳
noncestring随机数
msgstringJson string类型的支付结果信息,参见2.1.3PaymentInfo对象
signaturestring签名,签名算法参见2.1.4签名部分
3.3.1.3 PaymentInfo​
参数名类型说明
appidstring平台方小游戏appid
cp_ordernostring开发者自定义订单号,tj.requestPayment方法中的customId参数
cp_extrastring开发者传的额外参数,tj.requestPayment方法中的extra参数
order_no_channelstring小游戏后台交易单号
amount_centint64订单金额,单位人民币分
amount_coinint64购买游戏币数量,平台方集成中不涉及该场景,可忽略
currencystring支付币种,CNY 人民币,DIAMOND 钻石
3.3.1.4 签名​

根据 token,timestamp,nonce,msg ,使用 SHA1 算法生成 signature,与平台方的服务端返回的 signature 对比。

前置条件:在关联游戏时,平台方需要将token传递给宿主,用于签名校验

golang示例代码:

sortedString := make([]string, 4)
sortedString = append(sortedString, token)
sortedString = append(sortedString, timestamp)
sortedString = append(sortedString, nonce)
sortedString = append(sortedString, string(msg))
sort.Strings(sortedString)h := sha1.New()
h.Write([]byte(strings.Join(sortedString, "")))_signature := h.Sum(nil)
3.3.1.5 响应​

宿主服务器返回200状态码,作为成功响应。若返回 http 状态码非200,表示通知失败,需要进行重试。

3.3.2 主动查询订单接口​

服务端支付回调可能由于网络异常等原因导致无法 100%触达,开发者可以通过该接口主动回查订单状态,防止超发漏发。开发者向宿主服务端发送查询订单请求,宿主封装后向平台方服务端发送查询订单请求。

3.3.2.1 接口地址​

由平台方提供

3.3.2.2 请求参数​
参数名类型说明
tokenstring双方约定服务端token
ordernostringcp 自定义订单号,在下单的时候传入,tj.requestPayment方法中的customId参数
3.3.2.3 响应​
参数名类型说明
statusstring支付状态。success 表示支付成功且发币到账,unsuccess 表示支付失败
3.3.2.4 鉴权​

请求需包含双方约定好的服务器令牌,放在Auhthorization header里,格式形如

Bearer {service_token}

3.4 支付接口集成示例​

payment.js 示例

// payment.js - Custom payment implementation

// Override tj.requestPayment
tj.requestPayment = function(obj) {
tj.customCommand('platformPayment', obj);
};

// Override tj.requestVirtualPayment
tj.requestVirtualPayment = function(obj) {
tj.customCommand('virtualPayment', obj);
};

在宿主中实现支付逻辑

const { TJConstants } = require('webglhost-runtime-pc');
const path = require('path');

// Setup payment options
const options = {};
options[TJConstants.LAUNCH_KEY] = 'launch-key';
options[TJConstants.USER_ID] = 'user-123';
options[TJConstants.CUSTOM_SCRIPT_FILES] = [
path.join(__dirname, 'custom-scripts', 'payment.js')
];

await gameHandle.setGameStartOptions(options);

// Setup payment listeners
gameHandle.setCustomCommandListener('platformPayment', (request, handle) => {
// Call your payment SDK
yourPaymentSDK.requestPayment({
orderId: request.orderId,
amount: request.amount,
description: request.description
})
.then(result => {
handle.success({
transactionId: result.transactionId,
status: 'success'
});
})
.catch(error => {
handle.fail(40001, error.message);
});
});

gameHandle.setCustomCommandListener('virtualPayment', (request, handle) => {
// Handle virtual payment
yourPaymentSDK.requestVirtualPayment(request)
.then(result => {
handle.success(result);
})
.catch(error => {
handle.fail(40002, error.message);
});
});

4 完整平台集成示例​

const { TJHostHandle, TJConstants } = require('webglhost-runtime-pc');
const path = require('path');

class PlatformIntegrationManager {
constructor(gameHandle) {
this.gameHandle = gameHandle;
this.setupAllListeners();
}

setupAllListeners() {
this.setupAuthListeners();
this.setupAdListeners();
this.setupPaymentListeners();
}

// Authentication
setupAuthListeners() {
this.gameHandle.setCustomCommandListener('platformLogin', (request, handle) => {
this.handleLogin(request, handle);
});

this.gameHandle.setCustomCommandListener('checkSession', (request, handle) => {
this.handleCheckSession(request, handle);
});
}

async handleLogin(request, handle) {
try {
const result = await yourPlatformSDK.login();
handle.success({
code: result.authCode,
userId: result.userId
});
} catch (error) {
handle.fail(20001, error.message);
}
}

async handleCheckSession(request, handle) {
try {
await yourPlatformSDK.checkSession();
handle.success({});
} catch (error) {
handle.fail(20002, 'Session expired');
}
}

// Ads
setupAdListeners() {
this.gameHandle.setCustomCommandListener('loadRewardAd', (request, handle) => {
this.handleLoadAd(request, handle);
});

this.gameHandle.setCustomCommandListener('showRewardAd', (request, handle) => {
this.handleShowAd(request, handle);
});
}

async handleLoadAd(request, handle) {
try {
await yourAdSDK.loadAd(request.adUnitId);
handle.success({});
this.notifyAdLoaded();
} catch (error) {
handle.fail(30001, error.message);
}
}

async handleShowAd(request, handle) {
try {
const result = await yourAdSDK.showAd(request.adUnitId);
handle.success({});
this.notifyAdClosed(result.isEnded);
} catch (error) {
handle.fail(30002, error.message);
}
}

notifyAdLoaded() {
this.gameHandle.runCustomScript('rewardedVideoLoadCallback();', () => {});
}

notifyAdClosed(isEnded) {
this.gameHandle.runCustomScript(
`rewardedVideoCloseCallback({ isEnded: ${isEnded} });`,
() => {}
);
}

// Payment
setupPaymentListeners() {
this.gameHandle.setCustomCommandListener('platformPayment', (request, handle) => {
this.handlePayment(request, handle);
});
}

async handlePayment(request, handle) {
try {
const result = await yourPaymentSDK.requestPayment(request);
handle.success(result);
} catch (error) {
handle.fail(40001, error.message);
}
}
}

// Initialize with all platform integrations
async function initializeWithPlatformIntegration() {
const hostHandle = await new Promise((resolve, reject) => {
TJHostHandle.initialize('sdk-key', 'sdk-secret', (handle, error) => {
if (error) reject(error);
else resolve(handle);
});
});

const gameHandle = await hostHandle.createGameHandle();

// Initialize platform integration manager
const platformManager = new PlatformIntegrationManager(gameHandle);

// Setup game options with all custom scripts
const options = {
[TJConstants.LAUNCH_KEY]: 'launch-key',
[TJConstants.USER_ID]: 'user-123',
[TJConstants.CUSTOM_SCRIPT_FILES]: [
path.join(__dirname, 'custom-scripts', 'auth.js'),
path.join(__dirname, 'custom-scripts', 'ad.js'),
path.join(__dirname, 'custom-scripts', 'payment.js')
]
};

await gameHandle.setGameStartOptions(options);
await gameHandle.start();
await gameHandle.play();

return { hostHandle, gameHandle, platformManager };
}

// Usage
initializeWithPlatformIntegration()
.then(({ gameHandle }) => {
console.log('Platform integration initialized successfully');
})
.catch(error => {
console.error('Platform integration failed:', error);
});