sjbillingclient 0.1.3__py3-none-any.whl → 0.2.0__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,8 +1,16 @@
1
- from jnius import JavaClass, MetaJavaClass, JavaStaticMethod
1
+ from jnius import JavaClass, MetaJavaClass, JavaStaticMethod, JavaMethod
2
2
 
3
- __all__ = ("PendingPurchasesParams",)
3
+ __all__ = ("PendingPurchasesParams", "PendingPurchasesParamsBuilder")
4
4
 
5
5
 
6
6
  class PendingPurchasesParams(JavaClass, metaclass=MetaJavaClass):
7
7
  __javaclass__ = f"com/android/billingclient/api/PendingPurchasesParams"
8
8
  newBuilder = JavaStaticMethod("()Lcom/android/billingclient/api/PendingPurchasesParams$Builder;")
9
+
10
+
11
+ class PendingPurchasesParamsBuilder(JavaClass, metaclass=MetaJavaClass):
12
+ __javaclass__ = f"com/android/billingclient/api/PendingPurchasesParams$Builder"
13
+
14
+ build = JavaMethod("()Lcom/android/billingclient/api/PendingPurchasesParams;")
15
+ enableOneTimeProducts = JavaMethod("()Lcom/android/billingclient/api/PendingPurchasesParams$Builder;")
16
+ enablePrepaidPlans = JavaMethod("()Lcom/android/billingclient/api/PendingPurchasesParams$Builder;")
@@ -1,4 +1,4 @@
1
- from jnius import JavaClass, MetaJavaClass, JavaStaticMethod
1
+ from jnius import JavaClass, MetaJavaClass, JavaStaticMethod, JavaMethod
2
2
 
3
3
  __all__ = ("QueryProductDetailsParams", "QueryProductDetailsParamsProduct")
4
4
 
@@ -11,3 +11,11 @@ class QueryProductDetailsParams(JavaClass, metaclass=MetaJavaClass):
11
11
  class QueryProductDetailsParamsProduct(JavaClass, metaclass=MetaJavaClass):
12
12
  __javaclass__ = f"com/android/billingclient/api/QueryProductDetailsParams$Product"
13
13
  newBuilder = JavaStaticMethod("()Lcom/android/billingclient/api/QueryProductDetailsParams$Product$Builder;")
14
+
15
+
16
+ class QueryProductDetailsResult(JavaClass, metaclass=MetaJavaClass):
17
+ __javaclass__ = f"com/android/billingclient/api/QueryProductDetailsResult"
18
+ create = JavaStaticMethod("(Ljava/util/List;Ljava/util/List;)"
19
+ "Lcom/android/billingclient/api/QueryProductDetailsResult;")
20
+ getProductDetailsList = JavaMethod("()Ljava/util/List;")
21
+ getUnfetchedProductList = JavaMethod("()Ljava/util/List;")
@@ -10,6 +10,7 @@ class ProductDetailsResponseListener(PythonJavaClass):
10
10
  def __init__(self, callback):
11
11
  self.callback = callback
12
12
 
13
- @java_method("(Lcom/android/billingclient/api/BillingResult;Ljava/util/List;)V")
14
- def onProductDetailsResponse(self, billing_result, product_details_list):
15
- self.callback(billing_result, product_details_list)
13
+ @java_method("(Lcom/android/billingclient/api/BillingResult;"
14
+ "Lcom/android/billingclient/api/QueryProductDetailsResult;)V")
15
+ def onProductDetailsResponse(self, billing_result, product_details_result):
16
+ self.callback(billing_result, product_details_result)
@@ -1,9 +1,48 @@
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, \
4
42
  BillingFlowParams
5
43
  from android.activity import _activity as activity # noqa
6
44
  from sjbillingclient.jclass.consume import ConsumeParams
45
+ from sjbillingclient.jclass.purchase import PendingPurchasesParams
7
46
  from sjbillingclient.jclass.queryproduct import QueryProductDetailsParams, QueryProductDetailsParamsProduct
8
47
  from sjbillingclient.jinterface.acknowledge import AcknowledgePurchaseResponseListener
9
48
  from sjbillingclient.jinterface.billing import BillingClientStateListener
@@ -11,115 +50,365 @@ from sjbillingclient.jinterface.consume import ConsumeResponseListener
11
50
  from sjbillingclient.jinterface.product import ProductDetailsResponseListener
12
51
  from sjbillingclient.jinterface.purchases import PurchasesUpdatedListener
13
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
+
14
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
+
81
+ def __init__(
82
+ self,
83
+ on_purchases_updated,
84
+ enable_one_time_products: bool = True,
85
+ enable_prepaid_plans: bool = False
86
+ ) -> None:
87
+ """
88
+ Initializes an instance of the class with the given purchase update callback.
25
89
 
90
+ :param on_purchases_updated: A callback function that will be triggered when purchases
91
+ are updated. This function typically handles updates to purchases such as
92
+ processing the results or actions related to the purchases.
93
+ :type on_purchases_updated: callable
94
+ """
95
+ self.__billing_client_state_listener = None
96
+ self.__product_details_response_listener = None
97
+ self.__consume_response_listener = None
98
+ self.__acknowledge_purchase_response_listener = None
99
+
100
+ self.__purchase_update_listener = PurchasesUpdatedListener(on_purchases_updated)
101
+ pending_purchase_params = PendingPurchasesParams.newBuilder()
102
+ if enable_one_time_products:
103
+ pending_purchase_params.enableOneTimeProducts()
104
+ if enable_prepaid_plans:
105
+ pending_purchase_params.enablePrepaidPlans()
26
106
  self.__billing_client = (
27
107
  SJBillingClient.newBuilder(activity.context)
28
108
  .setListener(self.__purchase_update_listener)
29
- .enablePendingPurchases()
109
+ .enablePendingPurchases(pending_purchase_params.build())
30
110
  .build()
31
111
  )
32
112
 
33
- def start_connection(self, on_billing_setup_finished, on_billing_service_disconnected):
113
+ def start_connection(self, on_billing_setup_finished, on_billing_service_disconnected) -> None:
114
+ """
115
+ Starts a connection with the billing client and initializes the billing
116
+ client state listener. This method sets up a listener to handle billing
117
+ setup finished and billing service disconnection callbacks.
118
+
119
+ :param on_billing_setup_finished: A callable that will be invoked when
120
+ the billing setup has finished.
121
+ :param on_billing_service_disconnected: A callable that will be invoked
122
+ when the billing service gets disconnected.
123
+ :return: None
124
+ """
34
125
  self.__billing_client_state_listener = BillingClientStateListener(
35
126
  on_billing_setup_finished,
36
127
  on_billing_service_disconnected
37
128
  )
38
129
  self.__billing_client.startConnection(self.__billing_client_state_listener)
39
130
 
40
- def end_connection(self):
131
+ def end_connection(self) -> None:
132
+ """
133
+ Ends the connection with the billing client.
134
+
135
+ This method terminates the active connection with the billing client
136
+ by calling its `endConnection` method. It ensures proper cleanup of
137
+ resources related to the billing client.
138
+
139
+ :return: None
140
+ """
41
141
  self.__billing_client.endConnection()
42
142
 
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
- )
143
+ def query_product_details_async(self, product_type: str, products_ids: List[str],
144
+ on_product_details_response) -> None:
145
+ """
146
+ Queries product details asynchronously for a given list of product IDs and product type.
147
+
148
+ This function utilizes the provided product details response callback to handle the
149
+ resulting response from the query.
150
+
151
+ :param product_type: The type of the products to be queried (e.g., "inapp" or "subs").
152
+ :param products_ids: A list of product IDs to query details for.
153
+ :param on_product_details_response: A callback function that is triggered when the
154
+ product details query is complete.
155
+ :return: None
156
+ """
157
+ JavaList = autoclass("java.util.List")
158
+ product_list = [
159
+ self._build_product_params(product_id, product_type)
160
+ for product_id in products_ids
161
+ ]
162
+
163
+ params = (QueryProductDetailsParams.newBuilder()
164
+ .setProductList(JavaList.of(*product_list))
165
+ .build())
58
166
 
59
167
  self.__product_details_response_listener = ProductDetailsResponseListener(on_product_details_response)
168
+ self.__billing_client.queryProductDetailsAsync(params, self.__product_details_response_listener)
60
169
 
61
- self.__billing_client.queryProductDetailsAsync(
62
- queryProductDetailsParams,
63
- self.__product_details_response_listener
64
- )
170
+ @staticmethod
171
+ def _build_product_params(product_id: str, product_type: str):
172
+ """
173
+ Builds product parameters using the provided product ID and product type.
174
+
175
+ This is a static helper method designed to construct and return an object
176
+ representing the parameters for querying product details. It uses a builder
177
+ pattern for constructing the product details object.
178
+
179
+ :param product_id: The unique identifier of the product.
180
+ :type product_id: str
181
+ :param product_type: The type/category of the product (e.g., consumable, subscription).
182
+ :type product_type: str
183
+ :return: A constructed product details parameter object.
184
+ :rtype: QueryProductDetailsParamsProduct
185
+ """
186
+ return (QueryProductDetailsParamsProduct.newBuilder()
187
+ .setProductId(product_id)
188
+ .setProductType(product_type)
189
+ .build())
65
190
 
66
191
  @staticmethod
67
- def get_product_details(product_details, product_type):
68
- details = []
192
+ def get_unfetched_product(unfetched_product) -> Dict:
193
+ """
194
+ Retrieves detailed product information for an unfetched product.
195
+
196
+ This function takes an object representing an unfetched product and extracts
197
+ important details such as the product ID, product type, and status code.
198
+ The extracted details are then returned as a dictionary.
199
+
200
+ :param unfetched_product: The product object that has not yet been fetched.
201
+ Must provide methods to retrieve product ID, type, and status code.
202
+ :type unfetched_product: Any
203
+ :return: A dictionary containing detailed information about the unfetched
204
+ product, including its ID, type, and status code.
205
+ :rtype: Dict
206
+ """
207
+ return {
208
+ "product_id": unfetched_product.getProductId(),
209
+ "product_type": unfetched_product.getProductType(),
210
+ "status_code": unfetched_product.getStatusCode(),
211
+ }
212
+
213
+ def get_product_details(self, product_details, product_type: str) -> List[Dict]:
214
+ """
215
+ Retrieves the details of a product based on the provided product type. The function processes
216
+ different types of products, such as subscriptions and in-app purchases, and returns the corresponding
217
+ details.
218
+
219
+ If the product type is not recognized, an exception is raised.
220
+
221
+ :param product_details: The details of the product to process.
222
+ :param product_type: The type of the product. It can either be 'SUBS' for subscriptions or 'INAPP' for in-app purchases.
223
+ :return: A list of dictionaries containing the processed product details.
224
+ :rtype: List[Dict]
225
+ :raises Exception: If the specified product type is invalid.
226
+ """
69
227
  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
228
+ return self._get_subscription_details(product_details)
80
229
  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
- )
230
+ return self._get_inapp_purchase_details(product_details)
231
+ raise Exception(ERROR_INVALID_PRODUCT_TYPE)
232
+
233
+ def _get_subscription_details(self, product_details) -> List[Dict]:
234
+ """
235
+ Retrieves subscription details from the provided product details by parsing its
236
+ subscription offer details and related pricing phases. The extracted details
237
+ include product ID, formatted price, price amount in micros, and the currency code.
238
+
239
+ :param product_details: Contains information about the product including
240
+ subscription offers and pricing.
241
+ :type product_details: Any
242
+ :return: List of dictionaries, each containing subscription details such as
243
+ product ID, formatted price, price amount in micros, and currency code.
244
+ :rtype: List[Dict]
245
+ """
246
+ details = []
247
+ offer_details = product_details.getSubscriptionOfferDetails()
248
+ for offer in offer_details:
249
+ pricing_phase = offer.getPricingPhases().getPricingPhaseList().get(0)
250
+ details.append(self._create_product_detail_dict(
251
+ product_details.getProductId(),
252
+ pricing_phase.getFormattedPrice(),
253
+ pricing_phase.getPriceAmountMicros,
254
+ pricing_phase.getPriceCurrencyCode()
255
+ ))
256
+ return details
257
+
258
+ def _get_inapp_purchase_details(self, product_details) -> List[Dict]:
259
+ """
260
+ Retrieve and construct in-app purchase product details.
261
+
262
+ This function takes product details from an in-app purchase API response and
263
+ constructs a list of dictionaries containing formatted product details. Each
264
+ dictionary includes details such as product ID, formatted price, raw price
265
+ amount, and currency code. Only one-time purchase offer details are supported.
266
+
267
+ :param product_details: Product details object containing information about
268
+ an in-app purchase product.
269
+ :type product_details: Any
270
+ :return: A list of dictionaries representing constructed product details
271
+ based on the provided product information.
272
+ :rtype: List[Dict]
273
+ """
274
+ offer_details = product_details.getOneTimePurchaseOfferDetails()
275
+ return [self._create_product_detail_dict(
276
+ product_details.getProductId(),
277
+ offer_details.getFormattedPrice(),
278
+ offer_details.getPriceAmountMicros,
279
+ offer_details.getPriceCurrencyCode()
280
+ )]
281
+
282
+ @staticmethod
283
+ def _create_product_detail_dict(product_id: str, formatted_price: str,
284
+ price_amount_micros, price_currency_code: str) -> Dict:
285
+ """
286
+ Creates a dictionary containing product details.
287
+
288
+ This method generates and returns a dictionary that encapsulates product
289
+ details such as the product identifier, formatted price, price in micros,
290
+ and the price currency code.
291
+
292
+ :param product_id: The unique identifier for the product.
293
+ :type product_id: str
294
+ :param formatted_price: The human-readable price of the product.
295
+ :type formatted_price: str
296
+ :param price_amount_micros: The price of the product in micro-units.
297
+ :type price_amount_micros: int
298
+ :param price_currency_code: The currency code associated with the product price.
299
+ :type price_currency_code: str
300
+ :return: A dictionary holding the product details.
301
+ :rtype: Dict
302
+ """
303
+ return {
304
+ "product_id": product_id,
305
+ "formatted_price": formatted_price,
306
+ "price_amount_micros": price_amount_micros,
307
+ "price_currency_code": price_currency_code,
308
+ }
309
+
310
+ def launch_billing_flow(self, product_details: List, offer_token: Optional[str] = None):
311
+ """
312
+ Initiates the in-app billing flow for the specified product details and an optional
313
+ offer token. The method constructs billing flow parameters using the provided product
314
+ details and triggers the billing process.
315
+
316
+ :param product_details: A list of product detail objects representing the items
317
+ available for purchase through the billing flow.
318
+ :param offer_token: Optional string representing a unique token to identify
319
+ specific offers for the product being purchased.
320
+ :return: An integer identifier returned by the billing client representing the
321
+ result of the billing flow launch attempt.
322
+ """
323
+ JavaList = autoclass("java.util.List")
324
+ product_params_list = [
325
+ self._create_product_params(product_detail, offer_token)
326
+ for product_detail in product_details
327
+ ]
328
+ print(product_params_list)
329
+
330
+ billing_flow_params = (BillingFlowParams.newBuilder()
331
+ .setProductDetailsParamsList(JavaList.of(*product_params_list))
332
+ .build())
333
+
334
+ return self.__billing_client.launchBillingFlow(activity, billing_flow_params)
118
335
 
119
- billing_result = self.__billing_client.launchBillingFlow(activity, billing_flow_params)
120
- return billing_result
336
+ def _create_product_params(self, product_detail, offer_token: Optional[str]):
337
+ """
338
+ Creates and builds product parameters for a given product detail and optional offer token.
339
+
340
+ This method initializes the `ProductDetailsParams` through its builder,
341
+ populates it with the provided product detail, and conditionally sets the
342
+ offer token if the product type is a subscription (SUBS). Once all
343
+ necessary values are set, it builds and returns the params.
344
+
345
+ :param product_detail: The product details used for generating the params.
346
+ :type product_detail: ProductDetails
347
+
348
+ :param offer_token: Optional token specific to the offer for the subscription
349
+ product. Will be resolved internally if not provided and the product type
350
+ is a subscription.
351
+ :type offer_token: Optional[str]
352
+
353
+ :return: A fully constructed `ProductDetailsParams` object populated
354
+ with the input product data and offer token, if applicable.
355
+ :rtype: ProductDetailsParams
356
+ """
357
+ params = ProductDetailsParams.newBuilder()
358
+ params.setProductDetails(product_detail)
359
+
360
+ if product_detail.getProductType() == ProductType.SUBS:
361
+ offer_token = self._resolve_offer_token(product_detail, offer_token)
362
+ params.setOfferToken(offer_token)
363
+
364
+ return params.build()
365
+
366
+ @staticmethod
367
+ def _resolve_offer_token(product_detail, offer_token: Optional[str]) -> str:
368
+ """
369
+ Resolves the offer token for a given product detail. If the provided offer token is
370
+ not `None`, it returns the same. Otherwise, it determines the offer token using
371
+ the subscription offer details from the product detail. Raises exceptions if the
372
+ required information is missing.
373
+
374
+ :param product_detail: The product detail object containing subscription offer
375
+ details.
376
+ :type product_detail: Any
377
+ :param offer_token: A string value representing the offer token if provided,
378
+ or None to attempt resolving it from the product detail.
379
+ :type offer_token: Optional[str]
380
+ :return: The resolved offer token as a string.
381
+ :rtype: str
382
+ :raises JavaException: When no base plan or base plan ID is found in the
383
+ subscription offer details.
384
+ """
385
+ if offer_token:
386
+ return offer_token
387
+
388
+ offer_list = product_detail.getSubscriptionOfferDetails()
389
+ if not offer_list or offer_list.isEmpty():
390
+ raise JavaException(ERROR_NO_BASE_PLAN)
391
+
392
+ base_plan_id = offer_list.get(0).getBasePlanId()
393
+ if not base_plan_id:
394
+ raise JavaException(ERROR_NO_BASE_PLAN_ID)
395
+
396
+ return offer_list.get(0).getOfferToken()
121
397
 
122
398
  def consume_async(self, purchase, on_consume_response):
399
+ """
400
+ Consumes a given purchase asynchronously using the billing client. The method takes
401
+ a purchase object and a callback function that is triggered upon the consumption's
402
+ completion. This process involves creating appropriate consume parameters and invoking
403
+ the consume operation on the billing client.
404
+
405
+ :param purchase: The purchase object to consume.
406
+ :type purchase: Purchase
407
+ :param on_consume_response: The callback to execute upon completion of the consumption
408
+ process. This should handle the result of the consumption.
409
+ :type on_consume_response: Callable
410
+ :return: None
411
+ """
123
412
  consume_params = (
124
413
  ConsumeParams.newBuilder()
125
414
  .setPurchaseToken(purchase.getPurchaseToken())
@@ -129,6 +418,20 @@ class BillingClient:
129
418
  self.__billing_client.consumeAsync(consume_params, self.__consume_response_listener)
130
419
 
131
420
  def acknowledge_purchase(self, purchase_token, on_acknowledge_purchase_response):
421
+ """
422
+ Acknowledges a purchase using the provided purchase token. This method communicates
423
+ with the billing client to confirm the completion of a purchase, ensuring its validity
424
+ and acknowledgment. A callback function is triggered once the acknowledgment process
425
+ is complete.
426
+
427
+ :param purchase_token: The token representing the purchase to be acknowledged.
428
+ :type purchase_token: str
429
+ :param on_acknowledge_purchase_response: A callback function to handle the
430
+ response of the acknowledgment process. It is triggered upon completion.
431
+ :type on_acknowledge_purchase_response: Callable[[AcknowledgePurchaseResponse], None]
432
+
433
+ :return: None
434
+ """
132
435
  acknowledge_purchase_params = (
133
436
  AcknowledgePurchaseParams.newBuilder()
134
437
  .setPurchaseToken(purchase_token)
@@ -0,0 +1,507 @@
1
+ Metadata-Version: 2.1
2
+ Name: sjbillingclient
3
+ Version: 0.2.0
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 8.0.0 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:8.0.0
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
+ Builder.load_file(join(dirname(__file__), basename(__file__).split(".")[0] + ".kv"))
174
+
175
+
176
+ class HomeScreen(Screen):
177
+ """
178
+ A screen that demonstrates Google Play Billing integration with Kivy.
179
+
180
+ This screen provides functionality to make in-app purchases and subscriptions
181
+ using the Google Play Billing Library through the SJBillingClient wrapper.
182
+
183
+ Attributes:
184
+ billing_client (BillingClient): The client used to interact with Google Play Billing.
185
+ """
186
+ billing_client = None
187
+
188
+ def support(self):
189
+ """
190
+ Initializes the billing client and starts a connection to the Google Play Billing service.
191
+
192
+ This method is called when the user wants to make a purchase or subscription.
193
+ If a billing client already exists, it ends the connection before creating a new one.
194
+ """
195
+ if self.billing_client:
196
+ self.billing_client.end_connection()
197
+
198
+ self.billing_client = BillingClient(on_purchases_updated=self.on_purchases_updated)
199
+ self.billing_client.start_connection(
200
+ on_billing_setup_finished=self.on_billing_setup_finished,
201
+ on_billing_service_disconnected=lambda: print("disconnected")
202
+ )
203
+
204
+ def on_purchases_updated(self, billing_result, null, purchases):
205
+ """
206
+ Callback method that is called when purchases are updated.
207
+
208
+ This method handles the result of a purchase flow, either acknowledging
209
+ a subscription or consuming a one-time purchase.
210
+
211
+ Args:
212
+ billing_result: The result of the billing operation.
213
+ null: Boolean indicating if the purchases list is null.
214
+ purchases: List of purchases that were updated.
215
+ """
216
+ if billing_result.getResponseCode() == BillingResponseCode.OK and not null:
217
+ for purchase in purchases:
218
+ if self.ids.subscribe.active:
219
+ self.billing_client.acknowledge_purchase(
220
+ purchase_token=purchase.getPurchaseToken(),
221
+ on_acknowledge_purchase_response=self.on_acknowledge_purchase_response
222
+ )
223
+ else:
224
+ self.billing_client.consume_async(purchase, self.on_consume_response)
225
+ print(billing_result.getResponseCode(), billing_result.getDebugMessage())
226
+
227
+ def on_acknowledge_purchase_response(self, billing_result):
228
+ """
229
+ Callback method that is called when a purchase acknowledgement is complete.
230
+
231
+ Args:
232
+ billing_result: The result of the acknowledgement operation.
233
+ """
234
+ print(billing_result.getDebugMessage())
235
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
236
+ self.toast("Thank you for subscribing to buy us a cup of coffee! monthly")
237
+
238
+ def on_consume_response(self, billing_result):
239
+ """
240
+ Callback method that is called when a purchase consumption is complete.
241
+
242
+ Args:
243
+ billing_result: The result of the consumption operation.
244
+ """
245
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
246
+ self.toast("Thank you for buying us a cup of coffee!")
247
+
248
+ def on_product_details_response(self, billing_result, product_details_result):
249
+ """
250
+ Callback method that is called when product details are retrieved.
251
+
252
+ This method processes the product details and launches the billing flow.
253
+
254
+ Args:
255
+ billing_result: The result of the product details query.
256
+ product_details_result: The result containing product details and unfetched products.
257
+ """
258
+ product_details_list = product_details_result.getProductDetailsList()
259
+ unfetched_product_list = product_details_result.getUnfetchedProductList()
260
+
261
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
262
+ for product_details in product_details_list:
263
+ self.billing_client.get_product_details(
264
+ product_details,
265
+ ProductType.SUBS if self.ids.subscribe.active else ProductType.INAPP)
266
+ for unfetched_product in unfetched_product_list:
267
+ print(self.billing_client.get_unfetched_product(unfetched_product))
268
+ self.billing_client.launch_billing_flow(product_details=product_details_list)
269
+
270
+ def on_billing_setup_finished(self, billing_result):
271
+ """
272
+ Callback method that is called when the billing setup is complete.
273
+
274
+ This method queries product details if the billing setup was successful.
275
+
276
+ Args:
277
+ billing_result: The result of the billing setup operation.
278
+ """
279
+ product_id = self.ids.btn.product_id
280
+ if billing_result.getResponseCode() == BillingResponseCode.OK:
281
+ self.billing_client.query_product_details_async(
282
+ product_type=ProductType.SUBS if self.ids.subscribe else ProductType.INAPP,
283
+ products_ids=[product_id],
284
+ on_product_details_response=self.on_product_details_response,
285
+ )
286
+
287
+ def toast(self, message):
288
+ """
289
+ Display a toast message.
290
+
291
+ This is a simple implementation that just prints the message.
292
+ In a real app, you would use platform-specific toast functionality.
293
+
294
+ Args:
295
+ message: The message to display.
296
+ """
297
+ # Implementation of toast message (platform specific)
298
+ print(message)
299
+
300
+
301
+ class BillingApp(App):
302
+ """
303
+ Main application class for the SJBillingClient demo.
304
+
305
+ This class sets up the application and creates the screen manager
306
+ with the HomeScreen.
307
+ """
308
+ def build(self):
309
+ """
310
+ Build the application UI.
311
+
312
+ Returns:
313
+ ScreenManager: The root widget of the application.
314
+ """
315
+ # Create screen manager
316
+ sm = ScreenManager()
317
+ sm.add_widget(HomeScreen(name='home'))
318
+ return sm
319
+
320
+
321
+ if __name__ == '__main__':
322
+ BillingApp().run()
323
+ ```
324
+
325
+ #### Kivy Layout File (main.kv)
326
+
327
+ ```kivy
328
+ <HomeScreen>:
329
+ BoxLayout:
330
+ orientation: 'vertical'
331
+ padding: '20dp'
332
+ spacing: '10dp'
333
+
334
+ Label:
335
+ text: 'SJBillingClient Demo'
336
+ font_size: '24sp'
337
+ size_hint_y: None
338
+ height: '50dp'
339
+
340
+ BoxLayout:
341
+ orientation: 'horizontal'
342
+ size_hint_y: None
343
+ height: '50dp'
344
+
345
+ Label:
346
+ text: 'Subscribe'
347
+ size_hint_x: 0.5
348
+
349
+ CheckBox:
350
+ id: subscribe
351
+ size_hint_x: 0.5
352
+ active: False
353
+
354
+ Button:
355
+ id: btn
356
+ text: 'Buy Coffee'
357
+ product_id: 'coffee_product_id'
358
+ size_hint_y: None
359
+ height: '60dp'
360
+ on_release: root.support()
361
+
362
+ Widget:
363
+ # Spacer
364
+ ```
365
+
366
+ This example demonstrates:
367
+
368
+ 1. A `HomeScreen` class that extends `Screen` and handles all billing operations
369
+ 2. A `BillingApp` class that sets up the Kivy application and screen manager
370
+ 3. A Kivy layout file that defines the UI with:
371
+ - A checkbox to toggle between one-time purchase and subscription
372
+ - A button to initiate the purchase flow
373
+
374
+ 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:
375
+ - Handling purchase updates with `on_purchases_updated`
376
+ - Acknowledging subscription purchases with `acknowledge_purchase`
377
+ - Consuming one-time purchases with `consume_async`
378
+ - Processing product details with `on_product_details_response`, including handling unfetched products
379
+ - Querying product details with `query_product_details_async`
380
+
381
+ This example is designed to be copy-and-paste runnable, with no need for the user to add or remove anything to test it.
382
+
383
+ ## API Reference
384
+
385
+ ### BillingClient
386
+
387
+ The main class for interacting with Google Play Billing.
388
+
389
+ #### Constructor
390
+
391
+ - `__init__(on_purchases_updated, enable_one_time_products=True, enable_prepaid_plans=False)`:
392
+ - Initializes a new BillingClient instance
393
+ - `on_purchases_updated`: Callback function that will be triggered when purchases are updated
394
+ - `enable_one_time_products`: Boolean to enable one-time products (default: True)
395
+ - `enable_prepaid_plans`: Boolean to enable prepaid plans (default: False)
396
+
397
+ #### Connection Methods
398
+
399
+ - `start_connection(on_billing_setup_finished, on_billing_service_disconnected)`:
400
+ - Starts a connection with the billing client
401
+ - `on_billing_setup_finished`: Callback when billing setup is complete
402
+ - `on_billing_service_disconnected`: Callback when billing service is disconnected
403
+
404
+ - `end_connection()`:
405
+ - Ends the connection with the billing client
406
+
407
+ #### Product Details Methods
408
+
409
+ - `query_product_details_async(product_type, products_ids, on_product_details_response)`:
410
+ - Queries product details asynchronously
411
+ - `product_type`: Type of products (INAPP or SUBS)
412
+ - `products_ids`: List of product IDs to query
413
+ - `on_product_details_response`: Callback for product details response
414
+
415
+ - `get_product_details(product_details, product_type)`:
416
+ - Gets formatted product details
417
+ - `product_details`: Product details object
418
+ - `product_type`: Type of product (INAPP or SUBS)
419
+ - Returns a list of dictionaries with product details
420
+
421
+ - `get_unfetched_product(unfetched_product)`:
422
+ - Gets details about an unfetched product
423
+ - `unfetched_product`: Unfetched product object
424
+ - Returns a dictionary with product ID, type, and status code
425
+
426
+ #### Purchase Methods
427
+
428
+ - `launch_billing_flow(product_details, offer_token=None)`:
429
+ - Launches the billing flow for purchase
430
+ - `product_details`: List of product details objects
431
+ - `offer_token`: Optional token for subscription offers
432
+
433
+ - `consume_async(purchase, on_consume_response)`:
434
+ - Consumes a purchase asynchronously
435
+ - `purchase`: Purchase object to consume
436
+ - `on_consume_response`: Callback for consume response
437
+
438
+ - `acknowledge_purchase(purchase_token, on_acknowledge_purchase_response)`:
439
+ - Acknowledges a purchase
440
+ - `purchase_token`: Token of the purchase to acknowledge
441
+ - `on_acknowledge_purchase_response`: Callback for acknowledge response
442
+
443
+ ### PendingPurchasesParams
444
+
445
+ Parameters for handling pending purchases.
446
+
447
+ #### Methods
448
+
449
+ - `newBuilder()`: Creates a new builder for PendingPurchasesParams
450
+ - `build()`: Builds the PendingPurchasesParams object
451
+ - `enableOneTimeProducts()`: Enables one-time products
452
+ - `enablePrepaidPlans()`: Enables prepaid plans
453
+
454
+ ### QueryProductDetailsParams
455
+
456
+ Parameters for querying product details.
457
+
458
+ #### Methods
459
+
460
+ - `newBuilder()`: Creates a new builder for QueryProductDetailsParams
461
+ - `setProductList(product_list)`: Sets the list of products to query
462
+ - `build()`: Builds the QueryProductDetailsParams object
463
+
464
+ ### QueryProductDetailsResult
465
+
466
+ Result of a product details query.
467
+
468
+ #### Methods
469
+
470
+ - `getProductDetailsList()`: Gets the list of product details
471
+ - `getUnfetchedProductList()`: Gets the list of unfetched products
472
+
473
+ ### ProductType
474
+
475
+ Constants for product types:
476
+
477
+ - `ProductType.INAPP`: One-time purchases
478
+ - `ProductType.SUBS`: Subscriptions
479
+
480
+ ### BillingResponseCode
481
+
482
+ Constants for billing response codes:
483
+
484
+ - `BillingResponseCode.OK`: Success (0)
485
+ - `BillingResponseCode.USER_CANCELED`: User canceled (1)
486
+ - `BillingResponseCode.SERVICE_UNAVAILABLE`: Service unavailable (2)
487
+ - `BillingResponseCode.BILLING_UNAVAILABLE`: Billing unavailable (3)
488
+ - `BillingResponseCode.ITEM_UNAVAILABLE`: Item unavailable (4)
489
+ - `BillingResponseCode.DEVELOPER_ERROR`: Developer error (5)
490
+ - `BillingResponseCode.ERROR`: General error (6)
491
+ - `BillingResponseCode.ITEM_ALREADY_OWNED`: Item already owned (7)
492
+ - `BillingResponseCode.ITEM_NOT_OWNED`: Item not owned (8)
493
+ - `BillingResponseCode.SERVICE_DISCONNECTED`: Service disconnected (10)
494
+ - `BillingResponseCode.FEATURE_NOT_SUPPORTED`: Feature not supported (12)
495
+
496
+ ## Contributing
497
+
498
+ Contributions are welcome! Please feel free to submit a Pull Request.
499
+
500
+ ## License
501
+
502
+ This project is licensed under the MIT License - see the LICENSE file for details.
503
+
504
+ ## Author
505
+
506
+ Kenechukwu Akubue <kengoon19@gmail.com>
507
+
@@ -3,15 +3,15 @@ sjbillingclient/jclass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
3
3
  sjbillingclient/jclass/acknowledge.py,sha256=FeaiOsIR2_WxJQC56gaeCtOPTEWb7Dz7AuWE6VYVMJI,358
4
4
  sjbillingclient/jclass/billing.py,sha256=Hkk_yvnPQel0Fw_SFVdhP3unmhRkWNDVUa4RM1qhzdY,5499
5
5
  sjbillingclient/jclass/consume.py,sha256=Hh8sLGosVvs22NhdQRudFc_NP_ahvrJSJdJTM-_nLBQ,310
6
- sjbillingclient/jclass/purchase.py,sha256=sLEihOLiyvwDIWBhHSpmMj1swNr4RJizceQOKmBqHM4,346
7
- sjbillingclient/jclass/queryproduct.py,sha256=0JkaC9E1Juxl79t7gJCUfrQntWiLWjfScSEI-mss8Lg,671
6
+ sjbillingclient/jclass/purchase.py,sha256=trFRSDHdMUPZsAxdyU7HuChT0qicDO-EjfPp9d6unrA,844
7
+ sjbillingclient/jclass/queryproduct.py,sha256=Akuxy3fO1djerVPidqmjUclKVtIDmlaNJxZDbyUWOUw,1115
8
8
  sjbillingclient/jinterface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  sjbillingclient/jinterface/acknowledge.py,sha256=OG5GWYO1TXfC7nscn8pPjsrdm2BGOCASRCzscKfyPFM,522
10
10
  sjbillingclient/jinterface/billing.py,sha256=lwBGN8Xe0Qbc1DCRSShrhL8oGqRbjugwrgNspEK-nmc,1265
11
11
  sjbillingclient/jinterface/consume.py,sha256=DiugwpRreoVLpPjeHSkNMgncg5f3HnIBCUh0OIyrWhI,524
12
- sjbillingclient/jinterface/product.py,sha256=rtlc-4hxToCa3buwNp8QufMYkoiUZHp_YXKeE-QZZyk,562
12
+ sjbillingclient/jinterface/product.py,sha256=kr_oPhRnQd0xbZItFLtACjBhZsnUV8q-kooqG_biVPs,627
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=PqsUr6tIBdAUN4NNKvWR3aRdNQ-ZN84MAzkBKcm6sik,21127
15
+ sjbillingclient-0.2.0.dist-info/METADATA,sha256=DICVzpmYPMuAUP9Co-yyBchoBMpT-oOOOTokqC6zAwY,18430
16
+ sjbillingclient-0.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
17
+ sjbillingclient-0.2.0.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
-