sjbillingclient 0.1.4__tar.gz → 0.2.0__tar.gz
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-0.1.4 → sjbillingclient-0.2.0}/PKG-INFO +194 -26
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/README.md +193 -25
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/pyproject.toml +1 -1
- sjbillingclient-0.2.0/sjbillingclient/jclass/purchase.py +16 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jclass/queryproduct.py +9 -1
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jinterface/product.py +4 -3
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/tools/__init__.py +37 -3
- sjbillingclient-0.1.4/sjbillingclient/jclass/purchase.py +0 -8
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/__init__.py +0 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jclass/__init__.py +0 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jclass/acknowledge.py +0 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jclass/billing.py +0 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jclass/consume.py +0 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jinterface/__init__.py +0 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jinterface/acknowledge.py +0 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jinterface/billing.py +0 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jinterface/consume.py +0 -0
- {sjbillingclient-0.1.4 → sjbillingclient-0.2.0}/sjbillingclient/jinterface/purchases.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sjbillingclient
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary:
|
5
5
|
Author: Kenechukwu Akubue
|
6
6
|
Author-email: kengoon19@gmail.com
|
@@ -36,7 +36,7 @@ SJBillingClient is a Python wrapper for the Google Play Billing Library that fac
|
|
36
36
|
|
37
37
|
- Python 3.9+
|
38
38
|
- pyjnius 1.6.1+
|
39
|
-
- Android application with Google Play Billing Library (version
|
39
|
+
- Android application with Google Play Billing Library (version 8.0.0 recommended)
|
40
40
|
|
41
41
|
## Installation
|
42
42
|
|
@@ -46,7 +46,7 @@ pip install sjbillingclient
|
|
46
46
|
|
47
47
|
# In Buildozer (add to buildozer.spec)
|
48
48
|
requirements = sjbillingclient
|
49
|
-
android.gradle_dependencies = com.android.billingclient:billing:
|
49
|
+
android.gradle_dependencies = com.android.billingclient:billing:8.0.0
|
50
50
|
```
|
51
51
|
|
52
52
|
## Quick Start
|
@@ -170,14 +170,28 @@ from kivy.uix.screenmanager import ScreenManager, Screen
|
|
170
170
|
from sjbillingclient.jclass.billing import BillingResponseCode, ProductType
|
171
171
|
from sjbillingclient.tools import BillingClient
|
172
172
|
|
173
|
-
# Load the KV file
|
174
173
|
Builder.load_file(join(dirname(__file__), basename(__file__).split(".")[0] + ".kv"))
|
175
174
|
|
176
175
|
|
177
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
|
+
"""
|
178
186
|
billing_client = None
|
179
187
|
|
180
|
-
def
|
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
|
+
"""
|
181
195
|
if self.billing_client:
|
182
196
|
self.billing_client.end_connection()
|
183
197
|
|
@@ -188,6 +202,17 @@ class HomeScreen(Screen):
|
|
188
202
|
)
|
189
203
|
|
190
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
|
+
"""
|
191
216
|
if billing_result.getResponseCode() == BillingResponseCode.OK and not null:
|
192
217
|
for purchase in purchases:
|
193
218
|
if self.ids.subscribe.active:
|
@@ -197,40 +222,96 @@ class HomeScreen(Screen):
|
|
197
222
|
)
|
198
223
|
else:
|
199
224
|
self.billing_client.consume_async(purchase, self.on_consume_response)
|
225
|
+
print(billing_result.getResponseCode(), billing_result.getDebugMessage())
|
200
226
|
|
201
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
|
+
"""
|
202
234
|
print(billing_result.getDebugMessage())
|
203
235
|
if billing_result.getResponseCode() == BillingResponseCode.OK:
|
204
236
|
self.toast("Thank you for subscribing to buy us a cup of coffee! monthly")
|
205
237
|
|
206
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
|
+
"""
|
207
245
|
if billing_result.getResponseCode() == BillingResponseCode.OK:
|
208
246
|
self.toast("Thank you for buying us a cup of coffee!")
|
209
247
|
|
210
|
-
def on_product_details_response(self, billing_result,
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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
|
+
|
215
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))
|
216
268
|
self.billing_client.launch_billing_flow(product_details=product_details_list)
|
217
269
|
|
218
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
|
+
"""
|
219
279
|
product_id = self.ids.btn.product_id
|
220
280
|
if billing_result.getResponseCode() == BillingResponseCode.OK:
|
221
281
|
self.billing_client.query_product_details_async(
|
222
|
-
product_type=ProductType.SUBS if self.ids.subscribe
|
282
|
+
product_type=ProductType.SUBS if self.ids.subscribe else ProductType.INAPP,
|
223
283
|
products_ids=[product_id],
|
224
284
|
on_product_details_response=self.on_product_details_response,
|
225
285
|
)
|
226
286
|
|
227
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
|
+
"""
|
228
297
|
# Implementation of toast message (platform specific)
|
229
298
|
print(message)
|
230
299
|
|
231
300
|
|
232
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
|
+
"""
|
233
308
|
def build(self):
|
309
|
+
"""
|
310
|
+
Build the application UI.
|
311
|
+
|
312
|
+
Returns:
|
313
|
+
ScreenManager: The root widget of the application.
|
314
|
+
"""
|
234
315
|
# Create screen manager
|
235
316
|
sm = ScreenManager()
|
236
317
|
sm.add_widget(HomeScreen(name='home'))
|
@@ -276,7 +357,7 @@ if __name__ == '__main__':
|
|
276
357
|
product_id: 'coffee_product_id'
|
277
358
|
size_hint_y: None
|
278
359
|
height: '60dp'
|
279
|
-
on_release: root.
|
360
|
+
on_release: root.support()
|
280
361
|
|
281
362
|
Widget:
|
282
363
|
# Spacer
|
@@ -284,13 +365,20 @@ if __name__ == '__main__':
|
|
284
365
|
|
285
366
|
This example demonstrates:
|
286
367
|
|
287
|
-
1. A `HomeScreen` class that handles all billing operations
|
368
|
+
1. A `HomeScreen` class that extends `Screen` and handles all billing operations
|
288
369
|
2. A `BillingApp` class that sets up the Kivy application and screen manager
|
289
370
|
3. A Kivy layout file that defines the UI with:
|
290
371
|
- A checkbox to toggle between one-time purchase and subscription
|
291
372
|
- A button to initiate the purchase flow
|
292
373
|
|
293
|
-
The `
|
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.
|
294
382
|
|
295
383
|
## API Reference
|
296
384
|
|
@@ -298,16 +386,89 @@ The `purchase_or_subscribe` method is called when the button is pressed, which i
|
|
298
386
|
|
299
387
|
The main class for interacting with Google Play Billing.
|
300
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
|
+
|
301
468
|
#### Methods
|
302
469
|
|
303
|
-
- `
|
304
|
-
- `
|
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
|
470
|
+
- `getProductDetailsList()`: Gets the list of product details
|
471
|
+
- `getUnfetchedProductList()`: Gets the list of unfetched products
|
311
472
|
|
312
473
|
### ProductType
|
313
474
|
|
@@ -320,10 +481,17 @@ Constants for product types:
|
|
320
481
|
|
321
482
|
Constants for billing response codes:
|
322
483
|
|
323
|
-
- `BillingResponseCode.OK`: Success
|
324
|
-
- `BillingResponseCode.USER_CANCELED`: User canceled
|
325
|
-
- `BillingResponseCode.SERVICE_UNAVAILABLE`: Service unavailable
|
326
|
-
-
|
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)
|
327
495
|
|
328
496
|
## Contributing
|
329
497
|
|
@@ -20,7 +20,7 @@ SJBillingClient is a Python wrapper for the Google Play Billing Library that fac
|
|
20
20
|
|
21
21
|
- Python 3.9+
|
22
22
|
- pyjnius 1.6.1+
|
23
|
-
- Android application with Google Play Billing Library (version
|
23
|
+
- Android application with Google Play Billing Library (version 8.0.0 recommended)
|
24
24
|
|
25
25
|
## Installation
|
26
26
|
|
@@ -30,7 +30,7 @@ pip install sjbillingclient
|
|
30
30
|
|
31
31
|
# In Buildozer (add to buildozer.spec)
|
32
32
|
requirements = sjbillingclient
|
33
|
-
android.gradle_dependencies = com.android.billingclient:billing:
|
33
|
+
android.gradle_dependencies = com.android.billingclient:billing:8.0.0
|
34
34
|
```
|
35
35
|
|
36
36
|
## Quick Start
|
@@ -154,14 +154,28 @@ from kivy.uix.screenmanager import ScreenManager, Screen
|
|
154
154
|
from sjbillingclient.jclass.billing import BillingResponseCode, ProductType
|
155
155
|
from sjbillingclient.tools import BillingClient
|
156
156
|
|
157
|
-
# Load the KV file
|
158
157
|
Builder.load_file(join(dirname(__file__), basename(__file__).split(".")[0] + ".kv"))
|
159
158
|
|
160
159
|
|
161
160
|
class HomeScreen(Screen):
|
161
|
+
"""
|
162
|
+
A screen that demonstrates Google Play Billing integration with Kivy.
|
163
|
+
|
164
|
+
This screen provides functionality to make in-app purchases and subscriptions
|
165
|
+
using the Google Play Billing Library through the SJBillingClient wrapper.
|
166
|
+
|
167
|
+
Attributes:
|
168
|
+
billing_client (BillingClient): The client used to interact with Google Play Billing.
|
169
|
+
"""
|
162
170
|
billing_client = None
|
163
171
|
|
164
|
-
def
|
172
|
+
def support(self):
|
173
|
+
"""
|
174
|
+
Initializes the billing client and starts a connection to the Google Play Billing service.
|
175
|
+
|
176
|
+
This method is called when the user wants to make a purchase or subscription.
|
177
|
+
If a billing client already exists, it ends the connection before creating a new one.
|
178
|
+
"""
|
165
179
|
if self.billing_client:
|
166
180
|
self.billing_client.end_connection()
|
167
181
|
|
@@ -172,6 +186,17 @@ class HomeScreen(Screen):
|
|
172
186
|
)
|
173
187
|
|
174
188
|
def on_purchases_updated(self, billing_result, null, purchases):
|
189
|
+
"""
|
190
|
+
Callback method that is called when purchases are updated.
|
191
|
+
|
192
|
+
This method handles the result of a purchase flow, either acknowledging
|
193
|
+
a subscription or consuming a one-time purchase.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
billing_result: The result of the billing operation.
|
197
|
+
null: Boolean indicating if the purchases list is null.
|
198
|
+
purchases: List of purchases that were updated.
|
199
|
+
"""
|
175
200
|
if billing_result.getResponseCode() == BillingResponseCode.OK and not null:
|
176
201
|
for purchase in purchases:
|
177
202
|
if self.ids.subscribe.active:
|
@@ -181,40 +206,96 @@ class HomeScreen(Screen):
|
|
181
206
|
)
|
182
207
|
else:
|
183
208
|
self.billing_client.consume_async(purchase, self.on_consume_response)
|
209
|
+
print(billing_result.getResponseCode(), billing_result.getDebugMessage())
|
184
210
|
|
185
211
|
def on_acknowledge_purchase_response(self, billing_result):
|
212
|
+
"""
|
213
|
+
Callback method that is called when a purchase acknowledgement is complete.
|
214
|
+
|
215
|
+
Args:
|
216
|
+
billing_result: The result of the acknowledgement operation.
|
217
|
+
"""
|
186
218
|
print(billing_result.getDebugMessage())
|
187
219
|
if billing_result.getResponseCode() == BillingResponseCode.OK:
|
188
220
|
self.toast("Thank you for subscribing to buy us a cup of coffee! monthly")
|
189
221
|
|
190
222
|
def on_consume_response(self, billing_result):
|
223
|
+
"""
|
224
|
+
Callback method that is called when a purchase consumption is complete.
|
225
|
+
|
226
|
+
Args:
|
227
|
+
billing_result: The result of the consumption operation.
|
228
|
+
"""
|
191
229
|
if billing_result.getResponseCode() == BillingResponseCode.OK:
|
192
230
|
self.toast("Thank you for buying us a cup of coffee!")
|
193
231
|
|
194
|
-
def on_product_details_response(self, billing_result,
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
232
|
+
def on_product_details_response(self, billing_result, product_details_result):
|
233
|
+
"""
|
234
|
+
Callback method that is called when product details are retrieved.
|
235
|
+
|
236
|
+
This method processes the product details and launches the billing flow.
|
237
|
+
|
238
|
+
Args:
|
239
|
+
billing_result: The result of the product details query.
|
240
|
+
product_details_result: The result containing product details and unfetched products.
|
241
|
+
"""
|
242
|
+
product_details_list = product_details_result.getProductDetailsList()
|
243
|
+
unfetched_product_list = product_details_result.getUnfetchedProductList()
|
244
|
+
|
199
245
|
if billing_result.getResponseCode() == BillingResponseCode.OK:
|
246
|
+
for product_details in product_details_list:
|
247
|
+
self.billing_client.get_product_details(
|
248
|
+
product_details,
|
249
|
+
ProductType.SUBS if self.ids.subscribe.active else ProductType.INAPP)
|
250
|
+
for unfetched_product in unfetched_product_list:
|
251
|
+
print(self.billing_client.get_unfetched_product(unfetched_product))
|
200
252
|
self.billing_client.launch_billing_flow(product_details=product_details_list)
|
201
253
|
|
202
254
|
def on_billing_setup_finished(self, billing_result):
|
255
|
+
"""
|
256
|
+
Callback method that is called when the billing setup is complete.
|
257
|
+
|
258
|
+
This method queries product details if the billing setup was successful.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
billing_result: The result of the billing setup operation.
|
262
|
+
"""
|
203
263
|
product_id = self.ids.btn.product_id
|
204
264
|
if billing_result.getResponseCode() == BillingResponseCode.OK:
|
205
265
|
self.billing_client.query_product_details_async(
|
206
|
-
product_type=ProductType.SUBS if self.ids.subscribe
|
266
|
+
product_type=ProductType.SUBS if self.ids.subscribe else ProductType.INAPP,
|
207
267
|
products_ids=[product_id],
|
208
268
|
on_product_details_response=self.on_product_details_response,
|
209
269
|
)
|
210
270
|
|
211
271
|
def toast(self, message):
|
272
|
+
"""
|
273
|
+
Display a toast message.
|
274
|
+
|
275
|
+
This is a simple implementation that just prints the message.
|
276
|
+
In a real app, you would use platform-specific toast functionality.
|
277
|
+
|
278
|
+
Args:
|
279
|
+
message: The message to display.
|
280
|
+
"""
|
212
281
|
# Implementation of toast message (platform specific)
|
213
282
|
print(message)
|
214
283
|
|
215
284
|
|
216
285
|
class BillingApp(App):
|
286
|
+
"""
|
287
|
+
Main application class for the SJBillingClient demo.
|
288
|
+
|
289
|
+
This class sets up the application and creates the screen manager
|
290
|
+
with the HomeScreen.
|
291
|
+
"""
|
217
292
|
def build(self):
|
293
|
+
"""
|
294
|
+
Build the application UI.
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
ScreenManager: The root widget of the application.
|
298
|
+
"""
|
218
299
|
# Create screen manager
|
219
300
|
sm = ScreenManager()
|
220
301
|
sm.add_widget(HomeScreen(name='home'))
|
@@ -260,7 +341,7 @@ if __name__ == '__main__':
|
|
260
341
|
product_id: 'coffee_product_id'
|
261
342
|
size_hint_y: None
|
262
343
|
height: '60dp'
|
263
|
-
on_release: root.
|
344
|
+
on_release: root.support()
|
264
345
|
|
265
346
|
Widget:
|
266
347
|
# Spacer
|
@@ -268,13 +349,20 @@ if __name__ == '__main__':
|
|
268
349
|
|
269
350
|
This example demonstrates:
|
270
351
|
|
271
|
-
1. A `HomeScreen` class that handles all billing operations
|
352
|
+
1. A `HomeScreen` class that extends `Screen` and handles all billing operations
|
272
353
|
2. A `BillingApp` class that sets up the Kivy application and screen manager
|
273
354
|
3. A Kivy layout file that defines the UI with:
|
274
355
|
- A checkbox to toggle between one-time purchase and subscription
|
275
356
|
- A button to initiate the purchase flow
|
276
357
|
|
277
|
-
The `
|
358
|
+
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:
|
359
|
+
- Handling purchase updates with `on_purchases_updated`
|
360
|
+
- Acknowledging subscription purchases with `acknowledge_purchase`
|
361
|
+
- Consuming one-time purchases with `consume_async`
|
362
|
+
- Processing product details with `on_product_details_response`, including handling unfetched products
|
363
|
+
- Querying product details with `query_product_details_async`
|
364
|
+
|
365
|
+
This example is designed to be copy-and-paste runnable, with no need for the user to add or remove anything to test it.
|
278
366
|
|
279
367
|
## API Reference
|
280
368
|
|
@@ -282,16 +370,89 @@ The `purchase_or_subscribe` method is called when the button is pressed, which i
|
|
282
370
|
|
283
371
|
The main class for interacting with Google Play Billing.
|
284
372
|
|
373
|
+
#### Constructor
|
374
|
+
|
375
|
+
- `__init__(on_purchases_updated, enable_one_time_products=True, enable_prepaid_plans=False)`:
|
376
|
+
- Initializes a new BillingClient instance
|
377
|
+
- `on_purchases_updated`: Callback function that will be triggered when purchases are updated
|
378
|
+
- `enable_one_time_products`: Boolean to enable one-time products (default: True)
|
379
|
+
- `enable_prepaid_plans`: Boolean to enable prepaid plans (default: False)
|
380
|
+
|
381
|
+
#### Connection Methods
|
382
|
+
|
383
|
+
- `start_connection(on_billing_setup_finished, on_billing_service_disconnected)`:
|
384
|
+
- Starts a connection with the billing client
|
385
|
+
- `on_billing_setup_finished`: Callback when billing setup is complete
|
386
|
+
- `on_billing_service_disconnected`: Callback when billing service is disconnected
|
387
|
+
|
388
|
+
- `end_connection()`:
|
389
|
+
- Ends the connection with the billing client
|
390
|
+
|
391
|
+
#### Product Details Methods
|
392
|
+
|
393
|
+
- `query_product_details_async(product_type, products_ids, on_product_details_response)`:
|
394
|
+
- Queries product details asynchronously
|
395
|
+
- `product_type`: Type of products (INAPP or SUBS)
|
396
|
+
- `products_ids`: List of product IDs to query
|
397
|
+
- `on_product_details_response`: Callback for product details response
|
398
|
+
|
399
|
+
- `get_product_details(product_details, product_type)`:
|
400
|
+
- Gets formatted product details
|
401
|
+
- `product_details`: Product details object
|
402
|
+
- `product_type`: Type of product (INAPP or SUBS)
|
403
|
+
- Returns a list of dictionaries with product details
|
404
|
+
|
405
|
+
- `get_unfetched_product(unfetched_product)`:
|
406
|
+
- Gets details about an unfetched product
|
407
|
+
- `unfetched_product`: Unfetched product object
|
408
|
+
- Returns a dictionary with product ID, type, and status code
|
409
|
+
|
410
|
+
#### Purchase Methods
|
411
|
+
|
412
|
+
- `launch_billing_flow(product_details, offer_token=None)`:
|
413
|
+
- Launches the billing flow for purchase
|
414
|
+
- `product_details`: List of product details objects
|
415
|
+
- `offer_token`: Optional token for subscription offers
|
416
|
+
|
417
|
+
- `consume_async(purchase, on_consume_response)`:
|
418
|
+
- Consumes a purchase asynchronously
|
419
|
+
- `purchase`: Purchase object to consume
|
420
|
+
- `on_consume_response`: Callback for consume response
|
421
|
+
|
422
|
+
- `acknowledge_purchase(purchase_token, on_acknowledge_purchase_response)`:
|
423
|
+
- Acknowledges a purchase
|
424
|
+
- `purchase_token`: Token of the purchase to acknowledge
|
425
|
+
- `on_acknowledge_purchase_response`: Callback for acknowledge response
|
426
|
+
|
427
|
+
### PendingPurchasesParams
|
428
|
+
|
429
|
+
Parameters for handling pending purchases.
|
430
|
+
|
431
|
+
#### Methods
|
432
|
+
|
433
|
+
- `newBuilder()`: Creates a new builder for PendingPurchasesParams
|
434
|
+
- `build()`: Builds the PendingPurchasesParams object
|
435
|
+
- `enableOneTimeProducts()`: Enables one-time products
|
436
|
+
- `enablePrepaidPlans()`: Enables prepaid plans
|
437
|
+
|
438
|
+
### QueryProductDetailsParams
|
439
|
+
|
440
|
+
Parameters for querying product details.
|
441
|
+
|
442
|
+
#### Methods
|
443
|
+
|
444
|
+
- `newBuilder()`: Creates a new builder for QueryProductDetailsParams
|
445
|
+
- `setProductList(product_list)`: Sets the list of products to query
|
446
|
+
- `build()`: Builds the QueryProductDetailsParams object
|
447
|
+
|
448
|
+
### QueryProductDetailsResult
|
449
|
+
|
450
|
+
Result of a product details query.
|
451
|
+
|
285
452
|
#### Methods
|
286
453
|
|
287
|
-
- `
|
288
|
-
- `
|
289
|
-
- `end_connection()`: End billing connection
|
290
|
-
- `query_product_details_async(product_type, products_ids, on_product_details_response)`: Query product details
|
291
|
-
- `get_product_details(product_details, product_type)`: Get formatted product details
|
292
|
-
- `launch_billing_flow(product_details, offer_token=None)`: Launch purchase flow
|
293
|
-
- `consume_async(purchase, on_consume_response)`: Consume a purchase
|
294
|
-
- `acknowledge_purchase(purchase_token, on_acknowledge_purchase_response)`: Acknowledge a purchase
|
454
|
+
- `getProductDetailsList()`: Gets the list of product details
|
455
|
+
- `getUnfetchedProductList()`: Gets the list of unfetched products
|
295
456
|
|
296
457
|
### ProductType
|
297
458
|
|
@@ -304,10 +465,17 @@ Constants for product types:
|
|
304
465
|
|
305
466
|
Constants for billing response codes:
|
306
467
|
|
307
|
-
- `BillingResponseCode.OK`: Success
|
308
|
-
- `BillingResponseCode.USER_CANCELED`: User canceled
|
309
|
-
- `BillingResponseCode.SERVICE_UNAVAILABLE`: Service unavailable
|
310
|
-
-
|
468
|
+
- `BillingResponseCode.OK`: Success (0)
|
469
|
+
- `BillingResponseCode.USER_CANCELED`: User canceled (1)
|
470
|
+
- `BillingResponseCode.SERVICE_UNAVAILABLE`: Service unavailable (2)
|
471
|
+
- `BillingResponseCode.BILLING_UNAVAILABLE`: Billing unavailable (3)
|
472
|
+
- `BillingResponseCode.ITEM_UNAVAILABLE`: Item unavailable (4)
|
473
|
+
- `BillingResponseCode.DEVELOPER_ERROR`: Developer error (5)
|
474
|
+
- `BillingResponseCode.ERROR`: General error (6)
|
475
|
+
- `BillingResponseCode.ITEM_ALREADY_OWNED`: Item already owned (7)
|
476
|
+
- `BillingResponseCode.ITEM_NOT_OWNED`: Item not owned (8)
|
477
|
+
- `BillingResponseCode.SERVICE_DISCONNECTED`: Service disconnected (10)
|
478
|
+
- `BillingResponseCode.FEATURE_NOT_SUPPORTED`: Feature not supported (12)
|
311
479
|
|
312
480
|
## Contributing
|
313
481
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from jnius import JavaClass, MetaJavaClass, JavaStaticMethod, JavaMethod
|
2
|
+
|
3
|
+
__all__ = ("PendingPurchasesParams", "PendingPurchasesParamsBuilder")
|
4
|
+
|
5
|
+
|
6
|
+
class PendingPurchasesParams(JavaClass, metaclass=MetaJavaClass):
|
7
|
+
__javaclass__ = f"com/android/billingclient/api/PendingPurchasesParams"
|
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;
|
14
|
-
|
15
|
-
|
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)
|
@@ -42,6 +42,7 @@ from sjbillingclient.jclass.billing import BillingClient as SJBillingClient, Pro
|
|
42
42
|
BillingFlowParams
|
43
43
|
from android.activity import _activity as activity # noqa
|
44
44
|
from sjbillingclient.jclass.consume import ConsumeParams
|
45
|
+
from sjbillingclient.jclass.purchase import PendingPurchasesParams
|
45
46
|
from sjbillingclient.jclass.queryproduct import QueryProductDetailsParams, QueryProductDetailsParamsProduct
|
46
47
|
from sjbillingclient.jinterface.acknowledge import AcknowledgePurchaseResponseListener
|
47
48
|
from sjbillingclient.jinterface.billing import BillingClientStateListener
|
@@ -49,7 +50,6 @@ from sjbillingclient.jinterface.consume import ConsumeResponseListener
|
|
49
50
|
from sjbillingclient.jinterface.product import ProductDetailsResponseListener
|
50
51
|
from sjbillingclient.jinterface.purchases import PurchasesUpdatedListener
|
51
52
|
|
52
|
-
|
53
53
|
ERROR_NO_BASE_PLAN = "You don't have a base plan"
|
54
54
|
ERROR_NO_BASE_PLAN_ID = "You don't have a base plan id"
|
55
55
|
ERROR_INVALID_PRODUCT_TYPE = "product_type not supported. Must be one of `ProductType.SUBS`, `ProductType.INAPP`"
|
@@ -77,7 +77,13 @@ class BillingClient:
|
|
77
77
|
:ivar __acknowledge_purchase_response_listener: Listener handling responses for acknowledging purchases.
|
78
78
|
:type __acknowledge_purchase_response_listener: AcknowledgePurchaseResponseListener | None
|
79
79
|
"""
|
80
|
-
|
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:
|
81
87
|
"""
|
82
88
|
Initializes an instance of the class with the given purchase update callback.
|
83
89
|
|
@@ -92,10 +98,15 @@ class BillingClient:
|
|
92
98
|
self.__acknowledge_purchase_response_listener = None
|
93
99
|
|
94
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()
|
95
106
|
self.__billing_client = (
|
96
107
|
SJBillingClient.newBuilder(activity.context)
|
97
108
|
.setListener(self.__purchase_update_listener)
|
98
|
-
.enablePendingPurchases()
|
109
|
+
.enablePendingPurchases(pending_purchase_params.build())
|
99
110
|
.build()
|
100
111
|
)
|
101
112
|
|
@@ -177,6 +188,28 @@ class BillingClient:
|
|
177
188
|
.setProductType(product_type)
|
178
189
|
.build())
|
179
190
|
|
191
|
+
@staticmethod
|
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
|
+
|
180
213
|
def get_product_details(self, product_details, product_type: str) -> List[Dict]:
|
181
214
|
"""
|
182
215
|
Retrieves the details of a product based on the provided product type. The function processes
|
@@ -292,6 +325,7 @@ class BillingClient:
|
|
292
325
|
self._create_product_params(product_detail, offer_token)
|
293
326
|
for product_detail in product_details
|
294
327
|
]
|
328
|
+
print(product_params_list)
|
295
329
|
|
296
330
|
billing_flow_params = (BillingFlowParams.newBuilder()
|
297
331
|
.setProductDetailsParamsList(JavaList.of(*product_params_list))
|
@@ -1,8 +0,0 @@
|
|
1
|
-
from jnius import JavaClass, MetaJavaClass, JavaStaticMethod
|
2
|
-
|
3
|
-
__all__ = ("PendingPurchasesParams",)
|
4
|
-
|
5
|
-
|
6
|
-
class PendingPurchasesParams(JavaClass, metaclass=MetaJavaClass):
|
7
|
-
__javaclass__ = f"com/android/billingclient/api/PendingPurchasesParams"
|
8
|
-
newBuilder = JavaStaticMethod("()Lcom/android/billingclient/api/PendingPurchasesParams$Builder;")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|