Unity in-app-purchase call CheckEntitlement in OnPurchaseConfirmed

在 OnPurchaseConfirmed 里立即调用 CheckEntitlement 存在竞态条件(race condition)

参看 unity 内购订阅 示例代码 BuyingSubscription.cs ,可以看到如下内容:

        void OnPurchaseConfirmed(Order order)
        {
            CheckSubscription();
        }

        void CheckSubscription()
        {
            var product = m_StoreController.GetProducts().FirstOrDefault(p => p.definition.id == subscriptionProductId);
            m_StoreController.CheckEntitlement(product);
        }
        
        void OnCheckEntitlement(Entitlement entitlement)
        {
            if (entitlement.Product.definition.id == subscriptionProductId)
            {
                switch (entitlement.Status)
                {
                    case EntitlementStatus.FullyEntitled:
                        isSubscribed = true;
                        UpdateUI();
                        break;
                    default:
                        isSubscribed = false;
                        UpdateUI();
                        break;
                }
            }
        }

上面的代码 OnCheckEntitlement 函数中,entitlement.Status 不为 EntitlementStatus.FullyEntitled ,获取到到订阅信息以及是否订阅有效的数据都是不对的。原因在于 在 OnPurchaseConfirmed 里立即调用 CheckEntitlement 存在竞态条件(race condition)

Unity 官方(SashaM,Unity IAP 团队)在论坛上明确回复过一模一样的场景: 在 OnPurchaseConfirmed 回调里调用 CheckEntitlement 可能出现竞态条件,这个问题已经被记录为已知 issue,但目前没有修复时间表。

更关键的是,Unity 的另一位工程师(Yannick_D)给出了正确做法: 其实根本不需要在 OnPurchaseConfirmed 里再调用 CheckEntitlement——你已经拿到了确认(Confirmed)的订单。对于非消耗品和订阅来说,一个 ConfirmedOrder 就意味着用户已经拥有该商品了,这和你去做 FetchPurchases 拿 ConfirmedOrder,或者调用 CheckEntitlement 判断是否拥有,本质上是等价的。

也就是说,你现在的写法(收到 OnPurchaseConfirmed → 马上 CheckEntitlement(product))本身就是触发这个 bug 的标准姿势。 因此,正确的处理代码如下:

void OnPurchaseConfirmed(Order order)
{
    var product = GetFirstProductInOrder(order);
    if ( product == null ) {
        SetCurrentState(IAPProcessState.Failure, IAP_PRODUCT_NOT_FOUND);
        OnIAPError?.Invoke(IAP_PRODUCT_NOT_FOUND);
        return;
    }

	if (order is ConfirmedOrder confirmedOrder)
	{
		string productId = product.definition.id;

		if (productId == subscriptionProductId)
		{
			isSubscribed = true;
			SubInfo = GetBestSubscriptionFromOrder(confirmedOrder);
		}
		
		SetCurrentState(IAPProcessState.Success);
	}
	else if (order is FailedOrder failedOrder)
	{
		string reason = $"{failedOrder.FailureReason}: {failedOrder.Details}";
		SetCurrentState(IAPProcessState.Failure, reason);
		OnIAPError?.Invoke(reason);
	}
	else
	{
		string msg = $"Unexpected order type in OnPurchaseConfirmed: {order?.GetType().Name}";
		SetCurrentState(IAPProcessState.Failure, msg);
		OnIAPError?.Invoke(msg);
	}
}

SubscriptionInfo GetBestSubscriptionFromOrder(ConfirmedOrder confirmedOrder)
{
    return confirmedOrder.Info?.PurchasedProductInfo?
        .FirstOrDefault()?.subscriptionInfo;
}

这样 OnPurchaseConfirmed 这条路径就完全不依赖 CheckEntitlement,自然也就绕开了 Unity 官方确认的那个竞态 bug。CheckEntitlement 只保留在启动时 OnPurchasesFetched/RestoreTransactions 之后去做批量刷新就够了。