2
0
Эх сурвалжийг харах

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 жил өмнө
parent
commit
dafb033c38

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

@@ -3,48 +3,37 @@
 Android in-app purchases
 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
 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>`__.
 `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
 Usage
 -----
 -----
 
 
 Getting started
 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`.
 and put both into `res://android/plugins`.
 The plugin should now show up in the Android export settings, where you can enable it.
 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
             # These are all signals supported by the API
             # You can drop some of these based on your needs
             # 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("connected", self, "_on_connected") # No params
             payment.connect("disconnected", self, "_on_disconnected") # 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("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("purchases_updated", self, "_on_purchases_updated") # Purchases (Dictionary[])
             payment.connect("purchase_error", self, "_on_purchase_error") # Response ID (int), Debug message (string)
             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[])
             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_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_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("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()
             payment.startConnection()
         else:
         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.")
             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:
       for available_sku in sku_details:
         print(available_sku)
         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
 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")
     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):
     func _on_purchases_updated(purchases):
         for purchase in 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
 Consumables
 ***********
 ***********
 
 
 If your in-app item is not a one-time purchase but a consumable item (e.g. coins) which can be purchased
 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
 *************
 *************
 
 
-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)