question

Mubarak Almehairbi avatar image
Mubarak Almehairbi asked

Xsolla for a specific item/bundle

Hi,

I have been following the documentation below for real money purchase using xsolla: https://learn.microsoft.com/en-us/gaming/playfab/features/economy/tutorials/non-receipt-payment-processing#initiating-the-purchase-xsolla

However, there is no argument to specify the item that I want the player to purchase. Where can I specify that?

In-Game Economy
10 |1200

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

Mubarak Almehairbi avatar image
Mubarak Almehairbi answered

I have written an answer on Sep 09 2023 but now I realized that that answer had 2 problems.

  1. The bought item does not appear on PlayFab inventory.

  2. Xsolla always assume that the buyer is from US.

After contacting Xsolla and after some trial and error, this is the solution that worked:

public static void RealMoneyPurchase(string itemId, Action<Dictionary<string, object>> successCallback = null, Action<PlayFabError> failureCallback = null)
    {
        // First, run a CloudScript function called CreateXsollaRequest
        var createXsollaRequest = new ExecuteCloudScriptRequest{
        FunctionName = "CreateXsollaRequest",
        FunctionParameter = new { itemId = itemId, sandbox = sandbox }, // sandbox is a boolean that determines whether to use the sandbox or production Xsolla environment
        };

        PlayFabClientAPI.ExecuteCloudScript(
        createXsollaRequest,
        success =>
        {
            
            var json = JsonConvert.DeserializeObject<Dictionary<string, object>>(success.ToJson());
            Debug.Log($"CreateXsollaRequest success: {success.ToJson()}");
            var functionResult = JsonConvert.DeserializeObject<Dictionary<string, object>>(JsonConvert.SerializeObject(json["FunctionResult"]));
            if (functionResult.ContainsKey("error"))
            {
                Debug.LogError($"CreateXsollaRequest error: {functionResult["error"]}");
                if (failureCallback != null)
                {
                    failureCallback(new PlayFabError() { ErrorMessage = functionResult["error"].ToString() });
                }
                return;
            }

            // Second, use the returned uri, headers, and body to make a POST request to Xsolla
            var uri = functionResult["uri"].ToString();
            var headers = JsonConvert.DeserializeObject<Dictionary<string, object>>(functionResult["headers"].ToString());
            var body = functionResult["body"].ToString();
            string response = Requests.PostRequest(uri, headers, body);
            Debug.Log($"GetXsollaPaymentToken response: {response}");
            var responseJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(response);
            if (responseJson.ContainsKey("error"))
            {
                Debug.LogError($"GetXsollaPaymentToken error: {responseJson["error"]}");
                if (failureCallback != null)
                {
                    failureCallback(new PlayFabError() { ErrorMessage = responseJson["error"].ToString() });
                }
                return;
            }

            // Third, use the returned token to open the Xsolla payment UI
            var token = responseJson["token"].ToString();
            var orderId = responseJson["order_id"].ToString();
            var url = "https://secure.xsolla.com/paystation4/?token=" + token;
            if (sandbox)
            {
                url = "https://sandbox-secure.xsolla.com/paystation4/?token=" + token;
            }
            Application.OpenURL(url);
            if (successCallback != null)
            {
                successCallback(functionResult);
            }
        },
        playfabError => {
            Debug.LogError($"GetXsollaAccessToken error: {playfabError.ErrorMessage}");
            if (failureCallback != null)
            {
                failureCallback(playfabError);
            }
    });
    }
  • The Requests.PostRequest function in the previous code is from this C# script:

using UnityEngine.Networking;
using UnityEngine;
using System.Collections;
using System;
using System.Collections.Generic;
using System.Net;
using System.IO;
using Newtonsoft.Json.Linq;

public class Requests
{

    public static string PostRequest(string uri, Dictionary<string, object> headers, string body)
    {
        var httpWebRequest = (HttpWebRequest)WebRequest.Create(uri);
        httpWebRequest.ContentType = "application/json";
        httpWebRequest.Method = "POST";
        foreach (var header in headers)
        {
            httpWebRequest.Headers.Add(header.Key, header.Value.ToString());
        }

        using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
        {
            streamWriter.Write(body);
        }

        var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
        using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
        {
            var result = streamReader.ReadToEnd();
            return result;
        }
    }
}
  • Add this code to CloudScript (make sure to fill the input parameters):

handlers.CreateXsollaRequest = function (args) {
    
    // The purpose of this CloudScript function is to create a uri, header,
    // and body that the client will then use to make an http request.
    
    ////// INPUT PARAMTERS //////
    
    // TODO replace with production credentials
    const projectId = // Include your Xsolla project ID here
    const apikey = // Include your Xsolla API key here
    const loginProjectId = // Include your Xsolla Login Project ID here
    const serverOauthClientId = // Include the Client ID of the server oauth client you set in step 1
    const serverOauthClientSecret =// Include the Client secret of the server oauth client you set in step 1
    
    ////// END OF INPUT PARAMTERS //////
    
    // Get server token
    const getServerTokenBody =
        "grant_type=client_credentials" +
        `&client_secret=${serverOauthClientSecret}` +
        `&client_id=${serverOauthClientId}`;

    const serverTokenResponse = JSON.parse(
        http.request(
            "https://login.xsolla.com/api/oauth2/token",
            "post",
            getServerTokenBody,
            "application/x-www-form-urlencoded",
            {})
    );

    let serverToken = ""
    if ('access_token' in serverTokenResponse) {
        serverToken = serverTokenResponse.access_token;
    } else {
        return {
            "error_message": "Server token not received"
        }
    }
    
    // Get user token
    const getUserTokenHeaders = {
        "X-Server-Authorization": serverToken
    }

    const getUserTokenBody = JSON.stringify(
        {
            "server_custom_id": currentPlayerId,
        }
    );

    const getUserTokenPath =
        "/api/users/login/server_custom_id?" +
        `projectId=${loginProjectId}&` +
        `publisher_project_id=${projectId}`;

    const userTokenResponse = JSON.parse(
        http.request(
            "https://login.xsolla.com" + getUserTokenPath,
            "post",
            getUserTokenBody,
            "application/json",
            getUserTokenHeaders)
    );
    
    let userToken = ""
    if ('token' in userTokenResponse) {
        userToken = userTokenResponse.token
        
        // Create a uri, header, and body and pass them to the client.
        const uri = `https://store.xsolla.com/api/v2/project/${projectId}/payment/item/${args.itemId}`
        const headers = JSON.stringify(
            {
                "Authorization": `Bearer ${userToken}`
            }    
        )
        const body = JSON.stringify(
            {
                "quantity": 1,
                "sandbox": args.sandbox,
                "locale": "en"
            }    
        )
        
        return {"uri": uri, "headers": headers, "body": body}
    } else {
        return {
            "error_message": "User token not received"
        }
    }
    
    
    
}

The cloudscript function will create a user token, then it will pass the parameters needed to create an http request to the unity game. The unity game will then use these parameters to get the payment token and open the payment UI.

10 |1200

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

Infer Wang avatar image
Infer Wang answered

According to the document, the api "GetPaymentToken" just gets token and creates a cart, so once you have performed that, you can complete the purchase(specify the items) in the Xsolla interface, refer to this guide.

10 |1200

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

Mubarak Almehairbi avatar image
Mubarak Almehairbi answered

EDIT: this answer is old and has a few problems, I wrote a new answer on Sep 26

I only get a page where the user should input the amount of virtual currency they want to buy. I looked into xsolla docs and the only conclusion I reached is that I need to get the token in a different way. I am currently following this documentation to get the payment token for a specific item: https://developers.xsolla.com/api/igs-bb/operation/admin-create-payment-token/ instead of using "GetPaymentToken", and that worked for me.

Below is my code in Unity:

public static void RealMoneyPurchase(string itemId, Action> successCallback = null, Action failureCallback = null)
    {
        var tokenRequest = new ExecuteCloudScriptRequest{
        FunctionName = "GetXsollaPaymentToken",
        FunctionParameter = new { itemId = itemId, sandbox = sandbox },
        };

        PlayFabClientAPI.ExecuteCloudScript(
        tokenRequest,
        success =>
        {
            
            var json = JsonConvert.DeserializeObject<Dictionary<string, object>>(success.ToJson());
            Debug.Log($"GetXsollaPaymentToken success: {success.ToJson()}");
            var functionResult = JsonConvert.DeserializeObject<Dictionary<string, object>>(JsonConvert.SerializeObject(json["FunctionResult"]));
            if (functionResult.ContainsKey("error"))
            {
                Debug.LogError($"GetXsollaPaymentToken error: {functionResult["error"]}");
                if (failureCallback != null)
                {
                    failureCallback(new PlayFabError() { ErrorMessage = functionResult["error"].ToString() });
                }
                return;
            }
            var token = functionResult["token"].ToString();
            var orderId = functionResult["order_id"].ToString();
            var url = "https://secure.xsolla.com/paystation4/?token=" + token;
            if (sandbox)
            {
                url = "https://sandbox-secure.xsolla.com/paystation4/?token=" + token;
            }
            Application.OpenURL(url);
            if (successCallback != null)
            {
                successCallback(functionResult);
            }
        },
        playfabError => {
            Debug.LogError($"GetXsollaAccessToken error: {playfabError.ErrorMessage}");
            if (failureCallback != null)
            {
                failureCallback(playfabError);
            }
    });
    }

And this is my code in CloudScript:

handlers.GetXsollaPaymentToken = function (args) {

    // TODO replace with production credentials
    const projectId = // Include your Xsolla project ID here
    const apikey = // Include your Xsolla api key here
    const sandbox = args.sandbox;
    
    const encodedKey = Base64.encode(`${projectId}:${apikey}`)

    const headers = {
        "Authorization": `Basic ${encodedKey}`
    }

    const body = JSON.stringify(
        {
          "sandbox": sandbox,
          "user": {
            "id": {
              "value": currentPlayerId
            }
          },
          "purchase": {
            "items": [
              {
                "sku": args.itemId,
                "quantity": 1
              }
            ]
          }
        }
    )

    var paymentTokenResponse = JSON.parse(http.request(
            `https://store.xsolla.com/api/v2/project/${projectId}/admin/payment/token`,
            "post",
            body,
            "application/json",
            headers))
    
    let token = ""
    if ('token' in paymentTokenResponse) {
        return {
            "token": paymentTokenResponse.token,
            "order_id": paymentTokenResponse.order_id
        }
    } else {
        return {
            "error_message": "User token not received"
        }
    }
    
}

// The purpose of the code below is only for base-64 encoding
var Base64 = {

 


    // private property


    _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",

 


    // public method for encoding


    encode : function (input) {


        var output = "";


        var chr1, chr2, chr3, enc1, enc2, enc3, enc4;


        var i = 0;

 


        input = Base64._utf8_encode(input);

 


        while (i < input.length) {

 


            chr1 = input.charCodeAt(i++);


            chr2 = input.charCodeAt(i++);


            chr3 = input.charCodeAt(i++);

 


            enc1 = chr1 >> 2;


            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);


            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);


            enc4 = chr3 & 63;

 


            if (isNaN(chr2)) {


                enc3 = enc4 = 64;


            } else if (isNaN(chr3)) {


                enc4 = 64;


            }

 


            output = output +


            this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +


            this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);

 


        }

 


        return output;


    },

 


    // public method for decoding


    decode : function (input) {


        var output = "";


        var chr1, chr2, chr3;


        var enc1, enc2, enc3, enc4;


        var i = 0;

 


        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

 


        while (i < input.length) {

 


            enc1 = this._keyStr.indexOf(input.charAt(i++));


            enc2 = this._keyStr.indexOf(input.charAt(i++));


            enc3 = this._keyStr.indexOf(input.charAt(i++));


            enc4 = this._keyStr.indexOf(input.charAt(i++));

 


            chr1 = (enc1 << 2) | (enc2 >> 4);


            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);


            chr3 = ((enc3 & 3) << 6) | enc4;

 


            output = output + String.fromCharCode(chr1);

 


            if (enc3 != 64) {


                output = output + String.fromCharCode(chr2);


            }


            if (enc4 != 64) {


                output = output + String.fromCharCode(chr3);


            }

 


        }

 


        output = Base64._utf8_decode(output);

 


        return output;

 


    },

 


    // private method for UTF-8 encoding


    _utf8_encode : function (string) {


        string = string.replace(/\r\n/g,"\n");


        var utftext = "";

 


        for (var n = 0; n < string.length; n++) {

 


            var c = string.charCodeAt(n);

 


            if (c < 128) {


                utftext += String.fromCharCode(c);


            }


            else if((c > 127) && (c < 2048)) {


                utftext += String.fromCharCode((c >> 6) | 192);


                utftext += String.fromCharCode((c & 63) | 128);


            }


            else {


                utftext += String.fromCharCode((c >> 12) | 224);


                utftext += String.fromCharCode(((c >> 6) & 63) | 128);


                utftext += String.fromCharCode((c & 63) | 128);


            }

 


        }

 


        return utftext;


    },

 


    // private method for UTF-8 decoding


    _utf8_decode : function (utftext) {


        var string = "";


        var i = 0;


        var c = c1 = c2 = 0;

 


        while ( i < utftext.length ) {

 


            c = utftext.charCodeAt(i);

 


            if (c < 128) {


                string += String.fromCharCode(c);


                i++;


            }


            else if((c > 191) && (c < 224)) {


                c2 = utftext.charCodeAt(i+1);


                string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));


                i += 2;


            }


            else {


                c2 = utftext.charCodeAt(i+1);


                c3 = utftext.charCodeAt(i+2);


                string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));


                i += 3;


            }

 


        }

 


        return string;


    }

 

}
1 comment
10 |1200

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

Infer Wang avatar image Infer Wang commented ·

Thanks for sharing, if you have any other playfab related issues, feel free to ask.

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.