baxi преди 4 дни
родител
ревизия
f03756f4c2
променени са 4 файла, в които са добавени 64 реда и са изтрити 13 реда
  1. 3 1
      assets/scripts/platform/IAP/IAP.ts
  2. 10 1
      assets/scripts/platform/IAP/IAPImpl.ts
  3. 25 6
      assets/scripts/platform/IAP/IOSIAP.ts
  4. 26 5
      native/engine/ios/service/IAPWrapper.mm

+ 3 - 1
assets/scripts/platform/IAP/IAP.ts

@@ -6,7 +6,9 @@ import { IOSIAP } from './IOSIAP';
 class UnsupportedIAP extends IAPImpl {
     private unsupported(): never { throw new Error('当前平台尚未实现内购。'); }
     async init(_ids: string[]): Promise<IAPProductsResult> { return this.unsupported(); }
-    async purchase(_id: string): Promise<IAPResult> { return this.unsupported(); }
+    async purchase(productId: string): Promise<IAPResult> {
+        return { status: 'error', productId, errorCode: 'UNSUPPORTED_PLATFORM', reason: '当前平台尚未实现内购。' };
+    }
     async restore(): Promise<IAPResult[]> { return this.unsupported(); }
     async getPendingTransactions(): Promise<IAPResult[]> { return this.unsupported(); }
     async finishTransaction(_id: string): Promise<void> { this.unsupported(); }

+ 10 - 1
assets/scripts/platform/IAP/IAPImpl.ts

@@ -6,8 +6,17 @@ export interface IAPProduct {
     localizedPrice: string;
     currencyCode: string;
 }
+export type IAPErrorCode = 'INVALID_PRODUCT_ID' | 'PRODUCT_NOT_LOADED' | 'PAYMENTS_NOT_ALLOWED'
+    | 'OPERATION_IN_PROGRESS' | 'PENDING_TRANSACTIONS' | 'PAYMENT_FAILED'
+    | 'TIMEOUT' | 'BRIDGE_ERROR' | 'UNSUPPORTED_PLATFORM' | 'UNKNOWN';
+
 export interface IAPResult {
-    status: 'purchased' | 'restored' | 'cancelled' | 'deferred';
+    status: 'purchased' | 'restored' | 'cancelled' | 'deferred' | 'error';
+    /** 请求失败原因;超时不代表 Apple 已取消或支付失败。 */
+    reason?: string;
+    errorCode?: IAPErrorCode;
+    /** Apple 原始错误,仅用于诊断,不依赖本地化 message 判断逻辑。 */
+    nativeError?: { domain: string; code: number };
     productId: string;
     transactionId?: string;
     originalTransactionId?: string;

+ 25 - 6
assets/scripts/platform/IAP/IOSIAP.ts

@@ -1,5 +1,9 @@
 import { native } from 'cc';
-import { IAPImpl, IAPProductsResult, IAPResult } from './IAPImpl';
+import { IAPImpl, IAPProductsResult, IAPResult, IAPErrorCode } from './IAPImpl';
+
+class IAPRequestError extends Error {
+    constructor(public readonly code: IAPErrorCode, message: string) { super(message); }
+}
 
 export class IOSIAP extends IAPImpl {
     private seed = 0;
@@ -23,21 +27,22 @@ export class IOSIAP extends IAPImpl {
             clearTimeout(pending.timer);
             this.pending.delete(response.requestId);
             if (response.success) pending.resolve(response.data);
-            else pending.reject(new Error(response.error || 'IAP request failed'));
+            else pending.reject(new IAPRequestError('UNKNOWN', response.error || 'IAP request failed'));
         });
         this.listening = true;
     }
     private request<T>(method: string, params: object = {}): Promise<T> {
-        this.listen();
+        try { this.listen(); }
+        catch (error) { throw new IAPRequestError('BRIDGE_ERROR', String(error)); }
         const requestId = `${Date.now()}-${++this.seed}`;
         return new Promise<T>((resolve, reject) => {
             const timer = setTimeout(() => {
                 this.pending.delete(requestId);
-                reject(new Error('IAP 请求超时;不代表支付失败,请查询未完成交易,勿直接重复购买。'));
+                reject(new IAPRequestError('TIMEOUT', 'IAP 请求超时;不代表支付失败,请查询未完成交易,勿直接重复购买。'));
             }, 180000);
             this.pending.set(requestId, { resolve, reject, timer });
             try { native.jsbBridgeWrapper.dispatchEventToNative('iap.request', JSON.stringify({ requestId, method, params })); }
-            catch (error) { clearTimeout(timer); this.pending.delete(requestId); reject(error); }
+            catch (error) { clearTimeout(timer); this.pending.delete(requestId); reject(new IAPRequestError('BRIDGE_ERROR', String(error))); }
         });
     }
     init(productIds: string[]): Promise<IAPProductsResult> {
@@ -46,7 +51,21 @@ export class IOSIAP extends IAPImpl {
         }
         return this.request('init', { productIds: [...new Set(productIds)] });
     }
-    purchase(productId: string): Promise<IAPResult> { return this.request('purchase', { productId }); }
+    async purchase(productId: string): Promise<IAPResult> {
+        if (typeof productId !== 'string' || !productId.trim()) {
+            return { status: 'error', productId: typeof productId === 'string' ? productId : '', errorCode: 'INVALID_PRODUCT_ID', reason: '商品 ID 不能为空。' };
+        }
+        try {
+            return await this.request<IAPResult>('purchase', { productId });
+        } catch (error) {
+            return {
+                status: 'error',
+                errorCode: error instanceof IAPRequestError ? error.code : 'UNKNOWN',
+                productId,
+                reason: error instanceof Error ? error.message : String(error),
+            };
+        }
+    }
     restore(): Promise<IAPResult[]> { return this.request('restore'); }
     getPendingTransactions(): Promise<IAPResult[]> { return this.request('pending'); }
     async finishTransaction(transactionId: string): Promise<void> {

+ 26 - 5
native/engine/ios/service/IAPWrapper.mm

@@ -47,6 +47,14 @@
         [[JsbBridgeWrapper sharedInstance] dispatchEventToScript:@"iap.response" arg:payload];
     });
 }
+- (void)purchaseFailure:(NSDictionary *)request code:(NSString *)code reason:(NSString *)reason nativeError:(NSError *)error {
+    id productID = request[@"params"][@"productId"];
+    NSMutableDictionary *data = [@{@"status": @"error", @"errorCode": code, @"reason": reason,
+        @"productId": [productID isKindOfClass:NSString.class] ? productID : @""} mutableCopy];
+    if (error) data[@"nativeError"] = @{@"domain": error.domain, @"code": @(error.code)};
+    // The bridge request completed; the purchase itself failed (data.status).
+    [self reply:request data:data error:nil];
+}
 - (NSDictionary *)dataFor:(SKPaymentTransaction *)transaction {
     NSURL *url = NSBundle.mainBundle.appStoreReceiptURL;
     NSData *receipt = url ? [NSData dataWithContentsOfURL:url] : nil;
@@ -84,13 +92,22 @@
         self.productsRequest.delegate = self;
         [self.productsRequest start];
     } else if ([method isEqualToString:@"purchase"]) {
-        if (self.purchaseRequest || self.restoreRequest || SKPaymentQueue.defaultQueue.transactions.count) {
-            [self reply:r data:nil error:@"存在进行中或未完成交易,请先处理未完成交易。"]; return;
+        if (self.purchaseRequest || self.restoreRequest) {
+            [self purchaseFailure:r code:@"OPERATION_IN_PROGRESS" reason:@"已有购买或恢复操作进行中。" nativeError:nil]; return;
+        }
+        if (SKPaymentQueue.defaultQueue.transactions.count) {
+            [self purchaseFailure:r code:@"PENDING_TRANSACTIONS" reason:@"存在未完成或待审批交易,请先处理或等待。" nativeError:nil]; return;
         }
         NSString *productID = p[@"productId"];
         SKProduct *product = [productID isKindOfClass:NSString.class] ? self.products[productID] : nil;
-        if (!product || ![SKPaymentQueue canMakePayments]) {
-            [self reply:r data:nil error:@"商品未初始化或此设备不允许内购。"]; return;
+        if (![productID isKindOfClass:NSString.class] || !productID.length) {
+            [self purchaseFailure:r code:@"INVALID_PRODUCT_ID" reason:@"商品 ID 不能为空。" nativeError:nil]; return;
+        }
+        if (!product) {
+            [self purchaseFailure:r code:@"PRODUCT_NOT_LOADED" reason:@"尚未加载此商品,请检查初始化返回的 products 和 invalidProductIds。" nativeError:nil]; return;
+        }
+        if (![SKPaymentQueue canMakePayments]) {
+            [self purchaseFailure:r code:@"PAYMENTS_NOT_ALLOWED" reason:@"设备不允许内购,请检查购买限制。" nativeError:nil]; return;
         }
         self.purchaseRequest = r;
         [SKPaymentQueue.defaultQueue addPayment:[SKPayment paymentWithProduct:product]];
@@ -146,7 +163,11 @@
             if (t.transactionState == SKPaymentTransactionStateFailed) {
                 BOOL cancelled = t.error.code == SKErrorPaymentCancelled && [t.error.domain isEqualToString:SKErrorDomain];
                 NSDictionary *data = @{@"status": @"cancelled", @"productId": t.payment.productIdentifier ?: @""};
-                if (matches) { [self reply:self.purchaseRequest data:cancelled ? data : nil error:cancelled ? nil : t.error.localizedDescription ?: @"支付失败。"]; self.purchaseRequest = nil; }
+                if (matches) {
+                    if (cancelled) [self reply:self.purchaseRequest data:data error:nil];
+                    else [self purchaseFailure:self.purchaseRequest code:@"PAYMENT_FAILED" reason:t.error.localizedDescription ?: @"支付失败。" nativeError:t.error];
+                    self.purchaseRequest = nil;
+                }
                 [queue finishTransaction:t];
                 continue;
             }