Hans♫ 75 Posted January 2, 2020 (edited) 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 January 2, 2020 by Hans♫ Share this post Link to post
Hans♫ 75 Posted January 6, 2020 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
Dave Nottage 557 Posted January 7, 2020 Remove the [Weak] attribute from FIAPService. Might still be a bug, though. Share this post Link to post
Hans♫ 75 Posted January 8, 2020 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
Francisco 1 Posted November 18, 2020 Hi: Any news from this idea? It is a pity not to have the inAap for MacOs Share this post Link to post
Hans♫ 75 Posted November 19, 2020 We use InApp purchase on MacOS and it works fine. Sorry for not updating this thread. I worked with David directly to solve the problem in our code, but I think David has implemented his own solution in his Kastri framework: https://github.com/DelphiWorlds/Kastri Share this post Link to post
Dave Nottage 557 Posted November 19, 2020 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. 1 Share this post Link to post
Eric58 6 Posted November 20, 2020 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: 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. 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; 1 Share this post Link to post
Dave Nottage 557 Posted November 20, 2020 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); 2 Share this post Link to post
Hans♫ 75 Posted November 20, 2020 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 4 1 Share this post Link to post