前言
游戏产品上线少不了产品内购这一块业务。得益于unity 官方推出的夸平台内购插件,使得我们集成内购就变得更加简单和方便。下面我们就一起利用Unity的内购插件[In App Purchasing]来设计、实现我们的游戏内购业务。
一、导入插件、阅读官方文档
我们打开包管理器,安装内购插件,结果如图:
对于接入第三方SDK、插件,我们还是需要过一遍官方文档,这样接入过程心中更加有数,并减少出错机率。官方文档如下:
地址一:docs.unity3d.com/2021.3/Docu…
地址二:docs.unity3d.com/Packages/co…
二、确定内购产品
根据文档描述,我们可以确定内购产品一般主要分为三类:
1、非一次性产品,也就是可以重复购买的产品。
2、一次性产品,也就是只可以购买一次的产品。
3、订阅产品,也就是购买时间段有效性的产品。
根据这样的产品特性,我们可以设计这样的两张表,方便夸平台使用。例如,运营的同学只需要按照这两张表内的数据,可以往Google play、Apple store、Amazon store后台填上内购产品数据,客户端也是依据这两张表读取产品数据进行初始化内购系统,这样就可以做到数据统一化。数据表设计具有父子关系,如下图:
对于数据表的导入和导出,生成为游戏所能读取的格式,不在本文所讨论的范围内,但一个完整的游戏,依据业务都会具备这些功能。
三、定义内购产品项结构
我们需要对内购产品进行交互,就必须有一个持久化数据项,数据项如下:
public class PurchasingItem
{
public string Id;
/// <summary>
/// 自定数据表内购项的ID
/// </summary>
public int ParentId;
/// <summary>
/// 内购产品的三种类型之一
/// </summary>
public ProductType PType;
/// <summary>
/// 已经本地化的价格字符
/// </summary>
public string PriceStr;
/// <summary>
/// 是否已购买过
/// </summary>
public bool Buyed = false;
}
四、定义基本的属性、方法和产品列表
/// <summary>
/// 所有的产品
/// </summary>
private Dictionary<string, PurchasingItem> _items = new Dictionary<string, PurchasingItem>();
public Dictionary<string, PurchasingItem> Items => _items;
/// <summary>
/// 订阅的产品
/// </summary>
public Dictionary<string, PurchasingItem> SubscriptionItems { private set; get; } = new Dictionary<string, PurchasingItem>();
private IStoreController _controller;
private IExtensionProvider _extensions;
private IAppleExtensions _appleExtensions;
private CrossPlatformValidator _validator;
public event Action<Dictionary<string, PurchasingItem>> OnInitSuccess;
public event Action<string> OnInitFaile;
public event Action<PurchasingItem> OnPurchasingSuccess;
public event Action<PurchasingItem, string> OnPurchasingFaile;
private bool _isInit = false;
public void AddProduct(string id, int parentId, ProductType t)
{
var item = new PurchasingItem { Id = id, ParentId = parentId, PType = t };
if(!_items.ContainsKey(id))
_items.Add(id, item);
}
public void ClearProduct()
{
_items.Clear();
}
public PurchasingItem GetItem(string pid)
{
_items.TryGetValue(pid, out var item);
return item;
}
public Product GetProduct(string pid)
{
if(IsInitialized())
{
var product = _controller.products.WithID(pid);
return product;
}
return null;
}
五、初始化内购系统
值得注意的是,我们初始化内购系统时,必须先初始化内购服务,内购服务初始始化成功之后才能接着初始化内购系统,它俩是先后顺序的关系。如下:
/// <summary>
/// 初始化内购
/// </summary>
public void InitPurchasing()
{
//先添加产品
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} products {_items.Count}");
_isInit = false;
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
foreach(var item in _items)
{
var v = item.Value;
builder.AddProduct(v.Id, v.PType);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} Purchasing product id {v.Id}, type {v.PType}");
}
//然后初始化内购服务
InitServices(builder);
}
/// <summary>
/// 然后初始化内购服务
/// </summary>
/// <param name="builder"></param>
private async void InitServices(ConfigurationBuilder builder)
{
try
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)} start");
var options = new InitializationOptions().SetEnvironmentName("production");
await UnityServices.InitializeAsync(options);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)} success, begin init purchasing");
UnityPurchasing.Initialize(this, builder);//内购服务初始化成功后方可初始化内购组件
}
catch(Exception e)
{
var message = e.Message;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)}, errorMsg {message}");
OnInitFaile?.Invoke(message);
}
}
public bool IsInitialized()
{
//return _controller != null && _extensions != null;
return _isInit;
}
六、初始化成功或失败后调用的方法
初始化成功后,我们需要检查产品购买的状态,以更新我们的游戏。例如产品是否已购买过、是否可以购买、订阅项是否到期等。
/// <summary>
/// 初始化内购成功后会调用此方法
/// </summary>
/// <param name="controller"></param>
/// <param name="extensions"></param>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
_controller = controller;
_extensions = extensions;
_appleExtensions = _extensions.GetExtension<IAppleExtensions>();
_validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product count: {_controller.products.all.Length}");
SubscriptionItems.Clear();
foreach(var item in _controller.products.all)
{
if (item.availableToPurchase)
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product id is: {item.definition.storeSpecificId}, priceStr {item.metadata.localizedPriceString}, type {item.definition.type}");
var pitem = GetItem(item.definition.storeSpecificId);
if (pitem != null)
{
pitem.PriceStr = item.metadata.localizedPriceString;
pitem.Buyed = item.hasReceipt;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product id is: {item.definition.storeSpecificId}, buyed {pitem.Buyed}, type {pitem.PType}");
}
if(item.receipt != null)
{//检查已订阅的状态
if(item.definition.type == ProductType.Subscription)
{
if (CheckIfProductIsAvailableForSubscriptionManager(item.receipt))
{
var p = new SubscriptionManager(item, null);
var info = p.getSubscriptionInfo();
var pid = info.getProductId();
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product id is: " + pid);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, purchase date is: " + info.getPurchaseDate());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, subscription next billing date is: " + info.getExpireDate());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is subscribed? " + info.isSubscribed().ToString());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is expired? " + info.isExpired().ToString());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is cancelled? " + info.isCancelled());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product is in free trial peroid? " + info.isFreeTrial());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product is auto renewing? " + info.isAutoRenewing());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, subscription remaining valid time until next billing date is: " + info.getRemainingTime());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is this product in introductory price period? " + info.isIntroductoryPricePeriod());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the product introductory localized price is: " + info.getIntroductoryPrice());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the product introductory price period is: " + info.getIntroductoryPricePeriod());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
if (info.isSubscribed() == Result.True && info.isExpired() == Result.False)
{
SubscriptionItems[pid] = _items[pid];
}
}
}
}
}
}
if (Application.platform == RuntimePlatform.IPhonePlayer)
{
var hasRestore = HasRestorePurchasingKey();
if (!hasRestore)
{
RestorePurchasing((a, b) => {
_isInit = true;
SaveRestorePurchasingKey();
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
OnInitSuccess?.Invoke(_items);
});
}
else
{
_isInit = true;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
OnInitSuccess?.Invoke(_items);
}
}
else
{
_isInit = true;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
OnInitSuccess?.Invoke(_items);
}
}
/// <summary>
/// 初始化失败会调用这方法
/// </summary>
/// <param name="error"></param>
/// <param name="message"></param>
public void OnInitializeFailed(InitializationFailureReason error, string message)
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitializeFailed)} error {error}, errorMsg {message}");
OnInitFaile?.Invoke(message);
}
七、购买产品
购买产品之前,需要确保内购系统已经成功初始化完毕,如下:
/// <summary>
/// 购买商品
/// </summary>
/// <param name="pid"></param>
public void Buy(string pid)
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} IsInitialized {IsInitialized()}");
if (!IsInitialized())
{
OnPurchasingFaile?.Invoke(null, "Purchasing not init.");
return;
}
var p = GetProduct(pid);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} produt id {pid} availableToPurchase {p.availableToPurchase}");
if (p == null || !p.availableToPurchase)
{
OnPurchasingFaile?.Invoke(null, "Product is null or availableToPurchase.");
return;
}
_controller.InitiatePurchase(p);
}
八、购买成功与失败调用的方法
/// <summary>
/// 购买失败会调用此方法
/// </summary>
/// <param name="product"></param>
/// <param name="failureDescription"></param>
public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
{
var pid = failureDescription.productId;
var errorMsg = failureDescription.message;
var item = GetItem(pid);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnPurchaseFailed)} productId {pid}, reason {failureDescription.reason }, errorMsg {errorMsg}");
OnPurchasingFaile?.Invoke(item, errorMsg);
}
/// <summary>
/// 购买成功会调用此方法
/// </summary>
/// <param name="purchaseEvent"></param>
/// <returns></returns>
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
var item = purchaseEvent.purchasedProduct;
var pid = purchaseEvent.purchasedProduct.definition.storeSpecificId;
var pitem = GetItem(pid);
if (pitem != null)
{
if (pitem.PType == ProductType.Subscription)
SubscriptionItems[pid] = _items[pid];
else
{
pitem.PriceStr = item.metadata.localizedPriceString;
pitem.Buyed = item.hasReceipt;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(ProcessPurchase)} product id is: {item.definition.storeSpecificId}, buyed {pitem.Buyed}");
}
}
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(ProcessPurchase)} success productId {pid}, type {pitem.PType}");
OnPurchasingSuccess?.Invoke(pitem);
return PurchaseProcessingResult.Complete;
}
必须要返回 PurchaseProcessingResult.Complete 类型,以通知unity服务,告知产品已成功购买完毕。
九、Apple store 的特殊处理
Apple 商店在游戏启动时需要重设一下商店产品的状态,才会拿到准确的商品状态数据,代码如下:
/// <summary>
/// apple 商店在游戏启动时需要重设一下商店状态
/// </summary>
/// <param name="callback"></param>
public void RestorePurchasing(Action<bool, string> callback)
{
if (Application.platform == RuntimePlatform.IPhonePlayer)
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(RestorePurchasing)} starting");
if(_appleExtensions != null)
{
_appleExtensions.RestoreTransactions((a, b) => {
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(RestorePurchasing)} completed result ok {a} , result str {b}");
callback?.Invoke(a, b);
});
}
}
else
{
callback?.Invoke(false, null);
}
}
十、完整的代码设计
using System;
using System.Collections.Generic;
using UnityEngine.Purchasing.Security;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
using Simple.Log;
using Unity.Services.Core;
using Unity.Services.Core.Environments;
using UnityEngine;
using Simple.AnalyticsSdk;
namespace Gamelogic
{
public class PurchasingItem
{
public string Id;
/// <summary>
/// 自定数据表内购项的ID
/// </summary>
public int ParentId;
/// <summary>
/// 内购产品的三种类型之一
/// </summary>
public ProductType PType;
/// <summary>
/// 已经本地化的价格字符
/// </summary>
public string PriceStr;
/// <summary>
/// 是否已购买过
/// </summary>
public bool Buyed = false;
}
public class PurchasingMgr : Singleton<PurchasingMgr>, IDetailedStoreListener
{
/// <summary>
/// 所有的产品
/// </summary>
private Dictionary<string, PurchasingItem> _items = new Dictionary<string, PurchasingItem>();
public Dictionary<string, PurchasingItem> Items => _items;
/// <summary>
/// 订阅的产品
/// </summary>
public Dictionary<string, PurchasingItem> SubscriptionItems { private set; get; } = new Dictionary<string, PurchasingItem>();
private IStoreController _controller;
private IExtensionProvider _extensions;
private IAppleExtensions _appleExtensions;
private CrossPlatformValidator _validator;
public event Action<Dictionary<string, PurchasingItem>> OnInitSuccess;
public event Action<string> OnInitFaile;
public event Action<PurchasingItem> OnPurchasingSuccess;
public event Action<PurchasingItem, string> OnPurchasingFaile;
private bool _isInit = false;
public void AddProduct(string id, int parentId, ProductType t)
{
var item = new PurchasingItem { Id = id, ParentId = parentId, PType = t };
if(!_items.ContainsKey(id))
_items.Add(id, item);
}
public void ClearProduct()
{
_items.Clear();
}
public PurchasingItem GetItem(string pid)
{
_items.TryGetValue(pid, out var item);
return item;
}
public Product GetProduct(string pid)
{
if(IsInitialized())
{
var product = _controller.products.WithID(pid);
return product;
}
return null;
}
/// <summary>
/// 初始化内购
/// </summary>
public void InitPurchasing()
{
//先添加产品
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} products {_items.Count}");
_isInit = false;
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
foreach(var item in _items)
{
var v = item.Value;
builder.AddProduct(v.Id, v.PType);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} Purchasing product id {v.Id}, type {v.PType}");
}
//然后初始化内购服务
InitServices(builder);
}
/// <summary>
/// 然后初始化内购服务
/// </summary>
/// <param name="builder"></param>
private async void InitServices(ConfigurationBuilder builder)
{
try
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)} start");
var options = new InitializationOptions().SetEnvironmentName("production");
await UnityServices.InitializeAsync(options);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)} success, begin init purchasing");
UnityPurchasing.Initialize(this, builder);//内购服务初始化成功后方可初始化内购组件
}
catch(Exception e)
{
var message = e.Message;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)}, errorMsg {message}");
OnInitFaile?.Invoke(message);
}
}
public bool IsInitialized()
{
//return _controller != null && _extensions != null;
return _isInit;
}
/// <summary>
/// 购买商品
/// </summary>
/// <param name="pid"></param>
public void Buy(string pid)
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} IsInitialized {IsInitialized()}");
if (!IsInitialized())
{
OnPurchasingFaile?.Invoke(null, "Purchasing not init.");
return;
}
var p = GetProduct(pid);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} produt id {pid} availableToPurchase {p.availableToPurchase}");
if (p == null || !p.availableToPurchase)
{
OnPurchasingFaile?.Invoke(null, "Product is null or availableToPurchase.");
return;
}
_controller.InitiatePurchase(p);
}
/// <summary>
/// 获取订阅项
/// </summary>
/// <param name="pid"></param>
/// <returns></returns>
public PurchasingItem GetSubscriptionItem(string pid)
{
SubscriptionItems.TryGetValue(pid, out var item);
return item;
}
private bool CheckIfProductIsAvailableForSubscriptionManager(string receipt)
{
var receipt_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(receipt);
if (!receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload"))
{
LogMgr.Log("The product receipt does not contain enough information");
return false;
}
var store = (string)receipt_wrapper["Store"];
var payload = (string)receipt_wrapper["Payload"];
if (payload != null)
{
switch (store)
{
case GooglePlay.Name:
{
var payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
if (!payload_wrapper.ContainsKey("json"))
{
LogMgr.Log("The product receipt does not contain enough information, the 'json' field is missing");
return false;
}
//var original_json_payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode((string)payload_wrapper["json"]);
//if (original_json_payload_wrapper == null || !original_json_payload_wrapper.ContainsKey("developerPayload"))
//{
// LogMgr.Log("The product receipt does not contain enough information, the 'developerPayload' field is missing");
// return false;
//}
//var developerPayloadJSON = (string)original_json_payload_wrapper["developerPayload"];
//var developerPayload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(developerPayloadJSON);
//if (developerPayload_wrapper == null || !developerPayload_wrapper.ContainsKey("is_free_trial") || !developerPayload_wrapper.ContainsKey("has_introductory_price_trial"))
//{
// LogMgr.Log("The product receipt does not contain enough information, the product is not purchased using 1.19 or later");
// return false;
//}
return true;
}
case AppleAppStore.Name:
case AmazonApps.Name:
case MacAppStore.Name:
{
return true;
}
default:
{
return false;
}
}
}
return false;
}
#region 过时的
/// <summary>
/// 过时的方法
/// </summary>
/// <param name="error"></param>
public void OnInitializeFailed(InitializationFailureReason error)
{
}
/// <summary>
/// 过时的方法
/// </summary>
/// <param name="product"></param>
/// <param name="failureReason"></param>
/// <exception cref="NotImplementedException"></exception>
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
}
#endregion
/// <summary>
/// 初始化内购成功后会调用此方法
/// </summary>
/// <param name="controller"></param>
/// <param name="extensions"></param>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
_controller = controller;
_extensions = extensions;
_appleExtensions = _extensions.GetExtension<IAppleExtensions>();
_validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product count: {_controller.products.all.Length}");
SubscriptionItems.Clear();
foreach(var item in _controller.products.all)
{
if (item.availableToPurchase)
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product id is: {item.definition.storeSpecificId}, priceStr {item.metadata.localizedPriceString}, type {item.definition.type}");
var pitem = GetItem(item.definition.storeSpecificId);
if (pitem != null)
{
pitem.PriceStr = item.metadata.localizedPriceString;
pitem.Buyed = item.hasReceipt;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product id is: {item.definition.storeSpecificId}, buyed {pitem.Buyed}, type {pitem.PType}");
}
if(item.receipt != null)
{//检查已订阅的状态
if(item.definition.type == ProductType.Subscription)
{
if (CheckIfProductIsAvailableForSubscriptionManager(item.receipt))
{
var p = new SubscriptionManager(item, null);
var info = p.getSubscriptionInfo();
var pid = info.getProductId();
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product id is: " + pid);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, purchase date is: " + info.getPurchaseDate());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, subscription next billing date is: " + info.getExpireDate());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is subscribed? " + info.isSubscribed().ToString());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is expired? " + info.isExpired().ToString());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is cancelled? " + info.isCancelled());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product is in free trial peroid? " + info.isFreeTrial());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product is auto renewing? " + info.isAutoRenewing());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, subscription remaining valid time until next billing date is: " + info.getRemainingTime());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is this product in introductory price period? " + info.isIntroductoryPricePeriod());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the product introductory localized price is: " + info.getIntroductoryPrice());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the product introductory price period is: " + info.getIntroductoryPricePeriod());
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
if (info.isSubscribed() == Result.True && info.isExpired() == Result.False)
{
SubscriptionItems[pid] = _items[pid];
}
}
}
}
}
}
if (Application.platform == RuntimePlatform.IPhonePlayer)
{
var hasRestore = HasRestorePurchasingKey();
if (!hasRestore)
{
RestorePurchasing((a, b) => {
_isInit = true;
SaveRestorePurchasingKey();
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
OnInitSuccess?.Invoke(_items);
});
}
else
{
_isInit = true;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
OnInitSuccess?.Invoke(_items);
}
}
else
{
_isInit = true;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
OnInitSuccess?.Invoke(_items);
}
}
/// <summary>
/// 初始化失败会调用这方法
/// </summary>
/// <param name="error"></param>
/// <param name="message"></param>
public void OnInitializeFailed(InitializationFailureReason error, string message)
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitializeFailed)} error {error}, errorMsg {message}");
OnInitFaile?.Invoke(message);
}
/// <summary>
/// 购买失败会调用此方法
/// </summary>
/// <param name="product"></param>
/// <param name="failureDescription"></param>
public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
{
var pid = failureDescription.productId;
var errorMsg = failureDescription.message;
var item = GetItem(pid);
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnPurchaseFailed)} productId {pid}, reason {failureDescription.reason }, errorMsg {errorMsg}");
OnPurchasingFaile?.Invoke(item, errorMsg);
}
/// <summary>
/// 购买成功会调用此方法
/// </summary>
/// <param name="purchaseEvent"></param>
/// <returns></returns>
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
var item = purchaseEvent.purchasedProduct;
var pid = purchaseEvent.purchasedProduct.definition.storeSpecificId;
#if (Amazon)//亚马逊平台后台商店管理填写错误,特殊处理
if (pid == "RemoveAdSet")
pid = "a83001";
#endif
var pitem = GetItem(pid);
if (pitem != null)
{
if (pitem.PType == ProductType.Subscription)
SubscriptionItems[pid] = _items[pid];
else
{
pitem.PriceStr = item.metadata.localizedPriceString;
pitem.Buyed = item.hasReceipt;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(ProcessPurchase)} product id is: {item.definition.storeSpecificId}, buyed {pitem.Buyed}");
}
}
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(ProcessPurchase)} success productId {pid}, type {pitem.PType}");
OnPurchasingSuccess?.Invoke(pitem);
return PurchaseProcessingResult.Complete;
}
/// <summary>
/// apple 商店在游戏启动时需要重设一下商店状态
/// </summary>
/// <param name="callback"></param>
public void RestorePurchasing(Action<bool, string> callback)
{
if (Application.platform == RuntimePlatform.IPhonePlayer)
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(RestorePurchasing)} starting");
if(_appleExtensions != null)
{
_appleExtensions.RestoreTransactions((a, b) => {
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(RestorePurchasing)} completed result ok {a} , result str {b}");
callback?.Invoke(a, b);
});
}
}
else
{
callback?.Invoke(false, null);
}
}
public bool HasRestorePurchasingKey()
{
var key = "AppleRestorePurchasing";
if (PlayerPrefs.HasKey(key))
{
var v = PlayerPrefs.GetInt(key);
return v == 1;
}
return false;
}
public void SaveRestorePurchasingKey()
{
var key = "AppleRestorePurchasing";
PlayerPrefs.SetInt(key, 1);
}
}
}
十一、使用方式
内购的初始化需要联网,我们需要尽可能的早让内购初始化完成,否则玩家进入到游戏后可能看不到商品或者购买不了。所以我们在进入游戏的时候就需要初始化内购,待进度条走完,内购也初始化成功了。如下:
/// <summary>
/// 进入游戏
/// </summary>
public void EnterGame()
{
#if (AppStore || GooglePlay || Amazon)
if(!PurchasingMgr.Instance.IsInitialized())
{
//内购,读取我们设计的商品表初始化内购
PurchasingMgr.Instance.ClearProduct();
var shopItems = ConfigMgr.Instance.GetList<ShopCategoryConfig>();
foreach (var item in shopItems)
{
if (item.CategoryType == ShopCategoryTypeEnum.Prop)
continue;
var ptype = ProductType.Consumable;
if (item.PurchasingType == ProductPurchasingTypeEnum.Consumable)
ptype = ProductType.Consumable;
else if (item.PurchasingType == ProductPurchasingTypeEnum.NonConsumable)
ptype = ProductType.NonConsumable;
else if (item.PurchasingType == ProductPurchasingTypeEnum.Subscription)
ptype = ProductType.Subscription;
PurchasingMgr.Instance.AddProduct(item.PurshasingId, item.Id, ptype);
}
PurchasingMgr.Instance.OnInitSuccess -= OnInitPurchasingCompleted;
PurchasingMgr.Instance.OnInitSuccess += OnInitPurchasingCompleted;
PurchasingMgr.Instance.InitPurchasing();
}
#endif
}
十二、测试效果
测试之前商店后台必须要添加完产品,并审核通过。我们以Apple store 商店为例来演示一下测试过程。我们打开游戏内的商品列表,点击购买产品,如下图:
稍等一回后,会弹出如下图的页面:
点击购买,会提示输入密码,如下图:
输入正确的密码后,点击登录后稍等一会,就返回购买成功了。如下图:
Google 平台的测试过程类似Apple,但Amazon平台测试会稍为复杂一点,需要下载官方的【App tester 】APP,并必须是Amazon的设备,文档如下:developer.amazon.com/zh/docs/in-…。在此就不再描述其它平台的测试方式。
感谢阅读!