sjbillingclient 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,41 @@
1
+ """
2
+ A Python wrapper for the Google Play Billing Library that facilitates in-app purchases and subscriptions.
3
+
4
+ This module provides a high-level interface to interact with Google Play's billing system through
5
+ the BillingClient class. It handles various billing operations including:
6
+
7
+ - Establishing and managing billing service connections
8
+ - Querying product details for in-app purchases and subscriptions
9
+ - Processing purchase flows
10
+ - Handling consumption of purchased items
11
+ - Managing purchase acknowledgments
12
+
13
+ Key Features:
14
+ - Asynchronous billing operations
15
+ - Support for both one-time purchases (INAPP) and subscriptions (SUBS)
16
+ - Product details querying with price formatting
17
+ - Purchase flow management
18
+ - Consumption and acknowledgment handling
19
+
20
+ Example:
21
+ ```python
22
+ def on_purchases_updated(billing_result, purchases):
23
+ # Handle purchase updates
24
+ pass
25
+
26
+ client = BillingClient(on_purchases_updated)
27
+ client.start_connection(
28
+ on_billing_setup_finished=lambda result: print("Billing setup complete"),
29
+ on_billing_service_disconnected=lambda: print("Billing service disconnected")
30
+ )
31
+ ```
32
+
33
+ Dependencies:
34
+ - jnius: For Java/Android interop
35
+ - android.activity: For access to the Android activity context
36
+ """
37
+
38
+ from typing import List, Dict, Optional
1
39
  from jnius import autoclass, JavaException
2
40
  from sjbillingclient.jclass.acknowledge import AcknowledgePurchaseParams
3
41
  from sjbillingclient.jclass.billing import BillingClient as SJBillingClient, ProductType, ProductDetailsParams, \
@@ -12,17 +50,48 @@ from sjbillingclient.jinterface.product import ProductDetailsResponseListener
12
50
  from sjbillingclient.jinterface.purchases import PurchasesUpdatedListener
13
51
 
14
52
 
53
+ ERROR_NO_BASE_PLAN = "You don't have a base plan"
54
+ ERROR_NO_BASE_PLAN_ID = "You don't have a base plan id"
55
+ ERROR_INVALID_PRODUCT_TYPE = "product_type not supported. Must be one of `ProductType.SUBS`, `ProductType.INAPP`"
56
+
57
+
15
58
  class BillingClient:
16
- __billing_client = None
17
- __purchase_update_listener = None
18
- __billing_client_state_listener = None
19
- __product_details_response_listener = None
20
- __consume_response_listener = None
21
- __acknowledge_purchase_response_listener = None
22
-
23
- def __init__(self, on_purchases_updated):
24
- self.__purchase_update_listener = PurchasesUpdatedListener(on_purchases_updated)
59
+ """
60
+ Provides methods and functionality to manage billing operations, including starting and terminating
61
+ billing connections, querying product details, launching billing flows, consuming purchases, and
62
+ acknowledging purchases.
63
+
64
+ This class acts as a wrapper around the Google Play Billing Library, simplifying the process of in-app
65
+ purchases and subscriptions by integrating higher-level methods for essential billing features.
66
+
67
+ :ivar __billing_client: The main billing client that interacts with the Google Play Billing Library.
68
+ :type __billing_client: SJBillingClient
69
+ :ivar __purchase_update_listener: Listener for purchase updates and processing purchase-related events.
70
+ :type __purchase_update_listener: PurchasesUpdatedListener
71
+ :ivar __billing_client_state_listener: Listener for tracking the state of the billing client connection.
72
+ :type __billing_client_state_listener: BillingClientStateListener | None
73
+ :ivar __product_details_response_listener: Listener that handles the response for product details queries.
74
+ :type __product_details_response_listener: ProductDetailsResponseListener | None
75
+ :ivar __consume_response_listener: Listener handling responses for consumption requests.
76
+ :type __consume_response_listener: ConsumeResponseListener | None
77
+ :ivar __acknowledge_purchase_response_listener: Listener handling responses for acknowledging purchases.
78
+ :type __acknowledge_purchase_response_listener: AcknowledgePurchaseResponseListener | None
79
+ """
80
+ def __init__(self, on_purchases_updated) -> None:
81
+ """
82
+ Initializes an instance of the class with the given purchase update callback.
25
83
 
84
+ :param on_purchases_updated: A callback function that will be triggered when purchases
85
+ are updated. This function typically handles updates to purchases such as
86
+ processing the results or actions related to the purchases.
87
+ :type on_purchases_updated: callable
88
+ """
89
+ self.__billing_client_state_listener = None
90
+ self.__product_details_response_listener = None
91
+ self.__consume_response_listener = None
92
+ self.__acknowledge_purchase_response_listener = None
93
+
94
+ self.__purchase_update_listener = PurchasesUpdatedListener(on_purchases_updated)
26
95
  self.__billing_client = (
27
96
  SJBillingClient.newBuilder(activity.context)
28
97
  .setListener(self.__purchase_update_listener)
@@ -30,96 +99,282 @@ class BillingClient:
30
99
  .build()
31
100
  )
32
101
 
33
- def start_connection(self, on_billing_setup_finished, on_billing_service_disconnected):
102
+ def start_connection(self, on_billing_setup_finished, on_billing_service_disconnected) -> None:
103
+ """
104
+ Starts a connection with the billing client and initializes the billing
105
+ client state listener. This method sets up a listener to handle billing
106
+ setup finished and billing service disconnection callbacks.
107
+
108
+ :param on_billing_setup_finished: A callable that will be invoked when
109
+ the billing setup has finished.
110
+ :param on_billing_service_disconnected: A callable that will be invoked
111
+ when the billing service gets disconnected.
112
+ :return: None
113
+ """
34
114
  self.__billing_client_state_listener = BillingClientStateListener(
35
115
  on_billing_setup_finished,
36
116
  on_billing_service_disconnected
37
117
  )
38
118
  self.__billing_client.startConnection(self.__billing_client_state_listener)
39
119
 
40
- def end_connection(self):
120
+ def end_connection(self) -> None:
121
+ """
122
+ Ends the connection with the billing client.
123
+
124
+ This method terminates the active connection with the billing client
125
+ by calling its `endConnection` method. It ensures proper cleanup of
126
+ resources related to the billing client.
127
+
128
+ :return: None
129
+ """
41
130
  self.__billing_client.endConnection()
42
131
 
43
- def query_product_details_async(self, product_type, products_ids: list, on_product_details_response):
44
- List = autoclass("java.util.List")
45
- queryProductDetailsParams = (
46
- QueryProductDetailsParams.newBuilder()
47
- .setProductList(
48
- List.of(*[
49
- QueryProductDetailsParamsProduct.newBuilder()
50
- .setProductId(product_id)
51
- .setProductType(product_type)
52
- .build()
53
- for product_id in products_ids
54
- ])
55
- )
56
- .build()
57
- )
132
+ def query_product_details_async(self, product_type: str, products_ids: List[str],
133
+ on_product_details_response) -> None:
134
+ """
135
+ Queries product details asynchronously for a given list of product IDs and product type.
58
136
 
59
- self.__product_details_response_listener = ProductDetailsResponseListener(on_product_details_response)
137
+ This function utilizes the provided product details response callback to handle the
138
+ resulting response from the query.
60
139
 
61
- self.__billing_client.queryProductDetailsAsync(
62
- queryProductDetailsParams,
63
- self.__product_details_response_listener
64
- )
140
+ :param product_type: The type of the products to be queried (e.g., "inapp" or "subs").
141
+ :param products_ids: A list of product IDs to query details for.
142
+ :param on_product_details_response: A callback function that is triggered when the
143
+ product details query is complete.
144
+ :return: None
145
+ """
146
+ JavaList = autoclass("java.util.List")
147
+ product_list = [
148
+ self._build_product_params(product_id, product_type)
149
+ for product_id in products_ids
150
+ ]
151
+
152
+ params = (QueryProductDetailsParams.newBuilder()
153
+ .setProductList(JavaList.of(*product_list))
154
+ .build())
155
+
156
+ self.__product_details_response_listener = ProductDetailsResponseListener(on_product_details_response)
157
+ self.__billing_client.queryProductDetailsAsync(params, self.__product_details_response_listener)
65
158
 
66
159
  @staticmethod
67
- def get_product_details(product_details, product_type):
68
- details = []
160
+ def _build_product_params(product_id: str, product_type: str):
161
+ """
162
+ Builds product parameters using the provided product ID and product type.
163
+
164
+ This is a static helper method designed to construct and return an object
165
+ representing the parameters for querying product details. It uses a builder
166
+ pattern for constructing the product details object.
167
+
168
+ :param product_id: The unique identifier of the product.
169
+ :type product_id: str
170
+ :param product_type: The type/category of the product (e.g., consumable, subscription).
171
+ :type product_type: str
172
+ :return: A constructed product details parameter object.
173
+ :rtype: QueryProductDetailsParamsProduct
174
+ """
175
+ return (QueryProductDetailsParamsProduct.newBuilder()
176
+ .setProductId(product_id)
177
+ .setProductType(product_type)
178
+ .build())
179
+
180
+ def get_product_details(self, product_details, product_type: str) -> List[Dict]:
181
+ """
182
+ Retrieves the details of a product based on the provided product type. The function processes
183
+ different types of products, such as subscriptions and in-app purchases, and returns the corresponding
184
+ details.
185
+
186
+ If the product type is not recognized, an exception is raised.
187
+
188
+ :param product_details: The details of the product to process.
189
+ :param product_type: The type of the product. It can either be 'SUBS' for subscriptions or 'INAPP' for in-app purchases.
190
+ :return: A list of dictionaries containing the processed product details.
191
+ :rtype: List[Dict]
192
+ :raises Exception: If the specified product type is invalid.
193
+ """
69
194
  if product_type == ProductType.SUBS:
70
- offer_details = product_details.getSubscriptionOfferDetails()
71
- for offer in offer_details:
72
- pricing_phase = offer.getPricingPhases().getPricingPhaseList().get(0)
73
- details.append({
74
- "product_id": product_details.getProductId(),
75
- "formatted_price": pricing_phase.getFormattedPrice(),
76
- "price_amount_micros": pricing_phase.getPriceAmountMicros,
77
- "price_currency_code": pricing_phase.getPriceCurrencyCode(),
78
- })
79
- return details
195
+ return self._get_subscription_details(product_details)
80
196
  elif product_type == ProductType.INAPP:
81
- offer_details = product_details.getOneTimePurchaseOfferDetails()
82
- details.append({
83
- "product_id": product_details.getProductId(),
84
- "formatted_price": offer_details.getFormattedPrice(),
85
- "price_amount_micros": offer_details.getPriceAmountMicros,
86
- "price_currency_code": offer_details.getPriceCurrencyCode(),
87
- })
88
- return details
89
- raise Exception("product_type not supported. Must be one of `ProductType.SUBS`, `ProductType.INAPP`")
90
-
91
- def launch_billing_flow(self, product_details: list, offer_token: str = None):
92
-
93
- product_details_param_list = []
94
-
95
- for product_detail in product_details:
96
- params = ProductDetailsParams.newBuilder()
97
- params.setProductDetails(product_detail)
98
- if product_detail.getProductType() == ProductType.SUBS:
99
- if not offer_token:
100
- offer_list = product_detail.getSubscriptionOfferDetails()
101
- if not offer_list or offer_list.isEmpty():
102
- raise JavaException("You don't have a base plan")
103
- base_plan_id = offer_list.get(0).getBasePlanId()
104
- offer_token = offer_list.get(0).getOfferToken()
105
- if not base_plan_id:
106
- raise JavaException("You don't have a base plan id")
107
- params.setOfferToken(offer_token)
108
- else:
109
- params.setOfferToken(offer_token)
110
- product_details_param_list.append(params.build())
111
- List = autoclass("java.util.List")
112
-
113
- billing_flow_params = (
114
- BillingFlowParams.newBuilder()
115
- .setProductDetailsParamsList(List.of(*product_details_param_list))
116
- .build()
117
- )
197
+ return self._get_inapp_purchase_details(product_details)
198
+ raise Exception(ERROR_INVALID_PRODUCT_TYPE)
199
+
200
+ def _get_subscription_details(self, product_details) -> List[Dict]:
201
+ """
202
+ Retrieves subscription details from the provided product details by parsing its
203
+ subscription offer details and related pricing phases. The extracted details
204
+ include product ID, formatted price, price amount in micros, and the currency code.
205
+
206
+ :param product_details: Contains information about the product including
207
+ subscription offers and pricing.
208
+ :type product_details: Any
209
+ :return: List of dictionaries, each containing subscription details such as
210
+ product ID, formatted price, price amount in micros, and currency code.
211
+ :rtype: List[Dict]
212
+ """
213
+ details = []
214
+ offer_details = product_details.getSubscriptionOfferDetails()
215
+ for offer in offer_details:
216
+ pricing_phase = offer.getPricingPhases().getPricingPhaseList().get(0)
217
+ details.append(self._create_product_detail_dict(
218
+ product_details.getProductId(),
219
+ pricing_phase.getFormattedPrice(),
220
+ pricing_phase.getPriceAmountMicros,
221
+ pricing_phase.getPriceCurrencyCode()
222
+ ))
223
+ return details
224
+
225
+ def _get_inapp_purchase_details(self, product_details) -> List[Dict]:
226
+ """
227
+ Retrieve and construct in-app purchase product details.
228
+
229
+ This function takes product details from an in-app purchase API response and
230
+ constructs a list of dictionaries containing formatted product details. Each
231
+ dictionary includes details such as product ID, formatted price, raw price
232
+ amount, and currency code. Only one-time purchase offer details are supported.
233
+
234
+ :param product_details: Product details object containing information about
235
+ an in-app purchase product.
236
+ :type product_details: Any
237
+ :return: A list of dictionaries representing constructed product details
238
+ based on the provided product information.
239
+ :rtype: List[Dict]
240
+ """
241
+ offer_details = product_details.getOneTimePurchaseOfferDetails()
242
+ return [self._create_product_detail_dict(
243
+ product_details.getProductId(),
244
+ offer_details.getFormattedPrice(),
245
+ offer_details.getPriceAmountMicros,
246
+ offer_details.getPriceCurrencyCode()
247
+ )]
248
+
249
+ @staticmethod
250
+ def _create_product_detail_dict(product_id: str, formatted_price: str,
251
+ price_amount_micros, price_currency_code: str) -> Dict:
252
+ """
253
+ Creates a dictionary containing product details.
254
+
255
+ This method generates and returns a dictionary that encapsulates product
256
+ details such as the product identifier, formatted price, price in micros,
257
+ and the price currency code.
258
+
259
+ :param product_id: The unique identifier for the product.
260
+ :type product_id: str
261
+ :param formatted_price: The human-readable price of the product.
262
+ :type formatted_price: str
263
+ :param price_amount_micros: The price of the product in micro-units.
264
+ :type price_amount_micros: int
265
+ :param price_currency_code: The currency code associated with the product price.
266
+ :type price_currency_code: str
267
+ :return: A dictionary holding the product details.
268
+ :rtype: Dict
269
+ """
270
+ return {
271
+ "product_id": product_id,
272
+ "formatted_price": formatted_price,
273
+ "price_amount_micros": price_amount_micros,
274
+ "price_currency_code": price_currency_code,
275
+ }
276
+
277
+ def launch_billing_flow(self, product_details: List, offer_token: Optional[str] = None):
278
+ """
279
+ Initiates the in-app billing flow for the specified product details and an optional
280
+ offer token. The method constructs billing flow parameters using the provided product
281
+ details and triggers the billing process.
282
+
283
+ :param product_details: A list of product detail objects representing the items
284
+ available for purchase through the billing flow.
285
+ :param offer_token: Optional string representing a unique token to identify
286
+ specific offers for the product being purchased.
287
+ :return: An integer identifier returned by the billing client representing the
288
+ result of the billing flow launch attempt.
289
+ """
290
+ JavaList = autoclass("java.util.List")
291
+ product_params_list = [
292
+ self._create_product_params(product_detail, offer_token)
293
+ for product_detail in product_details
294
+ ]
295
+
296
+ billing_flow_params = (BillingFlowParams.newBuilder()
297
+ .setProductDetailsParamsList(JavaList.of(*product_params_list))
298
+ .build())
299
+
300
+ return self.__billing_client.launchBillingFlow(activity, billing_flow_params)
118
301
 
119
- billing_result = self.__billing_client.launchBillingFlow(activity, billing_flow_params)
120
- return billing_result
302
+ def _create_product_params(self, product_detail, offer_token: Optional[str]):
303
+ """
304
+ Creates and builds product parameters for a given product detail and optional offer token.
305
+
306
+ This method initializes the `ProductDetailsParams` through its builder,
307
+ populates it with the provided product detail, and conditionally sets the
308
+ offer token if the product type is a subscription (SUBS). Once all
309
+ necessary values are set, it builds and returns the params.
310
+
311
+ :param product_detail: The product details used for generating the params.
312
+ :type product_detail: ProductDetails
313
+
314
+ :param offer_token: Optional token specific to the offer for the subscription
315
+ product. Will be resolved internally if not provided and the product type
316
+ is a subscription.
317
+ :type offer_token: Optional[str]
318
+
319
+ :return: A fully constructed `ProductDetailsParams` object populated
320
+ with the input product data and offer token, if applicable.
321
+ :rtype: ProductDetailsParams
322
+ """
323
+ params = ProductDetailsParams.newBuilder()
324
+ params.setProductDetails(product_detail)
325
+
326
+ if product_detail.getProductType() == ProductType.SUBS:
327
+ offer_token = self._resolve_offer_token(product_detail, offer_token)
328
+ params.setOfferToken(offer_token)
329
+
330
+ return params.build()
331
+
332
+ @staticmethod
333
+ def _resolve_offer_token(product_detail, offer_token: Optional[str]) -> str:
334
+ """
335
+ Resolves the offer token for a given product detail. If the provided offer token is
336
+ not `None`, it returns the same. Otherwise, it determines the offer token using
337
+ the subscription offer details from the product detail. Raises exceptions if the
338
+ required information is missing.
339
+
340
+ :param product_detail: The product detail object containing subscription offer
341
+ details.
342
+ :type product_detail: Any
343
+ :param offer_token: A string value representing the offer token if provided,
344
+ or None to attempt resolving it from the product detail.
345
+ :type offer_token: Optional[str]
346
+ :return: The resolved offer token as a string.
347
+ :rtype: str
348
+ :raises JavaException: When no base plan or base plan ID is found in the
349
+ subscription offer details.
350
+ """
351
+ if offer_token:
352
+ return offer_token
353
+
354
+ offer_list = product_detail.getSubscriptionOfferDetails()
355
+ if not offer_list or offer_list.isEmpty():
356
+ raise JavaException(ERROR_NO_BASE_PLAN)
357
+
358
+ base_plan_id = offer_list.get(0).getBasePlanId()
359
+ if not base_plan_id:
360
+ raise JavaException(ERROR_NO_BASE_PLAN_ID)
361
+
362
+ return offer_list.get(0).getOfferToken()
121
363
 
122
364
  def consume_async(self, purchase, on_consume_response):
365
+ """
366
+ Consumes a given purchase asynchronously using the billing client. The method takes
367
+ a purchase object and a callback function that is triggered upon the consumption's
368
+ completion. This process involves creating appropriate consume parameters and invoking
369
+ the consume operation on the billing client.
370
+
371
+ :param purchase: The purchase object to consume.
372
+ :type purchase: Purchase
373
+ :param on_consume_response: The callback to execute upon completion of the consumption
374
+ process. This should handle the result of the consumption.
375
+ :type on_consume_response: Callable
376
+ :return: None
377
+ """
123
378
  consume_params = (
124
379
  ConsumeParams.newBuilder()
125
380
  .setPurchaseToken(purchase.getPurchaseToken())
@@ -129,6 +384,20 @@ class BillingClient:
129
384
  self.__billing_client.consumeAsync(consume_params, self.__consume_response_listener)
130
385
 
131
386
  def acknowledge_purchase(self, purchase_token, on_acknowledge_purchase_response):
387
+ """
388
+ Acknowledges a purchase using the provided purchase token. This method communicates
389
+ with the billing client to confirm the completion of a purchase, ensuring its validity
390
+ and acknowledgment. A callback function is triggered once the acknowledgment process
391
+ is complete.
392
+
393
+ :param purchase_token: The token representing the purchase to be acknowledged.
394
+ :type purchase_token: str
395
+ :param on_acknowledge_purchase_response: A callback function to handle the
396
+ response of the acknowledgment process. It is triggered upon completion.
397
+ :type on_acknowledge_purchase_response: Callable[[AcknowledgePurchaseResponse], None]
398
+
399
+ :return: None
400
+ """
132
401
  acknowledge_purchase_params = (
133
402
  AcknowledgePurchaseParams.newBuilder()
134
403
  .setPurchaseToken(purchase_token)
@@ -0,0 +1,339 @@
1
+ Metadata-Version: 2.1
2
+ Name: sjbillingclient
3
+ Version: 0.1.4
4
+ Summary:
5
+ Author: Kenechukwu Akubue
6
+ Author-email: kengoon19@gmail.com
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: poetry-core (>=2.1.3,<3.0.0)
14
+ Requires-Dist: pyjnius (>=1.6.1,<2.0.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # SJBillingClient (Google Play Billing SDK for Python)
18
+
19
+ <!-- GitAds-Verify: 71CCWMQSMVD67LS4WF4N44EXISSL2UTQ -->
20
+ ## GitAds Sponsored
21
+ [![Sponsored by GitAds](https://gitads.dev/v1/ad-serve?source=simplejnius/sj-android-billingclient@github)](https://gitads.dev/v1/ad-track?source=simplejnius/sj-android-billingclient@github)
22
+
23
+ ## Overview
24
+
25
+ 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).
26
+
27
+ ### Key Features
28
+
29
+ - **Simplified Billing Integration**: Easy-to-use Python API for Google Play Billing
30
+ - **Asynchronous Operations**: Non-blocking billing operations
31
+ - **Comprehensive Purchase Management**: Support for querying, purchasing, consuming, and acknowledging products
32
+ - **Product Types Support**: Handles both one-time purchases (INAPP) and subscriptions (SUBS)
33
+ - **Detailed Product Information**: Access to formatted prices, currency codes, and other product details
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.9+
38
+ - pyjnius 1.6.1+
39
+ - Android application with Google Play Billing Library (version 7.1.1 recommended)
40
+
41
+ ## Installation
42
+
43
+ ```shell
44
+ # Using pip
45
+ pip install sjbillingclient
46
+
47
+ # In Buildozer (add to buildozer.spec)
48
+ requirements = sjbillingclient
49
+ android.gradle_dependencies = com.android.billingclient:billing:7.1.1
50
+ ```
51
+
52
+ ## Quick Start
53
+
54
+ Here's a basic example of how to initialize the billing client and start a connection:
55
+
56
+ ```python
57
+ from sjbillingclient.tools import BillingClient
58
+ from sjbillingclient.jclass.billing import ProductType, BillingResponseCode
59
+
60
+ # Define callback for purchase updates
61
+ def on_purchases_updated(billing_result, is_null, purchases):
62
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
63
+ if not is_null:
64
+ for purchase in purchases:
65
+ print(f"Purchase: {purchase.getProducts().get(0)}")
66
+ # Handle purchase here
67
+
68
+ # Create billing client
69
+ client = BillingClient(on_purchases_updated)
70
+
71
+ # Start connection
72
+ client.start_connection(
73
+ on_billing_setup_finished=lambda result: print(f"Billing setup complete: {result.getResponseCode()}"),
74
+ on_billing_service_disconnected=lambda: print("Billing service disconnected")
75
+ )
76
+ ```
77
+
78
+ ## Usage Examples
79
+
80
+ ### Querying Product Details
81
+
82
+ ```python
83
+ from sjbillingclient.tools import BillingClient
84
+ from sjbillingclient.jclass.billing import ProductType, BillingResponseCode
85
+
86
+ def on_product_details_response(billing_result, product_details_list):
87
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
88
+ if product_details_list and not product_details_list.isEmpty():
89
+ # Process product details
90
+ for i in range(product_details_list.size()):
91
+ product_detail = product_details_list.get(i)
92
+ print(f"Product: {product_detail.getProductId()}")
93
+
94
+ # Get formatted details
95
+ details = client.get_product_details(product_detail, ProductType.INAPP)
96
+ for detail in details:
97
+ print(f"Price: {detail['formatted_price']}")
98
+
99
+ # Query product details
100
+ client.query_product_details_async(
101
+ product_type=ProductType.INAPP,
102
+ products_ids=["product_id_1", "product_id_2"],
103
+ on_product_details_response=on_product_details_response
104
+ )
105
+ ```
106
+
107
+ ### Launching a Purchase Flow
108
+
109
+ ```python
110
+ from sjbillingclient.tools import BillingClient
111
+ from sjbillingclient.jclass.billing import ProductType, BillingResponseCode
112
+
113
+ def on_product_details_response(billing_result, product_details_list):
114
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
115
+ if product_details_list and not product_details_list.isEmpty():
116
+ # Launch billing flow with the first product
117
+ product_detail = product_details_list.get(0)
118
+ result = client.launch_billing_flow([product_detail])
119
+ print(f"Launch billing flow result: {result.getResponseCode()}")
120
+
121
+ # Query product details and then launch purchase
122
+ client.query_product_details_async(
123
+ product_type=ProductType.INAPP,
124
+ products_ids=["product_id"],
125
+ on_product_details_response=on_product_details_response
126
+ )
127
+ ```
128
+
129
+ ### Consuming a Purchase
130
+
131
+ ```python
132
+ from sjbillingclient.tools import BillingClient
133
+ from sjbillingclient.jclass.billing import BillingResponseCode
134
+
135
+ def on_consume_response(billing_result, purchase_token):
136
+ print(f"Consume result: {billing_result.getResponseCode()}")
137
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
138
+ print(f"Successfully consumed: {purchase_token}")
139
+
140
+ # Consume a purchase
141
+ client.consume_async(purchase, on_consume_response)
142
+ ```
143
+
144
+ ### Acknowledging a Purchase
145
+
146
+ ```python
147
+ from sjbillingclient.tools import BillingClient
148
+ from sjbillingclient.jclass.billing import BillingResponseCode
149
+
150
+ def on_acknowledge_purchase_response(billing_result):
151
+ print(f"Acknowledge result: {billing_result.getResponseCode()}")
152
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
153
+ print("Successfully acknowledged purchase")
154
+
155
+ # Acknowledge a purchase
156
+ client.acknowledge_purchase(purchase.getPurchaseToken(), on_acknowledge_purchase_response)
157
+ ```
158
+
159
+ ### Kivy Integration Example
160
+
161
+ Here's a complete example of integrating SJBillingClient with a Kivy application:
162
+
163
+ #### Python Code (main.py)
164
+
165
+ ```python
166
+ from os.path import join, dirname, basename
167
+ from kivy.app import App
168
+ from kivy.lang import Builder
169
+ from kivy.uix.screenmanager import ScreenManager, Screen
170
+ from sjbillingclient.jclass.billing import BillingResponseCode, ProductType
171
+ from sjbillingclient.tools import BillingClient
172
+
173
+ # Load the KV file
174
+ Builder.load_file(join(dirname(__file__), basename(__file__).split(".")[0] + ".kv"))
175
+
176
+
177
+ class HomeScreen(Screen):
178
+ billing_client = None
179
+
180
+ def purchase_or_subscribe(self):
181
+ if self.billing_client:
182
+ self.billing_client.end_connection()
183
+
184
+ self.billing_client = BillingClient(on_purchases_updated=self.on_purchases_updated)
185
+ self.billing_client.start_connection(
186
+ on_billing_setup_finished=self.on_billing_setup_finished,
187
+ on_billing_service_disconnected=lambda: print("disconnected")
188
+ )
189
+
190
+ def on_purchases_updated(self, billing_result, null, purchases):
191
+ if billing_result.getResponseCode() == BillingResponseCode.OK and not null:
192
+ for purchase in purchases:
193
+ if self.ids.subscribe.active:
194
+ self.billing_client.acknowledge_purchase(
195
+ purchase_token=purchase.getPurchaseToken(),
196
+ on_acknowledge_purchase_response=self.on_acknowledge_purchase_response
197
+ )
198
+ else:
199
+ self.billing_client.consume_async(purchase, self.on_consume_response)
200
+
201
+ def on_acknowledge_purchase_response(self, billing_result):
202
+ print(billing_result.getDebugMessage())
203
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
204
+ self.toast("Thank you for subscribing to buy us a cup of coffee! monthly")
205
+
206
+ def on_consume_response(self, billing_result):
207
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
208
+ self.toast("Thank you for buying us a cup of coffee!")
209
+
210
+ def on_product_details_response(self, billing_result, product_details_list):
211
+ for product_details in product_details_list:
212
+ self.billing_client.get_product_details(
213
+ product_details,
214
+ ProductType.SUBS if self.ids.subscribe.active else ProductType.INAPP)
215
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
216
+ self.billing_client.launch_billing_flow(product_details=product_details_list)
217
+
218
+ def on_billing_setup_finished(self, billing_result):
219
+ product_id = self.ids.btn.product_id
220
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
221
+ self.billing_client.query_product_details_async(
222
+ product_type=ProductType.SUBS if self.ids.subscribe.active else ProductType.INAPP,
223
+ products_ids=[product_id],
224
+ on_product_details_response=self.on_product_details_response,
225
+ )
226
+
227
+ def toast(self, message):
228
+ # Implementation of toast message (platform specific)
229
+ print(message)
230
+
231
+
232
+ class BillingApp(App):
233
+ def build(self):
234
+ # Create screen manager
235
+ sm = ScreenManager()
236
+ sm.add_widget(HomeScreen(name='home'))
237
+ return sm
238
+
239
+
240
+ if __name__ == '__main__':
241
+ BillingApp().run()
242
+ ```
243
+
244
+ #### Kivy Layout File (main.kv)
245
+
246
+ ```kivy
247
+ <HomeScreen>:
248
+ BoxLayout:
249
+ orientation: 'vertical'
250
+ padding: '20dp'
251
+ spacing: '10dp'
252
+
253
+ Label:
254
+ text: 'SJBillingClient Demo'
255
+ font_size: '24sp'
256
+ size_hint_y: None
257
+ height: '50dp'
258
+
259
+ BoxLayout:
260
+ orientation: 'horizontal'
261
+ size_hint_y: None
262
+ height: '50dp'
263
+
264
+ Label:
265
+ text: 'Subscribe'
266
+ size_hint_x: 0.5
267
+
268
+ CheckBox:
269
+ id: subscribe
270
+ size_hint_x: 0.5
271
+ active: False
272
+
273
+ Button:
274
+ id: btn
275
+ text: 'Buy Coffee'
276
+ product_id: 'coffee_product_id'
277
+ size_hint_y: None
278
+ height: '60dp'
279
+ on_release: root.purchase_or_subscribe()
280
+
281
+ Widget:
282
+ # Spacer
283
+ ```
284
+
285
+ This example demonstrates:
286
+
287
+ 1. A `HomeScreen` class that handles all billing operations
288
+ 2. A `BillingApp` class that sets up the Kivy application and screen manager
289
+ 3. A Kivy layout file that defines the UI with:
290
+ - A checkbox to toggle between one-time purchase and subscription
291
+ - A button to initiate the purchase flow
292
+
293
+ The `purchase_or_subscribe` 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 acknowledging purchases and consuming one-time purchases.
294
+
295
+ ## API Reference
296
+
297
+ ### BillingClient
298
+
299
+ The main class for interacting with Google Play Billing.
300
+
301
+ #### Methods
302
+
303
+ - `__init__(on_purchases_updated)`: Initialize with a callback for purchase updates
304
+ - `start_connection(on_billing_setup_finished, on_billing_service_disconnected)`: Start billing connection
305
+ - `end_connection()`: End billing connection
306
+ - `query_product_details_async(product_type, products_ids, on_product_details_response)`: Query product details
307
+ - `get_product_details(product_details, product_type)`: Get formatted product details
308
+ - `launch_billing_flow(product_details, offer_token=None)`: Launch purchase flow
309
+ - `consume_async(purchase, on_consume_response)`: Consume a purchase
310
+ - `acknowledge_purchase(purchase_token, on_acknowledge_purchase_response)`: Acknowledge a purchase
311
+
312
+ ### ProductType
313
+
314
+ Constants for product types:
315
+
316
+ - `ProductType.INAPP`: One-time purchases
317
+ - `ProductType.SUBS`: Subscriptions
318
+
319
+ ### BillingResponseCode
320
+
321
+ Constants for billing response codes:
322
+
323
+ - `BillingResponseCode.OK`: Success
324
+ - `BillingResponseCode.USER_CANCELED`: User canceled
325
+ - `BillingResponseCode.SERVICE_UNAVAILABLE`: Service unavailable
326
+ - And many others
327
+
328
+ ## Contributing
329
+
330
+ Contributions are welcome! Please feel free to submit a Pull Request.
331
+
332
+ ## License
333
+
334
+ This project is licensed under the MIT License - see the LICENSE file for details.
335
+
336
+ ## Author
337
+
338
+ Kenechukwu Akubue <kengoon19@gmail.com>
339
+
@@ -11,7 +11,7 @@ sjbillingclient/jinterface/billing.py,sha256=lwBGN8Xe0Qbc1DCRSShrhL8oGqRbjugwrgN
11
11
  sjbillingclient/jinterface/consume.py,sha256=DiugwpRreoVLpPjeHSkNMgncg5f3HnIBCUh0OIyrWhI,524
12
12
  sjbillingclient/jinterface/product.py,sha256=rtlc-4hxToCa3buwNp8QufMYkoiUZHp_YXKeE-QZZyk,562
13
13
  sjbillingclient/jinterface/purchases.py,sha256=BG6xF3H-35_XhiRr2kWDb1jzsxJ-hIw-jaEi-uSkTmY,574
14
- sjbillingclient/tools/__init__.py,sha256=SSq-BgkGt-4ws517TWL5aabaT2gS2pjb38AMXY-OKwU,6458
15
- sjbillingclient-0.1.3.dist-info/METADATA,sha256=uCryoWp-Phbmm_J5yf4hfoSJjztXuFRNv8-f7YUyYBg,721
16
- sjbillingclient-0.1.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
17
- sjbillingclient-0.1.3.dist-info/RECORD,,
14
+ sjbillingclient/tools/__init__.py,sha256=g0k73jc9mcbAe86gM8Q_r9fp5dxHJWIbeqzO9Ne3xA4,19622
15
+ sjbillingclient-0.1.4.dist-info/METADATA,sha256=6q45Ork4P_V9HG7Gtjx--ksqH5pxQ6UPOe0rRRA8VbM,12093
16
+ sjbillingclient-0.1.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
17
+ sjbillingclient-0.1.4.dist-info/RECORD,,
@@ -1,29 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: sjbillingclient
3
- Version: 0.1.3
4
- Summary:
5
- Author: Kenechukwu Akubue
6
- Author-email: kengoon19@gmail.com
7
- Requires-Python: >=3.9,<4.0
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.9
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Requires-Dist: pyjnius (>=1.6.1,<2.0.0)
14
- Description-Content-Type: text/markdown
15
-
16
- # SJBillingClient (Google Play Billing SDK for Python)
17
-
18
-
19
- ## Installation
20
- ```shell
21
- # pip
22
-
23
- pip install sjbillingclient
24
-
25
- # buildozer
26
- requirements=sjbillingclient
27
- android.gradle_dependencies=com.android.billingclient:billing:7.1.1
28
- ```
29
-