본문 바로가기

💻 내 소개 안녕하세요 엄청짱 프로그래머 손다빈 입니다.
  • 나이 : 96년생
  • 특이사항 : MZ세대, INFJ, 오른손잡이, 아이폰 유저
  • 좋아하는 음식 : 햄버거피자치킨솥뚜껑삼겹살떡볶이오튀김밥
  • 취미 : 개발, Programming, 코딩, 프로그래밍, Coding

🥷기술
Unity
Godot
Cpp
Javascript
D3
Vue

🐱 우리집 고양이 소개
츄르 먹은 후 츄르 먹기 전
  • 이름 : 콜라
  • 나이 : 8살
  • 종 : Nado moreum

📱 개인 프로젝트
🏢 참여한 프로젝트
빌런즈 Life is Pair 도씨어부키우기 직장상사혼내주기 서바이벌빙고 SlitherCoin

🌱 내 잔디밭

cocos2d-x | IAP [1] 아이템 구매하기 본문

글 묶음/거를때가 된 Cocos2d-x

cocos2d-x | IAP [1] 아이템 구매하기

초긍정 개발자 다빈맨 2019. 2. 14. 00:34

| Play Billing Library - Preview




구글에서 소개한 새로운 결제 라이브러리 Play Billing Library 를 사용해보기로 합니다.


그런데 현재 Play Billing Library 가 아직 preview 버전이라서 나중에 버전이 올라가면서 구현방식이 크게 바뀔 가능성이 있고 그렇게 되면 코드를 수정해야하는데 당연히 그 몫은 개발자의 몫입니다. 


현재 시점, 이미 preview 버전이 종료되고 정식으로 사용 가능한 라이브러리로 버전업이 되었습니다. 이 글을 작성했을 때 당시 preview 기준으로 코드를 작성했기 때문에 약간의 코드 수정이 필요할 수 있습니다. 아래 링크를 통해 수정사항을 확인하세요.

※ play billing library page URL




| 안드로이드 스튜디오에서의 작업 

먼저 프로젝트에서 빌링 라이브러리를 사용하기 위해 gradle 의존성에 라이브러리를 추가해야 합니다.

앱 수준에서의 build.gradledependencies 부분에 다음 내용을 추가합니다.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':libcocos2dx')
    compile 'com.android.billingclient:billing:dp-1' // 추가
}

이제 AppActivity.java 에 결제에 필요한 코드를 구현합니다.

package org.cocos2dx.cpp;

import android.os.Bundle;
import android.widget.Toast;

import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;

import org.cocos2dx.lib.Cocos2dxActivity;

import java.util.List;

public class AppActivity extends Cocos2dxActivity {
    public static AppActivity appActivity;
    private BillingClient mBillingClient;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        appActivity = this;

        initInAppBillingService();
    }

    @Override
    public void onDestroy() {
        mBillingClient.endConnection();
        super.onDestroy();
    }

    public void initInAppBillingService() {
        mBillingClient = new BillingClient.Builder(getContext()).setListener(new PurchasesUpdatedListener() {
            @Override
            public void onPurchasesUpdated(int responseCode, List purchases) {
                if (responseCode == BillingClient.BillingResponse.OK && purchases != null) {
                    //결제가 요청이 완료되면 호출되는 영역
                    for (Purchase purchase : purchases) {
                        //주문 정보 를 받아와서 처리하는 부분
                        String sku = purchase.getSku();
                        Toast.makeText(AppActivity.this, "주문한 상품 sku : "+sku, Toast.LENGTH_SHORT).show();

                        /*
                        다시 주문할 수 있도록 주문한 아이템의 소진을 요청한다.
                        만약, 한번 구매한 아이템을 다시 구매하지 못하게하려면 아래 consumeItem() 을 사용하지 않고
                        여기서 바로 nativeCallBackSuccessBuyItem 함수를 호출하면 된다.
                        */
                        consumeItem(sku);
                    }
                } else if (responseCode == BillingClient.BillingResponse.USER_CANCELED) {
                    // 유저가 도중에 결제를 취소할 경우 호출되는 영역
                    Toast.makeText(AppActivity.this, "결제를 취소하였습니다.", Toast.LENGTH_SHORT).show();
                } else {
                    // 예기치 못한 결제 에러가 발생 되면 호출되는 영역
                    Toast.makeText(AppActivity.this, "결제 실패하였습니다.", Toast.LENGTH_SHORT).show();
                }
            }
        }).build();

        //이 과정을 통해 구글 플레이 앱과 내 앱이 통신이 가능해진다.
        mBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(int billingResponseCode) {
                if (billingResponseCode == BillingClient.BillingResponse.OK) {
                    // 성공적으로 준비가 끝나면 호출되는 영역
                    Toast.makeText(AppActivity.this, "결제 클라이언트 연결 성공", Toast.LENGTH_SHORT).show();
                }
            }
            @Override
            public void onBillingServiceDisconnected() {
                // 구글 플레이 앱과 연결되지 못하면 호출되는 영역
                Toast.makeText(AppActivity.this, "결제 클라이언트 연결 실패", Toast.LENGTH_SHORT).show();
            }
        });
    }

    public void buyItem(String skuId) {
        BillingFlowParams.Builder builder = new BillingFlowParams.Builder()
                .setSku(skuId).setType(BillingClient.SkuType.INAPP); //결제 유형을 설정 INAPP 은 소모 아이템, SUBS 는 구독 아이템

        mBillingClient.launchBillingFlow(this, builder.build());
    }

    public void consumeItem(String skuId) {
        ConsumeResponseListener listener = new ConsumeResponseListener() {
            @Override
            public void onConsumeResponse(String outToken, int responseCode) {
                if (responseCode == BillingClient.BillingResponse.OK) {
                    //성공적으로 아이템을 소모요청이 되었으면 호출되는 영역
                    Toast.makeText(AppActivity.this, "outToken :"+outToken+"responseCode :"+responseCode, Toast.LENGTH_SHORT).show();
                }
            }
        };

        Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
        List purchases = purchasesResult.getPurchasesList();

        for (Purchase purchase : purchases) {
            if (purchase.getSku().compareTo(skuId) == 0) {
                //여기서 C++ 콜백 함수를 호출시킨다.
                nativeCallBackSuccessBuyItem(purchase.getSku());
                mBillingClient.consumeAsync(purchase.getPurchaseToken(), listener);
            }
        }
    }

    public static void Native_BuyItem(final String sku) {
        appActivity.runOnUiThread(new Runnable()  {
            @Override
            public void run() {
                    appActivity.buyItem(sku);
            }
        });
    }

    private static native void nativeCallBackSuccessBuyItem(String skuId);
}

위 코드를 이해하려면 JNI 에 대한 이해가 기본적으로 깔려 있어야 하는데 여기서는 안드로이드(자바) 문법이나 JNI 대한 이야기는 하지 않기 때문에 위 코드에 대한 이해가 어렵다면 다른 글을 찾아보세요!..




| C++ 에서의 구현


코코스2d-x 에서 사용하려면 C++ 상으로 코드 호출이 가능해야 합니다. 따라서 JNI를 사용해서 HelloWorldScene.h 에 다음과 같이 구현했습니다.

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"
#include "ui/CocosGUI.h"     //추가
using namespace cocos2d::ui; //Button 클래스를 사용하기 위해 헤더와 네임스페이스를 선언했다.

class HelloWorld : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();

    virtual bool init();

    // a selector callback
    void menuCloseCallback(cocos2d::Ref* pSender);
    
    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);

    // <-----아이템 구매 관련
    static void callBackSuccessBuyItem(std::string skuId);
    void buyItemCallBack(Ref* target, Widget::TouchEventType eventType);
    void requestBuyItem(const char* sku);
    //---->
};

#endif // __HELLOWORLD_SCENE_H__

위와 같이 아이템 구매 관련 함수를 선언합니다.


callBackSuccssBuyItem : 성공적으로 결제가 진행된 후 호출되는 콜백 함수로 사용합니다. (매개변수로 결제 성공한 skuId 를 전달 받습니다)

buyItemCallBack : 아이템 구매 버튼을 터치했을때 호출되는 콜백 함수로 사용합니다.

requestBuyItem : 인앱상품 결제를 진행하는 함수


HelloWorldScene.cpp 에서 다음과 같은 내용을 구현한다. init() 함수에 구매버튼으로 사용할 버튼을 하나 만들어 줍니다.

// on "init" you need to initialize your instance
bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }
    
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    /////////////////////////////
    // 2. add a menu item with "X" image, which is clicked to quit the program
    //    you may modify it.

    // add a "close" icon to exit the progress. it's an autorelease object
    auto closeItem = MenuItemImage::create(
                                           "CloseNormal.png",
                                           "CloseSelected.png",
                                           CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));
    
    closeItem->setPosition(Vec2(origin.x + visibleSize.width - closeItem->getContentSize().width/2 ,
                                origin.y + closeItem->getContentSize().height/2));

    // create menu, it's an autorelease object
    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);

    /////////////////////////////
    // 3. add your codes below...

    // add a label shows "Hello World"
    // create and initialize a label
    
    auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);
    
    // position the label on the center of the screen
    label->setPosition(Vec2(origin.x + visibleSize.width/2,
                            origin.y + visibleSize.height - label->getContentSize().height));

    // add the label as a child to this layer
    this->addChild(label, 1);

    // add "HelloWorld" splash screen"
    auto sprite = Sprite::create("HelloWorld.png");

    // position the sprite on the center of the screen
    sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));

    // add the sprite as a child to this layer
    this->addChild(sprite, 0);

    //구매 버튼 추가
    Button* buyButton = Button::create("ButtonBuy.png");
    buyButton->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
    buyButton->setPositionX(origin.x + visibleSize.width / 2);
    buyButton->setPositionY(origin.y + visibleSize.height / 2);
    buyButton->addTouchEventListener(CC_CALLBACK_2(HelloWorld::buyItemCallBack, this));
    addChild(buyButton);

    return true;
}

그리고 헤더에서 선언한 함수들과 jni 를 통해 호출받을 네이티브 함수를 구현합니다.


void HelloWorld::callBackSuccessBuyItem(std::string skuId)
{
	//결제 성공하면 들어오는 콜백
	Director::getInstance()->getRunningScene()->runAction(
		RepeatForever::create(
			RotateBy::create(15.0f, 360.0f)));
}

void HelloWorld::buyItemCallBack(Ref* target, Widget::TouchEventType eventType)
{
	if (eventType == Widget::TouchEventType::BEGAN)
	{
		requestBuyItem("sample_item_1");
	}
}

void HelloWorld::requestBuyItem(const char* sku)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
	JniMethodInfo mInfo;
	
	if (JniHelper::getStaticMethodInfo(mInfo
		, "org/cocos2dx/cpp/AppActivity"
		, "Native_BuyItem"
		, "(Ljava/lang/String;)V"))
	{
		jstring _sku = mInfo.env->NewStringUTF(sku);
		mInfo.env->CallStaticVoidMethod(mInfo.classID, mInfo.methodID, _sku);
		mInfo.env->DeleteLocalRef(mInfo.classID);
		mInfo.env->DeleteLocalRef(_sku);
	}
#endif
}

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)

extern "C"
{
	JNIEXPORT void JNICALL Java_org_cocos2dx_cpp_AppActivity_nativeCallBackSuccessBuyItem(JNIEnv* env, jobject thiz, jstring skuId)
	{
		const char *nativeString = env->GetStringUTFChars(skuId, nullptr);
		std::string _skuId = std::string(nativeString);
		env->ReleaseStringUTFChars(skuId, nativeString);

		HelloWorld::callBackSuccessBuyItem(_skuId.c_str());
	}
};

#endif 

위 코드에서는 결제에 대한 성공여부를 명확히 확인하기 위해서 결제가 성공할경우 씬이 빙글빙글 돌아가도록 했습니다(?)




| 결제 테스트



작업이 끝난 후 apk 를 뽑아서 실행하면 위와 같이 '요청하신 항목은 구매할 수 없습니다.' 라는 메세지와 함께 결제 진행이 되지 않네요..


작업이 끝난 APK 를 반드시 알파버전 이상으로 구글 개발자 콘솔에 업로드 되어 있어야 하기 때문이랍니다.



버전 관리 메뉴에 들어가서 테스트 참여 대상 관리 의 사용자 목록에 테스터 계정을 등록하면 됩니다.


테스트 참여 대상 관리 메뉴의 아래에 있는 URL 을 통해서 받으시 앱을 다운로드 받아야 그때부터 결제를 진행할 수 있습니다.


위와같이 테스트 주문이 진행됩니다.




결제가 성공적으로 진행되네요!




AppActivity.java

HelloWorldScene.cpp

HelloWorldScene.h