In-App Purchase
Implementing In-App Purchase
Displaying Products In-Game
To show available products to your players, use the GetProductListAsync
method provided by the Noctua SDK.
try {
ProductList productList = await Noctua.IAP.GetProductListAsync();
for (int i = 0; i < productList.Count; i++)
{
// Use the Id, Price and Currency for the PurchaseItemAsync() parameters.
Debug.Log(productList[i].Id);
Debug.Log(productList[i].Price);
Debug.Log(productList[i].Currency);
Debug.Log(productList[i].Description);
}
} catch(Exception e) {
if (e is NoctuaException noctuaEx)
{
Debug.Log("NoctuaException: " + noctuaEx.ErrorCode + " : " + noctuaEx.Message);
outputConsole.text = "NoctuaException: " + noctuaEx.ErrorCode + " : " + noctuaEx.Message;
} else {
Debug.Log("Exception: " + e);
outputConsole.text = "Exception: " + e;
}
}
This method retrieves the latest product information, including prices in the user's local currency.
Callback for Items Delivery
To ensure players receive their purchased items, you need to set up a callback webhook. This webhook will be notified by the Noctua SDK Backend when a purchase is successful.
- Implement an API endpoint in your game server to receive purchase notifications
- Register this webhook URL in the Noctua Developer Dashboard. You can contact us if you need help with this.
- When a purchase is completed, your webhook will receive a POST request with purchase details
Callback Idempotency
Please ensure that your callback handler is idempotent. This means that when it is called multiple times with the same exact request payload, it should produce an effect only once—for example, ensuring that an item is delivered only once per purchase.
Request Headers
The webhook request will include a custom header for security validation:
X-CALLBACK-TOKEN: [your_secret_token]
Validating the X-CALLBACK-TOKEN
To ensure the callback is genuine and comes from the Noctua SDK Backend:
- Retrieve the
X-CALLBACK-TOKEN
from the request headers. - Compare this token with the secret token provided in your Noctua Developer Dashboard.
- If the tokens match, process the webhook. If not, reject the request.
Here's a simple example of how to validate the token
<?php
function validate_callback_token($headers) {
$expected_token = 'your_secret_token_from_noctua_dashboard';
// Check if the X-CALLBACK-TOKEN header exists
if (isset($headers['X-CALLBACK-TOKEN'])) {
$received_token = $headers['X-CALLBACK-TOKEN'];
if ($received_token === $expected_token) {
// Token is valid, process the webhook
return true;
}
}
// Invalid token or missing header, reject the request
return false;
}
// Usage example:
$headers = getallheaders(); // This function gets all HTTP headers
if (validate_callback_token($headers)) {
// Process the webhook
process_webhook(json_decode(file_get_contents('php://input'), true));
} else {
// Reject the request
http_response_code(403);
echo 'Invalid token';
}
?>
Request Body
The webhook will receive a JSON payload with the following structure:
{
"data": {
"order_id": 1234567890,
"order_status": "completed",
"order_time": "2024-01-01T12:00:00Z",
"product_id": "com.yourgame.gems100",
"amount": 16000,
"currency": "IDR",
"amount_in_usd": 1.99,
"player_id": 987654321,
"ingame_item_id": "gems_100_pack",
"ingame_role_id": "player_role_123",
"ingame_server_id": "game_server_1",
"platform": "appstore",
"os": "ios",
"extra": {
"promotion_code": "SUMMERSALE"
}
},
"signed_data": "eyJhbGciOiJFUzI1NiIsIm..."
}
The data
field contains raw data while the signed_data
is the JWS (JSON Web Signature) version of the same data. It is recommended to load from signed_data
instead of from data
.
JWKS
The JWKS used to verify signed_data
is the same as the one used to verify user access token.
Here are some examples on how to verify and load the data from signed_data
.
<?php
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\Serializer\CompactSerializer;
// Fetch JWKS from the server
$jwksJson = file_get_contents('https://sdk-api-v2.noctuaprojects.com/api/v1/auth/jwks');
$jwks = JWKSet::createFromJson($jwksJson);
$signedData = 'SIGNED_DATA_STRING';
// Parse the signed data
$serializer = new CompactSerializer();
$jws = $serializer->unserialize($signedData);
// Get the key ID from the signed data header
$headers = $jws->getSignature(0)->getProtectedHeader();
$kid = $headers['kid'] ?? null;
if ($kid === null) {
throw new Exception('No "kid" found in signed data header');
}
// Find the corresponding key in the JWKS
$jwk = $jwks->get($kid);
if ($jwk === null) {
throw new Exception('No matching key found in JWKS');
}
// Verify the signature
$algorithm = new ES256();
$algorithmManager = new \Jose\Component\Core\AlgorithmManager([$algorithm]);
$jwsVerifier = new JWSVerifier($algorithmManager);
if (!$jwsVerifier->verifyWithKey($jws, $jwk, 0)) {
throw new Exception('Signed data signature verification failed');
}
// If we reach here, the signed data is valid
$payload = json_decode($jws->getPayload(), true);
// Now $payload contains the decoded signed data
The order_status
field is expected to have one of the following values:
Value | Description |
---|---|
unknown | Order status is unknown. This requires further investigation on our side. |
pending | Order is pending. It could be awaiting payment or delivery. |
completed | Order is completed successfully. |
failed | Order is failed. |
refunded | Order is refunded. |
canceled | Order is cancelled by user. |
expired | Order is expired. |
The platform
field is expected to have one of the following values:
Value | Distribution |
---|---|
direct | Direct distribution |
playstore | Google Play Store |
appstore | Apple App Store |
huaweistore | Huawei App Gallery |
playstationstore | PlayStation Store |
microsoftstore | Microsoft Store |
nintentdostore | Nintendo Store |
The os
field is expected to have one of the following values:
Value | Operating System |
---|---|
android | Android |
ios | Apple iOS |
windows | Microsoft Windows |
playstation4 | Sony PlayStation 4 |
playstation5 | Sony PlayStation 5 |
xboxxs | Microsoft Xbox XS |
nintendoswitch | Nintendo Switch |
Response
We will treat any HTTP status code of 200 as a successful callback attempt. The response body can be any content; for example, {"success": true}
is sufficient. If the callback API is called more than once for a specific transaction and the callback has already been processed on your end, please continue to return a 200 status code.
If your system is unable to handle the callback, whether due to an expected or unexpected issue, please respond with a non-200 status code and a clear message describing the problem.
- Invalid request payload: 400 (Bad Request)
- Invalid signature or decryption failure: 401 (Unauthorized)
- Unexpected error: 500 (Internal Server Error)
Integrate In-App Purchase
Update Player Account
Make sure to update the player account with the Noctua SDK. This is important to ensure that the purchase is correctly tracked and associated with the player. See Updating Player Account for more information.
To initiate a purchase, use the PurchaseItemAsync method provided by the Noctua SDK. This method handles the platform-specific purchase flow.
try {
var purchaseResponse = await Noctua.IAP.PurchaseItemAsync(new PurchaseRequest
{
// Mandatory parameters
ProductId = productId,
Price = 0.1m,
Currency = "USD",
IngameItemId = "789",
IngameItemName = "Skin X"
// Optional parameters
//RoleId = "123",
//ServerId = "456"
});
// Handle the purchase result
if (purchaseResponse.Status == OrderStatus.Completed)
{
Debug.Log("Purchase was successful!");
}
else
{
Debug.LogError("Purchase failed: " + purchaseResponse.Status);
}
} catch (Exception e) {
if (e is NoctuaException noctuaEx)
{
Debug.Log("NoctuaException: " + noctuaEx.ErrorCode + " : " + noctuaEx.Message);
outputConsole.text = "NoctuaException: " + noctuaEx.ErrorCode + " : " + noctuaEx.Message;
} else {
Debug.Log("Exception: " + e);
outputConsole.text = "Exception: " + e;
}
}
This method will trigger the appropriate store's purchase flow (App Store or Play Store) based on the user's device.
Testing Purchases
Use sandbox environments provided by App Store and Play Store to test your IAP integration without making real transactions.