Forum Archive

In App Purchases using Pythonista App Template - Objc_util issues

mapmangame

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)

```python
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()
mapmangame

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.

JonB

was len(valid_products) retrning 0? or just nothing came out of the loop?

JonB

one possibility, you might need to use PRODUCTS=[ns('xyz....')]

mapmangame

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.

mapmangame

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.