IsValidateGooglePurchase

public BackendReturnObject IsValidateGooglePurchase(String receipt, String receiptDescription);
public BackendReturnObject IsValidateGooglePurchase(String receipt, String receiptDescription , bool isSubscription);

파라미터

ValueTypeDescriptiondefault
receiptStringPurchasing.PurchaseEventArgs.purchasedProduct.receipt-
receiptDescriptionString추가로 저장하고자 하는 내용-
isSubscriptionbool해당 상품이 구독 상품인지 아닌지 여부. true일 경우 구독상품false

설명

유니티에서 지원하는 IAP 서비스의 IStoreListener.ProcessPurchase() 에서 구매한 상품에 대한 영수증을 받아 뒤끝 서버를 통해 영수증 검증을 받습니다.

  • 뒤끝은 영수증 자체의 유효성과, 구매한 productId를 검증합니다.
  • 뒤끝 로그인 없이 뒤끝 영수증 검증 기능을 사용하는 것은 불가능합니다.

구글 영수증 검증을 위해서는 뒤끝 및 구글 콘솔 설정이 필요합니다.
자세한 설명은 구글 결제 콘솔 설정 문서을 참고해주세요.

Example

동기

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) 
{
    /*
    뒤끝 영수증 검증 처리
    */
    BackendReturnObject validation = Backend.Receipt.IsValidateGooglePurchase ( args.purchasedProduct.receipt , "receiptDescription", false);
    
    // 영수증 검증에 성공한 경우
    if(validation.IsSuccess())
    {
        // 구매 성공한 제품에 대한 id 체크하여 그에맞는 보상 
        // A consumable product has been purchased by this user.
        if (String.Equals(args.purchasedProduct.definition.id, kProductIDConsumable, StringComparison.Ordinal))
        {
            Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
            // The consumable item has been successfully purchased, add 100 coins to the player's in-game score.
            ScoreManager.score += 100;
        }
        // Or ... a non-consumable product has been purchased by this user.
        else if (String.Equals(args.purchasedProduct.definition.id, kProductIDNonConsumable, StringComparison.Ordinal))
        {
            Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
            // TODO: The non-consumable item has been successfully purchased, grant this item to the player.
        }
        // Or ... a subscription product has been purchased by this user.
        else if (String.Equals(args.purchasedProduct.definition.id, kProductIDSubscription, StringComparison.Ordinal))
        {
            Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
            // TODO: The subscription item has been successfully purchased, grant this to the player.
        }
    }
    // 영수증 검증에 실패한 경우 
    else 
    {
        // Or ... an unknown product has been purchased by this user. Fill in additional products here....
        Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
    }

    // Return a flag indicating whether this product has completely been received, or if the application needs 
    // to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still 
    // saving purchased products to the cloud, and when that save is delayed. 
    return PurchaseProcessingResult.Complete;
}

비동기

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) 
{
    Backend.Receipt.IsValidateGooglePurchase( args.purchasedProduct.receipt, "receiptDescription", false, ( callback ) =>
    {
        // 영수증 검증에 성공한 경우
        if(callback.IsSuccess())
        {
              if (String.Equals(args.purchasedProduct.definition.id, kProductIDConsumable, StringComparison.Ordinal))
              {
                  Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                  // The consumable item has been successfully purchased, add 100 coins to the player's in-game score.
                  ScoreManager.score += 100;
              }
              // Or ... a non-consumable product has been purchased by this user.
              else if (String.Equals(args.purchasedProduct.definition.id, kProductIDNonConsumable, StringComparison.Ordinal))
              {
                  Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                  // TODO: The non-consumable item has been successfully purchased, grant this item to the player.
              }
              // Or ... a subscription product has been purchased by this user.
              else if (String.Equals(args.purchasedProduct.definition.id, kProductIDSubscription, StringComparison.Ordinal))
              {
                  Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                  // TODO: The subscription item has been successfully purchased, grant this to the player.
              }
        }
        else
        {
            // 영수증 검증에 실패한 경우
            Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
        }
    });
    // Return a flag indicating whether this product has completely been received, or if the application needs 
    // to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still 
    // saving purchased products to the cloud, and when that save is delayed. 
    return PurchaseProcessingResult.Complete;
}

SendQueue

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) 
{
    SendQueue.Enqueue(Backend.Receipt.IsValidateGooglePurchase, args.purchasedProduct.receipt, "receiptDescription", false, ( callback ) =>
    {
        // 영수증 검증에 성공한 경우
        if(callback.IsSuccess())
        {
              if (String.Equals(args.purchasedProduct.definition.id, kProductIDConsumable, StringComparison.Ordinal))
              {
                  Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                  // The consumable item has been successfully purchased, add 100 coins to the player's in-game score.
                  ScoreManager.score += 100;
              }
              // Or ... a non-consumable product has been purchased by this user.
              else if (String.Equals(args.purchasedProduct.definition.id, kProductIDNonConsumable, StringComparison.Ordinal))
              {
                  Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                  // TODO: The non-consumable item has been successfully purchased, grant this item to the player.
              }
              // Or ... a subscription product has been purchased by this user.
              else if (String.Equals(args.purchasedProduct.definition.id, kProductIDSubscription, StringComparison.Ordinal))
              {
                  Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                  // TODO: The subscription item has been successfully purchased, grant this to the player.
              }
        }
        else
        {
            // 영수증 검증에 실패한 경우
            Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
        }
    });
    // Return a flag indicating whether this product has completely been received, or if the application needs 
    // to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still 
    // saving purchased products to the cloud, and when that save is delayed. 
    return PurchaseProcessingResult.Complete;
}

ReturnCase

Success cases

검증에 성공한 경우(일반상품)
statusCode : 201
message : Success
returnValue : {"usedDate":"2018-10-15T05:17:49Z"}

검증에 성공한 경우(구독 상품)
statusCode : 200
message : Success
returnValue : GetReturnValueToJSON 참조

Error cases

이미 사용한 영수증 토큰
statusCode : 409
errorCode : UsedReceipt
message : This receipt has already been used. usedDate: 2018-02-15T04:01:50.000Z

이미 사용하거나 취소된 구독 상품의 영수증을 검증한 경우
statusCode : 409
errorCode : DuplicatedParameterException
message : Duplicated receipt, 중복된 receipt 입니다

환불/취소 영수증
statusCode : 402
errorCode : AbnormalReceipt
message : This receipt has changed status. purchaseState: cancelled

유효하지 않은 영수증 토큰
statusCode : 400
errorCode : BadParameterException
message : bad token, 잘못된 token 입니다

1-3번 항목의 JWT 권한의 확인이 필요합니다.
권한을 제대로 설정했는데도 bad token 에러가 발생할 경우 JWT를 재발급하고, 재발급 후에도 에러가 발생할 경우 JWT를 새로 생성한 후 다시 시도해야 합니다.
JWT 수정여부는 바로 적용이 안될 수 있어 1~2시간 후에 다시 시도하는 것을 권장합니다.

GetReturnValueToJSON(구독상품)

{
  "kind":"androidpublisher#subscriptionPurchase",
  "startTimeMillis":"1583722491833",
  "expiryTimeMillis":"1583722908247",
  "autoRenewing":true,
  "priceCurrencyCode":"KRW",
  "priceAmountMicros":"1000000000",
  "countryCode":"KR",
  "developerPayload":"{\"developerPayload\":\"\",\"is_free_trial\":false,\"has_introductory_price_trial\":false,\"is_updated\":false}",
  "paymentState":1,
  "orderId":"주문 Id",
  "purchaseType":0,
  "acknowledgementState":1
}  

IsValidateGooglePurchase

public BackendReturnObject IsValidateGooglePurchase(String productId, String token, String receiptDescription);
public BackendReturnObject IsValidateGooglePurchase(String productId, String token, String receiptDescription, bool isSubscription = false);

파라미터

ValueTypeDescriptiondefault
productIdString구매하고자 하는 productId-
tokenString구매 이후에 발행되는 영수증 토큰-
receiptDescriptionString추가로 저장하고자 하는 내용-
isSubscriptionbool해당 상품이 구독 상품인지 아닌지 여부. true일 경우 구독상품false

설명

유니티에서 제공하는 IAP 서비스를 사용하지 않아도 제품 productID와 영수증 token을 알고 있으면 뒤끝 서버를 통해 영수증을 검증받을 수 있습니다.

Example

동기

BackendReturnObject validation = Backend.Receipt.IsValidateGooglePurchase(productID , receiptToken , "receiptDescription" , true);
if(validation.IsSuccess())
{
  // 영수증 검증 성공 시 처리
}
else
{
  // 영수증 검증 실패 시 처리
}

비동기

Backend.Receipt.IsValidateGooglePurchase( productID , receiptToken , "receiptDescription", false, ( callback ) => 
{
  if(callback.IsSuccess())
  {
    // 영수증 검증 성공 시 처리
  }
  else
  {
    // 영수증 검증 실패 시 처리
  }
});

SendQueue

SendQueue.Enqueue(Backend.Receipt.IsValidateGooglePurchase, productID , receiptToken , "receiptDescription", false, ( callback ) => 
{
  if(callback.IsSuccess())
  {
    // 영수증 검증 성공 시 처리
  }
  else
  {
    // 영수증 검증 실패 시 처리
  }
});

ReturnCase

Success cases

검증에 성공한 경우(일반상품)
statusCode : 201
message : Success
returnValue : {"usedDate":"2018-10-15T05:17:49Z"}

검증에 성공한 경우(구독 상품)
statusCode : 200
message : Success
returnValue : GetReturnValueToJSON 참조

Error cases

이미 사용한 영수증 토큰
statusCode : 409
errorCode : UsedReceipt
message : This receipt has already been used. usedDate: 2018-02-15T04:01:50.000Z

이미 사용하거나 취소된 구독 상품의 영수증을 검증한 경우
statusCode : 409
errorCode : DuplicatedParameterException
message : Duplicated receipt, 중복된 receipt 입니다

환불/취소 영수증
statusCode : 402
errorCode : AbnormalReceipt
message : This receipt has changed status. purchaseState: cancelled

유효하지 않은 영수증 토큰
statusCode : 400
errorCode : BadParameterException
message : bad token, 잘못된 token 입니다

JWT 권한의 확인이 필요합니다.
권한을 제대로 설정했는데도 bad token 에러가 발생할 경우 JWT를 재발급하고, 재발급 후에도 에러가 발생할 경우 JWT를 새로 생성한 후 다시 시도해야 합니다.
JWT 수정여부는 바로 적용이 안될 수 있어 1~2시간 후에 다시 시도하는 것을 권장합니다.

GetReturnValueToJSON(구독상품)

{
  "kind":"androidpublisher#subscriptionPurchase",
  "startTimeMillis":"1583722491833",
  "expiryTimeMillis":"1583722908247",
  "autoRenewing":true,
  "priceCurrencyCode":"KRW",
  "priceAmountMicros":"1000000000",
  "countryCode":"KR",
  "developerPayload":"{\"developerPayload\":\"\",\"is_free_trial\":false,\"has_introductory_price_trial\":false,\"is_updated\":false}",
  "paymentState":1,
  "orderId":"주문 Id",
  "purchaseType":0,
  "acknowledgementState":1
}