Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google billing interface uses promises #17996

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion @types/jsb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ declare namespace jsb {
* @en Code returned in In-app Billing API calls.
* @zh 应用内结算 API 调用中返回的响应代码。
*/
readonly responseCode: string;
readonly responseCode: number;
readonly toStr: string;
}

Expand Down
181 changes: 154 additions & 27 deletions vendor/google/billing/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface BillingEventMap {
[google.BillingEventType.BILLING_SETUP_FINISHED]: (result: google.BillingResult) => void,
[google.BillingEventType.BILLING_SERVICE_DISCONNECTED]: () => void,
[google.BillingEventType.PRODUCT_DETAILS_RESPONSE]:
(result: google.BillingResult, productDetailsList: google.ProductDetails[]) => void,
(result: google.BillingResult, productDetailsList: google.ProductDetails[]) => void,
[google.BillingEventType.PURCHASES_UPDATED]: (result: google.BillingResult, purchases: google.Purchase[]) => void,
[google.BillingEventType.CONSUME_RESPONSE]: (result: google.BillingResult, purchaseToken: string) => void,
[google.BillingEventType.ACKNOWLEDGE_PURCHASES_RESPONSE]: (result: google.BillingResult) => void
Expand Down Expand Up @@ -131,8 +131,16 @@ class Billing {
* @en Starts up BillingClient setup process asynchronously.
* @zh 异步启动 BillingClient 设置过程。
*/
public startConnection (): void {
jsb.googleBilling?.startConnection();
public startConnection (): Promise<google.BillingResult> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don’t use an id for callbacks, could it lead to timing issues and result in incorrect outcomes?

var a = billing.startConnection();
var b = billing.startConnection();
//or use promise.all
Promise.all([billing.startConnection(), billing.startConnection(), billing.startConnection()])

return new Promise((resolve, reject) => {
this.once(google.BillingEventType.BILLING_SETUP_FINISHED, (result: google.BillingResult): void => {
resolve(result);
});
this.once(google.BillingEventType.BILLING_SERVICE_DISCONNECTED, (): void => {
reject();
});
jsb.googleBilling?.startConnection();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to reject if jsb.googleBilling is undefined or null?

});
}

/**
Expand Down Expand Up @@ -176,8 +184,20 @@ class Billing {
* @param productType @zh 产品类型。 @en product type.
*
*/
public queryProductDetailsParams (productId: string[], productType: google.ProductType): void {
jsb.googleBilling?.queryProductDetailsParams(productId, productType);
public queryProductDetailsParams (productId: string[], productType: google.ProductType): Promise<google.ProductDetails[]> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 Promise 返回的对象是错误的,没有和 Google Billing 的API保持一致

当前的返回列表信息 google.ProductDetails[] ,没有了 BillingResult 的结果

预期正确的返回 BillingResult billingResult,<ProductDetails> productDetailsList(这个是Android代码)

https://developer.android.com/google/play/billing/integrate?hl=zh-cn#show-products

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我们现在是希望封装得对 ts 开发者友好点。

Copy link

@zhitaocai zhitaocai Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同理,其他很多公开API也是一样的问题,请尽量保持和GoogleBillingAPI一致的返回结果,很多时候,BillingResult 是一个很重要的返回参数,实际项目开发中也会用到此参数做业务判断,不能忽略返回此参数

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我们现在是希望封装得对 ts 开发者友好点。

这个支付 sdk 很重要,项目的命脉,个人建议最好是和官方的 API 保持一致,而不是过度封装

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

接口都一样,只是说回调的方式对 ts 更友好点。担心 ts 的开发者不习惯 java 风格的 api。

Copy link

@zhitaocai zhitaocai Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感觉你们过于忽视对 BilingResult 相应状态码的处理,Google 甚至专门有一个文档链接详细介绍处理的

QQ_1733139240478

https://developer.android.com/google/play/billing/errors?hl=zh-cn

特别是这句:并非所有响应代码都是错误

所以,Google Billing 的API ,很多地方才返回这个 BillingResult 状态对象,因为是真的很多状态,并不是非对即错,一般业务都需要根据不同状态做对应的处理

Copy link
Contributor

@dumganhar dumganhar Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

此 PR 前的 ts API 本身也跟 Java 的 API 风格不一致。

// ts
public queryProductDetailsParams (productId: string[], productType: google.ProductType): void {
   ...
}
// java
billingClient.queryProductDetailsAsync(
    queryProductDetailsParams,
    new ProductDetailsResponseListener() {
        public void onProductDetailsResponse(BillingResult billingResult,
                List<ProductDetails> productDetailsList) {
            // check billingResult
            // process returned productDetailsList
        }
    }
)

如果是跟 java 风格一致,那么这个接口的 ts 声明应该是:

class QueryProductDetailsParams { 
    public static newBuilder(): QueryProductDetailsParamsBuilder;
}

class QueryProductDetailsParamsBuilder {
    public setProductList( ... );
    public build(): QueryProductDetailsParams;
}

interface ProductDetailsResponseListener {
    onProductDetailsResponse (billingResult: BillingResult, productDetailsList: ProductDetails[]) ) => void;
}

queryProductDetailsAsync(params: QueryProductDetailsParams, listener: ProductDetailsResponseListener): void;

// 用法:

queryProductDetailsAsync(
    QueryProductDetailsParams.newBuilder().setProductList(...).build(), {
        onProductDetailsResponse (billingResult: BillingResult, productDetailsList: ProductDetails[]) {
                // check billingResult
                // process returned productDetailsList
        }
    }
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

另外,为什么 ts 封装的接口 productType: google.ProductType 这个参数只支持传递一个?java 的接口是 id 与 type 的结构体的数组。这是不是也导致了不一致的行为?

Copy link

@zhitaocai zhitaocai Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

大佬们有点跑题了

请注意:我这里原本的问题是,保持一致的 返回参数,而不是保持java风格一致
请注意:我这里原本的问题是,保持一致的 返回参数,而不是保持java风格一致
请注意:我这里原本的问题是,保持一致的 返回参数,而不是保持java风格一致

感觉大佬们现在误解我的意思了

现在这里的代码的问题是过度封装,返回参接只是返回了商品列表google.ProductDetails[],不返回 BillingResult 的参数(而这个参数一般业务都会用到的)

在返回参数没保持一致的前提下,我再看了这个代码的 请求参数,也是错误的,或者不能说是错误的,只能说又是过度封装,导致本来可以做的事情,现在直接不能做

原来的 API 是可以这样子的

QQ_1733192481582

一次性请求不同类型的商品(消耗型、订阅型)不同id

但是现在的 API 不能做到

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个后面会调整

return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.PRODUCT_DETAILS_RESPONSE,
Copy link

@zhitaocai zhitaocai Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的 id 是唯一的,存在非常严重的问题。

当我连续 queryProductDetailsParams n次,此接口并不是按照调用顺序返回的,举个例子

queryProductDetailsParams1 
queryProductDetailsParams2 
queryProductDetailsParams3
 
实际的返回结果可能是

queryProductDetailsParams2 
queryProductDetailsParams1
queryProductDetailsParams3

正确的做法应该是在调用此接口的时候,实时生成一个回调事件(uuid之类),同时ts注册这个事件的回调监听,然后在 Android 端获取到结果的时候,回调对应的事件

注意:其他API需要检查,很大概率存在类似问题

(result: google.BillingResult, productDetailsList: google.ProductDetails[]): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve(productDetailsList);
} else {
reject(result);
}
},
);
jsb.googleBilling?.queryProductDetailsParams(productId, productType);
});
}

/**
Expand All @@ -186,76 +206,184 @@ class Billing {
* @param productDetails @zh 产品详情。 @en product details.
* @param selectedOfferToken @zh 选择提供的token。 @en selected offer token.
*/
public launchBillingFlow (productDetails: google.ProductDetails[], selectedOfferToken: string | null): void {
jsb.googleBilling?.launchBillingFlow(productDetails, selectedOfferToken);
public launchBillingFlow (productDetails: google.ProductDetails[], selectedOfferToken: string | null): Promise<google.Purchase[]> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里不应该是一个异步方法,更不应该是异步返回一个交易列表,正确的应该是一个同步方法,并且结果是返回此操作的状态码,详见

https://developer.android.com/google/play/billing/integrate?hl=zh-cn#launch

Copy link

@zhitaocai zhitaocai Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PurchaseUpdate 的回调,不一定是主动触发 launchBilingFlow 后回调的

它是 Google 那边在连接成功后,任何时间 都可能会触发的一个回调。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

现在过度封装后的 launchBillingFlow 甚至没法对齐 Google 官方文档的做法

https://developer.android.com/google/play/billing/integrate?hl=zh-cn#launch

QQ_1733135753214

launchBillingFlow 是拉起支付界面,但并不是一定能拉起成功,拉起成功也并不是一定会创建交易(比如用户拉起之后,就取消支付)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个地方没有必要保持一致,连接成功之后,基本上不会出错,内部也有连接是否成功的判断。因为cocos和UI不是在同一个线程,很难做到保持一致。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PurchaseUpdate

这个倒是没有注意到的问题。我看看

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个地方没有必要保持一致,连接成功之后,基本上不会出错,内部也有连接是否成功的判断。因为cocos和UI不是在同一个线程,很难做到保持一致。

要的!并非所有响应代码都是错误

https://developer.android.com/google/play/billing/errors?hl=zh-cn#item_already_owned

Copy link
Contributor Author

@qiuguohua qiuguohua Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个我测试过,如果取消支付,也是会有回调的
image

return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.PURCHASES_UPDATED,
(result: google.BillingResult, purchaseList: google.Purchase[]): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve(purchaseList);
} else {
reject(result);
}
},
);
jsb.googleBilling?.launchBillingFlow(productDetails, selectedOfferToken);
});
}

/**
* @en Consumes a given in-app product.
* @zh 消费指定的应用内产品。
* @param purchase @zh 已经购买的产品。 @en Purchased Products.
*/
public consumePurchases (purchase: google.Purchase[]): void {
jsb.googleBilling?.consumePurchases(purchase);
public consumePurchases (purchase: google.Purchase[]): Promise<string> {
return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.CONSUME_RESPONSE,
(result: google.BillingResult, token: string): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve(token);
} else {
reject(result);
}
},
);
jsb.googleBilling?.consumePurchases(purchase);
});
}

/**
* @en Acknowledges in-app purchases.
* @zh 确认应用内购买。
* @param purchase @zh 已经购买的产品。 @en Purchased Products.
*/
public acknowledgePurchase (purchase: google.Purchase[]): void {
jsb.googleBilling?.acknowledgePurchase(purchase);
public acknowledgePurchase (purchase: google.Purchase[]): Promise<void> {
return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.ACKNOWLEDGE_PURCHASES_RESPONSE,
(result: google.BillingResult): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve();
} else {
reject(result);
}
},
);
jsb.googleBilling?.acknowledgePurchase(purchase);
});
}

/**
* @en Returns purchases details for currently owned items bought within your app.
* @zh 返回您应用内当前拥有的购买商品的购买详情。
* @param productType @zh 产品类型 @en Product type.
*/
public queryPurchasesAsync (productType: google.ProductType): void {
jsb.googleBilling?.queryPurchasesAsync(productType);
public queryPurchasesAsync (productType: google.ProductType): Promise<google.Purchase[]> {
return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.QUERY_PURCHASES_RESPONSE,
(result: google.BillingResult, purchaseList: google.Purchase[]): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve(purchaseList);
} else {
reject(result);
}
},
);
jsb.googleBilling?.queryPurchasesAsync(productType);
});
}

/**
* @en Gets the billing config, which stores configuration used to perform billing operations.
* @zh 获取计费配置,其中存储用于执行计费操作的配置。
*/
public getBillingConfigAsync (): void {
jsb.googleBilling?.getBillingConfigAsync();
public getBillingConfigAsync (): Promise<google.BillingConfig> {
return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.BILLING_CONFIG_RESPONSE,
(result: google.BillingResult, billingConfig: google.BillingConfig): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve(billingConfig);
} else {
reject(result);
}
},
);
jsb.googleBilling?.getBillingConfigAsync();
});
}

/**
* @en Creates alternative billing only purchase details that can be used to report a transaction made
* via alternative billing without user choice to use Google Play billing.
* @zh 创建仅限替代结算的购买详情,可用于报告通过替代结算进行的交易,而无需用户选择使用 Google Play 结算。
*/
public createAlternativeBillingOnlyReportingDetailsAsync (): void {
jsb.googleBilling?.createAlternativeBillingOnlyReportingDetailsAsync();
public createAlternativeBillingOnlyReportingDetailsAsync (): Promise<void> {
return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.ALTERNATIVE_BILLING_ONLY_TOKEN_RESPONSE,
(result: google.BillingResult): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve();
} else {
reject(result);
}
},
);
jsb.googleBilling?.createAlternativeBillingOnlyReportingDetailsAsync();
});
}

/**
* @en Checks the availability of offering alternative billing without user choice to use Google Play billing.
* @zh 检查是否可以提供替代结算方式,而无需用户选择使用 Google Play 结算方式。
*/
public isAlternativeBillingOnlyAvailableAsync (): void {
jsb.googleBilling?.isAlternativeBillingOnlyAvailableAsync();
public isAlternativeBillingOnlyAvailableAsync (): Promise<void> {
return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.EXTERNAL_OFFER_REPORTING_DETAILS_RESPONSE,
(result: google.BillingResult): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve();
} else {
reject(result);
}
},
);
jsb.googleBilling?.isAlternativeBillingOnlyAvailableAsync();
});
}

/**
* @en Creates purchase details that can be used to report a transaction made via external offer.
* @zh 创建可用于报告通过外部报价进行的交易的购买详情。
*/
public createExternalOfferReportingDetailsAsync (): void {
jsb.googleBilling?.createExternalOfferReportingDetailsAsync();
public createExternalOfferReportingDetailsAsync (): Promise<void> {
return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.EXTERNAL_OFFER_REPORTING_DETAILS_RESPONSE,
(result: google.BillingResult): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve();
} else {
reject(result);
}
},
);
jsb.googleBilling?.createExternalOfferReportingDetailsAsync();
});
}

/**
* @en Checks the availability of providing external offer.
* @zh 检查提供外部报价的可用性。
*/
public isExternalOfferAvailableAsync (): void {
jsb.googleBilling?.isExternalOfferAvailableAsync();
public isExternalOfferAvailableAsync (): Promise<void> {
return new Promise((resolve, reject) => {
this.once(
google.BillingEventType.EXTERNAL_OFFER_AVAILABILITY_RESPONSE,
(result: google.BillingResult): void => {
if (result.responseCode === google.BillingResponseCode.OK) {
resolve();
} else {
reject(result);
}
},
);
jsb.googleBilling?.isExternalOfferAvailableAsync();
});
}

/**
Expand Down Expand Up @@ -307,15 +435,15 @@ class Billing {
return null;
}

public on<K extends keyof BillingEventMap> (type: K, callback: BillingEventMap[K], target?: unknown): BillingEventMap[K] {
private on<K extends keyof BillingEventMap> (type: K, callback: BillingEventMap[K], target?: unknown): BillingEventMap[K] {
this._eventTarget.on(type, callback, target);
return callback;
}
public once<K extends keyof BillingEventMap> (type: K, callback: BillingEventMap[K], target?: unknown): BillingEventMap[K] {
private once<K extends keyof BillingEventMap> (type: K, callback: BillingEventMap[K], target?: unknown): BillingEventMap[K] {
this._eventTarget.once(type, callback, target);
return callback;
}
public off<K extends keyof BillingEventMap> (eventType: K, callback?: BillingEventMap[K], target?: any): void {
private off<K extends keyof BillingEventMap> (eventType: K, callback?: BillingEventMap[K], target?: any): void {
this._eventTarget.off(eventType, callback, target);
}
}
Expand Down Expand Up @@ -816,7 +944,6 @@ export namespace google {
export type AlternativeBillingOnlyReportingDetails = jsb.AlternativeBillingOnlyReportingDetails;
export type ExternalOfferReportingDetails = jsb.ExternalOfferReportingDetails;
export type InAppMessageResult = jsb.InAppMessageResult;

/**
* @en
* Interface for Google Play blling module.
Expand Down
Loading