Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
In App Purchases using Pythonista App Template - Objc_util issues
-
Hi
I'm developing an app using pythonista and the pythonista app template. I've hit a snag trying to implement in app purchases using the code below (which is adapted from objective code here https://www.tutorialspoint.com/ios/ios_in_app_purchase.htm).
The issue is that the receive_products method, which does get called-back, is returned an empty list of products. I have checked the itunes connect set up and it seems fine.
To investigate the issue further I added some objectives code to the pythonista app template and managed to successfully return the correct list of products. This is not useful in practice as I want the user to be able to buy the products after the main.py python script has been started.
I therefore think the issue relates to threads i.e. I that that store kit probably needs the code to run on the main thread, but I'm not sure. I've tried adding the @on_main_thread decorator, but it's not working.
Does anyone have any suggestions? Any help really appreciated (as always)
from objc_util import * import ctypes import time def fetchAvailableProducts(_self, _cmd): obj = ObjCInstance(_self) sk_class = ObjCClass("SKProductsRequest") InApp.Instance.products_request = sk_class.alloc().init(productIdentifiers=ns(InApp.PRODUCTS)) InApp.Instance.products_request.delegate = obj InApp.Instance.products_request.start() def productsRequest_didReceiveResponse_(_self, _cmd, request, response): #defined here: https://developer.apple.com/documentation/storekit/skproductsrequestdelegate/1506070-productsrequest InApp.Instance.receive_products(response) def paymentQueue_updatedTransactions_(_self, _cmd, queue, transactions): InApp.Instance.update_transactions(queue, transactions) class Product: def __init__(self, identifier, title, description, price): self.identifier = identifier self.title = title self.description = description self.price = price class InApp: PRODUCTS = ['com.YYY.ZZZ'] Instance = None @classmethod def initialize(cls): cls.Instance = InApp() cls.Instance.fetch() @classmethod def initialize_dummy(cls): cls.Instance = InAppDummy() def log(self, message): self.log.append(message) def update_successfull_purchase(self, product_identifier): for observer in self.observers: observer.purchase_successful(product_identifier) def update_failed_purchase(self, product_identifier): for observer in self.observers: observer.purchase_failed(product_identifier) def update_restored_purchase(self, product_identifier): for observer in self.observers: observer.purchase_restored(product_identifier) def get_valid_product(self, product_identifier): for product in self.valid_products: if product.product_identifier == product_identifier: return product raise Exception('Product is not valid: {0}'.format(product_identifier)) def is_valid_product(self, product_identifier): for product in self.valid_products: if product.product_identifier == product_identifier: return True return False @on_main_thread def purchase(self, product_identifier): if not self.can_make_purchases: raise Exception('Purchases are disabled') else: product = self.get_valid_product(product_identifier) sk_payment_class = ObjCClass("SKPayment") payment = sk_payment_class.alloc().init(product=product) default_queue = ObjCClass("SKPaymentQueue").defaultQueue default_queue.addTransactionObserver(self.purchase_controller) default_queue.addPayment(sk_payment_queue_class) @on_main_thread def update_transactions(self, queue, transactions): for transaction in ObjCInstance(transactions): transaction_state = transaction.transactionState if transaction_state == "SKPaymentTransactionStatePurchasing": self.log('Purchasing') elif transaction_state == "SKPaymentTransactionStatePurchased": if transaction.payment.productIdentifier in InApp.PRODUCTS: self.log('Purchased') self.update_successfull_purchase(transaction.payment.productIdentifier) default_queue = ObjCClass("SKPaymentQueue").defaultQueue default_queue.finishTransaction(transaction) elif transaction_state == "SKPaymentTransactionStateRestored": self.log('Restored') self.update_restored_purchase(transaction.payment.productIdentifier) default_queue = ObjCClass("SKPaymentQueue").defaultQueue default_queue.finishTransaction(transaction) elif transaction_state == "SKPaymentTransactionStateFailed": self.update_failed_purchase(transaction.payment.productIdentifier) self.log('Failed') @on_main_thread def receive_products(self, response): self.products_validated = True self.valid_products = [] self.invalid_products = [] obj_response = ObjCInstance(response) valid_products = ObjCInstance(obj_response.products()) self.valid_count = len(valid_products) for valid_product in valid_products: print valid_product.productIdentifier if (valid_product.productIdentifier in InApp.PRODUCTS): product = Product(valid_product.productIdentifier, valid_product.localizedTitle, valid_product.localizedDescription, valid_product.price) self.valid_products.append(product) for invalid in obj_response.invalidProductIdentifiers(): self.invalid_products.append(invalid) def __init__(self): self.products_request = None self.observers = [] self.log = [] self.products = [] self.products_validated = False self.valid_count = 0 self.invalid_products = [] def add_observer(self, observer): self.observers.append(observer) @on_main_thread def fetch(self): self.check_purchases_enabled() ObjCClass('NSBundle').bundleWithPath_('/System/Library/Frameworks/StoreKit.framework').load() superclass = ObjCClass("NSObject") methods = [fetchAvailableProducts, productsRequest_didReceiveResponse_, paymentQueue_updatedTransactions_] protocols = ['SKProductsRequestDelegate', 'SKPaymentTransactionObserver'] purchase_controller_class = create_objc_class('PurchaseController', superclass, methods=methods, protocols=protocols) self.purchase_controller = purchase_controller_class.alloc().init() self.purchase_controller.fetchAvailableProducts() @on_main_thread def check_purchases_enabled(self): sk_payment_queue_class = ObjCClass("SKPaymentQueue") self.can_make_purchases = sk_payment_queue_class.canMakePayments()
-
quick update: I've been having some success shifting the purchase code into the Xcode App Template and then using objc_util to call the purchase code added to the template from within pythonista. I've managed to get the list of products back and I'm moving on to implementing purchases now.
-
was len(valid_products) retrning 0? or just nothing came out of the loop?
-
This post is deleted! -
well both.
obj_response.products() is an empty array.
there are a number of articles out there that explain that an empty array denotes something is wrong.
-
I have got this working by moving the code that uses storekit into the pythonista template. objc_util calls from within pythonista are used to control the code in the template.
I think the security around storekit might be conflicting with running the store kit calls from the python interpreter.