Jump to content
Hans♫

AV with InApp purchase on MacOS

Recommended Posts

The FMX.InAppPurchase component already implements in-app purchase for iOS, and since OSX uses the same StoreKit library, I simply added an OSX copy of the iOS implementation and adjusted it to "work" on OSX. It works as far as it compiles and runs, and I can call "QueryProducts", which also initiates a Delegate callback. 
Once in a while I can even successfully read the product details from App Store that I receive in the callback, but most of the time the callback fails with an AV. It seems to be random where it fails. If I restart paserver before each run I can increase the chance that it works, but except from that I did not find any correlation between changes I have made and a successful request.

The testprogram works fine on iOS. On OSX I have tried to target both Mojave and Cataline, and both 32 bit and 64 bit editions. They all fail.

As the OSX code is the same as the iOS code, I guess that the problems are related to differences between the iOS and the OSX target in the Objective-C handling and wrapping.


Any ideas what could be wrong, or what I should try?
(or maybesomeone with more knowledge about Objective-C wrapping and the inner workings of Delphi could help me with this?)
 

 

Below are some extracts from the code in my new unit "FMX.InAppPurchase.Mac". My test program creates TiOSInAppPurchaseService and call QueryProducts. It works without errors, and a few seconds later the TiOSProductsRequestDelegate.productsRequest callback is called. From here random AV's happens. Usually it fails on the first line FIAPService.FProductList.Clear, and sometimes it fails earlier in "DispatchToDelphi" or later in one of the following lines.

TIAPProductList = class(TList<TProduct>)
end;

procedure TiOSInAppPurchaseService.QueryProducts(const ProductIDs: TStrings);
var
  ProductIDsArray: NSMutableArray;
  ProductIDsSet: NSSet;
  ProductID: string;
begin
  ProductIDsArray := TNSMutableArray.Create;
  for ProductID in ProductIDs do
    ProductIDsArray.addObject(PStrToNSStr(ProductID));
  ProductIDsSet := TNSSet.Wrap(TNSSet.OCClass.setWithArray(ProductIDsArray));
  FProductsRequest := TSKProductsRequest.Wrap(TSKProductsRequest.Alloc.initWithProductIdentifiers(ProductIDsSet));
...
  FProductsRequest.setDelegate((FProductsRequestDelegate as ILocalObject).GetObjectID);
  FProductsRequest.start;
end;

constructor TiOSInAppPurchaseService.Create;
begin
...
  FProductsRequestDelegate := TiOSProductsRequestDelegate.Create(Self);
  FProductList := TIAPProductList.Create;
end;


constructor TiOSProductsRequestDelegate.Create(const IAPService: TiOSInAppPurchaseService);
begin
  inherited Create;
  FIAPService := IAPService;
end;

procedure TiOSProductsRequestDelegate.productsRequest(request: SKProductsRequest; didReceiveResponse: SKProductsResponse);
begin
  FIAPService.FProductList.Clear;
...
end;

 

Edited by Hans♫

Share this post


Link to post

Maybe this is a bug in the OSX64 compiler? I have attached the small test project that produces the error. It should compile and run out of the box.

Are there someone out there with the knowledge needed to look into this, that I could hire to solve this?

 

I see only two outcomes: Either some code in the test project can be changed to solve it, or a bug report should be filed to Embarcadero.

MacInAppTest.zip

Share this post


Link to post

Thanks a lot Dave for looking into this!

I removed the [Weak] attribute in the test program, but it still crashes regularly, though it does seem to increase the probability of success. 

 

When I have a successful request and then perform the same request again, then I also get an AV. It seems like the AppStore request messes up some memory. Sometimes it is in an area that prevents us from finishing the request, and sometimes it is a different place that will allow a successful request, but will cause problems later. Also, if I run the test program directly from the ScratchDir on the Mac, then it is never successful and crashes every time.

 

It's strange that the Objective-C wrapping seems to work all other places, but not here...

Share this post


Link to post
6 hours ago, Hans♫ said:

I think David has implemented his own solution in his Kastri framework:

That's news to me 🙂


I see now from an email that you said I should share what I did for you.. which I will do soon.

 

  • Haha 1

Share this post


Link to post
On 11/19/2020 at 6:39 AM, Francisco said:

Hi:

 

Any news from this idea?

 

It is a pity not to have the inAap for MacOs

Hi Francisco,

 

I faced the same problem a few months ago, and I solved it this way:

Start with the the sample code MacInAppTest.zip posted by Hans, then modify it as described further below; the modifications are necessary to account for these reasons that caused the AV crashes reported by Hans:

  1.  Unlike for iOS, the DispatchToDelphi for MacOS is called outside of the mainthread.  This is why the callback to TiOSProductsRequestDelegate.productsRequest behaves erratically - it is no longer threadsafe.  This can be fixed using the treatment shown in the code I attach below.  This treatment needs to be applied for all other callback procedures that also need to be threadsafe.
  2. Unlike for the iOS compiler, the OSX64 compiler does not adopt the ARC management model.  This difference requires that the field FProductsRequestDelegate in the TiOSInAppPurchaseService class be accompanied by another IInterface field which I've named FHoldProductsRequestDelegate.  See my comment in the code snippet below for the detailed explanation.

My original goal was to create a workable FMX.InAppPurchase.Mac, but I no longer have that module as my goal then expanded to incorporate my custom licensing requirements over the innate differences of Mac's and Window's IAP, and I ended up regutting what I needed from both platform's working IAP code into my cross-platform IAP+Licensing module.

 

 

 

 

Modifications Needed on Han's code:

 

procedure TiOSProductsRequestDelegate.productsRequest(request: SKProductsRequest; didReceiveResponse: SKProductsResponse);
var
  Product: SKProduct;
  LocalProduct: TProduct;
  Products: NSArray;
  InvalidProducts: NSArray;
  ProductID: NSString;
  I: Integer;
  InvalidProductIDList: TStrings;
  _retainedProducts, _retainedInvalidProducts: NSArray;
begin
  if TThread.Current.ThreadID <> MainThreadID then
    if TThread.Current.ThreadID <> MainThreadID then //breakpoint here to prove that we are not in mainthread 
      ;
  /// DispatchToDelphi call this through a thread that is not the mainthread.
  ///  We need to route this to the mainthread via Synchronize, but first we
  ///  extract the data needed and hold them as captured variables for use during
  ///  mainthread processng:
  _retainedProducts := didReceiveResponse.products;
  _retainedProducts.retain;
  _retainedInvalidProducts := didReceiveResponse.invalidProductIdentifiers;
  _retainedInvalidProducts.retain;

  TThread.Synchronize(nil,
  procedure
  var
    I: Integer;
  begin
    FIAPService.FProductList.Clear;
    InvalidProductIDList := TStringList.Create;
    if FIAPService.FProducts <> nil then
      FIAPService.FProducts.release;
    Products := _retainedProducts; //instead of didReceiveResponse.Products;
    FIAPService.FProducts := Products;
    FIAPService.FProducts.retain;
    InvalidProducts := _retainedInvalidProducts; //instead of didReceiveResponse.invalidProductIdentifiers;

    if (Products <> nil) and (Products.count > 0) then
      for I := 0 to Pred(Products.count) do
      begin
        Product := TSKProduct.Wrap(Products.objectAtIndex(I));
        LocalProduct := SKProductToProduct(Product);
        FIAPService.FProductList.Add(LocalProduct);
      end;
    if (InvalidProducts <> nil) and (InvalidProducts.count > 0) then
      for I := 0 to Pred(InvalidProducts.count) do
      begin
        ProductID := TNSString.Wrap(InvalidProducts.objectAtIndex(I));
        InvalidProductIDList.Add(NSStrToStr(ProductID));
      end;

    if FIAPService <> nil then
      FIAPService.DoProductsRequestResponse(FIAPService.FProductList, InvalidProductIDList);

    // Finally balance the retains with releases:
    _retainedProducts.release;
    _retainedInvalidProducts.release;
  end)
end;
 

 

  TiOSInAppPurchaseService = class(TInterfacedObject, IFMXInAppPurchaseService)
  private
    FProductsRequest: SKProductsRequest;
    /// FProductsRequestDelegate implements IInterface, so when QueryProducts access
    ///  it via the construct FProductsRequestDelegate as ILocalObject, it will trigger 
    ///  an AddRef followed by a Release when QueryProducts goes out of scope.  This
    ///  will cause FProductsRequestDelegate to be prematurely freed.  To prevent this
    ///  add a new field FHoldProductsRequestDelegate and assign it to FProductsRequestDelegate
    ///  in the constructor.  Note that the iOS compiler does not face this problem because
    ///  its ARC memory management will automatically do an AddRef on FProductsRequestDelegate
    FProductsRequestDelegate: TiOSProductsRequestDelegate;
    FHoldProductsRequestDelegate: IInterface;

    ......

  end;

  • Like 1

Share this post


Link to post
2 hours ago, Eric58 said:

Modifications Needed on Han's code:

The only changed that Hans needed to stop it crashing for him was to change this:

FProductsRequest.setDelegate((FProductsRequestDelegate as ILocalObject).GetObjectID);

to this:

FProductsRequest.setDelegate(FProductsRequestDelegate.GetObjectID);

 

  • Like 1

Share this post


Link to post

I have attached the FMX.InAppPurchase.Mac.pas that we use so it is easy to use for others. All you need is to add it to your project and also add a modified version of FMX.InAppPurchase.pas where you include the file (just search for FMX.InAppPurchase.IOS, and see how that file is added, and do the same with the FMX edition)

FMX.InAppPurchase.zip

  • Like 3

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×