24. External service part 2: Billing

When implementing in-app purchases using payment platforms such as Apple’s AppStore or Google Play: 1) The game client uses that platform’s SDK for payment, then receives product data and a payment receipt; 2) This is sent to the server to verify the payment and check that the receipt has not already been used.

For this reason, it is burdensome for developers to support all the various available platforms. iFun Engine includes a billing verification feature.

24.1. iFun Biller

iFun Engine uses a program called iFun Biller as an agent to exclusively handle billing. The game server sends billing requests to iFun Biller, and iFun Biller checks each actual platform and records user data with successful payments.

Note

iFun Engine’s use of a separate billing agent doesn’t affect game servers and is so that billing can be added or changed.

With the use of an agent, it is easy to use billing services through the agent’s REST API on the game server and other servers as well.

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://www.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

24.1.1. Supported platforms

The following external payment platforms are currently supported. (We plan to expand to more platforms in future.)

  • Google Play

  • Apple AppStore

  • OneStore(SK TStore)

24.1.2. How to install

Tip

If Setting up iFun Biller (MANIFEST.json)’s use_biller is set to false, the game server runs in test mode and assumes all billing requests are bypassed. This setting is used during the development phase, and there is no need to install iFun Biller in these instances.

24.1.2.1. For Ubuntu

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

24.1.2.2. For CentOS

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

24.1.3. How to execute

24.1.3.1. For Ubuntu 14.04 or CentOS 6

Open the file /etc/default/funapi-biller and modify enabled=1.

Run the following command:

$ sudo service funapi-biller start

24.1.3.2. For Ubuntu 16.04 or CentOS 7

Run the following command:

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

24.1.4. How to check the operation

24.1.4.1. For Ubuntu 14.04 or CentOS 6

$ sudo service funapi-biller status

24.1.4.2. For Ubuntu 16.04 or CentOS 7

$ sudo systemctl status funapi-biller

24.1.4.3. Log file

Logs are output to /var/log/funapi/funapi-biller/.

24.1.5. Setting up iFun Biller (MANIFEST.json)

Note

This is to set up iFun Biller itself. Settings for game servers using iFun Biller are in Game server-side billing parameters.

iFun Biller was, of course, also written with iFun Engine. Therefore, you can naturally change settings in iFun Biller’s MANIFEST.json. iFun Biller’s MANIFEST.json is in /usr/share/funapi-biller/default/manifests/MANIFEST.json.

Configure as follows.

  • protobuf_listen_port: Sets the TCP port number for the iFun Engine game server to communicate with iFun Biller. (type=uint16, default=12810)

  • http_listen_port: Sets the HTTP port number to communicate with iFun Biller through REST API. (type=uint16, default=12811)

  • bypass: Whether or not to bypass all billing. Runs iFun Biller in test mode. This has the same effect as setting use_biller=false on the iFun Engine game server. (type=bool, default=false)

  • mysql_server_url: Enter the IP address and port of the DB server where used billing data was saved. (type=string, default=”tcp://127.0.0.1:3306”)

  • mysql_id: Enter the user ID of the DB where billing data will be saved.(type=string, default=”funapibiller1”)

  • mysql_pw: Enter the user password for the DB where billing data will be saved. (type=string, default=”qlffj1!!”)

  • mysql_db_name: Enter the name of the DB where billing data will be saved. (type=string, default=”funapi_biller1”)

  • mysql_db_connection_count: Connection pool size used for iFun Biller to communicate with the MySQL server. (type=uint16, default=1)

  • biller_use_db_auto_schema_generation: Sets whether or not to automatically create a DB table and procedure when iFun Biller is running without one. (type=bool, default=true)

  • export_db_schema_to_file: If a file path is given in this item and biller_use_db_auto_schema_generation: false, iFun Biller creates the required DB schema under that path and closes. (type=string, default=””)

  • 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

When the agent is updated, the existing MANIFEST.json is overwritten. To prevent this, you can use an override file as described in Temporarily overriding MANIFEST.json.

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

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

24.2. Receipt Verification

You can use the function provided by iFun Engine (in this case, communication is through TCP) or independently use RESTful API.

24.2.1. Invoking the iFun Engine function

iFun Engine uses a single billing request interface regardless of platform type. Both asynchronous and synchronous functions are applied.

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)

You can think of ReceiptValidationRequest in the following struct.

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

Important

Be aware that the first and second parameters of ReceiptValidationRequest relate to user authentication and not payment. This is because iFun Biller records user data when authenticating payment.

The way a Receipt is created differs by platform type. The following utility functions are provided for each platform for this purpose.

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

See API documentation for more details.

24.2.1.1. Google Play billing

Let’s examine an example in which a Facebook user makes a payment while playing, the payment data is verified, and the paid Facebook user’s data is recorded synchronously and asynchronously.

24.2.1.1.1. Synchronous approach
 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);
  }
}
24.2.1.1.2. Asynchronous approach

Like the synchronous method, the asynchronous method uses ValidateReceipt() rather than ValidateReceiptSync().

 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);
}

24.2.1.2. Apple App Store billing

Let’s examine an example in which a Facebook user makes a payment while playing, the payment data is verified, and the paid Facebook user’s data is recorded synchronously and asynchronously.

24.2.1.2.1. Synchronous approach
 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);
  }
}
24.2.1.2.2. Asynchronous approach

Like the synchronous method, the asynchronous method uses ValidateReceipt() rather than ValidateReceiptSync().

 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);
}

24.2.1.3. OneStore(TStore) billing

Let’s examine an example in which a Facebook user makes a payment while playing, the payment data is verified, and the paid Facebook user’s data is recorded synchronously and asynchronously.

24.2.1.3.1. Synchronous approach
 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);
  }
}
24.2.1.3.2. Asynchronous approach

Refer to the Google Play and Apple AppStore explanations and the synchronous method description above to use the same method.

24.2.2. How to use REST API

You can request REST API directly from iFun Biller without using iFun Engine. All parameters below are in JSON format and can be put in the HTTP POST body for requests.

24.2.2.1. Resetting platform connections

POST /v1/authentication
Request JSON Object
  • biller_client_id (string) – Enter an appropriate value since authentication is not available in the current version.

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

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

  • (for GooglePlay) google_play_client_id (string) – GooglePlay client id

  • (for GooglePlay) google_play_client_secret (string) – GooglePlay client secret

  • (for GooglePlay) google_play_refresh_token (string) – GooglePlay refresh token

  • (for OneStore) one_store_client_id (string) – OneStore client id

  • (for OneStore) one_store_client_secret (string) – OneStore client_secret

Response JSON Object
  • result (integer) –

    • 0: Succeeded

    • 2: Parameter error

    • Other: Other error

    • 2: Already authenticated session

    • 3: Incorrect ID

    • 4: Incorrect authentication key

    • 5: Incorrect service provider

    • 6: Incorrect service provider (Google) authentication parameter

    • 2000: Parameter error

    • Others: Other error

  • description (string) – Error description

  • sessionid (string) – Included when sending session ID and billing requests.

24.2.2.2. Google Play billing

POST /v1/validation/googleplay
Request JSON Object
  • sessionid (string) – Verified session ID

  • player_id (string) – Empty text string or player ID saved in the biller DB

  • player_service_provider (string) – Empty text string or player service provider saved in the biller DB

  • package_name (string) – Google Play package name, received using Google Play client SDK

  • product_id (string) – Google Play product ID, received using Google Play client SDK

  • purchase_token (string) – Google Play purchase token, received using Google Play client SDK

Response JSON Object
  • result (integer) –

    • 0: Succeeded

    • 1000: Already processed receipt

    • 1001: Incorrect receipt

    • 1002: Incorrect service provider

    • 1003: Unauthorized service provider

    • 1004: Canceled receipt

    • 1005: Unauthorized session

    • 1007: 보류중인 영수증

    • 2000: Parameter error

    • Others: Other error

24.2.2.3. Apple AppStore billing

POST /v1/validation/appleappstore
Request JSON Object
  • sessionid (string) – Authenticated session ID

  • player_id (string) – Empty text string or player ID saved in the biller DB

  • player_service_provider (string) – Empty text string or player service provider saved in the biller DB

  • receipt_data (string) – App Store receipt data, received using Apple App Store client SDK

  • product_id (string) – App Store product ID, received using Apple App Store client SDK

  • quantity (string) – Quantity, received using Apple App Store client SDK

Response JSON Object
  • result (integer) –

    • 0: Succeeded

    • 1000: Already processed receipt

    • 1001: Incorrect receipt

    • 1002: Incorrect service provider

    • 1003: Unauthorized service provider

    • 1004: Canceled receipt

    • 1005: Unauthorized session

    • 1006: The receipt does not include product information

    • 2000: Parameter error

    • Others: Other error

24.2.2.4. OneStore(TStore) billing

POST /v1/validation/tstore
Request JSON Object
  • sessionid (string) – Authenticated session ID

  • player_id (string) – Empty text string or player ID saved in biller DB

  • player_service_provider (string) – Empty text string or player server provider saved in biller DB

  • txid (string) – OneStore txid (unique value issued by IAP server on purchase)

  • appid (string) – OneStore appid (partially paid parent product ID)

  • signdata (string) – OneStore signdata (electronic payment data)

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

  • test (boolean) – If true, uses OneStore test (for development) IAP server

Response JSON Object
  • result (integer) –

    • 0: Succeeded

    • 1000: Already processed receipt

    • 1001: Incorrect receipt

    • 1002: Incorrect service provider

    • 1003: Unauthorized service provider

    • 1005: Unauthorized session

    • 2000: Parameter error

    • Others: Other error

24.2.2.5. 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: 파라미터 오류

    • 나머지: 기타 오류

24.2.2.6. Example: Apple AppStore billing

Resetting

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

App Store billing

$ 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}

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

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

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

Important

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

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

24.4. 트랜잭션 아이디 가져오기

결제 검증 요청 시에 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);
}
추후 지원 예정입니다.

24.5. Game server-side billing parameters

Note

These parameters are iFun Engine game server values using iFun Biller. Please see Setting up iFun Biller (MANIFEST.json) for parameters for iFun Biller itself.

  • use_biller: Enables communication with iFun Biller agent. If false, all verification is bypassed and authentication is considered successful. (type=bool, default=false)

  • remote_biller_ip_address: IP address for remote host using iFun Biller. (type=string, default=”0.0.0.0”)

  • remote_biller_port: Port number for remote host using iFun Biller. (type=uint64, default=0)

Additional required settings for each billing process

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

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

  • googleplay_refresh_token: Google Play refresh token. (See`GooglePlay Authorization documentation`_ for details). (type=string, default=””)

  • googleplay_client_id: Google Play client ID. (See`GooglePlay Authorization documentation`_ for details). (type=string, default=””)

  • googleplay_client_secret: Google Play client secret. (See`GooglePlay Authorization documentation`_ for details). (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 파일을 위치시킨 후 상대 경로를 입력해 주세요.

Warning

  • Previous access tokens are canceled when a refresh token is issued by Google Play. Note that refresh tokens cannot be used elsewhere.