서비스 플랫폼 결제 검증

Google Play 나 Apple AppStore 등의 결제 플랫폼을 통해 In-App Purchase를 구현하는 과정은 1) 게임 클라이언트에서 해당 플랫폼의 SDK를 이용하여 결제를 한 뒤 상품정보, 결제 영수증을 받아오고, 2) 이를 서버로 전달하여 영수증의 유효성 검증과 이미 사용된 영수증인지의 확인 과정을 거치는 과정이 필요합니다.

그 때문에 다양한 플랫폼을 모두 지원하는 것은 게임 서버 개발자에게 상당히 버거운 일입니다. 아이펀 엔진은 여러 결제 플랫폼의 영수증 검증 기능을 포함하고 있습니다.

iFun Biller

아이펀 엔진은 결제 검증을 전담하는 agent 로서 iFun Biller 라는 프로그램을 사용합니다. 게임 서버는 iFun Biller 에 결제 검증 요청을 전달하고, iFun Biller 가 실제 플랫폼별 검증 과정을 거치고, 성공한 결제 검증을 사용자 정보와 함께 기록하게 됩니다.

Note

아이펀 엔진이 별도의 결제 검증 agent 를 사용하는 것은 게임 서버에 영향을 주지 않고 새로운 결제의 추가나 변경을 가능하게 하기 위해서입니다.

또한 agent 를 이용하면 게임 서버가 아닌 다른 서버 역시 agent 의 REST API 를 통해 결제 관련된 서비스를 이용할 수 있어 편리합니다.

Important

1.0.0-2466 보다 상위 버전 빌러에서는 기존 애플 영수증 테이블이 아닌 새로운 영수증 테이블을 사용합니다. 기존 테이블에서 새로운 테이블에 영수증 검증 데이터를 옮기기 위해서 아래 명령어를 입력합니다.

# mysql에 로그인합니다.
$ mysql -u {{mysql_id}} -p {{mysql_db_name}}

# 로그인 후 쿼리를 입력합니다.
> INSERT IGNORE INTO tb_receipt_apple_appstore2 SELECT * FROM tb_receipt_apple_appstore;

original_transaction_id 가 겹치거나 존재하지 않는 경우 해당 데이터는 새로운 테이블로 옮겨가지 못할 수 있습니다.

Important

1.0.0-3219 experimental1.0.0-4322 stable 버전 부터는 기존 구글 플레이 영수증 테이블이 아닌 새로운 테이블을 사용하도록 변경되었습니다.

기존 데이터가 쌓여있는 tb_receipt_google_play 테이블을 백업하신 후에 삭제하시거나 아래 명령어를 사용해서 새로운 테이블로 이전 해 주시기 바랍니다.

주의: 쌓여있는 데이터가 많은 경우 작업에 오랜 시간이 소요될 수 있습니다.

# 쿼리 스크립트를 다운로드 합니다.
$ wget https://ifunfactory.com/engine/support/ifun-biller-migrate-db-scheme-for-google-api-v3.sql

# 스크립트를 실행합니다.
$ mysql -u {{mysql_id}} -p {{mysql_db_name}} < ifun-biller-migrate-db-scheme-for-google-api-v3.sql

지원 플랫폼

현재 다음과 같은 외부 결제 플랫폼을 지원합니다. (지원 가능한 플랫폼은 지속적으로 추가될 예정입니다.)

  • Google Play

  • Apple AppStore

  • OneStore(SK TStore)

설치 방법

Tip

iFun Biller 설정 방법 (MANIFEST.json)use_billerfalse 로 설정하면 게임 서버는 모든 결제 검증 요청을 통과하는 것으로 가정하는 테스트 모드로 동작합니다. 실제 검증이 필요없는 개발 초기단계에 유용한 설정이며, 이 경우에는 iFun Biller 를 설치할 필요가 없습니다.

Ubuntu 인 경우

$ sudo apt-get update
$ sudo apt-get install funapi-biller1

CentOS 인 경우

$ sudo yum install funapi-biller1
$ sudo systemctl enable funapi-biller

실행 방법

Ubuntu 16.04 이상 혹은 CentOS 7 이상인 경우

다음 명령을 실행합니다.

$ sudo systemctl enable funapi-biller
$ sudo systemctl start funapi-biller

동작 확인

Ubuntu 16.04 이상 혹은 CentOS 7 이상인 경우

$ sudo systemctl status funapi-biller

Log 파일

/var/log/funapi/funapi-biller/ 에 로그를 생성합니다.

iFun Biller 설정 방법 (MANIFEST.json)

Note

이 항목은 iFun Biller 자체의 설정에 대한 내용입니다. iFun Biller 를 이용하는 게임 서버의 설정은 게임 서버 측 결제 검증 설정 파라미터 을 참고하세요.

iFun Biller 역시 아이펀 엔진으로 작성된 프로그램입니다. 따라서 iFun Biller 역시 MANIFEST.json 을 통해 설정값을 바꿀 수 있습니다. iFun Biller 의 MANIFEST.json 은 /usr/share/funapi-biller/default/manifests/MANIFEST.json 에 있습니다.

다음과 같은 설정 값을 지정할 수 있습니다.

  • protobuf_listen_port: 아이펀 엔진 게임 서버가 iFun Biller 와 통신하는 TCP port 번호를 지정합니다. (type=uint16, default=12810)

  • http_listen_port: REST API 로 iFun Biller 와 통신하기 위한 HTTP port 번호를 지정합니다. (type=uint16, default=12811)

  • bypass: 결제 검증을 무조건 통과시킬지 여부입니다. iFun Biller 를 테스트 모드로 동작시킵니다. 아이펀 엔진 게임 서버에서 use_biller=false 로 지정한 것과 같은 효과를 냅니다. (type=bool, default=false)

  • mysql_server_url: 사용한 영수증 정보를 저장한 DB 서버의 IP 주소와 포트를 입력합니다. (type=string, default=”tcp://127.0.0.1:3306”)

  • mysql_id: 사용한 영수증 정보를 저장할 DB 의 유저 ID 를 입력합니다.(type=string, default=”funapibiller1”)

  • mysql_pw: 사용한 영수증 정보를 저장할 DB 의 유저 비밀번호를 입력합니다. (type=string, default=”qlffj1!!”)

  • mysql_db_name: 사용할 영수증 정보를 저장할 DB 의 이름을 입력합니다. (type=string, default=”funapi_biller1”)

  • mysql_db_connection_count: iFun Biller 가 MySQL Server 과 통신하기 위해 사용하는 connection pool size 입니다. (type=uint16, default=1)

  • mysql_db_character_set: 사용할 character set 을 지정합니다. 기본값은 utf8 입니다. (type=string, default=”utf8”)

  • biller_use_db_auto_schema_generation: iFun Biller 가 구동될 때, DB 테이블과 프로시져가 없을 경우 자동 생성할지 여부. (type=bool, default=true)

  • export_db_schema_to_file: 이 항목에 파일 경로가 주어지고 biller_use_db_auto_schema_generation: false 일 경우, 해당 경로에 iFun Biller 에서 필요한 DB schema 를 생성하고 iFun Biller 를 종료함. (type=string, default=””)

  • biller_use_google_play_developer_api_v3: 구글 플레이 결제 검증 시 버전 3 API 사용 여부 설정. (type=bool, default=false)

  • biller_use_one_store_test_server: 원스토어 결제 검증 시 원스토어 테스트용 호스트 사용 여부 설정. (type=bool, default=false)

Warning

  • 2019년 12월 이후로 버전 3 이전 버전의 Google Play Developer API 지원이 종료됩니다. 이전 버전 API 의 지원이 종료되면 biller_use_google_play_developer_api_v3 옵션이 사라지고 항상 버전 3 API 을 사용하도록 수정됩니다.

Tip

에이전트를 업데이트하게 되면 기존 MANIFEST.json 은 덮어씌여지게 됩니다. 이를 방지하기 위해서는 MANIFEST.json 오버라이드하기 에 언급된대로 override 파일을 사용할 수 있습니다.

/etc/funapi-biller/MANIFEST.override.json:

{
  "override": {
    "FunapiBillerServer": {
      "mysql_server_url": "tcp://10.10.10.10:36060",
      "mysql_id": "biller",
      "mysql_pw": "biller",
      ...
    },
    ...
  }
}

결제 검증

아이펀 엔진이 제공하는 함수를 이용하는 방법과(이 경우 TCP 로 통신하게 됨) 독립적으로 RESTful API를 이용하는 방법이 있습니다.

아이펀 엔진에서 함수 호출하는 방식

아이펀 엔진은 플랫폼 종류와 상관없이 같은 결제 검증 인터페이스를 사용할 수 있으며 동기 방식과 비동기 방식으로 제공됩니다. 각 함수 내용은 다음과 같습니다.

typedef function<void(const ReceiptValidationRequest &request,
                      const ReceiptValidationResponse &response,
                      const bool &error)> BillingResponseHandler

typedef function<void(const ReceiptValidationRequest &request,
                      const ReceiptValidationResponse &response,
                      const std::vector<string> &transaction_ids,
                      const bool &error)> BillingResponseHandler2


void ValidateReceipt(const ReceiptValidationRequest &request,
                     const BillingResponseHandler &handler)
void ValidateReceipt2(const ReceiptValidationRequest &request,
                      const BillingResponseHandler2 &handler)

bool ValidateReceiptSync(const ReceiptValidationRequest &request,
                         ReceiptValidationResponse *response)
bool ValidateReceiptSync2(const ReceiptValidationRequest &request,
                          ReceiptValidationResponse *response,
                          std::vector<string> *transaction_ids)

이 때 ReceiptValidationRequest 는 다음과 struct 로 생각하시면 됩니다.

ReceiptValidationRequest(const string &authentication_provider,
                         const string &authentication_id,
                         const Receipt &receipt)

Important

ReceiptValidationRequest 의 첫번째와 두번째 인자가 결제가 아닌 사용자 ID 인증과 관련된 내용임에 유의해주세요. 이는 iFun Biller 가 결제 검증시 사용자 정보를 같이 기록하기 때문입니다.

그리고 플랫폼 종류별로 Receipt 을 생성하는 방식이 달라집니다. 이를 위해 플랫폼별로 다음의 유틸리티 함수가 제공됩니다.

Receipt MakeGooglePlayReceipt(const string &package_name,
                              const string &product_id,
                              const string &purchase_token)

Receipt MakeAppleAppStoreReceipt(const string &receipt_data,
                                 const string &product_id,
                                 int64_t quantity)

Receipt MakeOneStoreReceipt(const string &txid,
                            const string &appid,
                            const string &signdata,
                            const std::vector< string > &products,
                            bool use_one_store_test_server)

Receipt MakeOneStoreReceiptV5(const string &purchase_id,
                              const string &package_name,
                              const string &product_id)

Tip

보다 자세한 내용은 API 문서 를 참고하세요.

구글 플레이 결제 검증

여기서는 Facebook 사용자가 게임을 하다가 결제를 하는 경우 해당 결제 영수증 내용을 검증하고, 결제한 Facebook 사용자 정보를 기록하는 것을 동기 방식, 비동기 방식 두 가지로 살펴보겠습니다.

동기화 방식
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void example(const string &facebook_id,
             const string &google_package_name, const string &google_product_id,
             const string &google_purchase_token) {

  Receipt receipt = MakeGooglePlayReceipt(google_package_name, google_product_id,
      google_purchase_token);

  ReceiptValidationRequest request("Facebook", facebook_id, receipt);
  ReceiptValidationResponse response;

  if (not ValidateReceiptSync(request, &response)) {
    LOG(ERROR) << "billing system error";
    return;
  }

  if (response == kSuccess) {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (response == kFailWrongReceipt) {
    LOG(WARNING) << "Maybe forged receipt";
  } else if (response == kFailAlreadyProvisioned) {
    LOG(WARNING) << "Already provisioned";
  } else {
    LOG(WARNING) << "Receipt validation error: " << response;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void Example(
    string facebook_id,
    string google_package_name,
    string google_product_id,
    string google_purchase_token)
{
  string receipt = Billing.MakeGooglePlayReceipt (
      google_package_name,  google_product_id, google_purchase_token);

  Billing.ReceiptValidationRequest req =
      new Billing.ReceiptValidationRequest (
          "Facebook", facebook_id, receipt);

  Billing.ReceiptValidationResponse rep;

  if (!Billing.ValidateReceiptSync (req, out rep))
  {
    Log.Info ("billing system error");
    return;
  }

  if (rep == Billing.ReceiptValidationResponse.kSuccess)
  {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (rep == Billing.ReceiptValidationResponse.kFailWrongReceipt) {
    Log.Warning ("Maybe forged receipt");
  } else if (rep == Billing.ReceiptValidationResponse.kFailAlreadyProvisioned) {
    Log.Warning ("Already provisioned");
  } else {
    Log.Warning ("Receipt validation error: {0}", rep);
  }
}
비동기 방식

비동기 방식은 동기화 방식과 유사하지만, ValidateReceiptSync() 대신 ValidateReceipt() 를 사용합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void OnReceiptValidated(
    const ReceiptValidationRequest &request,
    const ReceiptValidationResponse &response,
    const bool &error) {
  if (error) {
    LOG(ERROR) << "billing system error";
    return;
  }

  if (response == kSuccess) {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (response == kFailWrongReceipt) {
    LOG(WARNING) << "Maybe forged receipt";
  } else if (response == kFailAlreadyProvisioned) {
    LOG(WARNING) << "Already provisioned";
  } else {
    LOG(WARNING) << "Receipt validation error: " << response;
  }
}


void example(const string &facebook_id,
             const string &google_package_name, const string &google_product_id,
             const string &google_purchase_token) {
  Receipt receipt = MakeGooglePlayReceipt(
      google_package_name, google_product_id, google_purchase_token);

  ReceiptValidationRequest request("Facebook", facebook_id, receipt);
  BillingResponseHandler callback = OnReceiptValidated;

  ValidateReceipt(request, callback);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
void OnReceiptValidated(
    Billing.ReceiptValidationRequest request,
    Billing.ReceiptValidationResponse response,
    bool error)
{
  if (error)
  {
    Log.Info ("billing system error");
    return;
  }

  if (response == Billing.ReceiptValidationResponse.kSuccess)
  {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (response == Billing.ReceiptValidationResponse.kFailWrongReceipt)
  {
    Log.Warning ("Maybe forged receipt");
  }
  else if (response == Billing.ReceiptValidationResponse.kFailAlreadyProvisioned)
  {
    Log.Warning ("Already provisioned");
  }
  else
  {
    Log.Warning ("Receipt validation error: {0}", response);
  }
}


void Example(
    string facebook_id,
    string google_package_name,
    string google_product_id,
    string google_purchase_token)
{
  string receipt = Billing.MakeGooglePlayReceipt (
      google_package_name, google_product_id, google_purchase_token);

  Billing.ReceiptValidationRequest req =
      new Billing.ReceiptValidationRequest (
          "Facebook", facebook_id, receipt);

  Billing.ValidateReceipt (req, (
      Billing.ReceiptValidationRequest request,
      Billing.ReceiptValidationResponse response,
      bool error) => {
    OnReceiptValidated (request, response, error);
  });

  // 아래와 같이 delegate를 사용하여 콜백 함수를 등록 할 수도 있습니다.
  // Billing.ResponseHandler handler = delegate (
  //     Billing.ReceiptValidationRequest request,
  //     Billing.ReceiptValidationResponse response,
  //     bool error) {
  //    ...
  // };
  // Billing.ValidateReceipt (req, handler);
}

애플 앱스토어 결제 검증

여기서는 Facebook 사용자가 게임을 하다가 결제를 하는 경우 해당 결제 영수증 내용을 검증하고, 결제한 Facebook 사용자 정보를 기록하는 것을 동기 방식, 비동기 방식 두 가지로 살펴보겠습니다.

동기화 방식
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void example(const string &facebook_id,
             const string &apple_receipt_data, const string &apple_product_id,
             int quantity) {
  Receipt receipt = MakeAppleAppStoreReceipt(
      apple_receipt_data, apple_product_id, quantity);

  ReceiptValidationRequest request("Facebook", facebook_id, receipt);
  ReceiptValidationResponse response;

  if (not ValidateReceiptSync(request, &response)) {
    LOG(ERROR) << "billing system error";
    return;
  }

  if (response == kSuccess) {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (response == kFailWrongReceipt) {
    LOG(WARNING) << "wrong receipt";
  } else if (response == kFailAlreadyProvisioned) {
    LOG(WARNING) << "already provisioned";
  } else {
    LOG(WARNING) << "receipt validation error: " << response;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void Example(
    string facebook_id,
    string apple_receipt_data,
    string apple_product_id,
    int quantity)
{
  string receipt = Billing.MakeAppleAppStoreReceipt (
      apple_receipt_data,  apple_product_id, quantity);

  Billing.ReceiptValidationRequest req =
      new Billing.ReceiptValidationRequest (
          "Facebook", facebook_id, receipt);

  Billing.ReceiptValidationResponse rep;

  if (!Billing.ValidateReceiptSync (req, out rep))
  {
    Log.Info ("billing system error");
    return;
  }

  if (rep == Billing.ReceiptValidationResponse.kSuccess)
  {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (rep == Billing.ReceiptValidationResponse.kFailWrongReceipt) {
    Log.Warning ("wrong receipt");
  } else if (rep == Billing.ReceiptValidationResponse.kFailAlreadyProvisioned) {
    Log.Warning ("already provisioned");
  } else {
    Log.Warning ("receipt validation error: {0}", rep);
  }
}
비동기 방식

비동기 방식은 동기화 방식과 유사하지만, ValidateReceiptSync() 대신 ValidateReceipt() 를 사용합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void OnReceiptValidated(
    const ReceiptValidationRequest &request,
    const ReceiptValidationResponse &response,
    const bool &error) {
  if (error) {
    LOG(ERROR) << "billing system error";
    return;
  }

  if (response == kSuccess) {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (response == kFailWrongReceipt) {
    LOG(WARNING) << "wrong receipt";
  } else if (response == kFailAlreadyProvisioned) {
    LOG(WARNING) << "already provisioned";
  } else {
    LOG(WARNING) << "receipt validation error: " << response;
  }
}


void example(const string &facebook_id,
             const string &apple_receipt_data, const string &apple_product_id,
             int quantity) {
  Receipt receipt = MakeAppleAppStoreReceipt(
      apple_receipt_data, apple_product_id, quantity);

  ReceiptValidationRequest request("Facebook", facebook_id, receipt);

  BillingResponseHandler callback = bind(&OnReceiptValidated, _1, _2, _3);
  ValidateReceipt(request, callback);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
void OnReceiptValidated(
    Billing.ReceiptValidationRequest request,
    Billing.ReceiptValidationResponse response,
    bool error)
{
  if (error)
  {
    Log.Info ("billing system error");
    return;
  }

  if (response == Billing.ReceiptValidationResponse.kSuccess)
  {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (response == Billing.ReceiptValidationResponse.kFailWrongReceipt)
  {
    Log.Warning ("wrong receipt");
  }
  else if (response == Billing.ReceiptValidationResponse.kFailAlreadyProvisioned)
  {
    Log.Warning ("already provisioned");
  }
  else
  {
    Log.Warning ("receipt validation error: {0}", response);
  }
}


void Example(
    string facebook_id,
    string apple_receipt_data,
    string apple_product_id,
    int quantity)
{
  string receipt = Billing.MakeAppleAppStoreReceipt (
      google_package_name, google_product_id, google_purchase_token);

  Billing.ReceiptValidationRequest req =
      new Billing.ReceiptValidationRequest (
          "Facebook", facebook_id, receipt);

  Billing.ValidateReceipt (req, (
      Billing.ReceiptValidationRequest request,
      Billing.ReceiptValidationResponse response,
      bool error) => {
    OnReceiptValidated (request, response, error);
  });

  // 아래와 같이 delegate를 사용하여 콜백 함수를 등록 할 수도 있습니다.
  // Billing.ResponseHandler handler = delegate (
  //     Billing.ReceiptValidationRequest request,
  //     Billing.ReceiptValidationResponse response,
  //     bool error) {
  //    ...
  // };
  // Billing.ValidateReceipt (req, handler);
}

OneStore(TStore) 결제 검증

여기서는 Facebook 사용자가 게임을 하다가 결제를 하는 경우 해당 결제 영수증 내용을 검증하고, 결제한 Facebook 사용자 정보를 기록하는 것을 동기 방식, 비동기 방식 두 가지로 살펴보겠습니다.

동기화 방식
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void example(const string &facebook_id,
             const string &one_store_purchase_id, const string &one_store_package_name,
             const string &one_store_product_id) {
  Receipt receipt = MakeOneStoreReceiptV5(
      one_store_purchase_id, one_store_package_name, one_store_product_id);

  // SDK V16(API V4) 영수증 검증 시에는 MakeOneStoreReceipt() 함수로 영수증을 생성해 검증해야 합니다.
  // Receipt receipt = MakeOneStoreReceipt(one_store_txid, one_store_appid, one_store_signdata,
  //                                       products);

  ReceiptValidationRequest request("Facebook", facebook_id, receipt);
  ReceiptValidationResponse response;

  if (not ValidateReceiptSync(request, &response)) {
    LOG(ERROR) << "billing system error";
    return;
  }

  if (response == kSuccess) {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (response == kFailWrongReceipt) {
    LOG(WARNING) << "wrong receipt";
  } else if (response == kFailAlreadyProvisioned) {
    LOG(WARNING) << "already provisioned";
  } else {
    LOG(WARNING) << "receipt validation error: " << response;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public void Example(string facebook_id,
                    string one_store_purchase_id, string one_store_package_name,
                    string one_store_product_id)
{
  string receipt = Billing.MakeOneStoreReceiptV5 (
      one_store_purchase_id,  one_store_package_name, one_store_product_id);

  // SDK V16(API V4) 영수증 검증 시에는 MakeOneStoreReceipt() 함수로 영수증을 생성해 검증해야 합니다.
  // string receipt = Billing.MakeOneStoreReceipt (
  //     one_store_txid,  one_store_appid, one_store_signdata, products, true);

  Billing.ReceiptValidationRequest req =
      new Billing.ReceiptValidationRequest (
          "Facebook", facebook_id, receipt);

  Billing.ReceiptValidationResponse rep;

  if (!Billing.ValidateReceiptSync (req, out rep))
  {
    Log.Info ("billing system error");
    return;
  }

  if (rep == Billing.ReceiptValidationResponse.kSuccess)
  {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (rep == Billing.ReceiptValidationResponse.kFailWrongReceipt) {
    Log.Warning ("wrong receipt");
  } else if (rep == Billing.ReceiptValidationResponse.kFailAlreadyProvisioned) {
    Log.Warning ("already provisioned");
  } else {
    Log.Warning ("receipt validation error: {0}", rep);
  }
}
비동기 방식

비동기 방식은 Google Play, Apple AppStore 의 설명과 위 동기 방식의 설명을 참고해서 같은 방식으로 작성하시면 됩니다.

REST API 를 이용하는 방식

아이펀 엔진을 이용하지 않고 iFun Biller 에게 직접 REST API 를 요청할 수 있습니다. 아래 명세에 따라 각 parameter 들을 JSON format 으로 HTTP POST 의 body 에 담아 요청을 하면 됩니다.

플랫폼 연결 초기화

POST /v1/authentication
Request JSON Object
  • biller_client_id (string) – 현재 버전에서는 인증을 하지 않기 때문에 적당한 값을 입력합니다.

  • (GooglePlay 서비스 계정을 이용하는 경우) google_play_private_key (string) – GooglePlay service account private key

  • (GooglePlay 서비스 계정을 이용하는 경우) google_play_client_email (string) – GooglePlay service account client email

  • (GooglePlay 인 경우) google_play_client_id (string) – GooglePlay client id

  • (GooglePlay 인 경우) google_play_client_secret (string) – GooglePlay client secret

  • (GooglePlay 인 경우) google_play_refresh_token (string) – GooglePlay refresh token

  • (OneStore 인 경우) one_store_client_id (string) – OneStore client id

  • (OneStore 인 경우) one_store_client_secret (string) – OneStore client_secret

Response JSON Object
  • result (integer) –

    • 0: 성공

    • 2: 패러미터 오류

    • 그 외: 기타 오류

    • 2: 이미 인증된 세션

    • 3: 올바르지 않은 id

    • 4: 올바르지 않은 인증키

    • 5: 올바르지 않은 서비스 프로바이더

    • 6: 올바르지 않은 서비스 프로바이더(google) 인증 파라미터

    • 2000: 파라미터 오류

    • 나머지: 기타 오류

  • description (string) – 오류 설명

  • sessionid (string) – session id, 영수증 검증 요청을 할 때 포함해서 보냅니다.

Google Play 영수증 검증

POST /v1/validation/googleplay
Request JSON Object
  • sessionid (string) – 인증을 통하여 얻은 session id

  • player_id (string) – 빈 문자열 또는 biller database 에 저장할 player id

  • player_service_provider (string) – 빈 문자열 또는 biller database 에 저장할 player service provider

  • package_name (string) – google play package name, google play client sdk 를 이용하여 얻음

  • product_id (string) – google play product id, google play client sdk 를 이용하여 얻음

  • purchase_token (string) – google play purchase token, google play client sdk 를 이용하여 얻음

Response JSON Object
  • result (integer) –

    • 0: 성공

    • 1000: 이미 처리된 영수증

    • 1001: 올바르지 않은 영수증

    • 1002: 올바르지 않은 서비스 프로바이더

    • 1003: 인증되지 않은 서비스 프로바이더

    • 1004: 취소된 영수증

    • 1005: 인증되지 않은 세션

    • 1007: 보류중인 영수증

    • 2000: 파라미터 오류

    • 나머지: 기타 오류

Apple AppStore 영수증 검증

POST /v1/validation/appleappstore
Request JSON Object
  • sessionid (string) – 인증을 통하여 얻은 session id

  • player_id (string) – 빈 문자열 또는 biller database 에 저장할 player id

  • player_service_provider (string) – 빈 문자열 또는 biller database 에 저장할 player service provider

  • receipt_data (string) – appstore receipt data, apple appstore client sdk 를 이용하여 얻음

  • product_id (string) – appstore product id, apple appstore client sdk 를 이용하여 얻음

  • quantity (string) – quantity, apple appstore client sdk 를 이용하여 얻음

Response JSON Object
  • result (integer) –

    • 0: 성공

    • 1000: 이미 처리된 영수증

    • 1001: 올바르지 않은 영수증

    • 1002: 올바르지 않은 서비스 프로바이더

    • 1003: 인증되지 않은 서비스 프로바이더

    • 1004: 취소된 영수증

    • 1005: 인증되지 않은 세션

    • 1006: 구매 내역이 담기지 않은 영수증

    • 2000: 파라미터 오류

    • 나머지: 기타 오류

OneStore(TStore) 영수증 검증

POST /v1/validation/tstore
Request JSON Object
  • sessionid (string) – 인증을 통하여 얻은 session id

  • player_id (string) – 빈 문자열 또는 biller database 에 저장할 player id

  • player_service_provider (string) – 빈 문자열 또는 biller database 에 저장할 player service provider

  • txid (string) – OneStore txid(구매시 IAP서버에서 발급되는 Unique한 값)

  • appid (string) – OneStore appid(부분유료화 부모상품 ID)

  • signdata (string) – OneStore signdata(전자영수증 데이터)

  • products (string[]) – OneStore product id(string) array

  • test (boolean) – true 이면 OneStore 테스트(개발용) IAP서버를 이용

Response JSON Object
  • result (integer) –

    • 0: 성공

    • 1000: 이미 처리된 영수증

    • 1001: 올바르지 않은 영수증

    • 1002: 올바르지 않은 서비스 프로바이더

    • 1003: 인증되지 않은 서비스 프로바이더

    • 1005: 인증되지 않은 세션

    • 2000: 파라미터 오류

    • 나머지: 기타 오류

OneStore(SDK17, API5) 영수증 검증

POST /v1/validation/onestore
Request JSON Object
  • sessionid (string) – 인증을 통하여 얻은 session id

  • player_id (string) – 빈 문자열 또는 biller database 에 저장할 player id

  • player_service_provider (string) – 빈 문자열 또는 biller database 에 저장할 player service provider

  • purchase_id (string) – OneStore purchase id(구매시 IAP서버에서 발급되는 구매 ID)

  • package_name (string) – OneStore package name(API를 호출하는 앱의 패키지 네임)

  • product_id (string) – OneStore product id(In-App ID)

Response JSON Object
  • result (integer) –

    • 0: 성공

    • 1000: 이미 처리된 영수증

    • 1001: 올바르지 않은 영수증

    • 1002: 올바르지 않은 서비스 프로바이더

    • 1003: 인증되지 않은 서비스 프로바이더

    • 1005: 인증되지 않은 세션

    • 2000: 파라미터 오류

    • 나머지: 기타 오류

예제: Apple AppStore 결제 검증

초기화

$ curl -X POST --data '{"biller_client_id":"test"}' \
       http://localhost:12811/v1/authentication
  {"result": 0, "description": "", "sessionid": "f43fd097-7c64-49a9-8edb-efeb0969d05f"}

Appstore 영수증 검증

$ curl -X POST --data '{"sessionid":"f43fd097-7c64-49a9-8edb-efeb0969d05f", "quantity": 1, \
                        "product_id":"xxx", "receipt_data":"xxx"}' \
       http://localhost:12811/v1/validation/appleappstore
  {"result": 0}

실제 구매 내역이 없는 영수증 처리

애플 앱스토어의 경우 결제 후 발급되는 영수증 안에 구매한 상품 정보가 비어있는 경우가 있습니다. 이 경우 빌러는 결제를 검증할 방법이 없기 때문에 kFailProductInformationNotFound(에러 코드: 1006) 를 응답하게 됩니다.

만약 빌러로 결제 검증 요청을 보냈을 때 응답 결과가 kFailProductInformationNotFound(에러 코드: 1006) 라면, 게임 서버는 이 내용을 클라이언트로 전달해서 클라이언트가 구매 내역을 포함한 영수증을 재발급하도록 해야 합니다.

Important

영수증에 실제 구매내역이 담겨있지 않다고해서, 올바르지 않은 영수증은 아닙니다. 상황에 따라서 영수증에는 구매내역이 담겨있지 않을 수 있습니다.

영수증을 재발급하는 방법에 대해서는 In-App Purchase Programming Guide - Refreshing the App Receipt 를 참고해주세요.

트랜잭션 아이디 가져오기

결제 검증 요청 시에 ValidateReceipt2 혹은 ValidateReceiptSync2 함수를 이용하면 영수증에 포함된 구매 내역의 transaction id 를 가져올 수 있습니다.

게임 서버는 응답 받은 transaction id 를 클라이언트로 다시 돌려주어 클라이언트가 직접 구매한 정보와 맞는지 대조해볼 수 있습니다.

지원하는 결제 검증 플랫폼 마다 해당하는 transaction id 는 아래와 같습니다.

플랫폼

정보

애플 앱스토어

검증 된 영수증 내 in_app 필드 내 original_transaction_id

원스토어(구 티스토어)

결제 검증 요청 시에 사용된 tx_id

Warning

GooglePlay 결제 검증 요청 시에는 transaction id 를 반환하지 않습니다.

동기화 방식

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void example(const string &facebook_id,
             const string &apple_receipt_data,
             const string &apple_product_id,
             int quantity) {
  Receipt receipt = MakeAppleAppStoreReceipt(
      apple_receipt_data, apple_product_id, quantity);

  std::vector<std::string> transaction_ids;

  ReceiptValidationRequest request("Facebook", facebook_id, receipt);
  ReceiptValidationResponse response;

  if (not ValidateReceiptSync2(request, &response, &transaction_ids)) {
    LOG(ERROR) << "billing system error";
    return;
  }

  if (response == kSuccess) {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (response == kFailWrongReceipt) {
    LOG(WARNING) << "wrong receipt";
  } else if (response == kFailAlreadyProvisioned) {
    LOG(WARNING) << "already provisioned";
  } else {
    LOG(WARNING) << "receipt validation error: " << response;
  }
}
추후 지원 예정입니다.

비동기 방식

비동기 방식은 동기화 방식과 유사하지만, ValidateReceiptSync2() 대신 ValidateReceipt2() 를 사용합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void OnReceiptValidated(
    const ReceiptValidationRequest &request,
    const ReceiptValidationResponse &response,
    const std::vector<string> &transaction_ids,
    const bool &error) {
  if (error) {
    LOG(ERROR) << "billing system error";
    return;
  }

  if (response == kSuccess) {
    // Give the item matched to product_id
    // ...
    return;
  }

  if (response == kFailWrongReceipt) {
    LOG(WARNING) << "wrong receipt";
  } else if (response == kFailAlreadyProvisioned) {
    LOG(WARNING) << "already provisioned";
  } else {
    LOG(WARNING) << "receipt validation error: " << response;
  }
}


void example(const string &facebook_id,
             const string &apple_receipt_data,
             const string &apple_product_id,
             int quantity) {
  Receipt receipt = MakeAppleAppStoreReceipt(
      apple_receipt_data, apple_product_id, quantity);

  ReceiptValidationRequest request("Facebook", facebook_id, receipt);

  BillingResponseHandler callback =
      bind(&OnReceiptValidated, _1, _2, _3, _4);
  ValidateReceipt2(request, callback);
}
추후 지원 예정입니다.

게임 서버 측 결제 검증 설정 파라미터

Note

이 설정 파라미터는 iFun Biller 를 사용하는 아이펀 엔진 게임 서버의 값입니다. iFun Biller 자체의 설정 파라미터는 iFun Biller 설정 방법 (MANIFEST.json) 를 참고하세요.

  • use_biller: iFun Biller agent 와의 통신을 활성화시킬지 여부. 만일 false 이면 모든 검증 과정을 bypass 하고 성공으로 간주함. (type=bool, default=false)

  • remote_biller_ip_address: iFun Biller 가 돌고 있는 원격 호스트의 IP 주소. (type=string, default=”0.0.0.0”)

  • remote_biller_port: iFun Biller를 돌리고 있는 원격 호스트의 포트번호. (type=uint64, default=0)

빌링 처리별 추가로 필요한 설정들

  • use_googleplay_service_account: 구글 플레이 서비스 계정 사용 유무. (type=bool, default=false)

  • googleplay_service_account_json_path: 구글 플레이 서비스 계정 json 파일 경로. (type=string, default=””)

  • googleplay_refresh_token: 구글 플레이 리프레시 토큰. (자세한 내용은 GooglePlay Authorization 문서 참고). (type=string, default=””)

  • googleplay_client_id: 구글 플레이의 클라이언트 아이디. (자세한 내용은 GooglePlay Authorization 문서 참고). (type=string, default=””)

  • googleplay_client_secret: 구글 플레이의 클라이언트 시크릿. (자세한 내용은 GooglePlay Authorization 문서 참고). (type=string, default=””)

  • onestore_client_id: 원스토어의 클라이언트 아이디. (개발자 센터의 Apps - InApp정보 - 인증 및 라이선스 창에서 확인 가능). (type=string, default=””)

  • onestore_client_secret: 원스토어의 클라이언트 시크릿. (개발자 센터의 Apps - InApp정보 - 인증 및 라이선스 창에서 확인 가능). (type=string, default=””)

Note

구글 플레이 서비스 계정을 사용할 경우 서비스 계정에 재무 데이터 권한을 주어야 합니다. 권한 적용 시까지 24 시간 이상이 걸릴 수 있습니다.

Note

googleplay_service_account_json_path 에 상대 경로를 입력할 경우 {project-name}-source/misc_data/ 을 기준으로 파일을 찾습니다. {project-name}-source/misc_data/ 에 있는 파일들은 서버와 함께 패키징 되므로 필요 시 해당 폴더에 서비스 계정 json 파일을 위치시킨 후 상대 경로를 입력해 주세요.

Important

misc_data/ 폴더 관련한 기능은 2020년 1월에 추가되었습니다. 그 이전 시점에 funapi_initiator 를 통해서 프로젝트를 생성한 경우 misc_data/ 가 존재하지 않을 수 있는데 이 경우는 수작업으로 등록하셔야 됩니다.

  • Ubuntu 나 CentOS의 경우 다음과 같이 처리합니다.

  • 소스 디렉토리 최상위 레벨의 CMakeLists.txt 에서 set(RESOURCE_DIRS ...) 부분을 찾아서 misc_data 를 추가합니다.

  • 소스디렉토리의 최상위 레벨에서 mkdir misc_data 를 실행합니다. game_data 폴더가 있는 곳에 생성되었다면 제대로 생성된 것입니다.

  • Windows 의 경우 다음과 같이 처리합니다.

  • 소스디렉토리의 최상위 레벨에서 misc_data 라는 이름의 폴더를 만듭니다. game_data 폴더가 있는 곳에 생성되었다면 제대로 생성된 것입니다.

Warning

  • GooglePlay에서 refresh token을 하면 이전 access token이 무효화됩니다. Refresh token을 다른 곳에서 사용하지 않도록 유의해주세요.