Skip to main content

A Python wrapper for the Google Play Billing Library that facilitates in-app purchases and subscriptions in Android applications

Project description

SJBillingClient (Google Play Billing SDK for Python)

GitAds Sponsored

Sponsored by GitAds

Overview

SJBillingClient is a Python wrapper for the Google Play Billing Library that facilitates in-app purchases and subscriptions in Android applications. It provides a high-level, Pythonic interface to interact with Google Play's billing system, making it easier to implement and manage in-app purchases in Python-based Android apps (like those built with Kivy/Python-for-Android).

Key Features

  • Simplified Billing Integration: Easy-to-use Python API for Google Play Billing
  • Asynchronous Operations: Non-blocking billing operations
  • Comprehensive Purchase Management: Support for querying, purchasing, consuming, and acknowledging products
  • Product Types Support: Handles both one-time purchases (INAPP) and subscriptions (SUBS)
  • Detailed Product Information: Access to formatted prices, currency codes, and other product details

Requirements

  • Python 3.9+
  • pyjnius 1.6.1+
  • Android application with Google Play Billing Library (version 8.0.0 required)

Note: This library is specifically designed for Google Play Billing Library version 8.0.0. Earlier or later versions may not be compatible due to API changes.

Installation

# Using pip
pip install sjbillingclient
# In Buildozer (add to buildozer.spec)
requirements = sjbillingclient
android.gradle_dependencies = com.android.billingclient:billing:8.0.0

Quick Start

Here's a basic example of how to initialize the billing client and start a connection:

from sjbillingclient.tools import BillingClient
from sjbillingclient.jclass.billing import ProductType, BillingResponseCode

# Define callback for purchase updates
def on_purchases_updated(billing_result, is_null, purchases):
    if billing_result.getResponseCode() == BillingResponseCode.OK:
        if not is_null:
            for purchase in purchases:
                print(f"Purchase: {purchase.getProducts().get(0)}")
                # Handle purchase here

# Create billing client
client = BillingClient(on_purchases_updated)

# Start connection
client.start_connection(
    on_billing_setup_finished=lambda result: print(f"Billing setup complete: {result.getResponseCode()}"),
    on_billing_service_disconnected=lambda: print("Billing service disconnected")
)

Usage Examples

Querying Product Details

from sjbillingclient.tools import BillingClient
from sjbillingclient.jclass.billing import ProductType, BillingResponseCode

def on_product_details_response(billing_result, product_details_list):
    if billing_result.getResponseCode() == BillingResponseCode.OK:
        if product_details_list and not product_details_list.isEmpty():
            # Process product details
            for i in range(product_details_list.size()):
                product_detail = product_details_list.get(i)
                print(f"Product: {product_detail.getProductId()}")

                # Get formatted details
                details = client.get_product_details(product_detail, ProductType.INAPP)
                for detail in details:
                    print(f"Price: {detail['formatted_price']}")

# Query product details
client.query_product_details_async(
    product_type=ProductType.INAPP,
    products_ids=["product_id_1", "product_id_2"],
    on_product_details_response=on_product_details_response
)

Launching a Purchase Flow

from sjbillingclient.tools import BillingClient
from sjbillingclient.jclass.billing import ProductType, BillingResponseCode

def on_product_details_response(billing_result, product_details_list):
    if billing_result.getResponseCode() == BillingResponseCode.OK:
        if product_details_list and not product_details_list.isEmpty():
            # Launch billing flow with the first product
            product_detail = product_details_list.get(0)
            result = client.launch_billing_flow([product_detail])
            print(f"Launch billing flow result: {result.getResponseCode()}")

# Query product details and then launch purchase
client.query_product_details_async(
    product_type=ProductType.INAPP,
    products_ids=["product_id"],
    on_product_details_response=on_product_details_response
)

Consuming a Purchase

from sjbillingclient.tools import BillingClient
from sjbillingclient.jclass.billing import BillingResponseCode

def on_consume_response(billing_result, purchase_token):
    print(f"Consume result: {billing_result.getResponseCode()}")
    if billing_result.getResponseCode() == BillingResponseCode.OK:
        print(f"Successfully consumed: {purchase_token}")

# Consume a purchase
client.consume_async(purchase, on_consume_response)

Acknowledging a Purchase

from sjbillingclient.tools import BillingClient
from sjbillingclient.jclass.billing import BillingResponseCode

def on_acknowledge_purchase_response(billing_result):
    print(f"Acknowledge result: {billing_result.getResponseCode()}")
    if billing_result.getResponseCode() == BillingResponseCode.OK:
        print("Successfully acknowledged purchase")

# Acknowledge a purchase
client.acknowledge_purchase(purchase.getPurchaseToken(), on_acknowledge_purchase_response)

Querying Purchases

from sjbillingclient.tools import BillingClient
from sjbillingclient.jclass.billing import ProductType, BillingResponseCode
from sjbillingclient.jclass.purchase import PurchaseState

def on_query_purchases_response(billing_result, purchases):
    if billing_result.getResponseCode() == BillingResponseCode.OK:
        if purchases and not purchases.isEmpty():
            for i in range(purchases.size()):
                purchase = purchases.get(i)
                # Get formatted purchase details
                purchase_details = client.get_purchase(purchase)
                print(f"Products: {purchase_details['products']}")
                print(f"Purchase token: {purchase_details['purchase_token']}")
                print(f"Purchase state: {purchase_details['purchase_state']}")

                # Handle the purchase based on its state
                if purchase_details['purchase_state'] == PurchaseState.PURCHASED:
                    if not purchase_details['is_acknowledged']:
                        # Acknowledge the purchase
                        client.acknowledge_purchase(purchase.getPurchaseToken(), on_acknowledge_purchase_response)

# Query purchases for a specific product type
client.query_purchase_async(
    product_type=ProductType.INAPP,
    on_query_purchases_response=on_query_purchases_response
)

Kivy Integration Example

Here's a complete example of integrating SJBillingClient with a Kivy application:

Python Code (main.py)

from os.path import join, dirname, basename
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from sjbillingclient.jclass.billing import BillingResponseCode, ProductType
from sjbillingclient.tools import BillingClient

Builder.load_file(join(dirname(__file__), basename(__file__).split(".")[0] + ".kv"))


class HomeScreen(Screen):
    """
    A screen that demonstrates Google Play Billing integration with Kivy.

    This screen provides functionality to make in-app purchases and subscriptions
    using the Google Play Billing Library through the SJBillingClient wrapper.

    Attributes:
        billing_client (BillingClient): The client used to interact with Google Play Billing.
    """
    billing_client = None

    def support(self):
        """
        Initializes the billing client and starts a connection to the Google Play Billing service.

        This method is called when the user wants to make a purchase or subscription.
        If a billing client already exists, it ends the connection before creating a new one.
        """
        if self.billing_client:
            self.billing_client.end_connection()

        self.billing_client = BillingClient(on_purchases_updated=self.on_purchases_updated)
        self.billing_client.start_connection(
            on_billing_setup_finished=self.on_billing_setup_finished,
            on_billing_service_disconnected=lambda: print("disconnected")
        )

    def on_purchases_updated(self, billing_result, null, purchases):
        """
        Callback method that is called when purchases are updated.

        This method handles the result of a purchase flow, either acknowledging
        a subscription or consuming a one-time purchase.

        Args:
            billing_result: The result of the billing operation.
            null: Boolean indicating if the purchases list is null.
            purchases: List of purchases that were updated.
        """
        if billing_result.getResponseCode() == BillingResponseCode.OK and not null:
            for purchase in purchases:
                if self.ids.subscribe.active:
                    self.billing_client.acknowledge_purchase(
                        purchase_token=purchase.getPurchaseToken(),
                        on_acknowledge_purchase_response=self.on_acknowledge_purchase_response
                    )
                else:
                    self.billing_client.consume_async(purchase, self.on_consume_response)
        print(billing_result.getResponseCode(), billing_result.getDebugMessage())

    def on_acknowledge_purchase_response(self, billing_result):
        """
        Callback method that is called when a purchase acknowledgement is complete.

        Args:
            billing_result: The result of the acknowledgement operation.
        """
        print(billing_result.getDebugMessage())
        if billing_result.getResponseCode() == BillingResponseCode.OK:
            self.toast("Thank you for subscribing to buy us a cup of coffee! monthly")

    def on_consume_response(self, billing_result):
        """
        Callback method that is called when a purchase consumption is complete.

        Args:
            billing_result: The result of the consumption operation.
        """
        if billing_result.getResponseCode() == BillingResponseCode.OK:
            self.toast("Thank you for buying us a cup of coffee!")

    def on_product_details_response(self, billing_result, product_details_result):
        """
        Callback method that is called when product details are retrieved.

        This method processes the product details and launches the billing flow.

        Args:
            billing_result: The result of the product details query.
            product_details_result: The result containing product details and unfetched products.
        """
        product_details_list = product_details_result.getProductDetailsList()
        unfetched_product_list = product_details_result.getUnfetchedProductList()

        if billing_result.getResponseCode() == BillingResponseCode.OK:
            for product_details in product_details_list:
                self.billing_client.get_product_details(
                    product_details,
                    ProductType.SUBS if self.ids.subscribe.active else ProductType.INAPP)
            for unfetched_product in unfetched_product_list:
                print(self.billing_client.get_unfetched_product(unfetched_product))
            self.billing_client.launch_billing_flow(product_details=product_details_list)

    def on_billing_setup_finished(self, billing_result):
        """
        Callback method that is called when the billing setup is complete.

        This method queries product details if the billing setup was successful.

        Args:
            billing_result: The result of the billing setup operation.
        """
        product_id = self.ids.btn.product_id
        if billing_result.getResponseCode() == BillingResponseCode.OK:
            self.billing_client.query_product_details_async(
                product_type=ProductType.SUBS if self.ids.subscribe.active else ProductType.INAPP,
                products_ids=[product_id],
                on_product_details_response=self.on_product_details_response,
            )

    def toast(self, message):
        """
        Display a toast message.

        This is a simple implementation that just prints the message.
        In a real app, you would use platform-specific toast functionality.

        Args:
            message: The message to display.
        """
        # Implementation of toast message (platform specific)
        print(message)


class BillingApp(App):
    """
    Main application class for the SJBillingClient demo.

    This class sets up the application and creates the screen manager
    with the HomeScreen.
    """
    def build(self):
        """
        Build the application UI.

        Returns:
            ScreenManager: The root widget of the application.
        """
        # Create screen manager
        sm = ScreenManager()
        sm.add_widget(HomeScreen(name='home'))
        return sm


if __name__ == '__main__':
    BillingApp().run()

Kivy Layout File (main.kv)

<HomeScreen>:
    BoxLayout:
        orientation: 'vertical'
        padding: '20dp'
        spacing: '10dp'

        Label:
            text: 'SJBillingClient Demo'
            font_size: '24sp'
            size_hint_y: None
            height: '50dp'

        BoxLayout:
            orientation: 'horizontal'
            size_hint_y: None
            height: '50dp'

            Label:
                text: 'Subscribe'
                size_hint_x: 0.5

            CheckBox:
                id: subscribe
                size_hint_x: 0.5
                active: False

        Button:
            id: btn
            text: 'Buy Coffee'
            product_id: 'coffee_product_id'
            size_hint_y: None
            height: '60dp'
            on_release: root.support()

        Widget:
            # Spacer

This example demonstrates:

  1. A HomeScreen class that extends Screen and handles all billing operations
  2. A BillingApp class that sets up the Kivy application and screen manager
  3. A Kivy layout file that defines the UI with:
    • A checkbox to toggle between one-time purchase and subscription
    • A button to initiate the purchase flow

The support method is called when the button is pressed, which initializes the billing client and starts the connection. The various callback methods handle different stages of the billing process, including:

  • Handling purchase updates with on_purchases_updated
  • Acknowledging subscription purchases with acknowledge_purchase
  • Consuming one-time purchases with consume_async
  • Processing product details with on_product_details_response, including handling unfetched products
  • Querying product details with query_product_details_async

This example is designed to be copy-and-paste runnable, with no need for the user to add or remove anything to test it.

API Reference

BillingClient

The main class for interacting with Google Play Billing.

Constructor

  • __init__(on_purchases_updated, enable_one_time_products=True, enable_prepaid_plans=False):
    • Initializes a new BillingClient instance
    • on_purchases_updated: Callback function that will be triggered when purchases are updated
    • enable_one_time_products: Boolean to enable one-time products (default: True)
    • enable_prepaid_plans: Boolean to enable prepaid plans (default: False)

Connection Methods

  • start_connection(on_billing_setup_finished, on_billing_service_disconnected):

    • Starts a connection with the billing client
    • on_billing_setup_finished: Callback when billing setup is complete
    • on_billing_service_disconnected: Callback when billing service is disconnected
  • end_connection():

    • Ends the connection with the billing client

Product Details Methods

  • query_product_details_async(product_type, products_ids, on_product_details_response):

    • Queries product details asynchronously
    • product_type: Type of products (INAPP or SUBS)
    • products_ids: List of product IDs to query
    • on_product_details_response: Callback for product details response
  • get_product_details(product_details, product_type):

    • Gets formatted product details
    • product_details: Product details object
    • product_type: Type of product (INAPP or SUBS)
    • Returns a list of dictionaries with product details
  • get_unfetched_product(unfetched_product):

    • Gets details about an unfetched product
    • unfetched_product: Unfetched product object
    • Returns a dictionary with product ID, type, and status code
  • query_purchase_async(product_type, on_query_purchases_response):

    • Queries purchases asynchronously
    • product_type: Type of products (INAPP or SUBS)
    • on_query_purchases_response: Callback for purchases response
  • get_purchase(purchase):

    • Gets formatted purchase details
    • purchase: Purchase object
    • Returns a dictionary with purchase details including products, purchase token, purchase state, etc.

Purchase Methods

  • launch_billing_flow(product_details, offer_token=None):

    • Launches the billing flow for purchase
    • product_details: List of product details objects
    • offer_token: Optional token for subscription offers
  • consume_async(purchase, on_consume_response):

    • Consumes a purchase asynchronously
    • purchase: Purchase object to consume
    • on_consume_response: Callback for consume response
  • acknowledge_purchase(purchase_token, on_acknowledge_purchase_response):

    • Acknowledges a purchase
    • purchase_token: Token of the purchase to acknowledge
    • on_acknowledge_purchase_response: Callback for acknowledge response

PendingPurchasesParams

Parameters for handling pending purchases.

Methods

  • newBuilder(): Creates a new builder for PendingPurchasesParams
  • build(): Builds the PendingPurchasesParams object
  • enableOneTimeProducts(): Enables one-time products
  • enablePrepaidPlans(): Enables prepaid plans

QueryProductDetailsParams

Parameters for querying product details.

Methods

  • newBuilder(): Creates a new builder for QueryProductDetailsParams
  • setProductList(product_list): Sets the list of products to query
  • build(): Builds the QueryProductDetailsParams object

QueryProductDetailsResult

Result of a product details query.

Methods

  • getProductDetailsList(): Gets the list of product details
  • getUnfetchedProductList(): Gets the list of unfetched products

Purchase

Represents a purchase made by a user.

Methods

  • getProducts(): Gets the list of product IDs associated with the purchase
  • getPurchaseToken(): Gets the token that uniquely identifies the purchase

PurchaseState

Constants for purchase states:

  • PurchaseState.PENDING: Purchase is pending
  • PurchaseState.PURCHASED: Purchase is completed
  • PurchaseState.UNSPECIFIED: Purchase state is unspecified

PendingPurchaseUpdate

Represents a pending update to a purchase.

Methods

  • getProducts(): Gets the list of product IDs associated with the pending purchase update
  • getPurchaseToken(): Gets the token that uniquely identifies the pending purchase update

AccountIdentifiers

Contains account identifiers for a purchase.

Methods

  • getObfuscatedAccountId(): Gets the obfuscated account ID
  • getProfileId(): Gets the profile ID

ProductType

Constants for product types:

  • ProductType.INAPP: One-time purchases
  • ProductType.SUBS: Subscriptions

BillingFlowParamsBuilder

Builder for BillingFlowParams.

Methods

  • build(): Builds the BillingFlowParams object
  • setIsOfferPersonalized(boolean): Sets whether the offer is personalized
  • setObfuscatedAccountId(String): Sets the obfuscated account ID
  • setObfuscatedProfileId(String): Sets the obfuscated profile ID
  • setProductDetailsParamsList(List): Sets the list of product details parameters
  • setSubscriptionUpdateParams(SubscriptionUpdateParams): Sets the subscription update parameters

SubscriptionUpdateParams

Parameters for updating a subscription.

Methods

  • newBuilder(): Creates a new builder for SubscriptionUpdateParams
  • build(): Builds the SubscriptionUpdateParams object

SubscriptionUpdateParamsBuilder

Builder for SubscriptionUpdateParams.

Methods

  • build(): Builds the SubscriptionUpdateParams object
  • setOldPurchaseToken(String): Sets the token of the old purchase to be replaced
  • setSubscriptionReplacementMode(int): Sets the replacement mode for the subscription

ReplacementMode

Constants for subscription replacement modes:

  • ReplacementMode.CHARGE_FULL_PRICE: Charge the full price for the new subscription
  • ReplacementMode.CHARGE_PRORATED_PRICE: Charge a prorated price for the new subscription
  • ReplacementMode.DEFERRED: Defer the replacement until the next billing cycle
  • ReplacementMode.WITHOUT_PRORATION: Replace without proration
  • ReplacementMode.WITH_TIME_PRORATION: Replace with time proration

BillingResponseCode

Constants for billing response codes:

  • BillingResponseCode.OK: Success (0)
  • BillingResponseCode.USER_CANCELED: User canceled (1)
  • BillingResponseCode.SERVICE_UNAVAILABLE: Service unavailable (2)
  • BillingResponseCode.BILLING_UNAVAILABLE: Billing unavailable (3)
  • BillingResponseCode.ITEM_UNAVAILABLE: Item unavailable (4)
  • BillingResponseCode.DEVELOPER_ERROR: Developer error (5)
  • BillingResponseCode.ERROR: General error (6)
  • BillingResponseCode.ITEM_ALREADY_OWNED: Item already owned (7)
  • BillingResponseCode.ITEM_NOT_OWNED: Item not owned (8)
  • BillingResponseCode.SERVICE_DISCONNECTED: Service disconnected (10)
  • BillingResponseCode.FEATURE_NOT_SUPPORTED: Feature not supported (12)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Author

Kenechukwu Akubue kengoon19@gmail.com

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sjbillingclient-1.1.0.tar.gz (20.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sjbillingclient-1.1.0-py3-none-any.whl (21.1 kB view details)

Uploaded Python 3

File details

Details for the file sjbillingclient-1.1.0.tar.gz.

File metadata

  • Download URL: sjbillingclient-1.1.0.tar.gz
  • Upload date:
  • Size: 20.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.0 CPython/3.13.8 Linux/6.14.0-36-generic

File hashes

Hashes for sjbillingclient-1.1.0.tar.gz
Algorithm Hash digest
SHA256 135c650461454418b42397ba01ba5f142c48a58ae5640f1dec864afee78587de
MD5 23292e7224d811fc383ae3170dbd7cd4
BLAKE2b-256 6d9132195d8a0608806e0722539b3775ee9ad08cdd61e278c22d685b617718ef

See more details on using hashes here.

File details

Details for the file sjbillingclient-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: sjbillingclient-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.0 CPython/3.13.8 Linux/6.14.0-36-generic

File hashes

Hashes for sjbillingclient-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f22178102b5bffa006cb7151dc323b69a1c3aad90147d103a034a30f705ae71d
MD5 64388bbf285bcd1c114152e826748eae
BLAKE2b-256 64b1ab7ef81947b24d15f4300ad6dfec2e083df2b3841dcfd12139a5d3359b68

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page