본문으로 건너뛰기
버전: 5.15.1

IsValidateGooglePurchase

public BackendReturnObject IsValidateGooglePurchase(string receipt, string receiptDescription);
public BackendReturnObject IsValidateGooglePurchase(string json, string receiptDescription, decimal iapPrice, string iapCurrency);
public BackendReturnObject IsValidateGooglePurchase(string receipt, string receiptDescription , bool isSubscription);
public BackendReturnObject IsValidateGooglePurchase(string receipt, string receiptDescription , bool isSubscription);
public BackendReturnObject IsValidateGooglePurchase(string json, string receiptDescription,bool isSubscription, decimal iapPrice, string iapCurrency);
public BackendReturnObject IsValidateGooglePurchase(string productId, string token, string receiptDescription, bool isSubscription, decimal iapPrice, string iapCurrency);

인앱상품 이름 및 설명에 특수문자 포함 시 에러 발생

구글 플레이 콘솔에서 설정하는 인앱 상품 > 상품 세부 정보의 이름과 설명에는 일부 특수문자 사용이 불가능합니다.
인앱을 생성할 때 이름과 설명에 다음과 같은 값이 포함되어있는지 확인해주세요.

사용 불가능한 특수문자

특수문자한글명
'작은 따옴표
\ 역슬래시

사용 가능한 특수문자

특수문자한글명
.마침표
,쉼표
"큰따옴표
()괄호
!느낌표
?물음표
~물결
@골뱅이
*곱하기
+더하기
-빼기
/슬래시

표기가 되지 않은 특수문자를 사용할 경우에는 영수증 검증이 정상적으로 진행되는지 확인 후 적용해주세요.

파라미터

ValueTypeDescriptiondefault
receiptstringPurchasing.PurchaseEventArgs.purchasedProduct.receipt-
receiptDescriptionstring추가로 저장하고자 하는 내용-
isSubscriptionbool해당 상품의 구독 상품 여부. true일 경우 구독 상품false

설명

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

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

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

금액 표시(선택 사항)

뒤끝 콘솔의 영수증 검증 항목에서 해당 구매 내역의 금액을 표시하고자 할 경우, 인자값 iapPrice와 iapCurrency를 추가해야합니다.

 private void GetCheckGoogleReceiptWithPrice(PurchaseEventArgs purchaseEvent)
{
string receiptToken = purchaseEvent.purchasedProduct.receipt;
var bro = Backend.Receipt.IsValidateGooglePurchase(
json: receiptToken,
receiptDescription: "구매 했습니다",
isSubscription: false,
iapPrice: purchaseEvent.purchasedProduct.metadata.localizedPrice,
iapCurrency: purchaseEvent.purchasedProduct.metadata.isoCurrencyCode);
}

Example

동기

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
string receiptJson = args.purchasedProduct.receipt;
/*
뒤끝 영수증 검증 처리
*/
BackendReturnObject validation = Backend.Receipt.IsValidateGooglePurchase(receiptJson, "receiptDescription", false);


// 금액을 콘솔에서 표시하고자 할 경우
decimal iapPrice = args.purchasedProduct.metadata.localizedPrice;
string iapCurrency = args.purchasedProduct.metadata.isoCurrencyCode;
BackendReturnObject validation = Backend.Receipt.IsValidateGooglePurchase(receiptJson, "receiptDescription", iapPrice, iapCurrency);

// 영수증 검증에 성공한 경우
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)
{
string receiptJson = args.purchasedProduct.receipt;
decimal iapPrice = args.purchasedProduct.metadata.localizedPrice; // 콘솔에서 금액을 표시하고자 할 경우, 입력(iapCurrency와 같이 입력해야합니다.)
string iapCurrency = args.purchasedProduct.metadata.isoCurrencyCode;

Backend.Receipt.IsValidateGooglePurchase(args.purchasedProduct.receipt, "receiptDescription", false, iapPrice, iapCurrency, (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 : 200
message : Success
returnValue : GetReturnValuetoJSON 참조

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

Error cases

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

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

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

이미 사용한 영수증 토큰
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 입니다

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 : 200
message : Success
returnValue : GetReturnValuetoJSON 참조

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

Error cases

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

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

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

이미 사용한 영수증 토큰
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 입니다

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
}