Hello, I'm trying to add PayPal integration to a Unity WebGL game following the guide at https://docs.microsoft.com/en-us/gaming/playfab/features/commerce/economy/non-receipt-payment-processing. I'm having issues with the ConfirmPurchase step of the process, specifically when trying to determine if the user has successfully completed the purchase in the pop-up window containing the PayPal checkout interface.
First of all, I'm unable to use GetPurchase to poll for completion since GetPurchaseResult.TransactionStatus is always equal to "Init," even when the purchase has been completed. See the PlayFab event data below for player_paid_for_purchase where the "Status" is still "Init":
{ "PlayFabEnvironment": { "Application": "mainserverlinux", "Vertical": "master", "Commit": "a33ae4a", "Cloud": "main" }, "EventNamespace": "com.playfab", "SourceType": "BackEnd", "EntityType": "player", "Timestamp": "2021-06-16T00:48:25.7365616Z", "EventName": "player_paid_for_purchase", "EntityId": "DD9EF74AA43DBF13", "EventId": "c43682c78f854d0aa1096eccfcf1e1b9", "TitleId": "#####", "Source": "PlayFab", "PurchaseConfirmationPageURL": "https://www.paypal.com/webscr?cmd=_express-checkout&useraction=commit&token=EC-#################", "VirtualCurrencyBalances": {}, "VirtualCurrencyGrants": {}, "PurchaseCurrency": "RM", "OrderId": "################", "PurchasePrice": 99, "ProviderData": "####################", "ProviderName": "PayPal", "Status": "Init" }
As a result, I poll for completion using ConfirmPurchase and only make a new API call once the previous call has succeeded or failed. When ConfirmPurchase succeeds and the resulting ConfirmPurchaseResult.Items is not null, that signals that the purchase was a success and I can stop polling. See full code below:
using System.Collections; using System.Collections.Generic; using PlayFab; using PlayFab.ClientModels; using UnityEngine; public partial class PlayFabManager { PayForPurchaseResult _currentPurchaseResult; Coroutine _pollingPurchaseStatus; bool _readyToConfirmPurchase = true; void Update() { if (Input.GetMouseButtonDown(2)) { TestPayPalStartPurchase(); } } void TestPayPalStartPurchase() { PlayFabClientAPI.StartPurchase(new StartPurchaseRequest() { CatalogVersion = "TestCatalog", Items = new List<ItemPurchaseRequest>() { new ItemPurchaseRequest() { ItemId = "TestItem1", Quantity = 1, } } }, TestPayPalPayForPurchase, HandlePurchaseError); } void TestPayPalPayForPurchase(StartPurchaseResult startPurchaseResult) { PlayFabClientAPI.PayForPurchase(new PayForPurchaseRequest() { OrderId = startPurchaseResult.OrderId, ProviderName = "PayPal", Currency = "RM", }, PromptOpenPayPalWindow, HandlePurchaseError); } void PromptOpenPayPalWindow(PayForPurchaseResult payForPurchaseResult) { // TODO: prompt user to press a "continue to PayPal" button here, then open a popup window with the URL below _currentPurchaseResult = payForPurchaseResult; OpenPayPalWindow(); } public void OpenPayPalWindow() { if (_currentPurchaseResult == null) { Debug.LogWarning("No PayPal payment in progress"); return; } GameHelpers.OpenURLNewTab(_currentPurchaseResult.PurchaseConfirmationPageURL); // Open URL in new window, preferably a popup // grey out the screen and block game interaction until PayPal is dealt with, or make clicking off cancel _pollingPurchaseStatus = StartCoroutine(PollPurchaseStatus()); // TODO: receive notification when the popup has closed (through JavaScript) so we can cancel polling } IEnumerator PollPurchaseStatus() { float timeoutEndTime = Time.unscaledTime + 300f; while (Time.unscaledTime < timeoutEndTime) { yield return new WaitForSecondsRealtime(1f); // Only make the API call when the previous API call has been completed to avoid conflicts/409 errors if (!_readyToConfirmPurchase) continue; _readyToConfirmPurchase = false; PlayFabClientAPI.ConfirmPurchase(new ConfirmPurchaseRequest() { OrderId = _currentPurchaseResult.OrderId, }, result => { _readyToConfirmPurchase = true; // Signal that we're ready to make another API call if (result.Items != null) { HandlePurchaseSuccess(result); } }, error => { _readyToConfirmPurchase = true; // Signal that we're ready to make another API call if (error.Error != PlayFabErrorCode.FailedByPaymentProvider) { HandlePurchaseError(error); // FailedByPaymentProvider means nothing while we're polling } }); } } void HandlePurchaseSuccess(ConfirmPurchaseResult confirmPurchaseResult) { Debug.Log("PURCHASE SUCCESSFUL"); CancelPayPalPopupPolling(); foreach (ItemInstance newItem in confirmPurchaseResult.Items) { Debug.Log($"Added new item: {newItem.DisplayName}"); } // TODO: do a manual client-side refresh of the player's inventory and store UI to update the items they own } void HandlePurchaseError(PlayFabError playFabError) { Debug.LogWarning("PlayFab Payment API call failed:"); Debug.LogWarning(playFabError.GenerateErrorReport()); CancelPayPalPopupPolling(); } void CancelPayPalPopupPolling() { if (_pollingPurchaseStatus != null) StopCoroutine(_pollingPurchaseStatus); _currentPurchaseResult = null; _readyToConfirmPurchase = true; // TODO: turn off screen overlay that was preventing interaction while PayPal window was open } }
The first time running this code on a new PlayFab player results in a successful purchase, as indicated by calling the HandlePurchaseSuccess() method. This purchased item correctly indicates 1 "Uses left" in the PlayFab Inventory view. Subsequent attempts, however, throw 409 Confict errors and have blank "Uses left" fields.
Let me know if I'm missing something here, or if this truly is unintended behavior on PlayFab's end. Thanks a bunch!