Sfoglia il codice sorgente

Update Android IAP docs for v1.1 of PBL plugin

These doc changes are dependent on the following PR:
https://github.com/godotengine/godot-google-play-billing/pull/25

Summary of changes:

* Update queryPurchases section to reflect API change to be async
* Added details on functions added to v1.1
* Expanded detail on operation and options of the plugin
Nate Trost 3 anni fa
parent
commit
dafb033c38
1 ha cambiato i file con 310 aggiunte e 70 eliminazioni
  1. 310 70
      tutorials/platform/android/android_in_app_purchases.rst

+ 310 - 70
tutorials/platform/android/android_in_app_purchases.rst

@@ -3,48 +3,37 @@
 Android in-app purchases
 ========================
 
-Godot offers a first-party ``GodotGooglePlayBilling`` Android plugin since Godot 3.2.2.
-The new plugin uses the `Google Play Billing library <https://developer.android.com/google/play/billing>`__
-instead of the now deprecated AIDL IAP implementation.
+Godot offers a first-party ``GodotGooglePlayBilling`` Android plugin compatible with Godot 3.2.2 and higher.
+This plugin uses the `Google Play Billing library <https://developer.android.com/google/play/billing>`__
+instead of the now deprecated AIDL IAP implementation. For details of how to migrate from the older
+``GodotPaymentsV3``, see the migration guide: `Migrating from Godot 3.2.1 and lower (GodotPaymentsV3)`_.
 
 If you learn better by looking at an example, you can find the demo project
 `here <https://github.com/godotengine/godot-demo-projects/tree/master/mobile/android_iap>`__.
 
 
-Migrating from Godot 3.2.1 and lower (GodotPaymentsV3)
-------------------------------------------------------
-
-The new ``GodotGooglePlayBilling`` API is not compatible with its predecessor ``GodotPaymentsV3``.
-
-Changes
-*******
-
-- You need to enable the Custom Build option in your Android export settings and install
-  the ``GodotGooglePlayBilling`` plugin manually (see below for details)
-- All purchases have to be acknowledged by your app. This is a
-  `requirement from Google <https://developer.android.com/google/play/billing/integrate#process>`__.
-  Purchases that are not acknowledged by your app will be refunded.
-- Support for subscriptions
-- Signals (no polling or callback objects)
-
-
 Usage
 -----
 
 Getting started
 ***************
 
-If not already done, make sure you have enabled and successfully set up :ref:`Android Custom Builds <doc_android_custom_build>`.
-Grab the``GodotGooglePlayBilling`` plugin binary and config from the `releases page <https://github.com/godotengine/godot-google-play-billing/releases>`__
+Make sure you have enabled and successfully set up :ref:`Android Custom Builds <doc_android_custom_build>`.
+Grab the ``GodotGooglePlayBilling`` plugin binary and config from the `releases page <https://github.com/godotengine/godot-google-play-billing/releases>`__
 and put both into `res://android/plugins`.
 The plugin should now show up in the Android export settings, where you can enable it.
 
 
-Getting started
-***************
+Initialize the plugin
+*********************
+
+To use the ``GodotGooglePlayBilling`` API: 
+
+1. Obtain a reference to the ``GodotGooglePlayBilling`` singleton
+2. Connect handlers for the plugin signals
+3. Call ``startConnection``
 
-To use the ``GodotGooglePlayBilling`` API you first have to get the ``GodotGooglePlayBilling``
-singleton and start the connection:
+Initialization example:
 
 ::
 
@@ -56,9 +45,11 @@ singleton and start the connection:
 
             # These are all signals supported by the API
             # You can drop some of these based on your needs
+            payment.connect("billing_resume", self, "_on_billing_resume") # No params
             payment.connect("connected", self, "_on_connected") # No params
             payment.connect("disconnected", self, "_on_disconnected") # No params
             payment.connect("connect_error", self, "_on_connect_error") # Response ID (int), Debug message (string)
+            payment.connect("price_change_acknowledged", self, "_on_price_acknowledged") # Response ID (int)
             payment.connect("purchases_updated", self, "_on_purchases_updated") # Purchases (Dictionary[])
             payment.connect("purchase_error", self, "_on_purchase_error") # Response ID (int), Debug message (string)
             payment.connect("sku_details_query_completed", self, "_on_sku_details_query_completed") # SKUs (Dictionary[])
@@ -67,20 +58,42 @@ singleton and start the connection:
             payment.connect("purchase_acknowledgement_error", self, "_on_purchase_acknowledgement_error") # Response ID (int), Debug message (string), Purchase token (string)
             payment.connect("purchase_consumed", self, "_on_purchase_consumed") # Purchase token (string)
             payment.connect("purchase_consumption_error", self, "_on_purchase_consumption_error") # Response ID (int), Debug message (string), Purchase token (string)
+            payment.connect("query_purchases_response", self, "_on_query_purchases_response") # Purchases (Dictionary[])
 
             payment.startConnection()
         else:
             print("Android IAP support is not enabled. Make sure you have enabled 'Custom Build' and the GodotGooglePlayBilling plugin in your Android export settings! IAP will not work.")
 
-All API methods only work if the API is connected. You can use ``payment.isReady()`` to check the connection status.
+The API must be in a connected state prior to use. The ``connected`` signal is sent
+when the connection process succeeds. You can also use ``isReady()`` to determine if the plugin
+is ready for use. The ``getConnectionState()`` function returns the current connection state
+of the plugin.
 
+Return values for ``getConnectionState()``:
+
+::
+
+    # Matches BillingClient.ConnectionState in the Play Billing Library
+    enum ConnectionState {
+        DISCONNECTED, # not yet connected to billing service or was already closed
+        CONNECTING, # currently in process of connecting to billing service
+        CONNECTED, # currently connected to billing service
+        CLOSED, # already closed and shouldn't be used again
+    }
 
-Querying available items
-************************
 
-As soon as the API is connected, you can query SKUs using ``querySkuDetails``.
+Query available items
+*********************
 
-Full example:
+Once the API has connected, query SKUs using ``querySkuDetails()``. You must successfully complete
+a SKU query before before calling the ``purchase()`` or ``queryPurchases()`` functions,
+or they will return an error. ``querySkuDetails()`` takes two parameters: an array
+of SKU name strings, and a string specifying the type of SKU being queried.
+The SKU type string should be ``"inapp"`` for normal in-app purchases or ``"subs"`` for subscriptions.
+The name strings in the array should match the SKU product ids defined in the Google Play Console entry
+for your app.
+
+Example use of ``querySkuDetails()``:
 
 ::
 
@@ -91,76 +104,303 @@ Full example:
       for available_sku in sku_details:
         print(available_sku)
 
+    func _on_sku_details_query_error(response_id, error_message, skus_queried):
+        print("on_sku_details_query_error id:", response_id, " message: ",
+                error_message, " skus: ", skus_queried)
+
+
+Query user purchases
+********************
+
+To retrieve a user's purchases, call the ``queryPurchases()`` function passing
+a string with the type of SKU to query. The SKU type string should be
+``"inapp"`` for normal in-app purchases or ``"subs"`` for subscriptions.
+The ``query_purchases_response`` signal is sent with the result. 
+The signal has a single parameter: a :ref:`Dictionary <class_Dictionary>` with
+a status code and either an array of purchases or an error message.
+Only active subscriptions and non-consumed one-time purchases are
+included in the purchase array.
+
+Example use of ``queryPurchases()``:
+
+::
+
+    func _query_purchases():
+        payment.queryPurchases("inapp") # Or "subs" for subscriptions
+
+    func _on_query_purchases_response(query_result):
+        if query_result.status == OK:
+            for purchase in query_result.purchases:
+                _process_purchase(purchase)
+        else:
+            print("queryPurchases failed, response code: ",
+                    query_result.response_code,
+                    " debug message: ", query_result.debug_message)
+
+
+You should query purchases during startup after succesfully retrieving SKU details.
+Since the user may make a purchase or resolve a pending transaction from
+outside your app, you should recheck for purchases when resuming from the
+background. To accomplish this, you can use the ``billing_resume`` signal.
+
+Example use of ``billing_resume``:
+
+::
+
+    func _on_billing_resume():
+        if payment.getConnectionState() == ConnectionState.CONNECTED:
+            _query_purchases()
+
+
+For more information on processing the purchase items returned by
+``queryPurchases()``, see `Processing a purchase item`_
+
 
 Purchase an item
 ****************
 
-To initiate the purchase flow for an item, call ``purchase``.
-You **must** query the SKU details for an item before you can
-initiate the purchase flow for it.
+To initiate the purchase flow for an item, call ``purchase()`` passing the
+product id string of the SKU you wish to purchase.
+Reminder: you **must** query the SKU details for an item before you can
+pass it to ``purchase()``.
+
+Example use of ``purchase()``:
 
 ::
 
     payment.purchase("my_iap_item")
 
-Then, wait for the ``_on_purchases_updated`` callback and handle the purchase result:
+
+The payment flow will send a ``purchases_updated`` signal on success or a
+``purchase_error`` signal on failure.
 
 ::
 
     func _on_purchases_updated(purchases):
         for purchase in purchases:
-            if purchase.purchase_state == 1: # 1 means "purchased", see https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState#constants_1
-                # enable_premium(purchase.sku) # unlock paid content, add coins, save token on server, etc. (you have to implement enable_premium yourself)
-                if not purchase.is_acknowledged:
-                    payment.acknowledgePurchase(purchase.purchase_token) # call if non-consumable product
-                    if purchase.sku in list_of_consumable_products:
-                        payment.consumePurchase(purchase.purchase_token) # call if consumable product
+            _process_purchase(purchase)
+
+    func _on_purchase_error(response_id, error_message):
+        print("purchase_error id:", response_id, " message: ", error_message)
+
+
+Processing a purchase item
+**************************
+
+The ``query_purchases_response`` and ``purchases_updated`` signals provide an array
+of purchases in :ref:`Dictionary <class_Dictionary>` format. The purchase Dictionary
+includes keys that map to values of the Google Play Billing
+`Purchase <https://developer.android.com/reference/com/android/billingclient/api/Purchase>`_ class.
+
+Purchase fields:
+
+::
+
+    dictionary.put("order_id", purchase.getOrderId());
+    dictionary.put("package_name", purchase.getPackageName());
+    dictionary.put("purchase_state", purchase.getPurchaseState());
+    dictionary.put("purchase_time", purchase.getPurchaseTime());
+    dictionary.put("purchase_token", purchase.getPurchaseToken());
+    dictionary.put("quantity", purchase.getQuantity());
+    dictionary.put("signature", purchase.getSignature());
+    // PBL V4 replaced getSku with getSkus to support multi-sku purchases,
+    // use the first entry for "sku" and generate an array for "skus"
+    ArrayList<String> skus = purchase.getSkus();
+    dictionary.put("sku", skus.get(0));
+    String[] skusArray = skus.toArray(new String[0]);
+    dictionary.put("skus", skusArray);
+    dictionary.put("is_acknowledged", purchase.isAcknowledged());
+    dictionary.put("is_auto_renewing", purchase.isAutoRenewing());
 
 
-Check if the user purchased an item
-***********************************
+Check purchase state
+********************
 
-To get all purchases, call ``queryPurchases``. Unlike most of the other functions, ``queryPurchases`` is
-a synchronous operation and returns a :ref:`Dictionary <class_Dictionary>` with a status code
-and either an array of purchases or an error message. Only active subscriptions and non-consumed one-time purchases are returned.
+Check the ``purchase_state`` value of a purchase to determine if a 
+purchase was completed or is still pending.
 
-Full example:
+PurchaseState values:
 
 ::
 
-    var query = payment.queryPurchases("inapp") # Or "subs" for subscriptions
-    if query.status == OK:
-        for purchase in query.purchases:
-            if purchase.sku == "my_iap_item" and purchase.purchase_state == 1:
-                # enable_premium(purchase.sku) # unlock paid content, save token on server, etc.
-                if !purchase.is_acknowledged:
-                    payment.acknowledgePurchase(purchase.purchase_token)
-                    # Or wait for the _on_purchase_acknowledged callback before giving the user what they bought
+    # Matches Purchase.PurchaseState in the Play Billing Library
+    enum PurchaseState {
+        UNSPECIFIED, 
+        PURCHASED, 
+        PENDING,
+    }
+
+
+If a purchase is in a ``PENDING`` state, you should not award the contents of the
+purchase or do any further processing of the purchase until it reaches the
+``PURCHASED`` state. If you have a store interface, you may wish to display
+information about pending purchases needing to be completed in the Google Play Store.
+For more details on pending purchases, see
+`Handling pending transactions <https://developer.android.com/google/play/billing/integrate#pending>`_
+in the Google Play Billing Library documentation.
 
 
 Consumables
 ***********
 
 If your in-app item is not a one-time purchase but a consumable item (e.g. coins) which can be purchased
-multiple times, you can consume an item by calling ``consumePurchase`` with a purchase token.
-Call ``queryPurchases`` to get the purchase token. Calling ``consumePurchase`` automatically
-acknowledges a purchase.
-Consuming a product allows the user to purchase it again, and removes it from appearing in subsequent ``queryPurchases`` calls.
+multiple times, you can consume an item by calling ``consumePurchase()`` passing
+the ``purchase_token`` value from the purchase dictionary.
+Calling ``consumePurchase()`` automatically acknowledges a purchase.
+Consuming a product allows the user to purchase it again, it will no longer appear 
+in subsequent ``queryPurchases()`` calls unless it is repurchased.
+
+Example use of ``consumePurchase()``:
+
+::
+
+    func _process_purchase(purchase):
+        if purchase.sku == "my_consumable_iap_item" and purchase.purchase_state == PurchaseState.PURCHASED:
+            # Add code to store payment so we can reconcile the purchase token
+            # in the completion callback against the original purchase
+            payment.consumePurchase(purchase.purchase_token)
+
+    func _on_purchase_consumed(purchase_token):
+        _handle_purchase_token(purchase_token, true)
+
+    func _on_purchase_consumption_error(response_id, error_message, purchase_token):
+        print("_on_purchase_consumption_error id:", response_id,
+                " message: ", error_message)
+        _handle_purchase_token(purchase_token, false)
+
+    # Find the sku associated with the purchase token and award the
+    # product if successful
+    func _handle_purchase_token(purchase_token, purchase_successful):
+        # check/award logic, remove purchase from tracking list
+
+
+Acknowledging purchases
+***********************
+
+If your in-app item is a one-time purchase, you must acknowledge the purchase by
+calling the ``acknowledgePurchase()`` function, passing the ``purchase_token``
+value from the purchase dictionary. If you do not acknowledge a purchase within
+three days, the user automatically receives a refund, and Google Play revokes the purchase.
+If you are calling ``comsumePurchase()`` it automatically acknowledges the purchase and
+you do not need to call ``acknowledgePurchase()``.
+
+Example use of ``acknowledgePurchase()``:
 
 ::
 
-    var query = payment.queryPurchases("inapp") # Or "subs" for subscriptions
-    if query.status == OK:
-        for purchase in query.purchases:
-            if purchase.sku == "my_consumable_iap_item" and purchase.purchase_state == 1:
-                # enable_premium(purchase.sku) # add coins, save token on server, etc.
-                payment.consumePurchase(purchase.purchase_token)
-                # Or wait for the _on_purchase_consumed callback before giving the user what they bought
+    func _process_purchase(purchase):
+        if purchase.sku == "my_one_time_iap_item" and \
+                purchase.purchase_state == PurchaseState.PURCHASED and \
+                not purchase.is_acknowledged:
+            # Add code to store payment so we can reconcile the purchase token
+            # in the completion callback against the original purchase
+            payment.acknowledgePurchase(purchase.purchase_token)
+
+    func _on_purchase_acknowledged(purchase_token):
+        _handle_purchase_token(purchase_token, true)
+
+    func _on_purchase_acknowledgement_error(response_id, error_message, purchase_token):
+        print("_on_purchase_acknowledgement_error id: ", response_id,
+                " message: ", error_message)
+        _handle_purchase_token(purchase_token, false)
+
+    # Find the sku associated with the purchase token and award the
+    # product if successful
+    func _handle_purchase_token(purchase_token, purchase_successful):
+        # check/award logic, remove purchase from tracking list
+
 
 Subscriptions
 *************
 
-Subscriptions don't work much different from regular in-app items. Just use ``"subs"`` as second
-argument to ``querySkuDetails`` to get subscription details.
-Check ``is_auto_renewing`` in the results of ``queryPurchases()`` to see if a
-user has cancelled an auto-renewing subscription
+Subscriptions work mostly like regular in-app items. Use ``"subs"`` as the second
+argument to ``querySkuDetails()`` to get subscription details. Pass ``"subs"``
+to ``queryPurchases()`` to get subscription purchase details.
+
+You can check ``is_auto_renewing`` in the a subscription purchase
+returned from ``queryPurchases()`` to see if a user has cancelled an
+auto-renewing subscription.
+
+You need to acknowledge new subscription purchases, but not automatic
+subscription renewals.
+
+If you support upgrading or downgrading between different subscription levels,
+you should use ``updateSubscription()`` to use the subscription update flow to
+change an active subscription. Like ``purchase()``, results are returned by the
+``purchases_updated`` and ``purchase_error`` signals.
+There are three parameters to ``updateSubscription()``:
+
+1. The purchase token of the currently active subscription
+2. The product id string of the subscription SKU to change to
+3. The proration mode to apply to the subscription.
+
+The proration values are defined as:
+
+::
+
+    enum SubscriptionProrationMode {
+        # Replacement takes effect immediately, and the remaining time 
+        # will be prorated and credited to the user.
+        IMMEDIATE_WITH_TIME_PRORATION = 1,
+        # Replacement takes effect immediately, and the billing cycle remains the same. 
+        # The price for the remaining period will be charged. 
+        # This option is only available for subscription upgrade.
+        IMMEDIATE_AND_CHARGE_PRORATED_PRICE,
+        # Replacement takes effect immediately, and the new price will be charged on
+        # next recurrence time. The billing cycle stays the same.
+        IMMEDIATE_WITHOUT_PRORATION,
+        # Replacement takes effect when the old plan expires, and the new price
+        # will be charged at the same time.
+        DEFERRED,
+        # Replacement takes effect immediately, and the user is charged full price
+        # of new plan and is given a full billing cycle of subscription,
+        # plus remaining prorated time from the old plan.
+        IMMEDIATE_AND_CHARGE_FULL_PRICE,
+    }
+
+
+Default behavior is ``IMMEDIATE_WITH_TIME_PRORATION``.
+
+Example use of ``updateSubscription``:
+
+::
+
+    payment.updateSubscription(_active_subscription_purchase.purchase_token, \
+						"new_sub_sku", SubscriptionProrationMode.IMMEDIATE_WITH_TIME_PRORATION)
+
+
+The ``confirmPriceChange()`` function can be used to launch price change confirmation flow
+for a subscription. Pass the product id of the subscription SKU subject to the price change.
+The result will be sent by the ``price_change_acknowledged`` signal.
+
+Example use of ``confirmPriceChange()``:
+
+::
+
+    enum BillingResponse {SUCCESS = 0, CANCELLED = 1}
+
+    func confirm_price_change(product_id):
+        payment.confirmPriceChange(product_id)
+
+    func _on_price_acknowledged(response_id):
+        if response_id == BillingResponse.SUCCESS:
+            print("price_change_accepted")
+        elif response_id == BillingResponse.CANCELED:
+            print("price_change_canceled")
+
+
+Migrating from Godot 3.2.1 and lower (GodotPaymentsV3)
+------------------------------------------------------
+
+The new ``GodotGooglePlayBilling`` API is not compatible with its predecessor ``GodotPaymentsV3``.
+
+Changes
+*******
+
+- You need to enable the Custom Build option in your Android export settings and install
+  the ``GodotGooglePlayBilling`` plugin manually (see below for details)
+- All purchases have to be acknowledged by your app. This is a
+  `requirement from Google <https://developer.android.com/google/play/billing/integrate#process>`__.
+  Purchases that are not acknowledged by your app will be refunded.
+- Support for subscriptions
+- Signals (no polling or callback objects)