22. 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.

22.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}}

# 로그인 후 쿼리를 입력합니다.
$ MariaDB [{{mysql_db_name}}]> INSERT IGNORE INTO tb_receipt_apple_appstore2 SELECT * FROM tb_receipt_apple_appstore;

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

22.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)

22.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.

22.1.2.1. For Ubuntu

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

22.1.2.2. For CentOS

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

22.1.3. How to execute

22.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

22.1.3.2. For Ubuntu 16.04 or CentOS 7

Run the following command:

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

22.1.4. How to check the operation

22.1.4.1. For Ubuntu 14.04 or CentOS 6

$ sudo service funapi-biller status

22.1.4.2. For Ubuntu 16.04 or CentOS 7

$ sudo systemctl status funapi-biller

22.1.4.3. Log file

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

22.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=””)

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",
      ...
    },
    ...
  }
}

22.2. Receipt Verification

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

22.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)

Tip

See API documentation for more details.

22.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.

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

22.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.

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

22.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.

22.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
void example(const string &facebook_id,
             const string &one_store_txid, const string &one_store_appid,
             const string &one_store_signdata, const string &one_store_product) {
  std::vector<string> products;
  products.push_back(one_store_product);
  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
public void Example(string facebook_id,
                    string one_store_txid, string one_store_appid,
                    string one_store_signdata, string one_store_product)
{
  List<string> products = new List<string>();
  products.Add(one_store_product);

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

22.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.

22.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.
  • (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
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.

22.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
    • 2000: Parameter error
    • Others: Other error

22.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

22.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

22.2.2.5. 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}

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

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

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

Important

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

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

22.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);
}
추후 지원 예정입니다.

22.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

  • 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=””)

Warning

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