前言
最近由于运营需求,需要集成Amazon 商店的内购。由于项目之前已经接入了Google 与 apple 商店的内购,具体过程欢迎阅读前篇关于unity 内购的文章【Unity 产品内购系统的集成】。根据unity 官方文档描述,unity iap 也是支持 amazon 商店的,于是直接拿着amazon平板进行测试。想不到在amazon平台下unity iap居然存在一些明显的bug,最后不得不接入amazon官方的iap sdk。下面我们就一起来探讨一下如何接入的。
一、阅读官方文档,安装插件
接入SDK之前,我们最好细心阅读一到两篇官方文档,避免少走弯路。
文档地址如下:
developer.amazon.com/zh/docs/in-…
SDK包下载地址如下:
developer.amazon.com/zh/docs/app…
官方SDK包有两个版本,如图所示:
Appstore SDK是新版的SDK,下面的是旧版SDK。两个版本的SDK API 通用,可以无缝切换。下面以旧版本为例,我们一起来探讨一下集成整个内购SDK的过程。至于为什么不用新版本,文章最后会说明。
下载完SDK包后,会有如下图的文件:
我们把它导入到Unity 工程,这样安装SDK插件就算成功了。
二、设计内购基类
我们要把与内购业务无关的商品业务代码抽离出来放到其类里面,这样Amazon 平台和其它平台都可以用一套接口暴露给使用者。如下所示:
using System;
using System.Collections.Generic;
namespace Gamelogic
{
public enum PurchaseItemType
{
None = 0,
Consumable = 1,
NonConsumable = 2,
Subscription = 3,
}
public class PurchasingItem
{
public string Id;
/// <summary>
/// 自定数据表内购项的ID
/// </summary>
public int ParentId;
/// <summary>
/// 内购产品的三种类型之一
/// </summary>
public PurchaseItemType PType;
/// <summary>
/// 已经本地化的价格字符
/// </summary>
public string PriceStr;
/// <summary>
/// 是否已购买过
/// </summary>
public bool Buyed = false;
}
public enum PuchasingPlatformType
{
Amazon = 0,
GoogleAndApple = 1
}
public class PurchasingBase
{
public bool IsInitialized { protected set; get; } = false;
/// <summary>
/// 所有的产品
/// </summary>
protected 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>();
public event Action<Dictionary<string, PurchasingItem>> OnInitSuccess;
public event Action<string> OnInitFaile;
public event Action<PurchasingItem> OnPurchasingSuccess;
public event Action<PurchasingItem, string> OnPurchasingFaile;
public PuchasingPlatformType Platform { set; get; } = PuchasingPlatformType.GoogleAndApple;
public void AddProduct(string id, int parentId, PurchaseItemType 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;
}
protected void CallOnInitSuccess(Dictionary<string, PurchasingItem> items)
{
OnInitSuccess?.Invoke(items);
}
protected void CallOnInitFaile(string errorMsg)
{
OnInitFaile?.Invoke(errorMsg);
}
protected void CallOnPurchasingSuccess(PurchasingItem item)
{
OnPurchasingSuccess?.Invoke(item);
}
protected void CallOnPurchasingFaile(PurchasingItem item, string errorMsg)
{
OnPurchasingFaile?.Invoke(item, errorMsg);
}
public virtual void InitPurchasing() { }
/// <summary>
/// 获取订阅项
/// </summary>
/// <param name="pid"></param>
/// <returns></returns>
public PurchasingItem GetSubscriptionItem(string pid)
{
SubscriptionItems.TryGetValue(pid, out var item);
return item;
}
public virtual void Buy(string pid) { }
public virtual void RestorePurchasing(Action<bool, string> callback) { }
}
}
三、实现Amazon平台内购类
Amazon内购类的实现,核心是监听四个事件回调。一是获取用户数据,二是获取产品数据,三是获取购买状态数据,四是购买回调。实现这四个回调的逻辑,内购类就实现了90%。代码如下:
#if(Amazon)
using com.amazon.device.iap.cpt;
using Simple.Log;
using System;
using System.Collections.Generic;
namespace Gamelogic
{
public enum AmazonPurchaseResultType
{
None = -1,
/// <summary>
/// 成功
/// </summary>
Successful = 0,
/// <summary>
/// 失败
/// </summary>
Failed = 1,
/// <summary>
/// 无效的SKU
/// </summary>
InvalidSKU = 2,
/// <summary>
/// SKU已存在
/// </summary>
AlreadyPurchase = 3,
/// <summary>
/// 不支持
/// </summary>
NotSupported = 4
}
public class AmazonPurchasingMgr : PurchasingBase
{
private IAmazonIapV2 _iapService;
private string _scriptionParentId = "RemoveAdSet";
private string _subscriptionChildId = "a83001";
/// <summary>
/// 初始化内购
/// </summary>
public override void InitPurchasing()
{
IsInitialized = false;
_iapService = AmazonIapV2Impl.Instance;
_iapService.AddGetUserDataResponseListener(GetUserDataEventHandler);
_iapService.AddPurchaseResponseListener(PurchaseEventHandler);
_iapService.AddGetProductDataResponseListener(GetProductDataEventHandler);
_iapService.AddGetPurchaseUpdatesResponseListener(GetPurchaseUpdatesEventHandler);
//获取用户数据
var resUser = _iapService.GetUserData();
LogMgr.Log($"{nameof(InitPurchasing)} GetUserData, requestId {resUser.RequestId}");
}
private AmazonPurchaseResultType GetResultType(string status)
{
if (status == "SUCCESSFUL")
return AmazonPurchaseResultType.Successful;
else if (status == "NOT_SUPPORTED")
return AmazonPurchaseResultType.NotSupported;
else if (status == "FAILED")
return AmazonPurchaseResultType.Failed;
else if (status == "INVALID_SKU")
return AmazonPurchaseResultType.InvalidSKU;
else if (status == "ALREADY_PURCHASED")
return AmazonPurchaseResultType.AlreadyPurchase;
else
return AmazonPurchaseResultType.None;
}
/// <summary>
/// 获取用户数据影响
/// </summary>
/// <param name="args"></param>
private void GetUserDataEventHandler(GetUserDataResponse args)
{
string status = args.Status;
LogMgr.Log($"{nameof(GetUserDataEventHandler)} status {status}, toJson {args.ToJson()}");
var rt = GetResultType(status);
if (rt == AmazonPurchaseResultType.Successful)
{
string requestId = args.RequestId;
string userId = args.AmazonUserData.UserId;
string marketplace = args.AmazonUserData.Marketplace;
LogMgr.Log($"{nameof(GetUserDataEventHandler)} requestId {requestId}, userId {userId}, marketplace {marketplace}");
//更新购买状态
var request = new ResetInput();
request.Reset = true;
var response = _iapService.GetPurchaseUpdates(request);
LogMgr.Log($"{nameof(InitPurchasing)} GetPurchaseUpdates, requestId {response.RequestId}");
//获取产品数据
var reqSku = new SkusInput();
reqSku.Skus = new List<string>();
foreach (var item in _items)
{
var v = item.Value;
reqSku.Skus.Add(v.Id);
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(InitPurchasing)} Purchasing product id {v.Id}, type {v.PType}");
}
var resSku = _iapService.GetProductData(reqSku);
LogMgr.Log($"{nameof(InitPurchasing)} GetProductData, requestId {resSku.RequestId}");
}
else
{
IsInitialized = false;
CallOnInitFaile("GetUserData failed");
}
}
/// <summary>
/// 获取产品数据响应
/// </summary>
/// <param name="args"></param>
private void GetProductDataEventHandler(GetProductDataResponse args)
{
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetProductDataEventHandler)} status {args.Status}, toJson {args.ToJson()}");
string status = args.Status;
var rt = GetResultType(status);
if(rt == AmazonPurchaseResultType.Successful)
{
var productDataMap = args.ProductDataMap;
foreach (var productkv in productDataMap)
{
var product = productkv.Value;
var item = GetItem(product.Sku);
if(item != null)
{
item.PriceStr = product.Price;
}
else
{
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetProductDataEventHandler)} sku is null {product.Sku}");
}
}
IsInitialized = true;
CallOnInitSuccess(_items);
}
else
{
IsInitialized = false;
CallOnInitFaile("GetUserData failed");
}
}
/// <summary>
/// 更新购买产品数据响应
/// </summary>
/// <param name="args"></param>
private void GetPurchaseUpdatesEventHandler(GetPurchaseUpdatesResponse args)
{
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetPurchaseUpdatesEventHandler)} status {args.Status}, toJson {args.ToJson()}");
var status = args.Status;
var rt = GetResultType(status);
if(rt == AmazonPurchaseResultType.Successful)
{
SubscriptionItems.Clear();
var receipts = args.Receipts;
foreach( var receipt in receipts )
{
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetPurchaseUpdatesEventHandler)} toJson {args.ToJson()}");
var pid = receipt.Sku;
if (pid == _scriptionParentId)
{
pid = _subscriptionChildId;
}
var item = GetItem(pid);
if (item != null)
{
if(item.PType == PurchaseItemType.Subscription)
{
var subOk = false;
var cancelDate = receipt.CancelDate;
if (cancelDate != 0)
{//退出时间不为0,表示用户取消了订阅,但还有剩余的时间没过期。
//如果需要,需要用PurchaseDate + 有效时间差,算出过期时间,再对比当前时间,小于当前时间没过期,大于过期
//公式如下:已过期 = PurchaseDate + 有效时间差 >= system.now
//var purchaseDate = receipt.PurchaseDate;
//var dtStart = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Local);
//var lt = long.Parse(purchaseDate + "0000");
//var toRealyTime = new TimeSpan(lt);
//var pdt = dtStart.Add(toRealyTime);
////pdt = pdt.Add("有效时间");
//LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, type {item.PType}, buyed {item.Buyed}");
//LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, cancelDate {cancelDate}, purchaseDate {purchaseDate} {pdt}");
//var now = DateTime.Now;
//if (DateTime.Compare(now, pdt) < 0)
//{
// subOk = true;
// LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} pid {pid} subscription true, cancelDate {pdt}");
//}
}
else
{
subOk = true;
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} pid {pid} subscription true, cancelDate {cancelDate}");
}
if(subOk)
{//订阅有效
SubscriptionItems[pid] = item;
}
else
{//订阅过期
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetPurchaseUpdatesEventHandler)} pid {pid} subscription false");
}
}
item.Buyed = true;
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetPurchaseUpdatesEventHandler)} pid {pid} buyed {item.Buyed}");
}
}
}
}
/// <summary>
/// 购买响应
/// </summary>
/// <param name="args"></param>
private void PurchaseEventHandler(PurchaseResponse args)
{
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} status {args.Status}, toJson {args.ToJson()}");
var status = args.Status;
var rt = GetResultType(status);
if (rt == AmazonPurchaseResultType.Successful)
{
var item = args.PurchaseReceipt;
var pid = item.Sku;
if (item.Sku == _scriptionParentId)
{
pid = _subscriptionChildId;
}
var pitem = GetItem(pid);
if (pitem != null)
{
pitem.Buyed = true;
if (pitem.PType == PurchaseItemType.Subscription)
{
SubscriptionItems[pid] = _items[pid];
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, type {pitem.PType}, buyed {pitem.Buyed}");
//var cancelDate = args.PurchaseReceipt.CancelDate;
//var purchaseDate = args.PurchaseReceipt.PurchaseDate;
//var cdt = new DateTime(cancelDate);
//var pdt = new DateTime(purchaseDate);
//LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, cancelDate {cancelDate} {cdt}, purchaseDate {purchaseDate} {pdt}");
}
//通知Amazon您的应用程序已将购买的商品交付给用户
var request = new NotifyFulfillmentInput();
request.ReceiptId = args.PurchaseReceipt.ReceiptId;
request.FulfillmentResult = "FULFILLED";
_iapService.NotifyFulfillment(request);
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, type {pitem.PType}, buyed {pitem.Buyed}");
}
else
{
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success but sku local is null, productId {pid}");
}
CallOnPurchasingSuccess(pitem);
}
else
{
CallOnPurchasingFaile(null, null);
}
}
/// <summary>
/// 购买商品
/// </summary>
/// <param name="pid"></param>
public override void Buy(string pid)
{
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(Buy)} IsInitialized {IsInitialized}");
if (!IsInitialized)
{
return;
}
var item = GetItem(pid);
var request = new SkuInput();
request.Sku = item.Id;
var response = _iapService.Purchase(request);
LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(Buy)} produt id {pid} requestId {response.RequestId}");
}
}
}
#endif
注意,它必须继承基类PurchasingBase。
四、其它平台内购类的实现(GooglePlay、Apple)
基它平台内购的实现,是利用Unity 官方插件【In App Purchasing】实现的,具体过程请参考文章【Unity 产品内购系统的集成】,代码如下:
#if (AppStore || GooglePlay)
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 PurchasingMgr : PurchasingBase, IDetailedStoreListener
{
private IStoreController _controller;
private IExtensionProvider _extensions;
private IAppleExtensions _appleExtensions;
private CrossPlatformValidator _validator;
public Product GetProduct(string pid)
{
if(IsInitialized)
{
var product = _controller.products.WithID(pid);
return product;
}
return null;
}
/// <summary>
/// 初始化内购
/// </summary>
public override void InitPurchasing()
{
//先添加产品
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} products {_items.Count}");
IsInitialized = false;
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
foreach(var item in _items)
{
var v = item.Value;
var pt = ProductType.Consumable;
if (v.PType == PurchaseItemType.NonConsumable)
pt = ProductType.NonConsumable;
else if(v.PType== PurchaseItemType.Consumable)
pt = ProductType.Consumable;
else if(v.PType == PurchaseItemType.Subscription)
pt = ProductType.Subscription;
builder.AddProduct(v.Id, pt);
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}");
CallOnInitFaile(message);
}
}
/// <summary>
/// 购买商品
/// </summary>
/// <param name="pid"></param>
public override void Buy(string pid)
{
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} IsInitialized {IsInitialized}");
if (!IsInitialized)
{
CallOnPurchasingFaile(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)
{
CallOnPurchasingFaile(null, "Product is null or availableToPurchase.");
return;
}
_controller.InitiatePurchase(p);
}
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];
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info pid {pid}");
}
}
}
}
}
}
if (Application.platform == RuntimePlatform.IPhonePlayer)
{
var hasRestore = HasRestorePurchasingKey();
if (!hasRestore)
{
RestorePurchasing((a, b) => {
IsInitialized = true;
SaveRestorePurchasingKey();
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
CallOnInitSuccess(_items);
});
}
else
{
IsInitialized = true;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
CallOnInitSuccess(_items);
}
}
else
{
IsInitialized = true;
LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
CallOnInitSuccess(_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}");
CallOnInitFaile(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}");
CallOnPurchasingFaile(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 == PurchaseItemType.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}");
CallOnPurchasingSuccess(pitem);
return PurchaseProcessingResult.Complete;
}
/// <summary>
/// apple 商店在游戏启动时需要重设一下商店状态
/// </summary>
/// <param name="callback"></param>
public override 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);
}
}
}
#endif
五、使用方式
先初始化内购:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System;
using Newtonsoft.Json;
using Simple.Config;
namespace Gamelogic
{
public class GameProcess : Singleton<GameProcess>
{
public PurchasingBase PurchMgr { private set; get; }
public void Init()
{
#if (AppStore || GooglePlay)
PurchMgr = new PurchasingMgr();
PurchMgr.Platform = PuchasingPlatformType.GoogleAndApple;
#elif(Amazon)
PurchMgr = new AmazonPurchasingMgr();
PurchMgr.Platform = PuchasingPlatformType.Amazon;
#endif
/// <summary>
/// 进入游戏
/// </summary>
public void EnterGame()
{
#if (AppStore || GooglePlay || Amazon)
if (!PurchMgr.IsInitialized)
{
//内购
PurchMgr.ClearProduct();
var shopItems = ConfigMgr.Instance.GetList<ShopCategoryConfig>();
foreach (var item in shopItems)
{
if (item.CategoryType == ShopCategoryTypeEnum.Prop)
continue;
var ptype = PurchaseItemType.Consumable;
if (item.PurchasingType == ProductPurchasingTypeEnum.Consumable)
ptype = PurchaseItemType.Consumable;
else if (item.PurchasingType == ProductPurchasingTypeEnum.NonConsumable)
ptype = PurchaseItemType.NonConsumable;
else if (item.PurchasingType == ProductPurchasingTypeEnum.Subscription)
ptype = PurchaseItemType.Subscription;
PurchMgr.AddProduct(item.PurshasingId, item.Id, ptype);
}
PurchMgr.OnInitSuccess -= OnInitPurchasingCompleted;
PurchMgr.OnInitSuccess += OnInitPurchasingCompleted;
PurchMgr.InitPurchasing();
}
#endif
}
#if (AppStore || GooglePlay || Amazon)
private void OnInitPurchasingCompleted(Dictionary<string, PurchasingItem> products)
{
foreach (var item in products)
{
var p = item.Value;
if(p.PType == PurchaseItemType.NonConsumable && p.Buyed)
{
LogMgr.Log($"{nameof(GameProcess)}.{nameof(OnInitPurchasingCompleted)} product id is: {p.Id}, buyed {p.ParentId}, type {p.PType}");
var buyCount = GameData.Instance.PlayerData.GetGiftBuys(p.ParentId);
if (buyCount <= 0)
{
GameData.Instance.PlayerData.AddGiftBuys(p.ParentId, 1);
LogMgr.Log($"{nameof(GameProcess)}.{nameof(OnInitPurchasingCompleted)} GetGiftBuys id is: {p.Id}, buyed {p.ParentId}, type {p.PType}");
}
}
}
}
#endif
public void Dispose()
{
#if (AppStore || GooglePlay || Amazon)
PurchMgr.OnInitSuccess -= OnInitPurchasingCompleted;
#endif
}
}
}
然后才可以购买产品,注意要监听购买成功和失败的事件。
private void OnBuy(UIStageGiftBinder binder)
{
#if (AppStore || GooglePlay || Amazon)
if(GameProcess.Instance.PurchMgr.IsInitialized)
{
UIMgr.Instance.Open<AdLoadingContext>();
GameProcess.Instance.PurchMgr.Buy(_curBuyGift.Config.PurshasingId);
}
#endif
}
#if (AppStore || GooglePlay || Amazon)
public void OnPurchasingSuccess(PurchasingItem pitem)
{
UIMgr.Instance.Close<AdLoadingContext>();
}
public void OnPurchasingFaile(PurchasingItem pitem, string error)
{
UIMgr.Instance.Close<AdLoadingContext>();
}
#endif
六、添加配置节点
接入大多数SDK都需要在AndroidManifest.xml添加一些节点,以开启某些权限或服务,Amazon 也不例外。我们打开AndroidMainfest.xml,在application 节点下添加如下红框节点,并将主Activity 添加属性android:exported="true",如图:
七、旧版Amazon SDK内购测试
要进行测试,我们必须要遵循以下的步骤,否则有可能测试失败。
1、使用Amazon的设备,手机、平板都可以
2、从应用商店下载[App tester ] app 安装。
3、接着从已添加商品的后台管理系统中下载一个名为【amazon.sdktester.json】的文件。
4、把【amazon.sdktester.json】放到设备sd卡根目录下。
5、安装游戏。
6、打开CMD 或 PowerShell 窗口,输入 【./adb shell setprop debug.amazon.sandboxmode debug】命令,回车。如下图:
7、打开游戏,按正常操作进行内购。
8、按照以上7步走,基本上都可以测试一次成功。
关于[App tester ]的使用,具体可以参考官方文档,在此不在赘述。
八、为何Aamzon内购要独立于Untiy 官方插件【In App Purchasing】实现
经过测试,Unity【In App Purchasing】在google play和apple store 平台下都运行良好,但在amazon stroe 平台下却存在bug。bug 具体的表现是,购买商品成功后重新启动游戏却获取不到商品的正确购买状态。经多次测试,均是如此,最后不得不单独接入Amazon官方内购SDK。
接入 Amazon官方内购SDK也是一波三折,按文档说明接入新版SDK后,进行 App tester 本地测试或进行LAT动态测试均没任何效果。监听三个事件的回调结果,如获取用户数据、获取产品数据、获取购买状态数据,均是失败状态。经多翻折腾,都摸不到问题所在,最后不得不尝试更换为旧版SDK。令人意外的是,旧版SDK一经导入,不作任何改动居然一次测试成功。
感谢阅读!