如何使用Unity在遊戲之中結合Stripe的API進行金流付款和訂閱制的服務的教學流程。會使用到UnityWebRequest去呼叫Stripe 的API取得付款連結,和BuildShip進行快速的後端Json驗證。

Stripe是什麼?

Stripe 是一個提供金流付費服務的的平台。

它提供各種支付方式、一次性和訂閱的付款服務

讓你可以輕鬆管理線上支付業務。

幾乎所有知名的網站

如 ChatGPT, Google, Netflix

都選擇使用Stripe進行訂閱付款。

Stripe API 付款流程

  1. 在遊戲中點擊購買按鈕
  2. 呼叫Stripe API → 取得付款連結
  3. 自動打開瀏覽器 → 用戶進行付款
  4. 遊戲在後台循環偵測付款狀態
  5. 處理付款完成後的邏輯

串接 Stripe API 的準備

注意: 註冊Stripe帳號需要有一間公司

這篇文章預設你已經註冊好Stripe帳號了。

1. 打開測試模式

登入Stripe網站後

右上角可以打開測試模式

在開發測試階段打開測試模式

就不會收取真錢。

在測試模式下

可以輸入測試信用卡 4242-4242-4242 來付款

2. 取得Stripe API密鑰

點擊開發人員→API密鑰→顯示密鑰

API Key 會在呼叫Stripe API時用到

可以先保存起來。

3. 新增產品目錄

到產品目錄的頁面,點擊添加產品。

我們要新增兩個產品,第一個是一次性購買的

第二個是訂閱模式的:

4. 取得商品的價錢ID

把創建的產品點開 → 找到價格(再點開)

右上角有一個 price_xxx 的價錢ID

這個過後帶入參數會用到,先複製保存下來。

訂閱的產品也是一樣的操作。

5. 總結

操作完上面的步驟之後,你會得到

  1. Stripe API Key
  2. 一次性商品的 Price ID
  3. 訂閱模式的 Price ID

需要的 Stripe API

Stripe API的說明文件: https://docs.stripe.com/api

很多,但我們一共要知道的只有4個API:

1. Create Session API – 創建付款連結的API

Endpoint: https://api.stripe.com/v1/checkout/sessions

類型: POST

參數解釋
payment_method_types[]card
modepayment // 一次性
subscription // 訂閱制
success_url成功後跳轉的URL
cancel_url失敗/取消後跳轉的URL
customer_email用戶的Email
line_items[0][price]價格的 Price ID // price_xxx
line_items[0][quantity]購買數量 // 1

回傳的Json:

{
  "id": "cs_test_a11YYufWQzNY63zpQ6QSNRQhkUpVph4WRmzW0zWJO2znZKdVujZ0N0S22u",
  "object": "checkout.session",
  "after_expiration": null,
  "allow_promotion_codes": null,
  "amount_subtotal": 2198,
  "amount_total": 2198,
  "automatic_tax": {
    "enabled": false,
    "liability": null,
    "status": null
  },
  "billing_address_collection": null,
  "cancel_url": null,
  "client_reference_id": null,
  "consent": null,
  "consent_collection": null,
  "created": 1679600215,
  "currency": "usd",
  "custom_fields": [],
  "custom_text": {
    "shipping_address": null,
    "submit": null
  },
  "customer": null,
  "customer_creation": "if_required",
  "customer_details": null,
  "customer_email": null,
  "expires_at": 1679686615,
  "invoice": null,
  "invoice_creation": {
    "enabled": false,
    "invoice_data": {
      "account_tax_ids": null,
      "custom_fields": null,
      "description": null,
      "footer": null,
      "issuer": null,
      "metadata": {},
      "rendering_options": null
    }
  },
  "livemode": false,
  "locale": null,
  "metadata": {},
  "mode": "payment",
  "payment_intent": null,
  "payment_link": null,
  "payment_method_collection": "always",
  "payment_method_options": {},
  "payment_method_types": [
    "card"
  ],
  "payment_status": "unpaid",
  "phone_number_collection": {
    "enabled": false
  },
  "recovered_from": null,
  "setup_intent": null,
  "shipping_address_collection": null,
  "shipping_cost": null,
  "shipping_details": null,
  "shipping_options": [],
  "status": "open",
  "submit_type": null,
  "subscription": null,
  "success_url": "https://example.com/success",
  "total_details": {
    "amount_discount": 0,
    "amount_shipping": 0,
    "amount_tax": 0
  },
  "url": "https://checkout.stripe.com/c/pay/cs_test_a11YYufWQzNY63zpQ6QSNRQhkUpVph4WRmzW0zWJO2znZKdVujZ0N0S22u#fidkdWxOYHwnPyd1blpxYHZxWjA0SDdPUW5JbmFMck1wMmx9N2BLZjFEfGRUNWhqTmJ%2FM2F8bUA2SDRySkFdUV81T1BSV0YxcWJcTUJcYW5rSzN3dzBLPUE0TzRKTTxzNFBjPWZEX1NKSkxpNTVjRjN8VHE0YicpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBabHFgaCcpJ2BrZGdpYFVpZGZgbWppYWB3dic%2FcXdwYHgl"
}

JSON

我們需要用到的是最後一行的付款URL

2. Check Session API – 取得付款狀態的API3. Check Subscribe Status API – 檢查訂閱狀態的API4. Unsubscribe API – 取消訂閱的API

呼叫API的時候需要在Header 帶入Bearer Token – API Key

實作Unity C# 代碼

1. 先把Interface需要的功能列出來

public interface IPaymentServices
{
    public void Initialize(string apiKey, Action<object> callback);
    public void Pay(object sessionModel);
    public void Subscribe(object sessionModel);
    public void GetPaymentStatus(string sessionId, Action<object> callback);
    public void GetSubscribeStatus(string subscribeId, Action<object> callback);
    public void Unsubscribe(string subscribeId);
}

C#

2. 實作這個Interface

StripeServices.cs

 public class StripeServices : IPaymentServices{
 
    //定義 Stripe API 的 Endpoint
    private string SessionRoute = "https://api.stripe.com/v1/checkout/sessions";
    
    private string SubscribeRoute = "https://api.stripe.com/v1/subscriptions";


    private string ApiKey = "sk_stripe_secret_key";
    private Action<object> sessionIdCallback;

    // 一開始使用的時候要把API Key帶進來
    public void Initialize(string key, Action<object> callback)
    {
        ApiKey = key;
        sessionIdCallback = callback;
    }

    // 一次性付款的時候呼叫
    public void Pay(object sessionModel)
    {
        var session = sessionModel as StripeSessionRequestModel;
        if (session == null)
        {
            Debug.Log("Session Info Is Null");
            return;
        }

        session.mode = "payment";
          
CoroutineManager.StartStaticCoroutine(CreateCheckoutSession(session));
    }
    

    // 訂閱模式
    public void Subscribe(object sessionModel)
    {
        var session = sessionModel as StripeSessionRequestModel;
        if (session == null)
        {
            Debug.Log("Session Info Is Null");
            return;
        }

        session.mode = "subscription";
        session.successUrl += "?type=vip&subscribeId=null";
        CoroutineManager.StartStaticCoroutine(CreateCheckoutSession(session));
    }

    // 取得付款狀況
    public void GetPaymentStatus(string sessionId, Action<object> callback)
    {
        CoroutineManager.StartStaticCoroutine(GetSessionStatus(sessionId, callback));
    }

    // 取得訂閱狀況
    public void GetSubscribeStatus(string subscribeId, Action<object> callback)
    {
        CoroutineManager.StartStaticCoroutine(GetSubscribeStatusCoroutine(subscribeId, callback));
    }

    // 取消訂閱
    public void Unsubscribe(string subscribeId)
    {
        CoroutineManager.StartStaticCoroutine(CancelSubscribeCoroutine(subscribeId,null));
    }


    // 使用Unity的 Webrequest 呼叫Stripe 的API
    private IEnumerator CreateCheckoutSession(StripeSessionRequestModel sessionModel)
    {
        var form = new WWWForm();
        form.AddField("mode", sessionModel.mode);
        form.AddField("customer_email", sessionModel.customerEmail);
        form.AddField("line_items[0][price]", sessionModel.priceId);
        form.AddField("line_items[0][quantity]", sessionModel.quantity);
        form.AddField("success_url", sessionModel.successUrl);
        form.AddField("cancel_url", sessionModel.cancelUrl);
        form.AddField("payment_method_types[0]", "card");

        using var www = UnityWebRequest.Post(SessionRoute, form);
        www.SetRequestHeader("Authorization", "Bearer " + ApiKey);
        www.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        yield return www.SendWebRequest();

        if (www.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Stripe Call Error: " + www.error);
        }
        else
        {
            Debug.Log("Stripe Response: " + www.downloadHandler.text);
            var response = JsonConvert.DeserializeObject<StripeSessionResponseModel>(www.downloadHandler.text);
            sessionIdCallback?.Invoke(response.id);
            Application.OpenURL(response.url);
        }
    }
    
    private IEnumerator GetSessionStatus(string sessionId, Action<object> callback)
    {
        using var www = UnityWebRequest.Get(SessionRoute + $"/{sessionId}");
        www.SetRequestHeader("Authorization", "Bearer " + ApiKey);
        yield return www.SendWebRequest();
        if (www.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Stripe Call Error: " + www.error);
        }
        else
        {
            Debug.Log("Stripe Response: " + www.downloadHandler.text);
            var response = JsonConvert.DeserializeObject<StripeSessionStatusModel>(www.downloadHandler.text);
            callback?.Invoke(response);
        }
    }
    
    private IEnumerator GetSubscribeStatusCoroutine(string subscribeId, Action<object> callback)
    {
        using var www = UnityWebRequest.Get(SessionRoute + $"/{subscribeId}");
        www.SetRequestHeader("Authorization", "Bearer " + ApiKey);
        yield return www.SendWebRequest();
        if (www.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Stripe Call Error: " + www.error);
        }
        else
        {
            Debug.Log("Stripe Response: " + www.downloadHandler.text);
            var response = JsonConvert.DeserializeObject<StripeSubscribeStatusModel>(www.downloadHandler.text);
            callback?.Invoke(response);
        }
    }
    
    private IEnumerator CancelSubscribeCoroutine(string subscribeId, Action<object> callback)
    {
        using var www = UnityWebRequest.Delete(SubscribeRoute + $"/{subscribeId}");
        www.SetRequestHeader("Authorization", "Bearer " + ApiKey);
        yield return www.SendWebRequest();
        if (www.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Stripe Call Error: " + www.error);
        }
        else
        {
            Debug.Log("Stripe Response: " + www.downloadHandler.text);
            var response = JsonConvert.DeserializeObject<StripeSubscribeStatusModel>(www.downloadHandler.text);
            callback?.Invoke(response);
        }
    }
}

C#CoroutineManager.csStripe API Model Class.cs

Unity UI Scene

1 – 製作一個簡單的UI介面

  1. 付款按鈕
  2. 訂閱按鈕
  3. 取消訂閱按鈕
  4. 客戶的Member狀態
  5. 金幣數量的顯示

2 – 調用代碼:

StripeView.cs

public class StripeLabsView : MonoBehaviour
{
    public string StripeSecretApiKey;

    public string CustomerEmail;

    [Header("One Time Pay Info")] 
    public string PayPriceId;
    public string PayPriceQuantity;
    public string PaySuccessUrl;
    public string PayCancelUrl;

    [Header("Subscribe Info")] 
    public string SubscribePriceId;
    public string SubscribePriceQuantity;
    public string SubscribeSuccessUrl;
    public string SubscribeCancelUrl;


    public Button PayBtn;
    public Button SubscribeBtn;
    public Button UnsubscribeBtn;

    public Text MemberTypeText;
    public Text CoinText;

    public GameObject LoadingObj;
    public Button CancelLoadingBtn;

    public string GetCoinRoute = "https://bll2sj.buildship.run/GetCoin";
    public string GetMemberRoute = "https://bll2sj.buildship.run/GetType";

    private IPaymentServices PaymentServices = new StripeServices();
    private string currentSessionId;
    private StripeSessionStatusModel currentSessionStatus;


    private void Start()
    {
        PayBtn.onClick.AddListener(PayBtnOnClick);
        SubscribeBtn.onClick.AddListener(SubscribeBtnOnClick);
        PaymentServices.Initialize(StripeSecretApiKey, sessionIdCallback);
        CancelLoadingBtn.onClick.AddListener(() => LoadingObj.SetActive(false));
        CoroutineManager.Instance.StartCoroutine(GetCoin());
        CoroutineManager.Instance.StartCoroutine(GetMemberType());
    }

    private void sessionIdCallback(object obj)
    {
        currentSessionId = obj as string;
    }

    private void PayBtnOnClick()
    {
        var sessionInfo = new StripeSessionRequestModel()
        {
            mode = "payment",
            customerEmail = CustomerEmail,
            priceId = PayPriceId,
            quantity = PayPriceQuantity,
            successUrl = PaySuccessUrl,
            cancelUrl = PayCancelUrl
        };
        PaymentServices.Pay(sessionInfo);
        LoadingObj.SetActive(true);
        CoroutineManager.Instance.StartCoroutine(CheckSessionStatusLoop());
    }

    private void SubscribeBtnOnClick()
    {
        var sessionInfo = new StripeSessionRequestModel()
        {
            mode = "subscription",
            customerEmail = CustomerEmail,
            priceId = SubscribePriceId,
            quantity = SubscribePriceQuantity,
            successUrl = SubscribeSuccessUrl,
            cancelUrl = SubscribeCancelUrl
        };
        PaymentServices.Subscribe(sessionInfo);
        LoadingObj.SetActive(true);
        CoroutineManager.Instance.StartCoroutine(CheckSessionStatusLoop());
    }

    private IEnumerator GetCoin()
    {
        using var www = UnityWebRequest.Get(GetCoinRoute);
        yield return www.SendWebRequest();
        if (www.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Get Coin Call Error: " + www.error);
        }
        else
        {
            CoinText.text = www.downloadHandler.text;
        }
    }

    private IEnumerator GetMemberType()
    {
        using var www = UnityWebRequest.Get(GetMemberRoute);
        yield return www.SendWebRequest();
        if (www.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Get Member Call Error: " + www.error);
        }
        else
        {
            MemberTypeText.text = www.downloadHandler.text;
        }
    }

    private void SessionStatusCallBack(object obj)
    {
        currentSessionStatus = obj as StripeSessionStatusModel;
    }

    private IEnumerator CheckSessionStatusLoop()
    {
        currentSessionStatus = new StripeSessionStatusModel();
        currentSessionStatus.payment_status = "waiting";
        yield return new WaitForSeconds(5f);

        while (currentSessionStatus.payment_status != "paid")
        {
            Debug.Log(currentSessionStatus.payment_status);
            PaymentServices.GetPaymentStatus(currentSessionId, SessionStatusCallBack);
            yield return new WaitForSeconds(3f);
        }

        LoadingObj.SetActive(false);
        CoroutineManager.Instance.StartCoroutine(GetCoin());
        CoroutineManager.Instance.StartCoroutine(GetMemberType());
    }
}

3 – 在Inspector把需要的參數帶入:

BuildShip 後端架設

後端的實作邏輯我是用 Buildship

快速架設一個可以回傳資料庫的Json數值

主要用了幾個簡單的API

可以取得和設定金幣,Member Type:

它背後的資料庫表單長這樣:

Demo影片

0:00
/0:15

總結

以上就是在Unity中實作Stripe API付款的流程

一些注意事項:

  1. 上線時要把Stripe API Key換成正式模式
  2. 付款後的邏輯你可以寫在後端,用付款跳轉後的連結去處理也可以寫在Unity裡面,判斷付款成功之後處理遊戲邏輯(但這樣要考慮好會不會有安全風險)
  3. 不想要從遊戲跳轉到網頁的話,可以在遊戲裡面是做一個嵌入式的瀏覽器,在遊戲內完成付款。