[食譜好菜] Xamarin.Forms(iOS)的 In-App Purchases

Apple 在審核我們的 App 的時候會看一個東西,那就是我們的 App 內提供的對外連結是否具有引導消費的功能,消費的項目如果被認定踩中了 App 內購買的類型,比如說我在我的 App 放了一個按鈕,按下去之後用瀏覽器開啟我準備好的網頁,使用者在網頁中可以付費升級專業版,這樣的話有極大的機率會被 Apple Reject,然後叫我們用他們家的 In-App Purchases,不過實作上也不算太難。

建立 App 內購買項目

我們到 iTunes Connect 的「我的 App」裡面找到我們的 App(沒有的話就建一個),在「功能」->「App 內購買項目」建立購買項目。

選擇項目類型

紅框內及價格為必填項目

建立好之後,就可以在清單內看到.。

建立繼承自 SKPaymentTransactionObserver 的類別

SKPaymentTransactionObserverStoreKit 內建的一個抽象類別,繼承之後透過它我們可以取得交易結果,除此之外還額外定義了 TransactionCompleted Event,目的是為了將整個非同步的機制轉換成 Task-based 的非同步機制。

internal class IOSTransactionObserver : SKPaymentTransactionObserver
{
    public event Action<SKPaymentTransaction, bool> TransactionCompleted;

    public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransaction[] transactions)
    {
        foreach (var transaction in transactions)
        {
            if (transaction?.TransactionState == null) break;

            switch (transaction.TransactionState)
            {
                case SKPaymentTransactionState.Restored:
                case SKPaymentTransactionState.Purchased:
                    this.TransactionCompleted?.Invoke(transaction, true);
                    SKPaymentQueue.DefaultQueue.FinishTransaction(transaction);
                    break;

                case SKPaymentTransactionState.Failed:
                    this.TransactionCompleted?.Invoke(transaction, false);
                    SKPaymentQueue.DefaultQueue.FinishTransaction(transaction);
                    break;

                default: break;
            }
        }
    }
}

實作 IInAppPurchases 介面

IInAppPurchases 是我自己定義的介面,在 iOS 專案中去實作之後透過 Xamarin.Forms 的 Dependency 機制就可以取用了。

public class IOSInAppPurchases : IInAppPurchases, IDisposable
{
    private IOSTransactionObserver transactionObserver;

    public IOSInAppPurchases()
    {
        this.transactionObserver = new IOSTransactionObserver();

        // 添加交易結果的 Observer
        SKPaymentQueue.DefaultQueue.AddTransactionObserver(this.transactionObserver);
    }

    public async Task<PaymentTransaction> PurchaseAsync(string productId)
    {
        var paymentTrans = await this.Purchase(productId);

        var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);

        var purchase = new PaymentTransaction();
        purchase.TransactionUtcDate = reference.AddSeconds(paymentTrans.TransactionDate.SecondsSinceReferenceDate);
        purchase.Id = paymentTrans.TransactionIdentifier;
        purchase.ProductId = paymentTrans.Payment?.ProductIdentifier ?? string.Empty;
        purchase.State = paymentTrans.TransactionState.ToString();
        purchase.PurchaseToken =
            paymentTrans.TransactionReceipt?.GetBase64EncodedString(NSDataBase64EncodingOptions.None)
            ?? string.Empty;

        return purchase;
    }

    public void Dispose()
    {
        if (this.transactionObserver != null)
        {
            // 移除交易結果的 Observer
            SKPaymentQueue.DefaultQueue.RemoveTransactionObserver(this.transactionObserver);
            this.transactionObserver.Dispose();
            this.transactionObserver = null;
        }
    }

    private Task<SKPaymentTransaction> Purchase(string productId)
    {
        var tcsTransaction = new TaskCompletionSource<SKPaymentTransaction>();

        void Handler(SKPaymentTransaction trans, bool result)
        {
            if (trans?.Payment == null) return;

            if (productId != trans.Payment.ProductIdentifier) return;

            this.transactionObserver.TransactionCompleted -= Handler;

            if (result)
            {
                tcsTransaction.TrySetResult(trans);
                return;
            }

            var errorCode = trans.Error?.Code ?? -1;
            var description = trans.Error?.LocalizedDescription ?? string.Empty;

            tcsTransaction.TrySetException(new Exception($"交易失敗(errorCode: {errorCode})\r\n{description}"));
        }

        this.transactionObserver.TransactionCompleted += Handler;

        // 購買
        SKPaymentQueue.DefaultQueue.AddPayment(SKPayment.CreateFrom(productId));

        return tcsTransaction.Task;
    }
}

建立測試帳號用來測試

進入「使用者和職能」之後,到「沙箱技術測試人員」新增測試帳號,紅框必填。

測試購買

按下購買按鈕之後呼叫 IInAppPurchases.PurchaseAsync() 方法,把產品 ID 丟進去就可以了,記得將 Apple ID 切換成剛剛建立的測試帳號,我們來看測試結果。

參考資料

 < Source Code >

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學