Internal testing. Receipt not always contain last consumable purchase.

After game restart first purchase is contained in receipt but the next ones is the same as first one so new purchases is not added. I afraid players can be charged for purchase but on my server I will not receive new purchases instead receipt with old one so they can do not receive in game currency. Will in production I receive a receipts with new consumable every time player purchase it? I use Unity3d In-app purchasing 5.0.1.

Answered by bohdan_p in 858481022

I messed the code in previous post:

using Jose;
using Newtonsoft.Json;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
private static ECDsa LoadApplePrivateKey(string filePath)
{
    var keyText = File.ReadAllText(filePath)
        .Replace("-----BEGIN PRIVATE KEY-----", "")
        .Replace("-----END PRIVATE KEY-----", "")
        .Replace("\r", "")
        .Replace("\n", "")
        .Trim();

    var keyBytes = Convert.FromBase64String(keyText);
    var ecdsa = ECDsa.Create();
    ecdsa.ImportPkcs8PrivateKey(keyBytes, out _);
    return ecdsa;
}

private string GenerateApiToken()
{
    var payload = new Dictionary<string, object>
    {
        { "iss", _issuerId },
        { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
        { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() },
        { "aud", "appstoreconnect-v1" },
        { "bid", _bundleId }
    };

    var extraHeaders = new Dictionary<string, object>
    {
        { "alg", "ES256" },
        { "kid", _keyId },
        { "typ", "JWT" }
    };

    string token = JWT.Encode(payload, _privateKey, JwsAlgorithm.ES256, extraHeaders);

    _logger.LogInformation("Generated JWT: {Token}", token);
    return token;
}

public static ApplePurchase DecodeJws(string jws)
{
    var parts = jws.Split('.');
    if (parts.Length != 3) throw new ArgumentException("Invalid JWS format");

    var payloadJson = Encoding.UTF8.GetString(Base64Url.Decode(parts[1]));
    dynamic payload = JsonConvert.DeserializeObject(payloadJson);

    return new ApplePurchase
    {
        transactionId = payload.transactionId,
        productId = payload.productId,
        timePurchased = payload.purchaseDate,
        environment = payload.environment ?? "Unknown"
    };
}
//My validator 
  public static async Task<AppleValidationResult> ValidateReceiptAsync(string base64Receipt)
  {
      var requestJson = new JObject
      {
          ["receipt-data"] = base64Receipt,
          ["password"] = SharedSecret
      };

      var content = new StringContent(requestJson.ToString(), Encoding.UTF8, "application/json");

      // First try production
      var response = await client.PostAsync(ProductionUrl, content);
      var json = JObject.Parse(await response.Content.ReadAsStringAsync());

      int status = (int) json["status"];

      if (status == 21007) // Receipt from sandbox
      {
          response = await client.PostAsync(SandboxUrl, content);
          json = JObject.Parse(await response.Content.ReadAsStringAsync());
          status = (int) json["status"];
          Debug.Log("status=" + status);
      }
      Debug.Log("status="+status);
      if (status != 0)
          return new AppleValidationResult { Valid = false };

      // Get the last in-app purchase
      var latestReceipt = json["receipt"]?["in_app"]?.Last;
      Debug.Log("latest receipt =  " + latestReceipt!=null);
      if (latestReceipt == null)
          return new AppleValidationResult { Valid = false };

      return new AppleValidationResult
      {
          Valid = true,
          TransactionId = latestReceipt["transaction_id"]?.ToString(),
          ProductId = latestReceipt["product_id"]?.ToString()
      };
  } 

Now I try to use JWT. But I receive JWT is not well formed. I use .NET 9 my code is

 private string GenerateApiToken()
 {
     var now = DateTimeOffset.UtcNow;
     var expiry = now.AddMinutes(5);

     var credentials = new SigningCredentials(
         new ECDsaSecurityKey(_privateKey) { KeyId = _keyId },
         SecurityAlgorithms.EcdsaSha256);

     var claims = new Dictionary<string, object>
 {
     { "bid", _bundleId } // Apple requires this
 };

     var descriptor = new SecurityTokenDescriptor
     {
         Issuer = _issuerId,
         IssuedAt = now.UtcDateTime,
         Expires = expiry.UtcDateTime,
         Audience = "appstoreconnect-v1",
         Claims = claims,
         //NotBefore = now.UtcDateTime,
         SigningCredentials = credentials
     };

     var handler = new JwtSecurityTokenHandler();
     var token = handler.CreateJwtSecurityToken(descriptor);

     // Force Apple-required headers
     token.Header["alg"] = "ES256";
     token.Header["kid"] = _keyId;
     token.Header["typ"] = "JWT";

     var jwt = handler.WriteToken(token);

     Console.WriteLine($"Generated JWT: {jwt}");

     return jwt;
 }

using Jose; using Newtonsoft.Json; using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; This Generation of Api token works on .net 9 I post the main methods I struggle with:

{
    var keyText = File.ReadAllText(filePath)
        .Replace("-----BEGIN PRIVATE KEY-----", "")
        .Replace("-----END PRIVATE KEY-----", "")
        .Replace("\r", "")
        .Replace("\n", "")
        .Trim();

    var keyBytes = Convert.FromBase64String(keyText);
    var ecdsa = ECDsa.Create();
    ecdsa.ImportPkcs8PrivateKey(keyBytes, out _);
    return ecdsa;
}

private string GenerateApiToken()
{
    var payload = new Dictionary<string, object>
    {
        { "iss", _issuerId },
        { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
        { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() },
        { "aud", "appstoreconnect-v1" },
        { "bid", _bundleId }
    };

    var extraHeaders = new Dictionary<string, object>
    {
        { "alg", "ES256" },
        { "kid", _keyId },
        { "typ", "JWT" }
    };

    string token = JWT.Encode(payload, _privateKey, JwsAlgorithm.ES256, extraHeaders);

    _logger.LogInformation("Generated JWT: {Token}", token);
    return token;
}

public static ApplePurchase DecodeJws(string jws)
{
    var parts = jws.Split('.');
    if (parts.Length != 3) throw new ArgumentException("Invalid JWS format");

    var payloadJson = Encoding.UTF8.GetString(Base64Url.Decode(parts[1]));
    dynamic payload = JsonConvert.DeserializeObject(payloadJson);

    return new ApplePurchase
    {
        transactionId = payload.transactionId,
        productId = payload.productId,
        timePurchased = payload.purchaseDate,
        environment = payload.environment ?? "Unknown"
    };
}```
Accepted Answer

I messed the code in previous post:

using Jose;
using Newtonsoft.Json;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
private static ECDsa LoadApplePrivateKey(string filePath)
{
    var keyText = File.ReadAllText(filePath)
        .Replace("-----BEGIN PRIVATE KEY-----", "")
        .Replace("-----END PRIVATE KEY-----", "")
        .Replace("\r", "")
        .Replace("\n", "")
        .Trim();

    var keyBytes = Convert.FromBase64String(keyText);
    var ecdsa = ECDsa.Create();
    ecdsa.ImportPkcs8PrivateKey(keyBytes, out _);
    return ecdsa;
}

private string GenerateApiToken()
{
    var payload = new Dictionary<string, object>
    {
        { "iss", _issuerId },
        { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
        { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() },
        { "aud", "appstoreconnect-v1" },
        { "bid", _bundleId }
    };

    var extraHeaders = new Dictionary<string, object>
    {
        { "alg", "ES256" },
        { "kid", _keyId },
        { "typ", "JWT" }
    };

    string token = JWT.Encode(payload, _privateKey, JwsAlgorithm.ES256, extraHeaders);

    _logger.LogInformation("Generated JWT: {Token}", token);
    return token;
}

public static ApplePurchase DecodeJws(string jws)
{
    var parts = jws.Split('.');
    if (parts.Length != 3) throw new ArgumentException("Invalid JWS format");

    var payloadJson = Encoding.UTF8.GetString(Base64Url.Decode(parts[1]));
    dynamic payload = JsonConvert.DeserializeObject(payloadJson);

    return new ApplePurchase
    {
        transactionId = payload.transactionId,
        productId = payload.productId,
        timePurchased = payload.purchaseDate,
        environment = payload.environment ?? "Unknown"
    };
}
Internal testing. Receipt not always contain last consumable purchase.
 
 
Q