여기에서는 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_ANDROID ■ UNREAL_IOS ■ UNREAL_WINDOWS
void RequestPurchase(const FString& GamebaseProductId, const FGamebasePurchasableReceiptDelegate& Callback);
void RequestPurchase(const FString& GamebaseProductId, const FString& payload, const FGamebasePurchasableReceiptDelegate& Callback);
Example
void USample::RequestPurchase(const FString& GamebaseProductId)
{
UGamebaseSubsystem* Subsystem = UGameInstance::GetSubsystem<UGamebaseSubsystem>(GetGameInstance());
Subsystem->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 USample::RequestPurchaseWithPayload(const FString& GamebaseProductId)
{
FString UserPayload = TEXT("{\"description\":\"This is example\",\"channelId\":\"delta\",\"characterId\":\"abc\"}");
UGamebaseSubsystem* Subsystem = UGameInstance::GetSubsystem<UGamebaseSubsystem>(GetGameInstance());
Subsystem->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
struct FGamebasePurchasableReceipt
{
// 구매한 아이템의 상품 ID입니다.
FString GamebaseProductId;
// itemSeq 로 상품을 구매하는 Legacy API용 식별자입니다.
int64 ItemSeq;
// 구매한 상품의 가격입니다.
float Price;
// 통화 코드입니다.
FString Currency;
// 결제 식별자입니다.
// purchaseToken과 함께 'Consume' 서버 API를 호출하는 데 사용하는 중요한 정보입니다.
// Consume API : https://docs.toast.com/en/Game/Gamebase/en/api-guide/#purchase-iap
// 주의 : Consume API 는 게임 서버에서 호출하세요!
FString PaymentSeq;
// 결제 식별자입니다.
// paymentSeq 와 함께 'Consume' 서버 API를 호출하는데 사용하는 중요한 정보입니다.
// Consume API 에서는 'accessToken' 라는 이름의 파라메터로 전달해야 합니다.
// Consume API : https://docs.toast.com/en/Game/Gamebase/en/api-guide/#purchase-iap
// 주의 : Consume API 는 게임 서버에서 호출하세요!
FString PurchaseToken;
// Google, Apple 과 같이 스토어 콘솔에 등록된 상품 ID입니다.
FString MarketItemId;
// 상품 타입으로, 다음 값들이 올 수 있습니다.
// * UNKNOWN : 인식 불가능한 타입. Gamebase SDK 를 업데이트 하거나 Gamebase 고객 센터로 문의하세요.
// * CONSUMABLE : 소비성 상품.
// * AUTO_RENEWABLE : 구독형 상품.
// * CONSUMABLE_AUTO_RENEWABLE : 구독형 상품을 구매한 유저에게 정기적으로 소비가 가능한 상품을 지급하고자 하는 경우 사용되는 '소비가 가능한 구독 상품'.
FString ProductType;
// 상품을 구매했던 User ID.
// 상품을 구매하지 않은 User ID 로 로그인 한다면 구매한 아이템을 획득할 수 없습니다.
FString UserId;
// 스토어의 결제 식별자입니다.
FString PaymentId;
// 구독 상품은 갱신될 때마다 paymentId가 변경됩니다.
// 이 필드는 맨 처음 구독 상품을 결제 했을때의 paymentId 를 알려줍니다.
// 스토어에 따라, 결제 서버 상태에 따라 값이 존재하지 않을 수 있으므로
// 항상 유효한 값을 보장하지는 않습니다.
FString OriginalPaymentId;
// 상품을 구매했던 시각입니다.(epoch time)
int64 PurchaseTime;
// 구독이 종료되는 시각입니다.(epoch time)
int64 ExpiryTime;
// 결제한 스토어 코드입니다.
// GamebaseStoreCode 클래스에서 스토어 코드 목록을 확인할 수 있습니다.
FString StoreCode;
// RequestPurchase API 호출 시 payload로 전달했던 값입니다.
// 스토어 서버 상태에 따라 정보가 유실되는 경우가 있으므로 사용을 권장하지 않습니다.
FString Payload;
// 프로모션 결제 여부
// - (Android) Gamebase 결제 서버에서 일시적으로 검증 로직을 끄는 경우에는 false로만 출력되므로 유효한 값이 보장되지 않습니다.
bool bIsPromotion;
// 테스트 결제 여부
// - (Android) Gamebase 결제 서버에서 일시적으로 검증 로직을 끄는 경우에는 false로만 출력되므로 유효한 값이 보장되지 않습니다.
bool bIsTestPurchase;
};
아이템 목록을 조회하려면 다음 API를 호출합니다. 콜백으로 반환되는 목록 안에는 각 아이템들에 대한 정보가 있습니다.
API
Supported Platforms ■ UNREAL_ANDROID ■ UNREAL_IOS ■ UNREAL_WINDOWS
void RequestItemListPurchasable(const FGamebasePurchasableItemListDelegate& Callback);
Example
void USample::RequestItemListPurchasable()
{
UGamebaseSubsystem* Subsystem = UGameInstance::GetSubsystem<UGamebaseSubsystem>(GetGameInstance());
Subsystem->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
struct FGamebasePurchasableItem
{
// Gamebase 콘솔에 등록된 상품 ID입니다.
// Gamebase.Purchase.requestPurchase API로 상품을 구매할 때 사용됩니다.
FString GamebaseProductId;
// itemSeq 로 상품을 구매하는 Legacy API용 식별자입니다.
int64 ItemSeq;
// 상품의 가격입니다.
float Price;
// 통화 코드입니다.
FString Currency;
// Gamebase 콘솔에 등록된 상품 이름입니다.
FString ItemName;
// Google, Apple 과 같이 스토어 콘솔에 등록된 상품 ID입니다.
FString MarketItemId;
// 상품 타입으로, 다음 값들이 올 수 있습니다.
// * UNKNOWN : 인식 불가능한 타입. Gamebase SDK 를 업데이트 하거나 Gamebase 고객 센터로 문의하세요.
// * CONSUMABLE : 소비성 상품.
// * AUTORENEWABLE : 구독형 상품.
// * CONSUMABLE_AUTO_RENEWABLE : 구독형 상품을 구매한 유저에게 정기적으로 소비가 가능한 상품을 지급하고자 하는 경우 사용되는 '소비가 가능한 구독 상품'.
FString ProductType;
// 통화 기호가 포함된 현지화 된 가격 정보입니다.
FString LocalizedPrice;
// 스토어 콘솔에 등록된 현지화된 상품 이름입니다.
FString LocalizedTitle;
// 스토어 콘솔에 등록된 현지화된 상품 설명입니다.
FString LocalizedDescription;
// Gamebase 콘솔에서 해당 상품의 '사용 여부'를 나타냅니다.
bool bIsActive;
};
아이템을 구매했지만, 정상적으로 아이템이 소비(배송, 지급)되지 않은 미소비 결제 내역을 요청합니다. 미결제 내역이 있는 경우에는 게임 서버(아이템 서버)에 요청하여, 아이템을 배송(지급)하도록 처리해야 합니다.
FGamebasePurchasableConfiguration
API | Mandatory(M) / Optional(O) | Description |
---|---|---|
bAllStores | O | 동일한 UserID로 다른 스토어에서 구매한 미소비 내역도 반환합니다. 기본값은 false입니다. |
API
Supported Platforms ■ UNREAL_ANDROID ■ UNREAL_IOS ■ UNREAL_WINDOWS
void RequestItemListOfNotConsumed(const FGamebasePurchasableConfiguration& Configuration, const FGamebasePurchasableReceiptListDelegate& Callback);
Example
void USample::RequestItemListOfNotConsumed(bool bAllStores)
{
FGamebasePurchasableConfiguration Configuration;
Configuration.bAllStores = bAllStores;
UGamebaseSubsystem* Subsystem = UGameInstance::GetSubsystem<UGamebaseSubsystem>(GetGameInstance());
Subsystem->GetPurchase()->RequestItemListOfNotConsumed(Configuration, FGamebasePurchasableReceiptListDelegate::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 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("RequestItemListOfNotConsumed failed. (Error: %d)"), Error->Code);
}
}));
}
현재 사용자 ID 기준으로 활성화된 구독 목록을 조회합니다. 결제가 완료된 구독 상품(자동 갱신형 구독, 자동 갱신형 소비성 구독 상품)은 만료되기 전까지 계속 조회할 수 있습니다. 사용자 ID가 같다면 Android와 iOS에서 구매한 구독 상품이 모두 조회됩니다.
[주의]
Android에서는 Google Play 스토어에서만 현재 구독 상품을 지원합니다.
FGamebasePurchasableConfiguration
API | Mandatory(M) / Optional(O) | Description |
---|---|---|
bAllStores | O | 동일한 UserID로 다른 스토어에서 구매한 미소비 내역도 반환합니다. 기본값은 false입니다. |
API
Supported Platforms ■ UNREAL_ANDROID ■ UNREAL_IOS
void RequestActivatedPurchases(const FGamebasePurchasableConfiguration& Configuration, const FGamebasePurchasableReceiptListDelegate& Callback);
Example
void USample::RequestActivatedPurchases(bool bAllStores)
{
FGamebasePurchasableConfiguration Configuration;
Configuration.bAllStores = bAllStores;
UGamebaseSubsystem* Subsystem = UGameInstance::GetSubsystem<UGamebaseSubsystem>(GetGameInstance());
Subsystem->GetPurchase()->RequestActivatedPurchases(Configuration, 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);
}
}));
}
현재 사용자 ID 기준으로 구독 상품들의 상태를 조회합니다. 콜백으로 반환되는 목록 안에는 구독 상품들의 정보가 담겨 있습니다.
[주의]
- 아래 가이드에 따라 구독 이벤트를 설정해야 구독 상태 코드가 정상적으로 반환됩니다.
- Game > Gamebase > 스토어 콘솔 가이드 > Google 콘솔 가이드 > Google 시스템 내 실시간 구독 정보 이벤트 전파 설정
- 이벤트 설정을 하지 않은 상태에서 구매한 구독 상품의 상태 코드는 항상 0(PURCHASED)이 반환됩니다.
- 현재 구독 상품은 Google Play 스토어만 지원합니다.
FGamebasePurchasableConfiguration
API | Mandatory(M) / Optional(O) | Description |
---|---|---|
bIncludeExpiredSubscriptions | O | 만료된 구독 상품까지 포함하여 조회합니다. 기본값은 false입니다. |
API
Supported Platforms ■ UNREAL_ANDROID
void RequestSubscriptionsStatus(const FGamebasePurchasableConfiguration& Configuration, const FGamebasePurchasableSubscriptionStatusDelegate& Callback);
Example
void USample::RequestSubscriptionsStatus(bool bIncludeExpiredSubscriptions)
{
FGamebasePurchasableConfiguration Configuration;
Configuration.bAllStores = bAllStores;
UGamebaseSubsystem* Subsystem = UGameInstance::GetSubsystem<UGamebaseSubsystem>(GetGameInstance());
Subsystem->GetPurchase()->RequestSubscriptionsStatus(Configuration, FGamebasePurchasableSubscriptionStatusDelegate::CreateLambda(
[](const TArray<FGamebasePurchasableSubscriptionStatus>* purchasableReceiptList, const FGamebaseError* Error)
{
if (Gamebase::IsSuccess(Error))
{
UE_LOG(GamebaseTestResults, Display, TEXT("RequestSubscriptionsStatus succeeded."));
for (const FGamebasePurchasableSubscriptionStatus& 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("RequestSubscriptionsStatus failed. (Error: %d)"), Error->Code);
}
}));
}
VO
struct FGamebasePurchasableSubscriptionStatus
{
// 앱이 설치된 스토어에 대해 Gamebase에서 내부적으로 정의한 코드입니다.
FString StoreCode;
// 스토어의 결제 식별자입니다.
FString PaymentId;
// 구독 상품은 갱신될 때마다 paymentId가 변경됩니다.
// 이 필드는 구독 상품을 최초 결제했을 때의 paymentId를 알려줍니다.
// 스토어 및 결제 서버 상태에 따라 값이 존재하지 않을 수 있으므로
// 항상 유효한 값을 보장하지는 않습니다.
FString OriginalPaymentId;
// 결제 식별자입니다.
// purchaseToken과 함께 'Consume' 서버 API를 호출하는 데 사용하는 중요한 정보입니다.
//
// 주의: Consume API는 게임 서버에서 호출하세요! (https://docs.toast.com/en/Game/Gamebase/en/api-guide/#purchase-iap)
FString PaymentSeq;
// 구매한 상품의 상품 ID입니다.
FString MarketItemId;
// IAP 웹 콘솔의 항목 고유 식별자
int64 ItemSeq;
// 다음 값 중 하나를 가집니다.
// * UNKNOWN: 알 수 없는 유형입니다. Gamebase SDK를 업데이트하거나 Gamebase 고객 센터에 문의하세요.
// * CONSUMABLE: 소모품입니다.
// * AUTO_RENEWABLE: 구독 상품입니다.
FString ProductType;
// 상품을 구매한 사용자 아이디입니다.
// 상품 구매에 사용하지 않은 사용자 아이디로 로그인하면 구매한 상품을 받을 수 없습니다.
FString UserId;
// 상품의 가격입니다.
float Price;
// 통화 정보입니다.
FString Currency;
// Payment 식별자.
// paymentSeq로 'Consume' 서버 API를 호출하는 데 사용되는 중요한 정보입니다.
// Consume API에서 매개변수 이름을 'accessToken'으로 지정해야 전달됩니다.
// 참고: https://docs.toast.com/ko/Game/Gamebase/ko/api-guide/#purchase-iap
FString PurchaseToken;
// 상품을 구매한 시간.(epoch time)
int64 PurchaseTime;
// 구독이 만료되는 시간.(epoch time)
int64 ExpiryTime;
// RequestPurchase API 호출 시 페이로드에 전달되는 값입니다.
// 스토어 서버 상태에 따라 정보가 유실되는 경우가 있으므로 사용을 권장하지 않습니다.
FString Payload;
// 구독 상태
// 전체 상태 코드는 다음 문서를 참조하세요.
// - https://docs.nhncloud.com/en/TOAST/en/toast-sdk/iap-unity/#iapsubscriptionstatus
int32 StatusCode;
// 구독 상태에 대한 설명입니다.
FString StatusDescription;
// Gamebase 콘솔에 등록된 상품 ID입니다.
// RequestPurchase API로 상품을 구매할 때 사용됩니다.
FString GamebaseProductId;
// 이 값은 Google에서 구매할 때 사용되며 다음 값을 가질 수 있습니다.
// 단, Google 서버의 오류로 인해 Gamebase 결제 서버에서 일시적으로 인증 로직이 비활성화된 경우
// null만 반환하므로 항상 유효한 값을 보장하지 않을 수 있습니다.
// * null: 정상 결제
// * 테스트: 테스트 결제
// * 프로모션: 프로모션 결제
FString PurchaseType;
};
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->Messsage);
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->Messsage);
}
}