아래에는 now.gg 결제 모듈을 더 쉽게 이해하고 구현하기 위한 공식 now.gg 샘플 앱 및 게임 내 다양한 코드 샘플
이 포함되어 있습니다.
1. now.gg 결제 모듈 파일의 압축을 풀고 패키지에서 .aar 파일을 찾습니다.
Payments.aar
2. 게임의 build.gradle
파일에 다음 종속성(dependency)을 추가합니다.
dependencies { implementation fileTree(dir: 'libs', include: ['*.aar']) }
now.gg 결제 모듈 진행 방식:
now.gg 결제 모듈은 구매 토큰과 주문 ID를 사용하여 게임이나 앱 내 구매를 추적합니다.
Entitlement 또는 권한이라는 용어는 구매 후 유저에게 ‘인앱 상품 전달’한다는 의미입니다.
SERVICE_DISCONNECTED
오류 코드는 now.gg 결제 서비스와의 연결이 끊어짐을 뜻하며 이에 대한 해결책으로는 앱/게임을 다시 초기화하고 now.gg 결제 서비스와 다시 연결하는 것입니다.BillingClient 인터페이스는 now.gg 결제 모듈과 앱/게임 간의 기본 통신 메커니즘으로 일반 결제 청구 작업 관련 동기식 및 비동기식 방법을 모두 제공합니다.
BillingClient 생성 --> Use newBuilder() 구매 메시지 리스너 설정 --> Call setListener() 앱/게임 내 구매 관련 메시지 처리 --> PurchasesUpdatedListener 빌링을 위한 PAYMENT_ID 설정 --> use setAppId(PAYMENT_ID) 기존 인게임 유저 ID 사용 시 IN_GAME_ID 설정 --> use setInGameId(IN_GAME_ID) 초기화 후 미소비 구매 조회. --> use queryPurchasesAsync()
다음 샘플 코드와 같이 BillingClient
를 생성하고 초기화하는 동안 사용자의 고유 userID(IN_GAME_ID
)를 now.gg 결제 SDK에 전달합니다.
private PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() { @Override public void onPurchasesUpdated(int billingResult, List<Purchase> purchases) { if (billingResult == BillingResponse.OK) { if (purchases == null) { return; } for (Purchase purchase: purchases) { String payload = purchase.getDeveloperPayload(); // 개발자 페이로드 가져오기 // 여기에서 성공적인 구매 처리 } } else if (billingResult == BillingResponse.USER_CANCELED) { Log.i(TAG, "onPurchasesUpdated() - user cancelled the purchase flow - skipping"); } else { Log.w(TAG, "onPurchasesUpdated() got unknown billingResult: " + billingResult); } } }; private BillingClient billingClient = BillingClient.newBuilder(activity) .setListener(purchasesUpdatedListener) .setAppId(PAYMENT_ID) .setInGameId(IN_GAME_ID) // unique ID to identify the user. .build();
private val purchasesUpdatedListener = object : PurchasesUpdatedListener { override fun onPurchasesUpdated(billingResult: Int, purchases: List<Purchase>?) { if (billingResult == BillingResponse.OK) { if (purchases == null) { return } for (purchase in purchases) { val payload = purchase.developerPayload // 개발자 페이로드 가져오기 // 여기에서 성공적인 구매 처리 } } else if (billingResult == BillingResponse.USER_CANCELED) { Log.i(TAG, "onPurchasesUpdated() - user cancelled the purchase flow - skipping") } else { Log.w(TAG, "onPurchasesUpdated() got unknown billingResult: $billingResult") } } } private val billingClient = BillingClient.newBuilder(activity) .setListener(purchasesUpdatedListener) .setAppId(PAYMENT_ID) .setInGameId(IN_GAME_ID) // unique ID to identify the user. .build()
IN_GAME_ID
는 유저를 식별하기 위해 사용됩니다.
IN_GAME_ID
는 각 유저마다 고유해야 합니다.
IN_GAME_ID
는 고유해야 합니다.IN_GAME_ID
를 사용하여 결제를 테스트하려면 nowStudio의 내부 테스터로 추가하십시오.PAYMENT_ID
는 앱을 식별하기 위해 사용됩니다.
now.gg 결제 서비스는 비동기식(asynchronous) 연결 프로세스를 채택하며 클라이언트 설정 완료 시 콜백 메시지를 처리하기 위해 BillingClientStateListener
를 사용합니다.
startConnection()
을 호출합니다.onBillingServiceDisconnected()
콜백 함수를 오버라이딩해야 합니다.onBillingServiceDisconnected()
오버라이딩 중 BillingClient가 startConnection()
를 호출하여 now.gg 결제 서비스와의 연결을 다시 설정하는지 확인합니다.queryPurchasesAsync()
메서드를 사용하여 미소비된 구매를 조회해야 하며, 이를 통해 사용자의 이전 게임 세션에서 미소비된 구매가 올바르게 할당되도록 해야 합니다.
중요: 재연결 로직을 직접 작성하시거나 아래 예시를 사용하실 수도 있습니다.
예
billingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(int billingResult) { if (billingResult == 0) { // BillingClient가 준비되었습니다. 이제 여기서 구매 관련 처리를 하실 수 있습니다. QueryPurchasesParams params = QueryPurchasesParams.newBuilder().setProductType(BillingClient.SkuType.ALL).build(); billingClient.queryPurchasesAsync(params, new PurchasesResponseListener() { @Override public void onQueryPurchasesResponse(BillingResult billingResult, List < Purchase > list) { // 구매한 항목을 사용자에게 할당하고 소비/확인을 호출하십시오. } }); } } @Override public void onBillingServiceDisconnected() { // startConnection()를 호출하여 이후 결제 모듈 연결 요청을 하실 수 있습니다. } });
billingClient.startConnection(object: BillingClientStateListener { override fun onBillingSetupFinished(billingResult: int) { if (billingResult == 0) { // BillingClient가 준비되었습니다. 이제 여기서 구매 관련 처리를 하실 수 있습니다. val params = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.SkuType.ALL) .build() billingClient.queryPurchasesAsync(params, object: PurchasesResponseListener { override fun onQueryPurchasesResponse(billingResult: BillingResult, list: List) { // 구매한 항목을 사용자에게 할당하고 소비/확인을 호출하십시오. } }) } } override fun onBillingServiceDisconnected() { // startConnection()를 호출하여 이후 결제 모듈 연결 요청을 하실 수 있습니다. } });
앱/게임에서 사용 가능한 인앱 상품을 확인 및 호출하고 구매를 처리하는 과정입니다.
단계:
요구 사항:
첫 단계는 사용 가능한 상품을 호출하고 유저에게 표시하는 것으로 인앱 상품 세부정보를 얻기 위해서는 querySkuDetailsAsync()
를 호출하게 되며 이때 SKU 세부 정보 호출 시 now.gg 결제 모듈은 유저에게 표시될 현지화된 상품 정보를 반환합니다.
중요: 인앱 상품을 유저에게 표시하기 전 업데이트된 상품 정보를 반환하는 SKU 세부 정보를 호출합니다.
SKU 유형 및 주문 문자열
SKUType
:
querySkuDetailsAsync()
호출 시 nowStudio에서 설정한 상품 ID와 함께 SKUType
을 가지는 SkuDetailsParams
인스턴스를 전달합니다.
결과
사용 가능한 상품을 호출한 후 결과를 처리하기 위해 리스너를 할당합니다.
다음 예는 SkuDetailsResponseListener
인터페이스를 통해 onSkuDetailsResponse()
를 오버라이딩하여 호출이 완료되면 리스너에서 결과를 처리합니다.
List<String> skuList = new ArrayList<> (); skuList.add("premium_upgrade"); skuList.add("gas"); SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); params.setSkusList(skuList).setType(SkuType.ALL); billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() { @Override public void onSkuDetailsResponse(int billingResult, List<SkuDetails> skuDetailsList) { // 결과를 처리합니다. } });
fun querySkuDetails() { val skuList = ArrayList<String>() skuList.add("premium_upgrade") skuList.add("gas") val params = SkuDetailsParams.newBuilder() params.setSkusList(skuList).setType(SkuType.ALL) withContext(Dispatchers.IO) { billingClient.querySkuDetailsAsync(params.build()) { billingResult, skuDetailsList -> // 결과를 처리합니다. } } }
SkuDetails
를 다양한 방식으로 호출하여 인앱 상품에 대한 정보를 호출할 수 있습니다.SkuDetails
에 저장합니다.유저가 구매할 수 있는 상품을 호출한 후에는 앱/게임에서 초기화를 시작합니다.
초기화를 시작하기 위해 앱의 메인 스레드에서 launchBillingFlow()
를 호출합니다.
아래 BillingFlowParams는 SkuDetails
에 저장된 관련 세부 정보를 포함하므로 참조 목적으로 호출되며 이때 SkuDetails는 querySkuDetailsAsync()
를 호출하여 가져옵니다.
먼저 아래와 같이 BillingFlowParams.Builder 클래스를 사용하여 BillingFlowParams
객체를 생성합니다.
// 빌링 초기화 시 참조 목적 Activity activity = ...; // querySkuDetailsAsync()를 호출하여 skuDetails 값을 호출합니다. BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) .setDeveloperPayload("optional developer payload") .build(); billingClient.launchBillingFlow(activity, billingFlowParams); // 결과
// 빌링 초기화 시 참조 목적 val activity : Activity = ...; // querySkuDetailsAsync()를 호출하여 skuDetails 값을 호출합니다. val flowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) .setDeveloperPayload("optional developer payload") .build() billingClient.launchBillingFlow(activity, flowParams) // 결과
BillingClient.BillingResponse를 사용해 반환된 메시지 코드를 처리하실 수 있습니다.
0
)는 성공적인 실행을 나타냅니다.launchBillingFlow()
호출이 성공적으로 완료되면 구매 화면이 표시됩니다.onPurchasesUpdated()
를 호출하여 구매 결과를 PurchasesUpdatedListener
인터페이스에 연결된 리스너에 전달합니다.setListener()
를 통해 빌링 클라이언트를 초기화하는 동안 설정됩니다.다음은 onPurchasesUpdated()
오버라이딩 예시 입니다:
@Override void onPurchasesUpdated(int billingResult, List<Purchase>) { if (billingResult == 0 && purchases != null) { for (Purchase purchase : purchases) { String payload = purchase.getDeveloperPayload(); // 개발자 페이로드 가져오기 handlePurchase(purchase); } } else if (billingResult == 1) { // 오류 처리 - 유저가 구매 화면 닫음 } else { // 오류 처리 - 기타 (에러 코드 참조) } }
override fun onPurchasesUpdated(billingResult: int, purchases: List<Purchase>?) { if (billingResult == 0 && purchases != null) { for (purchase in purchases) { val payload: String = purchase.developerPayload // 개발자 페이로드 가져오기 handlePurchase(purchase) } } else if (billingResult == 1) { // 오류 처리 - 유저가 구매 화면 닫음 } else { // 오류 처리 - 기타 (에러 코드 참조) } }
중요: onPurchasesUpdated()
를 통해 기타 응답 코드를 처리하실 수 있습니다.
구매 시작 후에는 다음과 같은 방식으로 구매를 처리하실 수 있습니다.
인증 방식:
구매를 확인하는 첫 번째 방법은 verifyPurchase API를 사용하는 것입니다. 이 API를 사용하여 우리의 백엔드 서버에서 구매를 확인할 수 있습니다.
구매를 확인하려면, 앱 백엔드 서버에서 purchaseToken
을 사용하여 verifyPurchase API를 호출하십시오. 다음 샘플 요청 코드에 설명되어 있습니다:
curl --location 'https://cloud-api.bluestacks.cn/v2/seller/order/verifyPurchase' \ --header 'Authorization: <payment_api_key_here>' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'purchaseToken=<nowgg_purchase_token>'
import requests url = "https://cloud-api.bluestacks.cn/v2/seller/order/verifyPurchase" payload = 'purchaseToken=<nowgg_purchase_token>' headers = { 'Authorization': '<payment_api_key_here>', 'Content-Type': 'application/x-www-form-urlencoded' } response = requests.request("POST", url, headers=headers, data=payload) print(response.text)
구매를 인증하는 두 번째 방법은 공개 키를 사용하는 것으로 데모의 billingManager.java
에 자세히 설명되어 있습니다.
verifyValidSignature
및 verifyPurchase
를 확인합니다.데모의 billingManager.java
내 위에서 언급한 함수 및 샘플 코드를 참조하여 앱에서 구매 인증을 하실 수 있습니다.
중요:
공개 키
를 생성하실 수 있습니다.구매를 확인한 후, 앱에서 사용자가 구매한 제품의 권한을 부여하고 제품을 소비된 것으로 표시(소모품의 경우)하거나 승인된 것으로 표시(구독의 경우)할 수 있습니다.
구매를 인증한 후 구매한 상품에 대한 권한을 유저에게 부여하고 해당 제품 전달 완료 표시를 할 수 있습니다.
구매를 승인하는 데 사용할 수 있는 두 가지 방법을 제공했습니다:
권한을 부여하기 위해 consumeAsync()
를 호출하여 유저에게 상품에 대한 권한을 부여하고 소비/전달된 후 재구매가 가능하도록 합니다.
사용 consumeAsync():
consumed
(소비됨) 으로 전환 시 now.gg 결제 서비스에서 재구매가 가능하도록 설정됩니다.ConsumeResponseListener
인터페이스와 연결된 객체를 전달합니다.onConsumeResponse()
를 호출합니다.ConsumeResponseListener
인터페이스를 업데이트하는 객체가 업데이트되지 않는 경우 발생할 수 있습니다.다음 샘플 코드는 구매 인증 및 소비 작업을 간단히 보여드립니다.
void handlePurchase(Purchase purchase) { // BillingClient#queryPurchasesAsync 또는 PurchasesUpdatedListener로부터 구매 호출 Purchase purchase = ...; // 구매를 인증합니다. // 해당 purchaseToken에 대한 권한이 이미 부여되지 않았는지 확인합니다. // 유저에게 권한을 부여합니다. ConsumeResponseListener listener = new ConsumeResponseListener() { @Override public void onConsumeResponse(int billingResult, String purchaseToken) { if (billingResult == 0) { // 여기서 소비 작업 성공을 처리합니다. } } }; billingClient.consumeAsync(purchase.getPurchaseToken(), listener); }
fun handlePurchase(purchase: Purchase) { // BillingClient#queryPurchasesAsync 또는 PurchasesUpdatedListener로부터 구매 호출 val purchase: Purchase = ...; // 구매를 인증합니다. // 해당 purchaseToken에 대한 권한이 이미 부여되지 않았는지 확인합니다. // 유저에게 권한을 부여합니다. billingClient.consumeAsync(purchase.getPurchaseToken(), { billingResult, outToken - > if (billingResult == 0) { // 여기서 소비 작업 성공을 처리합니다. } }) }
백엔드 서버가 있는 앱의 서버측 구매 소비 시 consumePurchase
API를 사용하시기 바랍니다.
구매를 인증한 후에는 소비됨으로 표시해야 합니다.
구매 소비에는 다음이 포함됩니다.
다음 샘플 코드는 consumePurchase
API를 통한 관련 요청 방법을 보여드립니다
import requests url = "https://cloud-api.bluestacks.cn/v2/order/consumePurchase" payload = 'purchaseToken=nowgg-_purchase_token' headers = { 'Authorization': 'payment_api_key_here', 'Content-Type': 'application/x-www-form-urlencoded' } response = requests.request("POST", url, headers=headers, data=payload) print(response.text)
Reference
구매를 인정하는 두 가지 방법을 제공했습니다.
참고: 백엔드 서버가 없는 클라이언트 전용 앱인 경우 이 방법을 사용해야 합니다.
방법:
acknowledgePurchase()
메서드를 호출합니다.
acknowledgePurchase()
에 구매 토큰을 포함하여 acknowledgePurchase()를 호출하여 구매한 제품의 상태를 acknowledged
로 업데이트합니다. 이로 인해 now.gg 결제 서비스가 재구매를 가능하게 합니다.onAcknowledgePurchaseResponse()
를 호출합니다. 이 메서드는 사용자 정의가 가능합니다.중요한 정보:
AcknowledgePurchaseResponseListener
인터페이스를 업데이트하지 않은 경우 이와 같은 상황이 발생할 수 있습니다.다음 코드 스니펫은 구매 확인 및 인정 작업을 보여줍니다:
void handlePurchase(Purchase purchase) { // BillingClient#queryPurchasesAsync 또는 PurchasesUpdatedListener로부터 구매 호출 Purchase purchase = ...; // 구매를 인증합니다. // 해당 purchaseToken에 대한 권한이 이미 부여되지 않았는지 확인합니다. // 유저에게 권한을 부여합니다. // 구매 확인 final AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() { @Override public void onAcknowledgePurchaseResponse(@BillingResponse int responseCode) { if (responseCode == 0) { // Handle the success of the acknowledgement operation. } } }; AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchaseToken) .build(); BillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener); }
fun handlePurchase(purchase: Purchase) { // BillingClient#queryPurchasesAsync 또는 PurchasesUpdatedListener로부터 구매 호출 val purchase: Purchase = ...; // 구매를 인증합니다. // 해당 purchaseToken에 대한 권한이 이미 부여되지 않았는지 확인합니다. // 유저에게 권한을 부여합니다. // 구매 확인 val acknowledgePurchaseResponseListener = object : AcknowledgePurchaseResponseListener { override fun onAcknowledgePurchaseResponse(@BillingResponse responseCode: Int) { if (responseCode == 0) { // Handle the success of the acknowledgement operation. } } val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchaseToken) .build() BillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener) }
acknowledgePurchase
API는 백엔드 서버를 가진 앱의 서버 측 구매 인정에 사용해야 합니다.
구매를 확인한 후에는 반드시 인정 처리해야 합니다.
다음 샘플 코드는 acknowledgePurchase
API를 사용한 관련 요청을 설명합니다.
import requests url = "https://cloud-api.bluestacks.cn/v2/seller/order/acknowledgepurchase" payload = 'purchaseToken=nowgg-_purchase_token' headers = { 'Authorization': 'payment_api_key_here', 'Content-Type': 'application/x-www-form-urlencoded' } response = requests.request("POST", url, headers=headers, data=payload) print(response.text)
참조:
PurchasesUpdatedListener
로 구매 결과를 처리하실 수 있지만 일부 상황 속 유저가 성공적으로 구매한 모든 항목이 앱/게임에 바로 업데이트되지 않을 수도 있습니다.
일부 상황:
PurchasesUpdatedListener
로부터 성공적인 구매 알림을 받지 못할 시해결 방법:
onResume()
및 onCreate()
에서 앱이 BillingClient.queryPurchasesAsync(SkuType.ALL)
를 호출하는지 확인합니다.다음 섹션에서는 미소비된인앱 구매 및 구독을 확인하고 할당하는 과정을 설명합니다.
아래와 같이 queryPurchasesAsync를 사용하여 유저의 사용되지 않은 구매 목록을 호출하실 수 있습니다.
QueryPurchasesParams params = QueryPurchasesParams.newBuilder().setProductType(BillingClient.SkuType.ALL).build(); billingClient.queryPurchasesAsync(params, new PurchasesResponseListener() { @Override public void onQueryPurchasesResponse(BillingResult billingResult, List < Purchase > list) { // 구매한 항목을 사용자에게 할당하고 소비/확인을 호출하십시오. } });
val params = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.SkuType.ALL) .build() billingClient.queryPurchasesAsync(params, object : PurchasesResponseListener { override fun onQueryPurchasesResponse(billingResult: BillingResult, list: List) { // 구매한 항목을 사용자에게 할당하고 소비/확인을 호출하십시오. } })
queryPurchasesAsync()
메서드를 사용하여 미소비된 구매를 조회해야 하며, 이를 통해 사용자의 이전 게임 세션에서 미소비된 구매가 올바르게 할당되도록 해야 합니다.
구독 상태 업데이트에 대한 알림을 보내기 위해 SubscriptionStatusCallback
API를 제공하는 데 필요한 API입니다.
다음은 관련 워크플로입니다:
중요: Subscription Status Callback API.
목차
목차
문서 Rev. 1.0