diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..675fd28 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +cache: pip +matrix: + fast_finish: true + include: + - name: Black + env: RUN=black + language: python + python: 3.7 + os: linux + dist: bionic +install: +- pip install pycodestyle +script: +- pycodestyle iabwrapper/__init__.py +after_failure: +- sleep 10; +- echo == End == +deploy: + skip_existing: true + provider: pypi + user: __token__ + password: + secure: Ns5qkp4xcg0FveWIOKpBmRRRf0qx/gb2JXPzNOl0hC30MtEi9arOv/3I6eowx8BgjyRIqoevCsxF+3LA6+aomoqYHcdMxo8rO5XB0/24h7zcZ4Al6XiINIXJX5Auu1AuPJhwBOuorI1jS7XwcvXjPXEEzcIIAGTuWTu6fHPWcaJOvaNcOHpHA932LI7TdrxM1VKPjCfISnBUmoYTkydDppq1ywhaPe66WS90dEaanYJr1i2ivAvFnCI3TIRSaWD0Kz4WiVoM7YYV+SRaBON4DCNoj9UEDYjWqr+DfcRRmwxrxnyxV3ljWYlo84osVYW6cjIz8A1ppwUBaQcoAG7P7ZmyC/wyqNLvo7Pqs5ggl2viAapiHepwPuriwGt+Kbm7PejUnAIQ/MGz86aY9ZNwDtVZLAJdJjwdfKt+PAtX9HJSzJ2/owqsULeDuNh/aoREMfZ6vuFo9XtK74h1oQU652M5g4tuK/QWmP0BzVzBQVr5JNpQUzCarHNZO2pfSJ8a1s8gOzLuuomh8WGxHdUNo0QSwqeSOLianXIK6f/Q+K+6Hnwi1JBZk7NXO/osPdXPPi6p+FgZR78kZKuOeqinJiC0dU/9SaOETLRJ3MCRJyZ1qqK+RjQu8QG9V5nmXiw44VTmpgIN4IU/yVtTVBJ4rAIHtLdGI/BsIKGgOfitdsw= + distributions: sdist bdist_wheel + on: + tags: true diff --git a/README.md b/README.md index 58a258c..46b2c68 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# android-iab-v3-kivy +# IABwrapper Wrapper around anjlab's Android In-app Billing Version 3 diff --git a/iabwrapper/__init__.py b/iabwrapper/__init__.py new file mode 100644 index 0000000..4674c95 --- /dev/null +++ b/iabwrapper/__init__.py @@ -0,0 +1,16 @@ +from kivy.logger import Logger +from kivy.utils import platform + +__version__ = "0.0.1" +_log_message = ("IABwrapper:" + + f" {__version__}" + + f' (installed at "{__file__}")' + ) + +Logger.info(_log_message) + +if platform == "android": + from main import PythonBillingProcessor + +else: + Logger.error("IABwrapper: This module is only available on Android") diff --git a/iabwrapper/main.py b/iabwrapper/main.py new file mode 100644 index 0000000..a28861e --- /dev/null +++ b/iabwrapper/main.py @@ -0,0 +1,162 @@ +from jnius import PythonJavaClass, autoclass, java_method +from kivy.logger import Logger + +PythonActivity = autoclass("org.kivy.android.PythonActivity") + +BillingProcessor = autoclass("com.anjlab.android.iab.v3.BillingProcessor") +PurchaseInfo = autoclass("com.anjlab.android.iab.v3.PurchaseInfo") +SkuDetails = autoclass("com.anjlab.android.iab.v3.SkuDetails") + +context = PythonActivity.mActivity + + +class PythonBillingHandler(PythonJavaClass): + __javainterfaces__ = ["com/anjlab/android/iab/v3/BillingProcessor$IBillingHandler"] + __javacontext__ = "app" + + def __init__( + self, + onProductPurchasedMethod, + onBillingErrorMethod, + onPurchaseHistoryRestoredMethod=None, + onBillingInitializedMethod=None, + ): + self.onBillingInitializedMethod = onBillingInitializedMethod + self.onProductPurchasedMethod = onProductPurchasedMethod + self.onBillingErrorMethod = onBillingErrorMethod + self.onPurchaseHistoryRestoredMethod = onPurchaseHistoryRestoredMethod + super(PythonBillingHandler, self).__init__() + + @java_method("()V") + def onBillingInitialized(self): + Logger.info("onBillingInitialized") + if self.onBillingInitializedMethod: + self.onBillingInitializedMethod() + + @java_method("(Ljava/lang/String;Lcom/anjlab/android/iab/v3/PurchaseInfo;)V") + def onProductPurchased(self, productId, purchaseInfo): + Logger.info("onProductPurchased: " + productId) + self.onProductPurchasedMethod(productId, purchaseInfo) + + # Java method that takes an int and a Throwable + @java_method("(Ljava/lang/Integer;Ljava/lang/Throwable;)V") + def onBillingError(self, errorCode, error): + Logger.info("onBillingError: " + str(errorCode)) + self.onBillingErrorMethod(errorCode, error) + + @java_method("()V") + def onPurchaseHistoryRestored(self): + Logger.info("onPurchaseHistoryRestored") + if self.onPurchaseHistoryRestoredMethod: + self.onPurchaseHistoryRestoredMethod() + + +class PythonIPurchasesResponseListener(PythonJavaClass): + __javainterfaces__ = [ + "com/anjlab/android/iab/v3/BillingProcessor$IPurchasesResponseListener" + ] + __javacontext__ = "app" + + def __init__(self, onPurchasesSuccessMethod=None, onPurchasesErrorMethod=None): + self.onPurchasesSuccessMethod = onPurchasesSuccessMethod + self.onPurchasesErrorMethod = onPurchasesErrorMethod + super(PythonIPurchasesResponseListener, self).__init__() + + @java_method("()V") + def onPurchasesSuccess(self): + Logger.info("onPurchasesSuccess") + if self.onPurchasesSuccessMethod: + self.onPurchasesSuccessMethod() + + @java_method("()V") + def onPurchasesError(self): + Logger.info("onPurchasesError") + if self.onPurchasesErrorMethod: + self.onPurchasesErrorMethod() + + +class PythonISkuDetailsResponseListener(PythonJavaClass): + __javainterfaces__ = [ + "com/anjlab/android/iab/v3/BillingProcessor$ISkuDetailsResponseListener" + ] + __javacontext__ = "app" + + def __init__(self, onSkuDetailsSuccessMethod=None, onSkuDetailsErrorMethod=None): + self.onSkuDetailsSuccessMethod = onSkuDetailsSuccessMethod + self.onSkuDetailsErrorMethod = onSkuDetailsErrorMethod + super(PythonISkuDetailsResponseListener, self).__init__() + + @java_method("(Ljava/lang/Object;)V") + def onSkuDetailsResponse(self, products): + Logger.info("onSkuDetailsResponse") + if self.onSkuDetailsSuccessMethod: + self.onSkuDetailsSuccessMethod(products) + + @java_method("(Ljava/lang/String;)V") + def onSkuDetailsError(self, error): + Logger.info("onSkuDetailsError") + if self.onSkuDetailsErrorMethod: + self.onSkuDetailsErrorMethod(error) + + +class PythonBillingProcessor(BillingProcessor): + def __init__( + self, + license_key, + onProductPurchasedMethod, + onBillingErrorMethod, + onPurchaseHistoryRestoredMethod=None, + onBillingInitializedMethod=None, + ): + self.billing_handler = PythonBillingHandler( + onProductPurchasedMethod, + onBillingErrorMethod, + onPurchaseHistoryRestoredMethod, + onBillingInitializedMethod, + ) + + super().__init__(context, license_key, self.billing_handler) + self.initialize() + + def purchase(self, product_id): + super().purchase(context, product_id) + + def consumePurchaseAsync( + self, product_id, success_listener=None, error_listener=None + ): + purchases_response_listener = PythonIPurchasesResponseListener( + success_listener, error_listener + ) + super().consumePurchaseAsync(product_id, purchases_response_listener) + + def getPurchaseListingDetailsAsync( + self, product_id, success_listener=None, error_listener=None + ): + sku_details_listener = PythonISkuDetailsResponseListener( + success_listener, error_listener + ) + super().getPurchaseListingDetailsAsync(product_id, sku_details_listener) + + def subscribe(self, subscription_id): + super().subscribe(context, subscription_id) + + def loadOwnedPurchasesFromGoogleAsync( + self, success_listener=None, error_listener=None + ): + purchases_response_listener = PythonIPurchasesResponseListener( + success_listener, error_listener + ) + super().loadOwnedPurchasesFromGoogleAsync(purchases_response_listener) + + def getSubscriptionListingDetailsAsync( + self, subscription_id, success_listener=None, error_listener=None + ): + sku_details_listener = PythonISkuDetailsResponseListener( + success_listener, error_listener + ) + super().getSubscriptionListingDetailsAsync( + subscription_id, sku_details_listener + ) + + def __del__(self): + self.release() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6db77f6 --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +from setuptools import setup +import os, re + +with open("README.md", "r") as fh: + long_description = fh.read() + + +def get_version() -> str: + """Get __version__ from __init__.py file.""" + version_file = os.path.join(os.path.dirname(__file__), "iabwrapper", "__init__.py") + version_file_data = open(version_file, "rt", encoding="utf-8").read() + version_regex = r"(?<=^__version__ = ['\"])[^'\"]+(?=['\"]$)" + try: + version = re.findall(version_regex, version_file_data, re.M)[0] + return version + except IndexError: + raise ValueError(f"Unable to find version string in {version_file}.") + + +setup( + name="IABwrapper", + version=get_version(), + packages=["iabwrapper"], + package_data={"iabwrapper": ["*.py"],}, + # metadata to display on PyPI + author="Shashi Ranjan", + author_email="shashiranjankv@gmail.com", + description="Wrapper around anjlab's Android In-app Billing Version 3 to be used in Kivy apps", + long_description=long_description, + long_description_content_type="text/markdown", + keywords="iab playstore billing android kivy-application kivy python", + url="https://github.com/shashi278/android-iab-v3-kivy", + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: Android", + ], + install_requires=["kivy"], + python_requires=">=3.6", +) \ No newline at end of file