여기에서는 Unreal에서 인앱 결제 기능을 사용하기 위해 필요한 설정 방법을 알아보겠습니다. Gamebase는 하나의 통합된 결제 API를 제공해 게임에서 손쉽게 많은 스토어의 인앱 결제를 연동할 수 있도록 돕습니다.
Android나 iOS에서 인앱 결제 기능을 설정하는 방법은 다음 문서를 참고하시기 바랍니다.
[주의]
외부 플러그인에서 결제 관련 처리가 있는 경우, Gamebase 결제 기능이 정상적으로 동작하지 않을 수 있습니다.
Unreal에서 기본으로 활성화 되어있는 Online SubSystem 플러그인을 비활성화 혹은 스토어 기능을 이용하지 못하도록 변경해야 합니다.
Online SubSystem GooglePlay 플러그인 사용 시 /Config/Android/AndroidEngine.ini 파일을 편집합니다.
[OnlineSubsystemGooglePlay.Store]
bSupportsInAppPurchasing=False
bUseGooglePlayBillingApiV2=False
Online SubSystem iOS 플러그인 사용 시 /Config/IOS/IOSEngine.ini 파일을 편집합니다.
[OnlineSubsystemIOS.Store]
bSupportsInAppPurchasing=False
아이템 구매는 크게 결제 Flow 와 Consume Flow, 재처리 Flow 로 나누어 볼 수 있습니다. 결제 Flow는 다음과 같은 순서로 구현하시기 바랍니다.
미소비 결제 내역 목록에 값이 있으면 다음과 같은 순서로 Consume Flow 를 진행하시기 바랍니다.
[주의]
아이템이 중복 지급되는 일이 발생하지 않도록, 게임 서버에서 반드시 중복 지급 여부를 체크하시기 바랍니다.
구매하고자 하는 아이템의 gamebaseProductId를 이용해 다음의 API를 호출하여 구매를 요청합니다. 게임 유저가 구매를 취소하는 경우 PURCHASE_USER_CANCELED 오류가 반환됩니다.
API
Supported Platforms ■ UNREAL_IOS ■ UNREAL_ANDROID
void RequestPurchase(const FString& gamebaseProductId, const FGamebasePurchasableReceiptDelegate& onCallback);
void RequestPurchase(const FString& gamebaseProductId, const FString& payload, const FGamebasePurchasableReceiptDelegate& onCallback);
Example
void Sample::RequestPurchase(const FString& gamebaseProductId)
{
IGamebase::Get().GetPurchase().RequestPurchase(gamebaseProductId, FGamebasePurchasableReceiptDelegate::CreateLambda(
[](const FGamebasePurchasableReceipt* purchasableReceipt, const FGamebaseError* error)
{
if (Gamebase::IsSuccess(error))
{
UE_LOG(GamebaseTestResults, Display, TEXT("RequestPurchase succeeded. (gamebaseProductId= %s, price= %f, currency= %s, paymentSeq= %s, purchaseToken= %s)"),
*purchasableReceipt->gamebaseProductId, purchasableReceipt->price, *purchasableReceipt->currency,
*purchasableReceipt->paymentSeq, *purchasableReceipt->purchaseToken);
}
else
{
if (error->code == GamebaseErrorCode::PURCHASE_USER_CANCELED)
{
UE_LOG(GamebaseTestResults, Display, TEXT("User canceled purchase."));
}
else
{
// Check the error code and handle the error appropriately.
UE_LOG(GamebaseTestResults, Display, TEXT("RequestPurchase failed. (error: %d)"), error->code);
}
}
}));
}
void Sample::RequestPurchaseWithPayload(const FString& gamebaseProductId)
{
FString userPayload = TEXT("{\"description\":\"This is example\",\"channelId\":\"delta\",\"characterId\":\"abc\"}");
IGamebase::Get().GetPurchase().RequestPurchase(gamebaseProductId, userPayload, FGamebasePurchasableReceiptDelegate::CreateLambda(
[](const FGamebasePurchasableReceipt* purchasableReceipt, const FGamebaseError* error)
{
if (Gamebase::IsSuccess(error))
{
UE_LOG(GamebaseTestResults, Display, TEXT("RequestPurchase succeeded. (gamebaseProductId= %s, price= %f, currency= %s, paymentSeq= %s, purchaseToken= %s)"),
*purchasableReceipt->gamebaseProductId, purchasableReceipt->price, *purchasableReceipt->currency,
*purchasableReceipt->paymentSeq, *purchasableReceipt->purchaseToken);
FString payload = purchasableReceipt->payload;
}
else
{
if (error->code == GamebaseErrorCode::PURCHASE_USER_CANCELED)
{
UE_LOG(GamebaseTestResults, Display, TEXT("User canceled purchase."));
}
else
{
// Check the error code and handle the error appropriately.
UE_LOG(GamebaseTestResults, Display, TEXT("RequestPurchase failed. (error: %d)"), error->code);
}
}
}));
}
VO
USTRUCT()
struct FGamebasePurchasableReceipt
{
GENERATED_USTRUCT_BODY()
// 구매한 아이템의 상품 ID입니다.
UPROPERTY()
FString gamebaseProductId;
// itemSeq 로 상품을 구매하는 Legacy API용 식별자입니다.
UPROPERTY()
int64 itemSeq;
// 구매한 상품의 가격입니다.
UPROPERTY()
float price;
// 통화 코드입니다.
UPROPERTY()
FString currency;
// 결제 식별자입니다.
// purchaseToken 과 함께 'Consume' 서버 API 를 호출하는데 사용하는 중요한 정보입니다.
// Consume API : https://docs.toast.com/en/Game/Gamebase/en/api-guide/#purchase-iap
// 주의 : Consume API 는 게임 서버에서 호출하세요!
UPROPERTY()
FString paymentSeq;
// 결제 식별자입니다.
// paymentSeq 와 함께 'Consume' 서버 API 를 호출하는데 사용하는 중요한 정보입니다.
// Consume API 에서는 'accessToken' 라는 이름의 파라메터로 전달해야 합니다.
// Consume API : https://docs.toast.com/en/Game/Gamebase/en/api-guide/#purchase-iap
// 주의 : Consume API 는 게임 서버에서 호출하세요!
UPROPERTY()
FString purchaseToken;
// Google, Apple 과 같이 스토어 콘솔에 등록된 상품 ID입니다.
UPROPERTY()
FString marketItemId;
// 상품 타입으로, 다음 값들이 올 수 있습니다.
// * UNKNOWN : 인식 불가능한 타입. Gamebase SDK 를 업데이트 하거나 Gamebase 고객 센터로 문의하세요.
// * CONSUMABLE : 소비성 상품.
// * AUTO_RENEWABLE : 구독형 상품.
// * CONSUMABLE_AUTO_RENEWABLE : 구독형 상품을 구매한 유저에게 정기적으로 소비가 가능한 상품을 지급하고자 하는 경우 사용되는 '소비가 가능한 구독 상품'.
UPROPERTY()
FString productType;
// 상품을 구매했던 User ID.
// 상품을 구매하지 않은 User ID 로 로그인 한다면 구매한 아이템을 획득할 수 없습니다.
UPROPERTY()
FString userId;
// 스토어의 결제 식별자입니다.
UPROPERTY()
FString paymentId;
// 구독 상품은 갱신 될때마다 paymentId가 변경됩니다.
// 이 필드는 맨 처음 구독 상품을 결제 했을때의 paymentId 를 알려줍니다.
// 스토어에 따라, 결제 서버 상태에 따라 값이 존재하지 않을 수 있으므로
// 항상 유효한 값을 보장하지는 않습니다.
UPROPERTY()
FString originalPaymentId;
// 상품을 구매했던 시각입니다.(epoch time)
UPROPERTY()
int64 purchaseTime;
// 구독이 종료되는 시각입니다.(epoch time)
UPROPERTY()
int64 expiryTime;
// Gamebase.Purchase.requestPurchase API 호출시 payload 로 전달했던 값입니다.
//
// 이 필드는 예를 들어 동일한 User ID 로 구매 했음에도 게임 채널, 캐릭터 등에 따라
// 상품 구매 및 지급을 구분하고자 하는 경우 등
// 게임에서 필요로 하는 다양한 추가 정보를 담기 위한 목적으로 활용할 수 있습니다.
UPROPERTY()
FString payload;
// 프로모션 결제 여부
// - (Android) Gamebase 결제 서버에서 일시적으로 검증 로직을 끄는 경우에는 false로만 출력되므로 유효한 값이 보장되지 않습니다.
UPROPERTY()
bool isPromotion;
// 테스트 결제 여부
// - (Android) Gamebase 결제 서버에서 일시적으로 검증 로직을 끄는 경우에는 false로만 출력되므로 유효한 값이 보장되지 않습니다.
UPROPERTY()
bool isTestPurchase;
};
아이템 목록을 조회하려면 다음 API를 호출합니다. 콜백으로 반환되는 목록 안에는 각 아이템들에 대한 정보가 있습니다.
API
Supported Platforms ■ UNREAL_IOS ■ UNREAL_ANDROID
void RequestItemListPurchasable(const FGamebasePurchasableItemListDelegate& onCallback);
Example
void Sample::RequestItemListPurchasable()
{
IGamebase::Get().GetPurchase().RequestItemListPurchasable(FGamebasePurchasableItemListDelegate::CreateLambda(
[](const TArray<FGamebasePurchasableItem>* purchasableItemList, const FGamebaseError* error)
{
if (Gamebase::IsSuccess(error))
{
UE_LOG(GamebaseTestResults, Display, TEXT("RequestItemListPurchasable succeeded."));
for (const FGamebasePurchasableItem& purchasableItem : *purchasableItemList)
{
UE_LOG(GamebaseTestResults, Display, TEXT(" - gamebaseProductId= %s, price= %f, itemName= %s, itemName= %s, marketItemId= %s"),
*purchasableItem.gamebaseProductId, purchasableItem.price, *purchasableItem.currency, *purchasableItem.itemName, *purchasableItem.marketItemId);
}
}
else
{
UE_LOG(GamebaseTestResults, Display, TEXT("RequestItemListPurchasable failed. (error: %d)"), error->code);
}
}));
}
VO
USTRUCT()
struct FGamebasePurchasableItem
{
GENERATED_USTRUCT_BODY()
// Gamebase 콘솔에 등록된 상품 ID입니다.
// Gamebase.Purchase.requestPurchase API 로 상품을 구매할 때 사용됩니다.
UPROPERTY()
FString gamebaseProductId;
// itemSeq 로 상품을 구매하는 Legacy API용 식별자입니다.
UPROPERTY()
int64 itemSeq;
// 상품의 가격입니다.
UPROPERTY()
float price;
// 통화 코드입니다.
UPROPERTY()
FString currency;
// Gamebase 콘솔에 등록된 상품 이름입니다.
UPROPERTY()
FString itemName;
// Google, Apple 과 같이 스토어 콘솔에 등록된 상품 ID입니다.
UPROPERTY()
FString marketItemId;
// 상품 타입으로, 다음 값들이 올 수 있습니다.
// * UNKNOWN : 인식 불가능한 타입. Gamebase SDK 를 업데이트 하거나 Gamebase 고객 센터로 문의하세요.
// * CONSUMABLE : 소비성 상품.
// * AUTORENEWABLE : 구독형 상품.
// * CONSUMABLE_AUTO_RENEWABLE : 구독형 상품을 구매한 유저에게 정기적으로 소비가 가능한 상품을 지급하고자 하는 경우 사용되는 '소비가 가능한 구독 상품'.
UPROPERTY()
FString productType;
// 통화 기호가 포함된 현지화 된 가격 정보입니다.
UPROPERTY()
FString localizedPrice;
// 스토어 콘솔에 등록된 현지화된 상품 이름입니다.
UPROPERTY()
FString localizedTitle;
// 스토어 콘솔에 등록된 현지화된 상품 설명입니다.
UPROPERTY()
FString localizedDescription;
// Gamebase 콘솔에서 해당 상품의 '사용 여부'를 나타냅니다.
UPROPERTY()
bool isActive;
};
아이템을 구매했지만, 정상적으로 아이템이 소비(배송, 지급)되지 않은 미소비 결제 내역을 요청합니다. 미결제 내역이 있는 경우에는 게임 서버(아이템 서버)에 요청하여, 아이템을 배송(지급)하도록 처리해야 합니다.
API
Supported Platforms ■ UNREAL_IOS ■ UNREAL_ANDROID
void RequestItemListOfNotConsumed(const FGamebasePurchasableReceiptListDelegate& onCallback);
Example
void Sample::RequestItemListOfNotConsumed()
{
IGamebase::Get().GetPurchase().RequestItemListOfNotConsumed(FGamebasePurchasableItemListDelegate::CreateLambda(
[](const TArray<FGamebasePurchasableItem>* purchasableItemList, const FGamebaseError* error)
{
if (Gamebase::IsSuccess(error))
{
// Should Deal With This non-consumed Items.
// Send this item list to the game(item) server for consuming item.
UE_LOG(GamebaseTestResults, Display, TEXT("RequestItemListOfNotConsumed succeeded."));
for (const FGamebasePurchasableItem& purchasableItem : *purchasableItemList)
{
UE_LOG(GamebaseTestResults, Display, TEXT(" - gamebaseProductId= %s, price= %f, itemName= %s, itemName= %s, marketItemId= %s"),
*purchasableReceipt.gamebaseProductId, purchasableItem.price, *purchasableItem.currency, *purchasableItem.itemName, *purchasableItem.marketItemId);
}
}
else
{
UE_LOG(GamebaseTestResults, Display, TEXT("RequestItemListOfNotConsumed failed. (error: %d)"), error->code);
}
}));
}
현재 사용자 ID 기준으로 활성화된 구독 목록을 조회합니다. 결제가 완료된 구독 상품(자동 갱신형 구독, 자동 갱신형 소비성 구독 상품)은 만료되기 전까지 계속 조회할 수 있습니다. 사용자 ID가 같다면 Android와 iOS에서 구매한 구독 상품이 모두 조회됩니다.
[주의]
Android에서는 Google Play 스토어에서만 현재 구독 상품을 지원합니다.
API
Supported Platforms ■ UNREAL_IOS ■ UNREAL_ANDROID
void RequestActivatedPurchases(const FGamebasePurchasableReceiptListDelegate& onCallback);
Example
void Sample::RequestActivatedPurchases()
{
IGamebase::Get().GetPurchase().RequestActivatedPurchases(FGamebasePurchasableReceiptListDelegate::CreateLambda(
[](const TArray<FGamebasePurchasableReceipt>* purchasableReceiptList, const FGamebaseError* error)
{
if (Gamebase::IsSuccess(error))
{
UE_LOG(GamebaseTestResults, Display, TEXT("RequestActivatedPurchases succeeded."));
for (const FGamebasePurchasableReceipt& purchasableReceipt : *purchasableReceiptList)
{
UE_LOG(GamebaseTestResults, Display, TEXT(" - gamebaseProductId= %s, price= %f, currency= %s, paymentSeq= %s, purchaseToken= %s"),
*purchasableReceipt.gamebaseProductId, purchasableReceipt.price, *purchasableReceipt.currency, *purchasableReceipt.paymentSeq, *purchasableReceipt.purchaseToken);
}
}
else
{
UE_LOG(GamebaseTestResults, Display, TEXT("RequestActivatedPurchases failed. (error: %d)"), error->code);
}
}));
}
Error | Error Code | Description |
---|---|---|
PURCHASE_NOT_INITIALIZED | 4001 | Purchase 모듈이 초기화되지 않았습니다. gamebase-adapter-purchase-IAP 모듈을 프로젝트에 추가했는지 확인해 주세요. |
PURCHASE_USER_CANCELED | 4002 | 게임 유저가 아이템 구매를 취소하였습니다. |
PURCHASE_NOT_FINISHED_PREVIOUS_PURCHASING | 4003 | 구매 로직이 아직 완료되지 않은 상태에서 API가 호출되었습니다. |
PURCHASE_NOT_ENOUGH_CASH | 4004 | 해당 스토어의 캐시가 부족해 결제할 수 없습니다. |
PURCHASE_INACTIVE_PRODUCT_ID | 4005 | 해당 상품이 활성화 상태가 아닙니다. |
PURCHASE_NOT_EXIST_PRODUCT_ID | 4006 | 존재하지 않는 GamebaseProductID 로 결제를 요청하였습니다. |
PURCHASE_LIMIT_EXCEEDED | 4007 | 월 구매 한도를 초과했습니다. |
PURCHASE_NOT_SUPPORTED_MARKET | 4010 | 지원하지 않는 스토어입니다. 선택 가능한 스토어는 AS(App Store), GG(Google), ONESTORE, GALAXY입니다. |
PURCHASE_EXTERNAL_LIBRARY_ERROR | 4201 | NHN Cloud IAP 라이브러리 오류입니다. 상세 오류를 확인하십시오. |
PURCHASE_UNKNOWN_ERROR | 4999 | 정의되지 않은 구매 오류입니다. 전체 로그를 고객 센터에 올려 주시면 가능한 한 빠르게 답변 드리겠습니다. |
PURCHASE_EXTERNAL_LIBRARY_ERROR
GamebaseError* gamebaseError = error; // GamebaseError object via callback
if (Gamebase::IsSuccess(error))
{
// succeeded
}
else
{
UE_LOG(GamebaseTestResults, Display, TEXT("code: %d, message: %s"), error->code, *error->message);
GamebaseInnerError* moduleError = gamebaseError.error; // GamebaseError.error object from external module
if (moduleError.code != GamebaseErrorCode::SUCCESS)
{
UE_LOG(GamebaseTestResults, Display, TEXT("moduleErrorCode: %d, moduleErrorMessage: %s"), moduleError->code, *moduleError->message);
}
}