北極熊在沙漠中進行遊戲開發和設計的故事

如何在Unity使用Stripe API進行付款?

Unity Stripe API

如何使用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 – 取得付款狀態的API

Endpoint: https://api.stripe.com/v1/checkout/sessions/{session-id}

類型: GET

回傳的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

這裡要用到的是 payment_status

付款成功的話會從 unpaid → paid

3. Check Subscribe Status API – 檢查訂閱狀態的API

Endpoint: https://api.stripe.com/v1/subscriptions/{subscription-id}

類型: GET

回傳的Json:

{
  "id": "sub_1MowQVLkdIwHu7ixeRlqHVzs",
  "object": "subscription",
  "application": null,
  "application_fee_percent": null,
  "automatic_tax": {
    "enabled": false,
    "liability": null
  },
  "billing_cycle_anchor": 1679609767,
  "billing_thresholds": null,
  "cancel_at": null,
  "cancel_at_period_end": false,
  "canceled_at": null,
  "cancellation_details": {
    "comment": null,
    "feedback": null,
    "reason": null
  },
  "collection_method": "charge_automatically",
  "created": 1679609767,
  "currency": "usd",
  "current_period_end": 1682288167,
  "current_period_start": 1679609767,
  "customer": "cus_Na6dX7aXxi11N4",
  "days_until_due": null,
  "default_payment_method": null,
  "default_source": null,
  "default_tax_rates": [],
  "description": null,
  "discount": null,
  "discounts": null,
  "ended_at": null,
  "invoice_settings": {
    "issuer": {
      "type": "self"
    }
  },
  "items": {
    "object": "list",
    "data": [
      {
        "id": "si_Na6dzxczY5fwHx",
        "object": "subscription_item",
        "billing_thresholds": null,
        "created": 1679609768,
        "metadata": {},
        "plan": {
          "id": "price_1MowQULkdIwHu7ixraBm864M",
          "object": "plan",
          "active": true,
          "aggregate_usage": null,
          "amount": 1000,
          "amount_decimal": "1000",
          "billing_scheme": "per_unit",
          "created": 1679609766,
          "currency": "usd",
          "discounts": null,
          "interval": "month",
          "interval_count": 1,
          "livemode": false,
          "metadata": {},
          "nickname": null,
          "product": "prod_Na6dGcTsmU0I4R",
          "tiers_mode": null,
          "transform_usage": null,
          "trial_period_days": null,
          "usage_type": "licensed"
        },
        "price": {
          "id": "price_1MowQULkdIwHu7ixraBm864M",
          "object": "price",
          "active": true,
          "billing_scheme": "per_unit",
          "created": 1679609766,
          "currency": "usd",
          "custom_unit_amount": null,
          "livemode": false,
          "lookup_key": null,
          "metadata": {},
          "nickname": null,
          "product": "prod_Na6dGcTsmU0I4R",
          "recurring": {
            "aggregate_usage": null,
            "interval": "month",
            "interval_count": 1,
            "trial_period_days": null,
            "usage_type": "licensed"
          },
          "tax_behavior": "unspecified",
          "tiers_mode": null,
          "transform_quantity": null,
          "type": "recurring",
          "unit_amount": 1000,
          "unit_amount_decimal": "1000"
        },
        "quantity": 1,
        "subscription": "sub_1MowQVLkdIwHu7ixeRlqHVzs",
        "tax_rates": []
      }
    ],
    "has_more": false,
    "total_count": 1,
    "url": "/v1/subscription_items?subscription=sub_1MowQVLkdIwHu7ixeRlqHVzs"
  },
  "latest_invoice": "in_1MowQWLkdIwHu7ixuzkSPfKd",
  "livemode": false,
  "metadata": {},
  "next_pending_invoice_item_invoice": null,
  "on_behalf_of": null,
  "pause_collection": null,
  "payment_settings": {
    "payment_method_options": null,
    "payment_method_types": null,
    "save_default_payment_method": "off"
  },
  "pending_invoice_item_interval": null,
  "pending_setup_intent": null,
  "pending_update": null,
  "schedule": null,
  "start_date": 1679609767,
  "status": "active",
  "test_clock": null,
  "transfer_data": null,
  "trial_end": null,
  "trial_settings": {
    "end_behavior": {
      "missing_payment_method": "create_invoice"
    }
  },
  "trial_start": null
}
JSON

這裡要用到的是 status

可以用來檢查現在的狀態是不是活躍正常的

如果取消訂閱的話會變成 active → cancel

4. Unsubscribe API – 取消訂閱的API

Endpoint: https://api.stripe.com/v1/subscriptions/{subscription-id}

類型: DELETE

*不強制帶參數,但可以帶

參數解釋
cancellation_details// 取消訂閱的原因,帶入一個Dic
invoice_now// bool , 生成一個invoice, 預設是true
prorate// 時間到之後才取消訂閱, 預設是false

失敗回傳的Json:

{
    "error": {
        "code": "resource_missing",
        "doc_url": "https://stripe.com/docs/error-codes/resource-missing",
        "message": "No such subscription: 'sub_1PJZzrCZr3PVtIXeZwEj8xoc'",
        "param": "id",
        "request_log_url": "https://dashboard.stripe.com/test/logs/req_dxbczisvkhe209?t=1719759672",
        "type": "invalid_request_error"
    }
}
JSON

成功回傳的Json:

{
  "id": "sub_1MlPf9LkdIwHu7ixB6VIYRyX",
  "object": "subscription",
  "application": null,
  "application_fee_percent": null,
  "automatic_tax": {
    "enabled": false,
    "liability": null
  },
  "billing_cycle_anchor": 1678768838,
  "billing_thresholds": null,
  "cancel_at": null,
  "cancel_at_period_end": false,
  "canceled_at": 1678768842,
  "cancellation_details": {
    "comment": null,
    "feedback": null,
    "reason": "cancellation_requested"
  },
  "collection_method": "charge_automatically",
  "created": 1678768838,
  "currency": "usd",
  "current_period_end": 1681447238,
  "current_period_start": 1678768838,
  "customer": "cus_NWSaVkvdacCUi4",
  "days_until_due": null,
  "default_payment_method": null,
  "default_source": null,
  "default_tax_rates": [],
  "description": null,
  "discount": null,
  "ended_at": 1678768842,
  "invoice_settings": {
    "issuer": {
      "type": "self"
    }
  },
  "items": {
    "object": "list",
    "data": [
      {
        "id": "si_NWSaWTp80M123q",
        "object": "subscription_item",
        "billing_thresholds": null,
        "created": 1678768839,
        "metadata": {},
        "plan": {
          "id": "price_1MlPf7LkdIwHu7ixgcbP7cwE",
          "object": "plan",
          "active": true,
          "aggregate_usage": null,
          "amount": 1099,
          "amount_decimal": "1099",
          "billing_scheme": "per_unit",
          "created": 1678768837,
          "currency": "usd",
          "interval": "month",
          "interval_count": 1,
          "livemode": false,
          "metadata": {},
          "nickname": null,
          "product": "prod_NWSaMgipulx8IQ",
          "tiers_mode": null,
          "transform_usage": null,
          "trial_period_days": null,
          "usage_type": "licensed"
        },
        "price": {
          "id": "price_1MlPf7LkdIwHu7ixgcbP7cwE",
          "object": "price",
          "active": true,
          "billing_scheme": "per_unit",
          "created": 1678768837,
          "currency": "usd",
          "custom_unit_amount": null,
          "livemode": false,
          "lookup_key": null,
          "metadata": {},
          "nickname": null,
          "product": "prod_NWSaMgipulx8IQ",
          "recurring": {
            "aggregate_usage": null,
            "interval": "month",
            "interval_count": 1,
            "trial_period_days": null,
            "usage_type": "licensed"
          },
          "tax_behavior": "unspecified",
          "tiers_mode": null,
          "transform_quantity": null,
          "type": "recurring",
          "unit_amount": 1099,
          "unit_amount_decimal": "1099"
        },
        "quantity": 1,
        "subscription": "sub_1MlPf9LkdIwHu7ixB6VIYRyX",
        "tax_rates": []
      }
    ],
    "has_more": false,
    "total_count": 1,
    "url": "/v1/subscription_items?subscription=sub_1MlPf9LkdIwHu7ixB6VIYRyX"
  },
  "latest_invoice": "in_1MlPf9LkdIwHu7ixEo6hdgCw",
  "livemode": false,
  "metadata": {},
  "next_pending_invoice_item_invoice": null,
  "on_behalf_of": null,
  "pause_collection": null,
  "payment_settings": {
    "payment_method_options": null,
    "payment_method_types": null,
    "save_default_payment_method": "off"
  },
  "pending_invoice_item_interval": null,
  "pending_setup_intent": null,
  "pending_update": null,
  "plan": {
    "id": "price_1MlPf7LkdIwHu7ixgcbP7cwE",
    "object": "plan",
    "active": true,
    "aggregate_usage": null,
    "amount": 1099,
    "amount_decimal": "1099",
    "billing_scheme": "per_unit",
    "created": 1678768837,
    "currency": "usd",
    "interval": "month",
    "interval_count": 1,
    "livemode": false,
    "metadata": {},
    "nickname": null,
    "product": "prod_NWSaMgipulx8IQ",
    "tiers_mode": null,
    "transform_usage": null,
    "trial_period_days": null,
    "usage_type": "licensed"
  },
  "quantity": 1,
  "schedule": null,
  "start_date": 1678768838,
  "status": "canceled",
  "test_clock": null,
  "transfer_data": null,
  "trial_end": null,
  "trial_settings": {
    "end_behavior": {
      "missing_payment_method": "create_invoice"
    }
  },
  "trial_start": null
}
JSON

*status 會變成 canceled

基本上成功的話就會cancel掉這個訂閱

呼叫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.cs

public class CoroutineManager : MonoBehaviour
{
    private static CoroutineManager instance;

    private void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
            return;
        }

        instance = this;
    }

    public static CoroutineManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject singletonObject = new GameObject();
                instance = singletonObject.AddComponent<CoroutineManager>();
                singletonObject.name = "CoroutineManagerSingleton";
            }

            return instance;
        }
    }

    public static void Trigger(IEnumerator method)
    {
        Instance.StartCoroutine(method);
    }
}
C#

這個是方便沒有繼承MonoBehavior的Class使用Coroutine的代碼。

Stripe API Model Class.cs

可以透過 JsonToC# 把 Response的Json都先寫成C#Class

StripeSubscribeStatusModel.cs
public class StripeSubscribeStatusModel
    {
        public string id { get; set; }
        public string @object { get; set; }
        public object application { get; set; }
        public object application_fee_percent { get; set; }
        public AutomaticTax automatic_tax { get; set; }
        public int billing_cycle_anchor { get; set; }
        public object billing_cycle_anchor_config { get; set; }
        public object billing_thresholds { get; set; }
        public object cancel_at { get; set; }
        public bool cancel_at_period_end { get; set; }
        public int canceled_at { get; set; }
        public CancellationDetails cancellation_details { get; set; }
        public string collection_method { get; set; }
        public int created { get; set; }
        public string currency { get; set; }
        public int current_period_end { get; set; }
        public int current_period_start { get; set; }
        public string customer { get; set; }
        public object days_until_due { get; set; }
        public string default_payment_method { get; set; }
        public object default_source { get; set; }
        public List<object> default_tax_rates { get; set; }
        public object description { get; set; }
        public object discount { get; set; }
        public List<object> discounts { get; set; }
        public int ended_at { get; set; }
        public InvoiceSettings invoice_settings { get; set; }
        public Items items { get; set; }
        public string latest_invoice { get; set; }
        public bool livemode { get; set; }
        public Metadata metadata { get; set; }
        public object next_pending_invoice_item_invoice { get; set; }
        public object on_behalf_of { get; set; }
        public object pause_collection { get; set; }
        public PaymentSettings payment_settings { get; set; }
        public object pending_invoice_item_interval { get; set; }
        public object pending_setup_intent { get; set; }
        public object pending_update { get; set; }
        public Plan plan { get; set; }
        public int quantity { get; set; }
        public object schedule { get; set; }
        public int start_date { get; set; }
        public string status { get; set; }
        public object test_clock { get; set; }
        public object transfer_data { get; set; }
        public object trial_end { get; set; }
        public TrialSettings trial_settings { get; set; }
        public object trial_start { get; set; }
    }
C#
Stripe Session Status Model.cs
public class StripeSessionStatusModel
    {
        public string id { get; set; }
        public string @object { get; set; }
        public object after_expiration { get; set; }
        public object allow_promotion_codes { get; set; }
        public int amount_subtotal { get; set; }
        public int amount_total { get; set; }
        public AutomaticTax automatic_tax { get; set; }
        public object billing_address_collection { get; set; }
        public string cancel_url { get; set; }
        public object client_reference_id { get; set; }
        public object client_secret { get; set; }
        public object consent { get; set; }
        public object consent_collection { get; set; }
        public int created { get; set; }
        public string currency { get; set; }
        public object currency_conversion { get; set; }
        public List<object> custom_fields { get; set; }
        public CustomText custom_text { get; set; }
        public object customer { get; set; }
        public string customer_creation { get; set; }
        public CustomerDetails customer_details { get; set; }
        public string customer_email { get; set; }
        public int expires_at { get; set; }
        public object invoice { get; set; }
        public InvoiceCreation invoice_creation { get; set; }
        public bool livemode { get; set; }
        public object locale { get; set; }
        public object metadata { get; set; }
        public string mode { get; set; }
        public string payment_intent { get; set; }
        public object payment_link { get; set; }
        public string payment_method_collection { get; set; }
        public object payment_method_configuration_details { get; set; }
        public PaymentMethodOptions payment_method_options { get; set; }
        public List<string> payment_method_types { get; set; }
        public string payment_status { get; set; }
        public PhoneNumberCollection phone_number_collection { get; set; }
        public object recovered_from { get; set; }
        public object saved_payment_method_options { get; set; }
        public object setup_intent { get; set; }
        public object shipping_address_collection { get; set; }
        public object shipping_cost { get; set; }
        public object shipping_details { get; set; }
        public List<object> shipping_options { get; set; }
        public string status { get; set; }
        public object submit_type { get; set; }
        public object subscription { get; set; }
        public string success_url { get; set; }
        public TotalDetails total_details { get; set; }
        public string ui_mode { get; set; }
        public object url { get; set; }
    }
C#
StripeSessionResponseModel.cs
public class StripeSessionResponseModel
    {
        public string id { get; set; }
        public string @object { get; set; }
        public object after_expiration { get; set; }
        public object allow_promotion_codes { get; set; }
        public int amount_subtotal { get; set; }
        public int amount_total { get; set; }
        public AutomaticTax automatic_tax { get; set; }
        public object billing_address_collection { get; set; }
        public string cancel_url { get; set; }
        public object client_reference_id { get; set; }
        public object client_secret { get; set; }
        public object consent { get; set; }
        public object consent_collection { get; set; }
        public int created { get; set; }
        public string currency { get; set; }
        public object currency_conversion { get; set; }
        public List<object> custom_fields { get; set; }
        public CustomText custom_text { get; set; }
        public object customer { get; set; }
        public string customer_creation { get; set; }
        public CustomerDetails customer_details { get; set; }
        public string customer_email { get; set; }
        public int expires_at { get; set; }
        public object invoice { get; set; }
        public InvoiceCreation invoice_creation { get; set; }
        public bool livemode { get; set; }
        public object locale { get; set; }
        public object metadata { get; set; }
        public string mode { get; set; }
        public object payment_intent { get; set; }
        public object payment_link { get; set; }
        public string payment_method_collection { get; set; }
        public object payment_method_configuration_details { get; set; }
        public PaymentMethodOptions payment_method_options { get; set; }
        public List<string> payment_method_types { get; set; }
        public string payment_status { get; set; }
        public PhoneNumberCollection phone_number_collection { get; set; }
        public object recovered_from { get; set; }
        public object saved_payment_method_options { get; set; }
        public object setup_intent { get; set; }
        public object shipping_address_collection { get; set; }
        public object shipping_cost { get; set; }
        public object shipping_details { get; set; }
        public List<object> shipping_options { get; set; }
        public string status { get; set; }
        public object submit_type { get; set; }
        public object subscription { get; set; }
        public string success_url { get; set; }
        public TotalDetails total_details { get; set; }
        public string ui_mode { get; set; }
        public string url { get; set; } // core
    }
C#
StripeSessionRequestModel.cs
public class StripeSessionRequestModel
    {
        public string mode;
        public string priceId;
        public string quantity;
        public string successUrl;
        public string cancelUrl;
        public string customerEmail;
    }
C#
StripeErrorModel.cs
public class StripeErrorModel
    {
        public Error error { get; set; }
    }
    public class Error
    {
        public string code { get; set; }
        public string doc_url { get; set; }
        public string message { get; set; }
        public string param { get; set; }
        public string request_log_url { get; set; }
        public string type { get; set; }
    }
C#
StripeSubModel.cs
public class AutomaticTax
{
    public bool enabled { get; set; }
    public object liability { get; set; }
    public object status { get; set; }
}

public class Card
{
    public string request_three_d_secure { get; set; }
}

public class CustomerDetails
{
    public object address { get; set; }
    public string email { get; set; }
    public object name { get; set; }
    public object phone { get; set; }
    public string tax_exempt { get; set; }
    public object tax_ids { get; set; }
}

public class CustomText
{
    public object after_submit { get; set; }
    public object shipping_address { get; set; }
    public object submit { get; set; }
    public object terms_of_service_acceptance { get; set; }
}

public class InvoiceCreation
{
    public bool enabled { get; set; }
    public InvoiceData invoice_data { get; set; }
}

public class InvoiceData
{
    public object account_tax_ids { get; set; }
    public object custom_fields { get; set; }
    public object description { get; set; }
    public object footer { get; set; }
    public object issuer { get; set; }
    public object metadata { get; set; }
    public object rendering_options { get; set; }
}

public class PaymentMethodOptions
{
    public Card card { get; set; }
}

public class PhoneNumberCollection
{
    public bool enabled { get; set; }
}

public class TotalDetails
{
    public int amount_discount { get; set; }
    public int amount_shipping { get; set; }
    public int amount_tax { get; set; }
}

public class CancellationDetails
{
    public object comment { get; set; }
    public object feedback { get; set; }
    public string reason { get; set; }
}

public class InvoiceSettings
{
    public object account_tax_ids { get; set; }
    public Issuer issuer { get; set; }
}

public class Issuer
{
    public string type { get; set; }
}


public class Datum
{
    public string id { get; set; }
    public string @object { get; set; }
    public object billing_thresholds { get; set; }
    public int created { get; set; }
    public List<object> discounts { get; set; }
    public Metadata metadata { get; set; }
    public Plan plan { get; set; }
    public Price price { get; set; }
    public int quantity { get; set; }
    public string subscription { get; set; }
    public List<object> tax_rates { get; set; }
}


public class Price
{
    public string id { get; set; }
    public string @object { get; set; }
    public bool active { get; set; }
    public string billing_scheme { get; set; }
    public int created { get; set; }
    public string currency { get; set; }
    public object custom_unit_amount { get; set; }
    public bool livemode { get; set; }
    public object lookup_key { get; set; }
    public Metadata metadata { get; set; }
    public object nickname { get; set; }
    public string product { get; set; }
    public Recurring recurring { get; set; }
    public string tax_behavior { get; set; }
    public object tiers_mode { get; set; }
    public object transform_quantity { get; set; }
    public string type { get; set; }
    public int unit_amount { get; set; }
    public string unit_amount_decimal { get; set; }
}

public class Recurring
{
    public object aggregate_usage { get; set; }
    public string interval { get; set; }
    public int interval_count { get; set; }
    public object meter { get; set; }
    public object trial_period_days { get; set; }
    public string usage_type { get; set; }
}


public class Items
{
    public string @object { get; set; }
    public List<Datum> data { get; set; }
    public bool has_more { get; set; }
    public int total_count { get; set; }
    public string url { get; set; }
}

public class Metadata
{
}

public class PaymentSettings
{
    public PaymentMethodOptions payment_method_options { get; set; }
    public object payment_method_types { get; set; }
    public string save_default_payment_method { get; set; }
}

public class Plan
{
    public string id { get; set; }
    public string @object { get; set; }
    public bool active { get; set; }
    public object aggregate_usage { get; set; }
    public int amount { get; set; }
    public string amount_decimal { get; set; }
    public string billing_scheme { get; set; }
    public int created { get; set; }
    public string currency { get; set; }
    public string interval { get; set; }
    public int interval_count { get; set; }
    public bool livemode { get; set; }
    public Metadata metadata { get; set; }
    public object meter { get; set; }
    public object nickname { get; set; }
    public string product { get; set; }
    public object tiers_mode { get; set; }
    public object transform_usage { get; set; }
    public object trial_period_days { get; set; }
    public string usage_type { get; set; }
}

public class EndBehavior
{
    public string missing_payment_method { get; set; }
}

public class TrialSettings
{
    public EndBehavior end_behavior { get; set; }
}
C#

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());
    }
}
C#

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

BuildShip 後端架設

後端的實作邏輯我是用 Buildship

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

主要用了幾個簡單的API

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

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

Demo影片

總結

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

一些注意事項:

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

2024年 – 11個可商用的遊戲音效網站推薦

Next Post

如何在Text Mesh Pro使用中文字?

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Read next

與身體對話,就是與神對話

身體裡的每一個細胞、每一個器官都有「意識」同時也有「潛意識」 身體各個部位要帶給你什麼訊息: 身體部位 訊號 特質 怎麼做 肩部骨骼與肌肉 硬邦邦 不懂得麻煩人,不懂得撒嬌,不懂得拜托人…

讀書會 3 – <有錢人跟你想的不一樣>

這次我們讀《有錢人跟你想的不一樣》 這是一本影響我對金錢觀念最大的一本書 第一次瞭解到原來頭腦的意識觀念 -> 決定了自己可以擁有多少財富 讓我們一起來進入這場讀書會吧: 1. 檢視對於金錢的觀念…

如何在Text Mesh Pro使用中文字?

在Unity中要使用TextMeshPro中文字的時候會出現亂碼,每次要建字要輸入1萬多個中文字也很麻煩,本文將使用動態生成的方式建置TMP的中文字體。