├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── buildozer.txt
├── googleplayapi.py
├── java
├── KivyBillingClientStateListener.java
├── KivyConsumeResponseListener.java
├── KivyProductDetailsResponseListener.java
├── KivyPurchasesUpdatedListener.java
└── org
│ └── org
│ └── googleplay
│ ├── CLCallbackWrapper.java
│ ├── DLCallbackWrapper.java
│ ├── SLCallbackWrapper.java
│ └── ULCallbackWrapper.java
└── main.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105 | __pypackages__/
106 |
107 | # Celery stuff
108 | celerybeat-schedule
109 | celerybeat.pid
110 |
111 | # SageMath parsed files
112 | *.sage.py
113 |
114 | # Environments
115 | .env
116 | .venv
117 | env/
118 | venv/
119 | ENV/
120 | env.bak/
121 | venv.bak/
122 |
123 | # Spyder project settings
124 | .spyderproject
125 | .spyproject
126 |
127 | # Rope project settings
128 | .ropeproject
129 |
130 | # mkdocs documentation
131 | /site
132 |
133 | # mypy
134 | .mypy_cache/
135 | .dmypy.json
136 | dmypy.json
137 |
138 | # Pyre type checker
139 | .pyre/
140 |
141 | # pytype static type analyzer
142 | .pytype/
143 |
144 | # Cython debug symbols
145 | cython_debug/
146 |
147 | # PyCharm
148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
150 | # and can be added to the global gitignore or merged into this file. For a more nuclear
151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
152 | #.idea/
153 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 LionReal
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # In-app billing in Kivy Applications
2 |
3 | This repository contains an application structure that works with Kivy, which allows you to make in-app purchases using the Google Play Billing Library 6.0.1.
4 |
5 | You can use it for Purchased Consumable Products.
6 | There are no subscription modules.
7 |
8 | You must define the items used in the application as consumable products via the Google play console.
9 |
10 | Don't forget to define the items in the googleplayapi.py file and main.py
11 |
12 | You can access the license key used in the application in the following way:
13 | 1- Open Play Console and select the app.
14 | 2- Go to the Monetization setup page(Monetize > Monetization setup).
15 | 3- Your license key is under "Licensing."
16 | 4- Define license key in main.py and buildozer.spec
17 |
18 |
19 |
20 |
21 | Example Video:
22 |
23 | https://github.com/LionReal/GooglePlayBilling/assets/79577465/448ef0ed-302d-4263-9bef-0ddcdf25dcde
24 |
25 |
26 | Links:
27 |
28 | https://developer.android.com/google/play/billing/integrate
29 |
30 |
31 | https://medium.com/androiddevelopers/working-with-google-play-billing-part-2-b859b55426d2
32 |
33 |
34 |
--------------------------------------------------------------------------------
/buildozer.txt:
--------------------------------------------------------------------------------
1 |
2 | [app]
3 |
4 | # (str) Title of your application
5 | title = Google Play Billing
6 |
7 | # (str) Package name
8 | package.name = billing
9 |
10 | # (str) Package domain (needed for android/ios packaging)
11 | package.domain = org
12 |
13 | # (str) Source code where the main.py live
14 | source.dir = .
15 |
16 | # (list) Source files to include (let empty to include all the files)
17 | source.include_exts = py,png,jpg,kv,atlas
18 |
19 | # (list) List of inclusions using pattern matching
20 | #source.include_patterns = assets/*,images/*.png
21 |
22 | # (list) Source files to exclude (let empty to not exclude anything)
23 | #source.exclude_exts = spec
24 |
25 | # (list) List of directory to exclude (let empty to not exclude anything)
26 | #source.exclude_dirs = tests, bin, venv
27 |
28 | # (list) List of exclusions using pattern matching
29 | # Do not prefix with './'
30 | #source.exclude_patterns = license,images/*/*.jpg
31 |
32 | # (str) Application versioning (method 1)
33 | version = 0.1
34 |
35 | # (str) Application versioning (method 2)
36 | # version.regex = __version__ = ['"](.*)['"]
37 | # version.filename = %(source.dir)s/main.py
38 |
39 | # (list) Application requirements
40 | # comma separated e.g. requirements = sqlite3,kivy
41 | requirements = python3,kivy==master,pyjnius==master
42 |
43 | # (str) Custom source folders for requirements
44 | # Sets custom source for any requirements with recipes
45 | # requirements.source.kivy = ../../kivy
46 |
47 | # (str) Presplash of the application
48 | #presplash.filename = %(source.dir)s/data/presplash.png
49 |
50 | # (str) Icon of the application
51 | #icon.filename = %(source.dir)s/data/icon.png
52 |
53 | # (list) Supported orientations
54 | # Valid options are: landscape, portrait, portrait-reverse or landscape-reverse
55 | orientation = portrait
56 |
57 | # (list) List of service to declare
58 | #services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY
59 |
60 | #
61 | # OSX Specific
62 | #
63 |
64 | #
65 | # author = © Copyright Info
66 |
67 | # change the major version of python used by the app
68 | osx.python_version = 3
69 |
70 | # Kivy version to use
71 | osx.kivy_version = 1.9.1
72 |
73 | #
74 | # Android specific
75 | #
76 |
77 | # (bool) Indicate if the application should be fullscreen or not
78 | fullscreen = 0
79 |
80 | # (string) Presplash background color (for android toolchain)
81 | # Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
82 | # red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
83 | # darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
84 | # olive, purple, silver, teal.
85 | #android.presplash_color = #FFFFFF
86 |
87 | # (string) Presplash animation using Lottie format.
88 | # see https://lottiefiles.com/ for examples and https://airbnb.design/lottie/
89 | # for general documentation.
90 | # Lottie files can be created using various tools, like Adobe After Effect or Synfig.
91 | #android.presplash_lottie = "path/to/lottie/file.json"
92 |
93 | # (str) Adaptive icon of the application (used if Android API level is 26+ at runtime)
94 | #icon.adaptive_foreground.filename = %(source.dir)s/data/icon_fg.png
95 | #icon.adaptive_background.filename = %(source.dir)s/data/icon_bg.png
96 |
97 | # (list) Permissions
98 | # (See https://python-for-android.readthedocs.io/en/latest/buildoptions/#build-options-1 for all the supported syntaxes and properties)
99 | #android.permissions = android.permission.INTERNET, (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)
100 |
101 | android.permissions = INTERNET,ACCESS_NETWORK_STATE,com.android.vending.BILLING,
102 | android.meta_data = billing_pubkey = Your-license-key
103 | # (list) features (adds uses-feature -tags to manifest)
104 | #android.features = android.hardware.usb.host
105 |
106 | # (int) Target Android API, should be as high as possible.
107 | #android.api = 31
108 | android.api = 33
109 |
110 | # (int) Minimum API your APK / AAB will support.
111 | #android.minapi = 21
112 |
113 | # (int) Android SDK version to use
114 | #android.sdk = 20
115 |
116 | # (str) Android NDK version to use
117 | #android.ndk = 23b
118 |
119 | # (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi.
120 | #android.ndk_api = 21
121 |
122 | # (bool) Use --private data storage (True) or --dir public storage (False)
123 | #android.private_storage = True
124 |
125 | # (str) Android NDK directory (if empty, it will be automatically downloaded.)
126 | #android.ndk_path =
127 |
128 | # (str) Android SDK directory (if empty, it will be automatically downloaded.)
129 | #android.sdk_path =
130 |
131 | # (str) ANT directory (if empty, it will be automatically downloaded.)
132 | #android.ant_path =
133 |
134 | # (bool) If True, then skip trying to update the Android sdk
135 | # This can be useful to avoid excess Internet downloads or save time
136 | # when an update is due and you just want to test/build your package
137 | # android.skip_update = False
138 |
139 | # (bool) If True, then automatically accept SDK license
140 | # agreements. This is intended for automation only. If set to False,
141 | # the default, you will be shown the license when first running
142 | # buildozer.
143 | # android.accept_sdk_license = False
144 |
145 | # (str) Android entry point, default is ok for Kivy-based app
146 | #android.entrypoint = org.kivy.android.PythonActivity
147 |
148 | # (str) Full name including package path of the Java class that implements Android Activity
149 | # use that parameter together with android.entrypoint to set custom Java class instead of PythonActivity
150 | #android.activity_class_name = org.kivy.android.PythonActivity
151 |
152 | # (str) Extra xml to write directly inside the element of AndroidManifest.xml
153 | # use that parameter to provide a filename from where to load your custom XML code
154 | #android.extra_manifest_xml = ./src/android/extra_manifest.xml
155 |
156 | # (str) Extra xml to write directly inside the tag of AndroidManifest.xml
157 | # use that parameter to provide a filename from where to load your custom XML arguments:
158 | #android.extra_manifest_application_arguments = ./src/android/extra_manifest_application_arguments.xml
159 |
160 | # (str) Full name including package path of the Java class that implements Python Service
161 | # use that parameter to set custom Java class which extends PythonService
162 | #android.service_class_name = org.kivy.android.PythonService
163 |
164 | # (str) Android app theme, default is ok for Kivy-based app
165 | # android.apptheme = "@android:style/Theme.NoTitleBar"
166 |
167 | # (list) Pattern to whitelist for the whole project
168 | #android.whitelist =
169 |
170 | # (str) Path to a custom whitelist file
171 | #android.whitelist_src =
172 |
173 | # (str) Path to a custom blacklist file
174 | #android.blacklist_src =
175 |
176 | # (list) List of Java .jar files to add to the libs so that pyjnius can access
177 | # their classes. Don't add jars that you do not need, since extra jars can slow
178 | # down the build process. Allows wildcards matching, for example:
179 | # OUYA-ODK/libs/*.jar
180 | #android.add_jars = foo.jar,bar.jar,path/to/more/*.jar
181 |
182 | # (list) List of Java files to add to the android project (can be java or a
183 | # directory containing the files)
184 | #android.add_src =
185 | android.add_src = java
186 |
187 | # (list) Android AAR archives to add
188 | #android.add_aars =
189 |
190 | # (list) Put these files or directories in the apk assets directory.
191 | # Either form may be used, and assets need not be in 'source.include_exts'.
192 | # 1) android.add_assets = source_asset_relative_path
193 | # 2) android.add_assets = source_asset_path:destination_asset_relative_path
194 | #android.add_assets =
195 |
196 | # (list) Put these files or directories in the apk res directory.
197 | # The option may be used in three ways, the value may contain one or zero ':'
198 | # Some examples:
199 | # 1) A file to add to resources, legal resource names contain ['a-z','0-9','_']
200 | # android.add_resources = my_icons/all-inclusive.png:drawable/all_inclusive.png
201 | # 2) A directory, here 'legal_icons' must contain resources of one kind
202 | # android.add_resources = legal_icons:drawable
203 | # 3) A directory, here 'legal_resources' must contain one or more directories,
204 | # each of a resource kind: drawable, xml, etc...
205 | # android.add_resources = legal_resources
206 | #android.add_resources =
207 |
208 | # (list) Gradle dependencies to add
209 | #android.gradle_dependencies =
210 | android.gradle_dependencies =com.android.billingclient:billing:6.0.1
211 |
212 | # (bool) Enable AndroidX support. Enable when 'android.gradle_dependencies'
213 | # contains an 'androidx' package, or any package from Kotlin source.
214 | # android.enable_androidx requires android.api >= 28
215 | #android.enable_androidx = True
216 |
217 | # (list) add java compile options
218 | # this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option
219 | # see https://developer.android.com/studio/write/java8-support for further information
220 | # android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8"
221 |
222 | # (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies}
223 | # please enclose in double quotes
224 | # e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }"
225 | #android.add_gradle_repositories =
226 | android.gradle_repositories = "mavenCentral()"
227 |
228 | # (list) packaging options to add
229 | # see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
230 | # can be necessary to solve conflicts in gradle_dependencies
231 | # please enclose in double quotes
232 | # e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'"
233 | #android.add_packaging_options =
234 |
235 | # (list) Java classes to add as activities to the manifest.
236 | #android.add_activities = com.example.ExampleActivity
237 |
238 | # (str) OUYA Console category. Should be one of GAME or APP
239 | # If you leave this blank, OUYA support will not be enabled
240 | #android.ouya.category = GAME
241 |
242 | # (str) Filename of OUYA Console icon. It must be a 732x412 png image.
243 | #android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png
244 |
245 | # (str) XML file to include as an intent filters in tag
246 | #android.manifest.intent_filters =
247 |
248 | # (list) Copy these files to src/main/res/xml/ (used for example with intent-filters)
249 | #android.res_xml = PATH_TO_FILE,
250 |
251 | # (str) launchMode to set for the main activity
252 | #android.manifest.launch_mode = standard
253 |
254 | # (str) screenOrientation to set for the main activity.
255 | # Valid values can be found at https://developer.android.com/guide/topics/manifest/activity-element
256 | #android.manifest.orientation = fullSensor
257 |
258 | # (list) Android additional libraries to copy into libs/armeabi
259 | #android.add_libs_armeabi = libs/android/*.so
260 | #android.add_libs_armeabi_v7a = libs/android-v7/*.so
261 | #android.add_libs_arm64_v8a = libs/android-v8/*.so
262 | #android.add_libs_x86 = libs/android-x86/*.so
263 | #android.add_libs_mips = libs/android-mips/*.so
264 |
265 | # (bool) Indicate whether the screen should stay on
266 | # Don't forget to add the WAKE_LOCK permission if you set this to True
267 | #android.wakelock = False
268 |
269 | # (list) Android application meta-data to set (key=value format)
270 | #android.meta_data =
271 |
272 | # (list) Android library project to add (will be added in the
273 | # project.properties automatically.)
274 | #android.library_references =
275 |
276 | # (list) Android shared libraries which will be added to AndroidManifest.xml using tag
277 | #android.uses_library =
278 |
279 | # (str) Android logcat filters to use
280 | #android.logcat_filters = *:S python:D
281 |
282 | # (bool) Android logcat only display log for activity's pid
283 | #android.logcat_pid_only = False
284 |
285 | # (str) Android additional adb arguments
286 | #android.adb_args = -H host.docker.internal
287 |
288 | # (bool) Copy library instead of making a libpymodules.so
289 | #android.copy_libs = 1
290 |
291 | # (list) The Android archs to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
292 | # In past, was `android.arch` as we weren't supporting builds for multiple archs at the same time.
293 | android.archs = arm64-v8a, armeabi-v7a
294 |
295 | # (int) overrides automatic versionCode computation (used in build.gradle)
296 | # this is not the same as app version and should only be edited if you know what you're doing
297 | # android.numeric_version = 1
298 |
299 | # (bool) enables Android auto backup feature (Android API >=23)
300 | android.allow_backup = True
301 |
302 | # (str) XML file for custom backup rules (see official auto backup documentation)
303 | # android.backup_rules =
304 |
305 | # (str) If you need to insert variables into your AndroidManifest.xml file,
306 | # you can do so with the manifestPlaceholders property.
307 | # This property takes a map of key-value pairs. (via a string)
308 | # Usage example : android.manifest_placeholders = [myCustomUrl:\"org.kivy.customurl\"]
309 | # android.manifest_placeholders = [:]
310 |
311 | # (bool) Skip byte compile for .py files
312 | # android.no-byte-compile-python = False
313 |
314 | # (str) The format used to package the app for release mode (aab or apk or aar).
315 | # android.release_artifact = aab
316 |
317 | # (str) The format used to package the app for debug mode (apk or aar).
318 | # android.debug_artifact = apk
319 |
320 | #
321 | # Python for android (p4a) specific
322 | #
323 |
324 | # (str) python-for-android URL to use for checkout
325 | #p4a.url =
326 |
327 | # (str) python-for-android fork to use in case if p4a.url is not specified, defaults to upstream (kivy)
328 | #p4a.fork = kivy
329 |
330 | # (str) python-for-android branch to use, defaults to master
331 | #p4a.branch = master
332 |
333 | # (str) python-for-android specific commit to use, defaults to HEAD, must be within p4a.branch
334 | #p4a.commit = HEAD
335 |
336 | # (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
337 | #p4a.source_dir =
338 |
339 | # (str) The directory in which python-for-android should look for your own build recipes (if any)
340 | #p4a.local_recipes =
341 |
342 | # (str) Filename to the hook for p4a
343 | #p4a.hook =
344 |
345 | # (str) Bootstrap to use for android builds
346 | # p4a.bootstrap = sdl2
347 |
348 | # (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
349 | #p4a.port =
350 |
351 | # Control passing the --use-setup-py vs --ignore-setup-py to p4a
352 | # "in the future" --use-setup-py is going to be the default behaviour in p4a, right now it is not
353 | # Setting this to false will pass --ignore-setup-py, true will pass --use-setup-py
354 | # NOTE: this is general setuptools integration, having pyproject.toml is enough, no need to generate
355 | # setup.py if you're using Poetry, but you need to add "toml" to source.include_exts.
356 | #p4a.setup_py = false
357 |
358 | # (str) extra command line arguments to pass when invoking pythonforandroid.toolchain
359 | #p4a.extra_args =
360 |
361 |
362 |
363 | #
364 | # iOS specific
365 | #
366 |
367 | # (str) Path to a custom kivy-ios folder
368 | #ios.kivy_ios_dir = ../kivy-ios
369 | # Alternately, specify the URL and branch of a git checkout:
370 | ios.kivy_ios_url = https://github.com/kivy/kivy-ios
371 | ios.kivy_ios_branch = master
372 |
373 | # Another platform dependency: ios-deploy
374 | # Uncomment to use a custom checkout
375 | #ios.ios_deploy_dir = ../ios_deploy
376 | # Or specify URL and branch
377 | ios.ios_deploy_url = https://github.com/phonegap/ios-deploy
378 | ios.ios_deploy_branch = 1.10.0
379 |
380 | # (bool) Whether or not to sign the code
381 | ios.codesign.allowed = false
382 |
383 | # (str) Name of the certificate to use for signing the debug version
384 | # Get a list of available identities: buildozer ios list_identities
385 | #ios.codesign.debug = "iPhone Developer: ()"
386 |
387 | # (str) The development team to use for signing the debug version
388 | #ios.codesign.development_team.debug =
389 |
390 | # (str) Name of the certificate to use for signing the release version
391 | #ios.codesign.release = %(ios.codesign.debug)s
392 |
393 | # (str) The development team to use for signing the release version
394 | #ios.codesign.development_team.release =
395 |
396 | # (str) URL pointing to .ipa file to be installed
397 | # This option should be defined along with `display_image_url` and `full_size_image_url` options.
398 | #ios.manifest.app_url =
399 |
400 | # (str) URL pointing to an icon (57x57px) to be displayed during download
401 | # This option should be defined along with `app_url` and `full_size_image_url` options.
402 | #ios.manifest.display_image_url =
403 |
404 | # (str) URL pointing to a large icon (512x512px) to be used by iTunes
405 | # This option should be defined along with `app_url` and `display_image_url` options.
406 | #ios.manifest.full_size_image_url =
407 |
408 |
409 | [buildozer]
410 |
411 | # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
412 | log_level = 2
413 |
414 | # (int) Display warning if buildozer is run as root (0 = False, 1 = True)
415 | warn_on_root = 1
416 |
417 | # (str) Path to build artifact storage, absolute or relative to spec file
418 | # build_dir = ./.buildozer
419 |
420 | # (str) Path to build output (i.e. .apk, .aab, .ipa) storage
421 | # bin_dir = ./bin
422 |
423 | # -----------------------------------------------------------------------------
424 | # List as sections
425 | #
426 | # You can define all the "list" as [section:key].
427 | # Each line will be considered as a option to the list.
428 | # Let's take [app] / source.exclude_patterns.
429 | # Instead of doing:
430 | #
431 | #[app]
432 | #source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
433 | #
434 | # This can be translated into:
435 | #
436 | #[app:source.exclude_patterns]
437 | #license
438 | #data/audio/*.wav
439 | #data/images/original/*
440 | #
441 |
442 |
443 | # -----------------------------------------------------------------------------
444 | # Profiles
445 | #
446 | # You can extend section / key with a profile
447 | # For example, you want to deploy a demo version of your application without
448 | # HD content. You could first change the title to add "(demo)" in the name
449 | # and extend the excluded directories to remove the HD content.
450 | #
451 | #[app@demo]
452 | #title = My Application (demo)
453 | #
454 | #[app:source.exclude_patterns@demo]
455 | #images/hd/*
456 | #
457 | # Then, invoke the command line with the "demo" profile:
458 | #
459 | #buildozer --profile demo android debug
460 |
--------------------------------------------------------------------------------
/googleplayapi.py:
--------------------------------------------------------------------------------
1 | from android import PythonJavaClass, autoclass, java_method, mActivity
2 | from android.runnable import run_on_ui_thread
3 | from kivy.logger import Logger
4 | List = autoclass('java.util.List')
5 | ArrayList = autoclass("java.util.ArrayList")
6 | context = mActivity.getApplicationContext()
7 | PythonActivity = autoclass('org.kivy.android.PythonActivity')
8 | activity = PythonActivity.mActivity
9 | Context = autoclass('android.content.Context')
10 |
11 | BillingClient = autoclass("com.android.billingclient.api.BillingClient")
12 | KivyPurchasesUpdatedListener = autoclass('org.org.googleplay.KivyPurchasesUpdatedListener')
13 | KivyBillingClientStateListener = autoclass('org.org.googleplay.KivyBillingClientStateListener')
14 | KivyProductDetailsResponseListener = autoclass('org.org.googleplay.KivyProductDetailsResponseListener')
15 | KivyConsumeResponseListener = autoclass('org.org.googleplay.KivyConsumeResponseListener')
16 |
17 | QueryProductDetailsParams = autoclass('com.android.billingclient.api.QueryProductDetailsParams')
18 | QueryProductDetailsParamsProduct = autoclass('com.android.billingclient.api.QueryProductDetailsParams$Product')
19 | ProductType = autoclass('com.android.billingclient.api.BillingClient$ProductType')
20 |
21 | BillingFlowParams = autoclass('com.android.billingclient.api.BillingFlowParams')
22 | BillingFlowParamsProductDetailsParams = autoclass('com.android.billingclient.api.BillingFlowParams$ProductDetailsParams')
23 |
24 | ConsumeParams = autoclass('com.android.billingclient.api.ConsumeParams')
25 | from kivy.app import App
26 | import asyncio
27 | from kivy.clock import mainthread
28 |
29 | #PurchasesUpdatedListener
30 | class ULCallbackWrapper(PythonJavaClass):
31 | __javacontext__ = 'app'
32 | __javainterfaces__ = ['org/org/googleplay/ULCallbackWrapper']
33 |
34 | def __init__(self, callback):
35 | super().__init__()
36 | self.callback = callback
37 | @java_method('(Lcom/android/billingclient/api/BillingResult;Ljava/util/List;)V')
38 | def callback_data(self, billingResult, purchases):
39 | # print("ULCallbackWrapper callback_data ok")
40 | if self.callback:
41 | print("ULCallbackWrapper callback_data True ok")
42 | self.callback(billingResult, purchases)
43 |
44 | #BillingClientStateListener
45 | class SLCallbackWrapper(PythonJavaClass):
46 | __javacontext__ = 'app'
47 | __javainterfaces__ = ['org/org/googleplay/SLCallbackWrapper']
48 |
49 | def __init__(self, callback):
50 | super().__init__()
51 | self.callback = callback
52 | @java_method('(Lcom/android/billingclient/api/BillingResult;)V')
53 | def callback_data(self, billingResult):
54 | # print("SLCallbackWrapper callback_data ok", billingResult)
55 | if self.callback:
56 | print("SLCallbackWrapper callback_data True ok")
57 | self.callback(billingResult)
58 |
59 | #ProductDetailsResponseListener
60 | class DLCallbackWrapper(PythonJavaClass):
61 | __javacontext__ = 'app'
62 | __javainterfaces__ = ['org/org/googleplay/DLCallbackWrapper']
63 |
64 | def __init__(self, callback):
65 | super().__init__()
66 | self.callback = callback
67 | @java_method('(Ljava/util/List;)V')
68 | def callback_data(self, productDetails):
69 | # print("DLCallbackWrapper callback_data ok")
70 | if self.callback:
71 | print("DLCallbackWrapper callback_data True ok")
72 | self.callback(productDetails)
73 |
74 | #ConsumeResponseListener
75 | class CLCallbackWrapper(PythonJavaClass):
76 | __javacontext__ = 'app'
77 | __javainterfaces__ = ['org/org/googleplay/CLCallbackWrapper']
78 |
79 | def __init__(self, callback):
80 | super().__init__()
81 | self.callback = callback
82 |
83 | @java_method("(Lcom/android/billingclient/api/BillingResult;Ljava/lang/String;)V")
84 | def callback_data(self, billingResult, purchaseToken):
85 | if self.callback:
86 | print("CLCallbackWrapper callback_data True ok")
87 | self.callback(billingResult, purchaseToken)
88 |
89 | class BillingProcessor:
90 | _billing_client = None
91 | receiptData = {}
92 | def __init__(self, context, _billing_client):
93 | self._context = context
94 | self._billing_client = _billing_client
95 | self.mProductDetails = {}
96 | self.ul_callback_wrapper = ULCallbackWrapper(self.kivy_purchases_updated_event_handler)
97 | self.sl_callback_wrapper = SLCallbackWrapper(self.on_billing_setup_finished_event_handler)
98 | self.dl_callback_wrapper = DLCallbackWrapper(self.on_product_details_response)
99 | self.cl_callback_wrapper = CLCallbackWrapper(self.on_consume_response)
100 | self.start_connection()
101 |
102 | @run_on_ui_thread
103 | def start_connection(self, *args):
104 | try:
105 | self.purchases_updated_callback_wrapper = KivyPurchasesUpdatedListener(self.ul_callback_wrapper)
106 | self.on_billing_setup_finished_callback_wrapper = KivyBillingClientStateListener(self.sl_callback_wrapper)
107 | #bc = BillingClient.newBuilder(self._context)
108 | #bc.enablePendingPurchases()
109 | #bc.setListener(self.purchases_updated_callback_wrapper)
110 | #self._billing_client = bc.build()
111 | self._billing_client = BillingClient.newBuilder(self._context).enablePendingPurchases().setListener(
112 | self.purchases_updated_callback_wrapper).build()
113 | self._billing_client.startConnection(self.on_billing_setup_finished_callback_wrapper)
114 |
115 | except Exception as e:
116 | print("start_connection exception reason", e)
117 |
118 | # To pass the product information that we use when we initialize the purchase with the launch_billing_flow function
119 | # to the ProductDetails variable.
120 | async def products_data_get(self):
121 | print("products_data_get start")
122 | self.get_purchase_listing_async('Item1')
123 | await asyncio.sleep(0.2)
124 | self.get_purchase_listing_async('Item2')
125 | await asyncio.sleep(0.2)
126 | self.get_purchase_listing_async('Item3')
127 | await asyncio.sleep(0.2)
128 |
129 | @mainthread
130 | def on_billing_setup_finished_event_handler(self, billingResult):
131 | if billingResult.getResponseCode() == 0:
132 | asyncio.run(self.products_data_get())
133 | else:
134 | print("BillingClient setup failed :", billingResult.getResponseCode())
135 |
136 | @mainthread
137 | def on_consume_response(self, billingResult,purchaseToken):
138 |
139 | if billingResult.getResponseCode() == 0:
140 | print("OK: The consumable product has been successfully consumed.")
141 | app = App.get_running_app()
142 | if self.receiptData:
143 | receipt = self.receiptData
144 | #To send the receipt information to the server and verify it with the google api on the server
145 | # app.google.api_receipt_check(receipt)
146 | print("Recipt :", receipt)
147 | self.receiptData = {}
148 | else:
149 | print("self.receiptData boş")
150 | return (billingResult,purchaseToken)
151 | elif billingResult.getResponseCode() == 1:
152 | print("USER_CANCELED: The user has canceled the consumption of the consumable product.")
153 | elif billingResult.getResponseCode() == 2:
154 | print("SERVICE_UNAVAILABLE: Service Unavailable.")
155 | elif billingResult.getResponseCode() == 3:
156 | print("BILLING_UNAVAILABLE: Google Play services are unavailable.")
157 | elif billingResult.getResponseCode() == 4:
158 | print("ITEM_ALREADY_OWNED: The user has already purchased the consumable product.")
159 | elif billingResult.getResponseCode() == 5:
160 | print("INVALID_ITEM_SKU: The SKU of the consumable is invalid..")
161 | elif billingResult.getResponseCode() == 6:
162 | print("INVALID_PURCHASE_TOKEN: The purchase token of the consumable is invalid.")
163 | elif billingResult.getResponseCode() == 7:
164 | print("DEVELOPER_ERROR: developer error")
165 | elif billingResult.getResponseCode() == 8:
166 | print("ERROR: An unknown error has occurred.")
167 | else:
168 | print("There is an uncontrollable situation!!!")
169 |
170 | @mainthread
171 | def handlePurchase(self,purchase):
172 | try:
173 | #google play billing api consumeAsync call
174 | consume_params = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build()
175 | listener = KivyConsumeResponseListener(self.cl_callback_wrapper)
176 | self._billing_client.consumeAsync(consume_params, listener)
177 | except Exception as e:
178 | print("handlePurchase exception reason", e)
179 | pass
180 |
181 | @mainthread
182 | def kivy_purchases_updated_event_handler(self, billingResult, purchases):
183 | try:
184 | if billingResult.getResponseCode() == 0 and purchases != None:
185 | if purchases:
186 | for purchase in purchases:
187 | #I pass into variable to send receipt data to verify to server
188 | self.receiptData = {"purchaseData": purchase.getOriginalJson(),
189 | "signature": purchase.getSignature() ,
190 | }
191 | print( "Receipt data :", self.receiptData)
192 |
193 | self.handlePurchase(purchase)
194 | elif billingResult.getResponseCode() == 1:
195 | print("The purchase has been cancelled.")
196 | else:
197 | print("Purchase failed")
198 | except Exception as e:
199 | print("kivy_purchases_updated_event_handler exception reason", e)
200 |
201 | @mainthread
202 | def on_product_details_response(self, productDetailsList):
203 | if productDetailsList:
204 | for productDetail in productDetailsList:
205 | product_id = productDetail.getProductId()
206 | self.mProductDetails[str(product_id)] = productDetail
207 | print("Product ID:", productDetail.getProductId())
208 | print("Product title:", productDetail.getTitle())
209 | print("Product description:", productDetail.getDescription())
210 |
211 | @run_on_ui_thread
212 | def get_purchase_listing_async(self, product_id):
213 | try:
214 | product_details_listener = KivyProductDetailsResponseListener(self.dl_callback_wrapper)
215 | paramsBuilder = QueryProductDetailsParams.newBuilder()
216 | productBuilder = QueryProductDetailsParamsProduct.newBuilder()
217 | productBuilder.setProductId(product_id)
218 | productBuilder.setProductType(ProductType.INAPP)
219 | productList = productBuilder.build()
220 | jlist = autoclass('java.util.ArrayList')()
221 | jlist.add(productList)
222 | paramsBuilder.setProductList(jlist)
223 | params = paramsBuilder.build()
224 | print("params ok", params)
225 | self._billing_client.queryProductDetailsAsync(params, product_details_listener)
226 |
227 | except Exception as e:
228 | print("get_purchase_listing_async exception reason", e)
229 |
230 | def launch_billing_flow(self,product_id):
231 | try:
232 | if (len(self.mProductDetails) == 0) or (product_id not in self.mProductDetails):
233 | self.get_purchase_listing_async(product_id)
234 |
235 | if str(product_id) in self.mProductDetails:
236 | productDetails = self.mProductDetails[str(product_id)]
237 | paramsBuilder = BillingFlowParams.newBuilder()
238 | productBuilder = BillingFlowParamsProductDetailsParams.newBuilder()
239 | productBuilder.setProductDetails(productDetails)
240 | productList = productBuilder.build()
241 | jlist2 = autoclass('java.util.ArrayList')()
242 | jlist2.add(productList)
243 | paramsBuilder.setProductDetailsParamsList(jlist2)
244 | billingFlowParams = paramsBuilder.build()
245 | self._billing_client.launchBillingFlow(activity, billingFlowParams)
246 |
247 | except Exception as e:
248 | print("launch_billing_flow exception reason", e)
249 |
--------------------------------------------------------------------------------
/java/KivyBillingClientStateListener.java:
--------------------------------------------------------------------------------
1 | package org.org.googleplay;
2 | //import java.util.ArrayList;
3 | //import java.util.List;
4 | //import java.lang.String;
5 |
6 | import com.android.billingclient.api.BillingClient;
7 | //import com.android.billingclient.api.PurchasesUpdatedListener;
8 | import com.android.billingclient.api.BillingClientStateListener;
9 | import com.android.billingclient.api.BillingResult;
10 | //import com.android.billingclient.api.Purchase;
11 |
12 | //import com.android.os.Bundle;
13 | import org.org.googleplay.SLCallbackWrapper;
14 |
15 | //public class KivyBillingClientStateListener extends BillingClientStateListener {
16 | public class KivyBillingClientStateListener implements BillingClientStateListener {
17 |
18 | private SLCallbackWrapper callback_wrapper;
19 |
20 | public KivyBillingClientStateListener(SLCallbackWrapper callback_wrapper) {
21 | this.callback_wrapper = callback_wrapper;
22 | }
23 |
24 | @Override
25 | public void onBillingSetupFinished(BillingResult billingResult) {
26 | if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
27 | // The setup process was successful.
28 | System.out.println("The setup process was successful");
29 | this.callback_wrapper.callback_data(billingResult );
30 | } else {
31 | // The setup process failed.
32 | System.out.println("The setup process failed.");
33 | this.callback_wrapper.callback_data(billingResult );
34 | }
35 | }
36 |
37 | @Override
38 | public void onBillingServiceDisconnected() {
39 | // Handle the case where the BillingService has disconnected.
40 | }
41 |
42 |
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/java/KivyConsumeResponseListener.java:
--------------------------------------------------------------------------------
1 | package org.org.googleplay;
2 | import java.lang.String;
3 |
4 | import com.android.billingclient.api.ConsumeResponseListener;
5 | import com.android.billingclient.api.BillingResult;
6 | import org.org.googleplay.CLCallbackWrapper;
7 |
8 | public class KivyConsumeResponseListener implements ConsumeResponseListener {
9 |
10 | private CLCallbackWrapper callback_wrapper;
11 |
12 | public KivyConsumeResponseListener(CLCallbackWrapper callback_wrapper) {
13 | this.callback_wrapper = callback_wrapper;
14 | }
15 |
16 | @Override
17 | public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
18 | this.callback_wrapper.callback_data(billingResult,purchaseToken );
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/java/KivyProductDetailsResponseListener.java:
--------------------------------------------------------------------------------
1 | package org.org.googleplay;
2 | //import java.util.ArrayList;
3 | import java.util.List;
4 | //import java.lang.String;
5 |
6 | import com.android.billingclient.api.BillingClient;
7 | import com.android.billingclient.api.ProductDetailsResponseListener;
8 | import com.android.billingclient.api.BillingResult;
9 | import com.android.billingclient.api.ProductDetails;
10 | //import com.android.os.Bundle;
11 | import org.org.googleplay.DLCallbackWrapper;
12 |
13 | //public class KivyProductDetailsResponseListener extends ProductDetailsResponseListener {
14 | public class KivyProductDetailsResponseListener implements ProductDetailsResponseListener {
15 |
16 | private DLCallbackWrapper callback_wrapper;
17 |
18 | public KivyProductDetailsResponseListener(DLCallbackWrapper callback_wrapper) {
19 | this.callback_wrapper = callback_wrapper;
20 | }
21 |
22 | @Override
23 | public void onProductDetailsResponse(BillingResult billingResult, List productDetails) {
24 | //public void onProductDetailsResponse(@NonNull BillingResult billingResult, @Nullable List productDetails) {
25 | if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK ) {
26 | this.callback_wrapper.callback_data(productDetails);
27 | }
28 | else {
29 | // The product details query failed.
30 | System.out.println("The product details query failed.");
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/java/KivyPurchasesUpdatedListener.java:
--------------------------------------------------------------------------------
1 | package org.org.googleplay;
2 | //import java.util.ArrayList;
3 | import java.util.List;
4 | //import java.lang.String;
5 |
6 | import com.android.billingclient.api.BillingClient;
7 | import com.android.billingclient.api.PurchasesUpdatedListener;
8 | import com.android.billingclient.api.BillingResult;
9 | import com.android.billingclient.api.Purchase;
10 | //import com.android.os.Bundle;
11 | import org.org.googleplay.ULCallbackWrapper;
12 |
13 | //public class KivyPurchasesUpdatedListener extends PurchasesUpdatedListener {
14 | public class KivyPurchasesUpdatedListener implements PurchasesUpdatedListener {
15 |
16 | private ULCallbackWrapper callback_wrapper;
17 |
18 | public KivyPurchasesUpdatedListener(ULCallbackWrapper callback_wrapper) {
19 | this.callback_wrapper = callback_wrapper;
20 | }
21 |
22 | @Override
23 | public void onPurchasesUpdated(BillingResult billingResult, List purchases) {
24 | //public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List purchases) {
25 | if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
26 | this.callback_wrapper.callback_data(billingResult,purchases );
27 | }
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/java/org/org/googleplay/CLCallbackWrapper.java:
--------------------------------------------------------------------------------
1 | package org.org.googleplay;
2 | import com.android.billingclient.api.BillingResult;
3 | import java.lang.String;
4 |
5 | public interface CLCallbackWrapper {
6 | public void callback_data(BillingResult billingResult, String purchaseToken);
7 | }
8 |
--------------------------------------------------------------------------------
/java/org/org/googleplay/DLCallbackWrapper.java:
--------------------------------------------------------------------------------
1 | package org.org.googleplay;
2 | import com.android.billingclient.api.ProductDetails;
3 | import java.lang.String;
4 | import java.util.List;
5 |
6 | public interface DLCallbackWrapper {
7 | //public void callback_data(@Nullable List productDetails);
8 | public void callback_data(List productDetails);
9 | }
10 |
--------------------------------------------------------------------------------
/java/org/org/googleplay/SLCallbackWrapper.java:
--------------------------------------------------------------------------------
1 | package org.org.googleplay;
2 | import com.android.billingclient.api.BillingResult;
3 | import java.lang.String;
4 |
5 | public interface SLCallbackWrapper {
6 | //public void callback_data(@NonNull BillingResult billingResult);
7 | public void callback_data(BillingResult billingResult);
8 | }
9 |
--------------------------------------------------------------------------------
/java/org/org/googleplay/ULCallbackWrapper.java:
--------------------------------------------------------------------------------
1 | package org.org.googleplay;
2 | import com.android.billingclient.api.BillingResult;
3 | import com.android.billingclient.api.Purchase;
4 | import java.lang.String;
5 | import java.util.List;
6 |
7 | public interface ULCallbackWrapper {
8 | //public void callback_data(@NonNull BillingResult billingResult, @Nullable List purchases);
9 | public void callback_data(BillingResult billingResult, List purchases);
10 | }
11 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from kivy.app import App
2 | from kivy.lang import Builder
3 | from kivy.utils import platform
4 | if platform == "android":
5 | from jnius import autoclass
6 | # from android.runnable import run_on_ui_thread
7 | from android import python_act as PythonActivity
8 | from googleplayapi import BillingProcessor
9 |
10 | Toast = autoclass('android.widget.Toast')
11 | String = autoclass('java.lang.String')
12 | CharSequence = autoclass('java.lang.CharSequence')
13 | # Find your app 's license key
14 | #1-Open Play Console and select the app.
15 | #2-Go to the Monetization setup page(Monetize > Monetization setup).
16 | # Your license key is under "Licensing."
17 | LICENSE_KEY = "Your license key"
18 | context = PythonActivity.mActivity
19 | PROD_ONETIME_0 = "Item1"
20 | PROD_ONETIME_1 = "Item2"
21 | PROD_ONETIME_2 = "Item3"
22 | products = [PROD_ONETIME_0, PROD_ONETIME_1, PROD_ONETIME_2]
23 |
24 | kv_string = '''
25 | FloatLayout:
26 | Button:
27 | text: 'Buy Item 1'
28 | size_hint: None, None
29 | size: 350, 150
30 | pos_hint: {'center_x': 0.5, 'center_y': 0.7}
31 | on_release: app.purchase_product('Item1')
32 |
33 | Button:
34 | text: 'Buy Item 2'
35 | size_hint: None, None
36 | size: 350, 150
37 | pos_hint: {'center_x': 0.5, 'center_y': 0.5}
38 | on_release: app.purchase_product('Item2')
39 |
40 | Button:
41 | text: 'Buy Item 3'
42 | size_hint: None, None
43 | size: 350, 150
44 | pos_hint: {'center_x': 0.5, 'center_y': 0.3}
45 | on_release: app.purchase_product('Item3')
46 | '''
47 |
48 | class ProductPurchaseApp(App):
49 | def build(self):
50 | if platform == "android":
51 | self.bp = BillingProcessor(context, LICENSE_KEY)
52 | return Builder.load_string(kv_string)
53 |
54 |
55 | def purchase_product(self, product_id):
56 | if platform == "android":
57 | if product_id in products:
58 | self.bp.launch_billing_flow(product_id)
59 |
60 | def on_pause(self):
61 | return True
62 |
63 | def on_resume(self):
64 | pass
65 |
66 | if __name__ == '__main__':
67 | ProductPurchaseApp().run()
68 |
--------------------------------------------------------------------------------