如何使用Unity在遊戲之中結合Stripe的API進行金流付款和訂閱制的服務的教學流程。會使用到UnityWebRequest去呼叫Stripe 的API取得付款連結,和BuildShip進行快速的後端Json驗證。
Stripe是什麼?
Stripe 是一個提供金流付費服務的的平台。
它提供各種支付方式、一次性和訂閱的付款服務
讓你可以輕鬆管理線上支付業務。
幾乎所有知名的網站
如 ChatGPT, Google, Netflix
都選擇使用Stripe進行訂閱付款。
Stripe API 付款流程
- 在遊戲中點擊購買按鈕
- 呼叫Stripe API → 取得付款連結
- 自動打開瀏覽器 → 用戶進行付款
- 遊戲在後台循環偵測付款狀態
- 處理付款完成後的邏輯
串接 Stripe API 的準備
注意: 註冊Stripe帳號需要有一間公司
這篇文章預設你已經註冊好Stripe帳號了。
1. 打開測試模式
登入Stripe網站後
右上角可以打開測試模式
在開發測試階段打開測試模式
就不會收取真錢。
在測試模式下
可以輸入測試信用卡 4242-4242-4242 來付款
2. 取得Stripe API密鑰
點擊開發人員→API密鑰→顯示密鑰。
API Key 會在呼叫Stripe API時用到
可以先保存起來。
3. 新增產品目錄
到產品目錄的頁面,點擊添加產品。
我們要新增兩個產品,第一個是一次性購買的
第二個是訂閱模式的:
4. 取得商品的價錢ID
把創建的產品點開 → 找到價格(再點開)
右上角有一個 price_xxx 的價錢ID
這個過後帶入參數會用到,先複製保存下來。
訂閱的產品也是一樣的操作。
5. 總結
操作完上面的步驟之後,你會得到
- Stripe API Key
- 一次性商品的 Price ID
- 訂閱模式的 Price ID
需要的 Stripe API
Stripe API的說明文件: https://docs.stripe.com/api
很多,但我們一共要知道的只有4個API:
Endpoint: https://api.stripe.com/v1/checkout/sessions
類型: POST
參數 | 解釋 |
payment_method_types[] | card |
mode | payment // 一次性 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
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
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
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
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#
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的代碼。
可以透過 JsonToC# 把 Response的Json都先寫成C#Class
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#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#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#public class StripeSessionRequestModel
{
public string mode;
public string priceId;
public string quantity;
public string successUrl;
public string cancelUrl;
public string customerEmail;
}
C#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#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介面:
- 付款按鈕
- 訂閱按鈕
- 取消訂閱按鈕
- 客戶的Member狀態
- 金幣數量的顯示
2 – 調用代碼:
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付款的流程
一些注意事項:
- 上線時要把Stripe API Key換成正式模式
- 付款後的邏輯你可以寫在後端,用付款跳轉後的連結去處理也可以寫在Unity裡面,判斷付款成功之後處理遊戲邏輯(但這樣要考慮好會不會有安全風險)
- 不想要從遊戲跳轉到網頁的話,可以在遊戲裡面是做一個嵌入式的瀏覽器,在遊戲內完成付款。