question

nobloat avatar image
nobloat asked

(PayPal) TransactionStatus always "Init" and ConfirmPurchase returns "409 Conflict"

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!

Partner Add-onsPlayer Inventory
image1.png (50.8 KiB)
image2.png (5.3 KiB)
2 comments
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Sarah Zhang avatar image Sarah Zhang commented ·

We will dig into it.

0 Likes 0 ·
nobloat avatar image nobloat Sarah Zhang commented ·

Thanks Sarah!

I ran the exact same code the next day and didn't get any 409 Conflict errors, not sure why though.

What I've noticed since, however, is that my ConfirmPurchase() call successfully returns a ConfirmPurchaseResult with Items != null even when the purchase status in the PlayFab GameManager is "Failed by provider". The "player_realmoney_purchase" event is not being triggered in these instances, even though the transaction completes successfully according to PayPal (confirmed by emails sent to the buyer and seller accounts).

0 Likes 0 ·
nobloat avatar image
nobloat answered

For anyone running into this issue in the future, I could never avoid getting occassional 409 Conflicts when using the polling ConfirmPurchase() approach from my original code.

Instead, I detect when the user closes the PayPal popup/browser window (I poll the popup window in Javascript because I'm using Unity WebGL, but you might be able to use OnApplicationFocus if targeting other platforms) and only then do I call ConfirmPurchase(). This approach is working just fine in my production code, and my players' payments through PayPal have been successful.

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Sarah Zhang avatar image
Sarah Zhang answered

Thanks for the clarification. Have you checked the following threads for more information about the 409 Conflict error?

409 Conflict on UpdateUserData - Playfab Community

409 Conflict when calling GetContentUploadUrl - Playfab Community

"HTTP/1.1 409 Conflict" only on iOS - Playfab Community

5 comments
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

nobloat avatar image nobloat commented ·

Yes, I've looked into errors related to the 409 Conflicts and they no longer seem to be a problem. I am no longer getting any 409 Conflict errors, and you'll see that lines 81,82,90,99 of my attached code prevent additional calls of PlayFabClientAPI.ConfirmPurchase() until the previous call receives a response, as recommended in the linked threads.

0 Likes 0 ·
nobloat avatar image nobloat commented ·

Update: Now I'm getting 409 Conflicts again, though infrequently and at random. I don't understand how this could happen since lines 81,82,90,99 of my attached code prevent additional calls of PlayFabClientAPI.ConfirmPurchase() until the previous call receives a response.

Sometimes the ConfirmPurchase() call that returns a 409 Conflict successfully adds the purchased item to the player's Inventory and marks the purchase as Succeeded, though no money is transferred through PayPal. Is this a backend issue with the communication between PlayFab/PayPal?

0 Likes 0 ·
Sarah Zhang avatar image Sarah Zhang commented ·

Thanks for your full information. First, could you please clarify that do you use the PayPal sandbox testing environment? If so, PlayFab doesn’t support the PayPal Sandbox right now. If using PayPal Sandbox, the purchase should not be completed.

Sometimes the 409 Conflict will also happen when the only one API is called, this situation should be rare. We suggest you use the try catch to add the retry mechanism for the API call. We would also suggest you increasing the waiting time of the while loop to reduce the probability of errors.

If you are using PayPal Sandbox, but the purchase would be completed successfully when the 409 Conflict happened, please feel free to let us know.

0 Likes 0 ·
nobloat avatar image nobloat Sarah Zhang commented ·

No, I'm not using PayPal sandbox. My successful purchases do go through and correctly withdraw from my personal PayPal/deposit into my business PayPal. Thanks for the pointer about the try-catch, I'll just keep waiting and polling if I receive any 409 Conflict errors then.

I'll also increase the manual wait duration up from 1 second (line 78), but even when I do no manual waiting, i.e.

yield return new WaitForSecondsRealtime(1f);

// replace with:

yield return null;

I receive no 409 Conflicts while polling because of the API call completion check on line 81.

The only exception is a single error due to 409 Conflict (PlayFabErrorCode.ServiceUnavailable) when I complete my payment in the separate PayPal browser window. For now, I'll ignore errors with code PlayFabErrorCode.ServiceUnavailable and keep polling when I receive them.

0 Likes 0 ·
Sarah Zhang avatar image Sarah Zhang nobloat commented ·

Thanks for the clarification. Please feel free to let us know if you encounter any other abnormalities during the purchase process.

0 Likes 0 ·

Write an Answer

Hint: Notify or tag a user in this post by typing @username.

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.