In App purchases in C++

I’m going through iOS submission and need to get my InApp purchases working. Turns out the the C++ code I put together a year ago doesn’t work, go figure.

Some questions:

  • How long do you normally wait for QueryForAvailablePurchases tor return? (my 30 second wait isn’t enough).
  • How do I get a completion callback?
  • What are the product ID’s passed into QueryForAvailablePurchases? IAP ID’s or AppIDs?
  • Do I need to init the IOnlineStore somehow?
  • What’s the best working example to look at? [for C++ driven functionality preferably]
  • What is IOnlineStoreV2?

Happy Friday!

A further question: Can I even test this before Apple approve the IAP? I originally read their procedure as you needing to submit a build so they could approve the IAP but then I saw that you can setup sandbox users who can test IAP in different regions.

IOnlineStoreV2 is coming back null. Looks like you need bUseStoreV2=True to instantiate.

So that gets me my store v2 but the store v2 needs a valid Net Id and the one I’m getting from GetPreferredUniqueId is invalid (unsurprising as I’ve never joined a session). I’m not sure why you’d need a valid net ID to purchase on the iphone.

You don’t need a valid NetID, it’s ignored internally. The Query function just sent me to a non implemented function on iOS (GetCategories).

However when I attempt to list products there is nothing to list.

My DLC is not yet in the store, I need to get the app able to handle purchases for them to approve them, I was hoping to use the Apple SandBox testing as described here.

Also, as enabling StoreV2 disables StoreV1 it looks like purchases need to shift to using IOnlinePurchase so that’s on the immediate horizon.

iOS questions at the moment as I shift to the store v2 interface:

  1. Do I expect GetOffers to return the list of offers on iOS, or do I need to provide my own product IDs? [Edit: I’m currently providing my own list of productIDs, which seems to work (I can retrieve their details from the app store).]
  2. Can I use a Sandbox account to access products that haven’t been approved yet? [Edit: Yes]
  3. [Edited to add] How do I get a valid FUniqueNetId for a vanilla IOnlineSubsystemIOS user? Looks like I need one for GetReceipts to work [Edit: See response below]

m_UserId = MakeShareable(new FUniqueNetIdString(AppData::Inst().GetLoggedInUserID()));

This is how I’m creating an ID. GetLoggedInUserID is a unique user ID in my environment. Internally it’s not used to filter purchases (UE4 uses #define IOSUSER TEXT(“IOSUser”) internally).

Regarding GetOffers I’m currently providing my own list of productIDs, which seems to work (I can retrieve their details from the app store).

You need to call FinalizePurchase with a valid receipt after Checkout. If you try to do it later on two consumerables with the same ID the engine code crashes here (PendingTransactions nil before the break):

-(void)finalizeTransaction: (const FString&) receiptId
{
	UE_LOG(LogOnline, Verbose, TEXT("FStoreKitHelperV2::finalizeTransaction - %s"), *receiptId);
	for (SKPaymentTransaction* pendingTransaction in self.PendingTransactions)
	{
		const FString transId = pendingTransaction.transactionIdentifier;
		const FString originalTransId = GetOriginalTransactionId(pendingTransaction);
		UE_LOG(LogOnline, Verbose, TEXT("FStoreKitHelperV2::checking - id: %s origId: %s"), *transId,  *originalTransId);
		
		if ((!originalTransId.IsEmpty()) && (originalTransId == receiptId))
		{
			UE_LOG(LogOnline, Log, TEXT("FStoreKitHelperV2::finalizeTransaction - %s"), *receiptId);

			[self.PendingTransactions removeObject:pendingTransaction];
			// Remove the transaction from the payment queue.
			[[SKPaymentQueue defaultQueue] finishTransaction:pendingTransaction];
			break;
		}
	}
}

I get here because the following code does not have a valid transaction ID in the delegate:

auto delegate = [this] (const FOnlineError &result, const TSharedRef<FPurchaseReceipt> &receipt)
            {
                if (result.bSucceeded)
                {
                    m_eShopState = eCompletingPurchase;
                    m_Purchases->FinalizePurchase( GetUserId(), receipt->TransactionId );
                }
                else
                {
                     m_eShopState = eViewList;
                }
            };
            m_eShopState = eMakingPurchase;
            FPurchaseCheckoutRequest request;
            FOfferNamespace offerNamespace;
            
            request.AddPurchaseOffer(offerNamespace, m_OfferList[m_iDownloadIndex]->OfferId, 1, true);
            
        
            FOnPurchaseCheckoutComplete dele;
            dele.BindLambda(delegate);

            m_Purchases->Checkout(GetUserId(), request, dele);

Thank you for these posts…I am going through similar challenges right now, and these insights are helping.

The big things I would like to understand are:

  1. IOnlineStore is valid for me, IOnlinePurchase is not. Is that something I should care about?
  2. What is the difference between using storeV1 and storeV2?

storeV1 seemed a lot less robust than storeV2, but also simpler. My original code used it but as it didn’t work anyway I switched over to V2.

storeV2 and iOnlinePurchases are linked. To enable add the following to Config/IOS/IOSEngine.ini

 [OnlineSubsystemIOS.Store]
 bSupportsInAppPurchasing=True
 bUseStoreV2=True

My purchase code is below. CompletePurchase is code of mine that looks at my own DB using the offerID to workout what to give the player.

I only have consumables at the moment and found that FinalizePurchase needs to be called to prevent them being rewarded repeatedly. However this didn’t work in 4.15 without a minor engine modification - or I couldn’t work out how to make it work as the delegate was coming through without a valid TransactionId (added below).

auto delegate = [this](const FOnlineError &result, const TSharedRef<FPurchaseReceipt> &r)
{
	if (result.bSucceeded)
	{
			for (auto &o : r->ReceiptOffers)
			{
				CompletePurchase(o.OfferId);
			}

			m_Purchases->FinalizePurchase(GetUserId(), r->TransactionId);
	}
};
FPurchaseCheckoutRequest request;
FOfferNamespace offerNamespace;

request.AddPurchaseOffer(offerNamespace, rOfferId, 1, true);


FOnPurchaseCheckoutComplete dele;
dele.BindLambda(delegate);

m_Purchases->Checkout(GetUserId(), request, dele);

Engine modification - add the line below after GenerateReceipt:

     void FOnlinePurchaseIOS::OnTransactionCompleteResponse(EPurchaseTransactionState Result, const FStoreKitTransactionData& TransactionData)
     {
   ...
                 TSharedRef<FPurchaseReceipt> FinalReceipt = UserPendingTransaction->GenerateReceipt();
                 FinalReceipt->TransactionId = TransactionData.GetTransactionIdentifier();

This got me working, though I’m not through certification yet, just entering a TestFlight Beta…

In case someone reads this far…

My solutions was to change the engine code so that the delegate above gets a valid transactionID.

These apk mod apps are essential for the user. Thank you for sharing.