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.
- sjbillingclient/tools/__init__.py +351 -82
- sjbillingclient-0.1.4.dist-info/METADATA +339 -0
- {sjbillingclient-0.1.3.dist-info → sjbillingclient-0.1.4.dist-info}/RECORD +4 -4
- sjbillingclient-0.1.3.dist-info/METADATA +0 -29
- {sjbillingclient-0.1.3.dist-info → sjbillingclient-0.1.4.dist-info}/WHEEL +0 -0
@@ -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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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:
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
137
|
+
This function utilizes the provided product details response callback to handle the
|
138
|
+
resulting response from the query.
|
60
139
|
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
68
|
-
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
120
|
-
|
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
|
+
[](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=
|
15
|
-
sjbillingclient-0.1.
|
16
|
-
sjbillingclient-0.1.
|
17
|
-
sjbillingclient-0.1.
|
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
|
-
|
File without changes
|