← Volver al listado de tecnologías
Compras In-App para iOS y Android
Compras In-App para iOS y Android
Las compras in-app son una forma crucial de monetización en juegos móviles. Esta guía te enseñará a implementar un sistema completo de IAP para ambas plataformas.
Configuración Base de IAP
1. Extensión y Dependencias
game.project Setup
[project]
dependencies = https://github.com/defold/extension-iap/archive/main.zip
[android]
package = com.miestudio.mijuego
minimum_sdk_version = 21
target_sdk_version = 34
[ios]
bundle_identifier = com.miestudio.mijuego
Permisos Android (AndroidManifest.xml)
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.INTERNET" />
<application>
<!-- Google Play Billing -->
<service
android:name="com.android.billingclient.api.BillingService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.android.vending.billing.InAppBillingService.BIND" />
</intent-filter>
</service>
</application>
2. IAP Manager Base
Sistema de Gestión de Compras
-- iap_manager.script
local M = {}
function init(self)
self.initialized = false
self.products = {}
self.pending_purchases = {}
self.purchase_history = {}
-- Estados de IAP
self.state = {
connected = false,
processing = false,
last_error = nil
}
-- Configuración
self.config = {
auto_finish_transactions = true,
validate_receipts = true,
retry_failed_purchases = true,
max_retries = 3
}
-- Callbacks
self.purchase_callbacks = {}
self.product_callbacks = {}
print("IAP Manager initialized")
end
function M.initialize(self, product_list, callback)
if self.initialized then
if callback then callback(true) end
return
end
-- Configurar callback de IAP
iap.set_callback(function(self, message_id, message)
M.handle_iap_callback(self, message_id, message)
end)
-- Obtener lista de productos
iap.list(product_list, function(self, result)
if result.response == iap.RESPONSE_RESULT_OK then
self:process_product_list(result.products)
self.initialized = true
self.state.connected = true
print("IAP system initialized successfully")
if callback then callback(true) end
else
print("IAP initialization failed: " .. (result.error or "Unknown error"))
self.state.last_error = result.error
if callback then callback(false, result.error) end
end
end)
end
function M.process_product_list(self, products)
for _, product in ipairs(products) do
self.products[product.ident] = {
id = product.ident,
title = product.title,
description = product.description,
price = product.price,
price_string = product.price_string,
currency_code = product.currency_code,
type = product.type or "inapp"
}
print(string.format("Product loaded: %s - %s (%s)",
product.ident, product.title, product.price_string))
end
end
function M.handle_iap_callback(self, message_id, message)
if message_id == iap.MSG_PURCHASE then
self:handle_purchase_message(message)
elseif message_id == iap.MSG_RESTORE then
self:handle_restore_message(message)
end
end
function M.handle_purchase_message(self, message)
local product_id = message.ident
local callback = self.purchase_callbacks[product_id]
if message.state == iap.TRANS_STATE_PURCHASING then
print("Purchase started for: " .. product_id)
self.state.processing = true
elseif message.state == iap.TRANS_STATE_PURCHASED then
print("Purchase completed for: " .. product_id)
self:process_successful_purchase(message, callback)
elseif message.state == iap.TRANS_STATE_FAILED then
print("Purchase failed for: " .. product_id .. " - " .. (message.error or "Unknown"))
self:process_failed_purchase(message, callback)
elseif message.state == iap.TRANS_STATE_RESTORED then
print("Purchase restored for: " .. product_id)
self:process_restored_purchase(message)
end
end
return M
Configuración de Productos
1. Product Configuration
Catálogo de Productos
-- product_catalog.lua
local M = {}
M.PRODUCT_TYPES = {
CONSUMABLE = "consumable", -- Monedas, power-ups
NON_CONSUMABLE = "non_consumable", -- Contenido permanente
SUBSCRIPTION = "subscription" -- Suscripciones
}
M.PRODUCTS = {
-- Monedas
{
id = "coins_100",
type = M.PRODUCT_TYPES.CONSUMABLE,
category = "currency",
base_price = 0.99,
reward = {type = "coins", amount = 100},
bonus = 0,
popular = false
},
{
id = "coins_500",
type = M.PRODUCT_TYPES.CONSUMABLE,
category = "currency",
base_price = 3.99,
reward = {type = "coins", amount = 500},
bonus = 50, -- 10% bonus
popular = true
},
{
id = "coins_1000",
type = M.PRODUCT_TYPES.CONSUMABLE,
category = "currency",
base_price = 6.99,
reward = {type = "coins", amount = 1000},
bonus = 200, -- 20% bonus
popular = false
},
-- Power-ups
{
id = "power_up_bundle",
type = M.PRODUCT_TYPES.CONSUMABLE,
category = "power_ups",
base_price = 1.99,
reward = {
type = "bundle",
items = {
{type = "power_up", id = "shield", amount = 3},
{type = "power_up", id = "speed", amount = 3},
{type = "power_up", id = "magnet", amount = 3}
}
},
popular = false
},
-- Contenido permanente
{
id = "remove_ads",
type = M.PRODUCT_TYPES.NON_CONSUMABLE,
category = "premium",
base_price = 2.99,
reward = {type = "feature", id = "no_ads"},
popular = true
},
{
id = "unlock_all_levels",
type = M.PRODUCT_TYPES.NON_CONSUMABLE,
category = "content",
base_price = 4.99,
reward = {type = "feature", id = "all_levels"},
popular = false
},
-- Suscripciones
{
id = "premium_monthly",
type = M.PRODUCT_TYPES.SUBSCRIPTION,
category = "subscription",
base_price = 4.99,
period = "monthly",
reward = {
type = "subscription_benefits",
benefits = {
"no_ads",
"daily_coins_500",
"exclusive_skins",
"double_xp"
}
},
trial_period = 7, -- días
popular = true
}
}
function M.get_product_by_id(product_id)
for _, product in ipairs(M.PRODUCTS) do
if product.id == product_id then
return product
end
end
return nil
end
function M.get_products_by_category(category)
local filtered = {}
for _, product in ipairs(M.PRODUCTS) do
if product.category == category then
table.insert(filtered, product)
end
end
return filtered
end
function M.get_popular_products()
local popular = {}
for _, product in ipairs(M.PRODUCTS) do
if product.popular then
table.insert(popular, product)
end
end
return popular
end
return M
2. Store UI Manager
Interfaz de Tienda
-- store_ui_manager.gui_script
local product_catalog = require "main.product_catalog"
local iap_manager = require "main.iap_manager"
function init(self)
self.store_container = gui.get_node("store_container")
self.product_template = gui.get_node("product_template")
self.close_button = gui.get_node("close_button")
self.restore_button = gui.get_node("restore_button")
-- Ocultar template
gui.set_enabled(self.product_template, false)
-- Estado de la tienda
self.products_ui = {}
self.selected_product = nil
self.processing_purchase = false
-- Crear UI de productos
self:create_products_ui()
-- Verificar compras restauradas
self:check_owned_products()
end
function create_products_ui(self)
local categories = {"currency", "power_ups", "premium", "subscription"}
local y_offset = 0
for _, category in ipairs(categories) do
local products = product_catalog.get_products_by_category(category)
if #products > 0 then
-- Crear header de categoría
y_offset = y_offset - 60
self:create_category_header(category, y_offset)
-- Crear productos
for _, product in ipairs(products) do
y_offset = y_offset - 120
self:create_product_ui(product, y_offset)
end
end
end
end
function create_product_ui(self, product, y_position)
local product_node = gui.clone(self.product_template)
gui.set_enabled(product_node, true)
gui.set_parent(product_node, self.store_container)
gui.set_position(product_node, vmath.vector3(0, y_position, 0))
-- Configurar elementos del producto
local title_node = gui.get_node("product_title", product_node)
local desc_node = gui.get_node("product_description", product_node)
local price_node = gui.get_node("product_price", product_node)
local buy_button = gui.get_node("buy_button", product_node)
local popular_badge = gui.get_node("popular_badge", product_node)
-- Configurar textos
gui.set_text(title_node, product.title or product.id)
gui.set_text(desc_node, self:get_product_description(product))
-- Configurar precio
local iap_product = iap_manager:get_product(product.id)
if iap_product then
gui.set_text(price_node, iap_product.price_string)
else
gui.set_text(price_node, "$" .. product.base_price)
end
-- Mostrar badge de popular
gui.set_enabled(popular_badge, product.popular or false)
-- Configurar botón
local owned = self:is_product_owned(product.id)
if owned and product.type == product_catalog.PRODUCT_TYPES.NON_CONSUMABLE then
gui.set_text(buy_button, "OWNED")
gui.set_enabled(buy_button, false)
else
gui.set_text(buy_button, "BUY")
end
self.products_ui[product.id] = {
node = product_node,
product = product,
buy_button = buy_button,
price_node = price_node
}
end
function get_product_description(self, product)
if product.reward.type == "coins" then
local total = product.reward.amount + (product.bonus or 0)
if product.bonus and product.bonus > 0 then
return string.format("%d coins (+%d bonus!)", product.reward.amount, product.bonus)
else
return string.format("%d coins", total)
end
elseif product.reward.type == "bundle" then
return "Power-up bundle with shields, speed boost and coin magnet"
elseif product.reward.type == "feature" then
if product.reward.id == "no_ads" then
return "Remove all advertisements permanently"
elseif product.reward.id == "all_levels" then
return "Unlock all levels and content"
end
elseif product.reward.type == "subscription_benefits" then
return "Premium benefits: No ads, daily coins, exclusive content"
end
return product.description or "Premium content"
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
-- Botón cerrar
if gui.pick_node(self.close_button, action.x, action.y) then
msg.post("main:/ui", "close_store")
return true
-- Botón restaurar
elseif gui.pick_node(self.restore_button, action.x, action.y) then
self:restore_purchases()
return true
-- Botones de compra
else
for product_id, ui_data in pairs(self.products_ui) do
if gui.pick_node(ui_data.buy_button, action.x, action.y) then
self:purchase_product(product_id)
return true
end
end
end
end
return false
end
function purchase_product(self, product_id)
if self.processing_purchase then
print("Purchase already in progress")
return
end
local product = product_catalog.get_product_by_id(product_id)
if not product then
print("Product not found: " .. product_id)
return
end
print("Initiating purchase for: " .. product_id)
self.processing_purchase = true
self.selected_product = product
-- Mostrar loading
self:show_purchase_loading(true)
-- Realizar compra
iap_manager:purchase_product(product_id, function(success, error, receipt)
self:handle_purchase_result(product_id, success, error, receipt)
end)
end
function handle_purchase_result(self, product_id, success, error, receipt)
self.processing_purchase = false
self:show_purchase_loading(false)
if success then
print("Purchase successful: " .. product_id)
self:show_purchase_success(product_id)
self:grant_product_reward(product_id, receipt)
else
print("Purchase failed: " .. (error or "Unknown error"))
self:show_purchase_error(error)
end
self.selected_product = nil
end
function grant_product_reward(self, product_id, receipt)
local product = product_catalog.get_product_by_id(product_id)
if not product then return end
local reward = product.reward
if reward.type == "coins" then
local total_coins = reward.amount + (product.bonus or 0)
msg.post("main:/game_state", "add_coins", {amount = total_coins})
elseif reward.type == "bundle" then
for _, item in ipairs(reward.items) do
msg.post("main:/game_state", "add_item", item)
end
elseif reward.type == "feature" then
msg.post("main:/game_state", "unlock_feature", {feature = reward.id})
elseif reward.type == "subscription_benefits" then
msg.post("main:/game_state", "activate_subscription", {
type = "premium",
benefits = reward.benefits
})
end
-- Registrar compra
self:record_purchase(product_id, receipt)
end
return M
Validación de Recibos
1. Receipt Validation
Sistema de Validación
-- receipt_validator.lua
local M = {}
function init(self)
self.validation_endpoint = sys.get_config("iap.validation_url", "")
self.app_secret = sys.get_config("iap.app_secret", "")
self.pending_validations = {}
end
function M.validate_receipt(self, receipt_data, product_id, callback)
if not self.validation_endpoint or self.validation_endpoint == "" then
print("No validation endpoint configured - skipping validation")
if callback then callback(true, "validation_skipped") end
return
end
local validation_id = self:generate_validation_id()
self.pending_validations[validation_id] = {
callback = callback,
product_id = product_id,
timestamp = socket.gettime()
}
-- Preparar datos para validación
local platform = sys.get_sys_info().system_name
local validation_data = {
platform = platform == "iPhone OS" and "ios" or "android",
receipt = receipt_data,
product_id = product_id,
app_secret = self.app_secret,
validation_id = validation_id
}
-- Enviar al servidor
self:send_validation_request(validation_data)
end
function send_validation_request(self, data)
local headers = {
["Content-Type"] = "application/json",
["User-Agent"] = "DefoldGame/1.0"
}
local json_data = json.encode(data)
http.request(self.validation_endpoint, "POST", function(self, id, response)
self:handle_validation_response(data.validation_id, response)
end, headers, json_data)
end
function handle_validation_response(self, validation_id, response)
local validation = self.pending_validations[validation_id]
if not validation then
print("Validation response for unknown ID: " .. validation_id)
return
end
local success = false
local result = "validation_failed"
if response.status == 200 then
local ok, decoded = pcall(json.decode, response.response)
if ok and decoded then
success = decoded.valid == true
result = decoded.result or "validation_completed"
if success then
print("Receipt validation successful for: " .. validation.product_id)
else
print("Receipt validation failed: " .. (decoded.error or "Invalid receipt"))
end
else
print("Invalid JSON response from validation server")
end
else
print("Validation server error: " .. response.status)
result = "server_error"
end
-- Ejecutar callback
if validation.callback then
validation.callback(success, result)
end
self.pending_validations[validation_id] = nil
end
function generate_validation_id(self)
return "val_" .. os.time() .. "_" .. math.random(10000, 99999)
end
-- Validación local para iOS (básica)
function M.validate_ios_receipt_local(receipt_data)
-- Verificaciones básicas del formato
if not receipt_data or receipt_data == "" then
return false, "empty_receipt"
end
-- Verificar que es base64 válido
local decoded = base64.decode(receipt_data)
if not decoded then
return false, "invalid_base64"
end
-- Verificaciones adicionales se harían en el servidor
return true, "local_validation_passed"
end
-- Validación local para Android
function M.validate_android_receipt_local(receipt_data, signature)
if not receipt_data or not signature then
return false, "missing_data"
end
-- Verificar formato JSON
local ok, decoded = pcall(json.decode, receipt_data)
if not ok then
return false, "invalid_json"
end
-- Verificaciones adicionales se harían en el servidor
return true, "local_validation_passed"
end
return M
2. Server-Side Validation (Node.js Example)
Servidor de Validación
// validation_server.js
const express = require('express');
const https = require('https');
const {google} = require('googleapis');
const app = express();
app.use(express.json());
// Configuración
const APP_SECRET = process.env.APP_SECRET;
const GOOGLE_SERVICE_ACCOUNT = process.env.GOOGLE_SERVICE_ACCOUNT;
// Validación de recibos iOS
async function validateIOSReceipt(receiptData) {
const postData = JSON.stringify({
'receipt-data': receiptData,
'password': APP_SECRET, // App-specific shared secret
'exclude-old-transactions': true
});
// Primero intentar con producción
let result = await makeIOSRequest('https://buy.itunes.apple.com/verifyReceipt', postData);
// Si falla con código 21007, intentar con sandbox
if (result.status === 21007) {
result = await makeIOSRequest('https://sandbox.itunes.apple.com/verifyReceipt', postData);
}
return {
valid: result.status === 0,
result: result,
environment: result.environment
};
}
// Validación de recibos Android
async function validateAndroidReceipt(packageName, productId, purchaseToken) {
try {
const auth = new google.auth.GoogleAuth({
keyFile: GOOGLE_SERVICE_ACCOUNT,
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
const androidpublisher = google.androidpublisher({
version: 'v3',
auth: auth
});
const result = await androidpublisher.purchases.products.get({
packageName: packageName,
productId: productId,
token: purchaseToken
});
return {
valid: result.data.purchaseState === 0, // 0 = purchased
result: result.data
};
} catch (error) {
console.error('Android validation error:', error);
return {
valid: false,
error: error.message
};
}
}
// Endpoint de validación
app.post('/validate', async (req, res) => {
const {platform, receipt, product_id, app_secret, validation_id} = req.body;
// Verificar app secret
if (app_secret !== APP_SECRET) {
return res.status(401).json({
valid: false,
error: 'Invalid app secret'
});
}
try {
let validation_result;
if (platform === 'ios') {
validation_result = await validateIOSReceipt(receipt);
} else if (platform === 'android') {
const {packageName, purchaseToken} = JSON.parse(receipt);
validation_result = await validateAndroidReceipt(packageName, product_id, purchaseToken);
} else {
return res.status(400).json({
valid: false,
error: 'Unsupported platform'
});
}
res.json({
valid: validation_result.valid,
result: validation_result.result,
validation_id: validation_id
});
} catch (error) {
console.error('Validation error:', error);
res.status(500).json({
valid: false,
error: 'Validation failed'
});
}
});
function makeIOSRequest(url, postData) {
return new Promise((resolve, reject) => {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = https.request(url, options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (error) {
reject(error);
}
});
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
app.listen(3000, () => {
console.log('Validation server running on port 3000');
});
Purchase Analytics y Tracking
1. Purchase Analytics
Sistema de Analytics
-- purchase_analytics.lua
local M = {}
function init(self)
self.purchase_events = {}
self.revenue_tracking = {
daily_revenue = 0,
total_revenue = 0,
purchases_count = 0,
conversion_funnel = {
store_visits = 0,
product_views = 0,
purchase_attempts = 0,
successful_purchases = 0
}
}
end
function M.track_store_visit(self)
self.revenue_tracking.conversion_funnel.store_visits =
self.revenue_tracking.conversion_funnel.store_visits + 1
self:send_analytics_event("store_visit", {
timestamp = os.time(),
session_id = self:get_session_id()
})
end
function M.track_product_view(self, product_id)
self.revenue_tracking.conversion_funnel.product_views =
self.revenue_tracking.conversion_funnel.product_views + 1
self:send_analytics_event("product_view", {
product_id = product_id,
timestamp = os.time()
})
end
function M.track_purchase_attempt(self, product_id, price)
self.revenue_tracking.conversion_funnel.purchase_attempts =
self.revenue_tracking.conversion_funnel.purchase_attempts + 1
self:send_analytics_event("purchase_attempt", {
product_id = product_id,
price = price,
timestamp = os.time()
})
end
function M.track_successful_purchase(self, product_id, price, currency, receipt)
self.revenue_tracking.conversion_funnel.successful_purchases =
self.revenue_tracking.conversion_funnel.successful_purchases + 1
self.revenue_tracking.purchases_count = self.revenue_tracking.purchases_count + 1
self.revenue_tracking.total_revenue = self.revenue_tracking.total_revenue + price
self.revenue_tracking.daily_revenue = self.revenue_tracking.daily_revenue + price
-- Enviar a múltiples plataformas de analytics
self:send_purchase_events(product_id, price, currency, receipt)
end
function send_purchase_events(self, product_id, price, currency, receipt)
-- Firebase Analytics
self:send_analytics_event("purchase", {
currency = currency,
value = price,
item_id = product_id,
item_category = "iap"
})
-- Facebook Analytics
msg.post("main:/facebook", "track_purchase", {
amount = price,
currency = currency,
content_type = "product",
content_id = product_id
})
-- AppsFlyer
msg.post("main:/appsflyer", "track_purchase", {
revenue = price,
currency = currency,
product_id = product_id,
receipt_id = receipt
})
-- Adjust
msg.post("main:/adjust", "track_revenue", {
amount = price,
currency = currency,
product_id = product_id
})
end
function M.get_conversion_rates(self)
local funnel = self.revenue_tracking.conversion_funnel
return {
store_to_product = funnel.product_views / math.max(1, funnel.store_visits),
product_to_attempt = funnel.purchase_attempts / math.max(1, funnel.product_views),
attempt_to_purchase = funnel.successful_purchases / math.max(1, funnel.purchase_attempts),
overall_conversion = funnel.successful_purchases / math.max(1, funnel.store_visits)
}
end
function send_analytics_event(self, event_name, properties)
-- Enviar a sistema de analytics principal
msg.post("main:/analytics", "track_event", {
event = event_name,
properties = properties
})
end
function get_session_id(self)
return "session_" .. os.time()
end
return M
Subscription Management
1. Subscription Handler
Sistema de Suscripciones
-- subscription_manager.lua
local M = {}
function init(self)
self.active_subscriptions = {}
self.subscription_status = {}
self.validation_interval = 24 * 60 * 60 -- Validar cada 24 horas
end
function M.handle_subscription_purchase(self, product_id, receipt_data)
local subscription_data = {
product_id = product_id,
purchase_date = os.time(),
receipt_data = receipt_data,
status = "active",
last_validated = os.time()
}
self.active_subscriptions[product_id] = subscription_data
-- Activar beneficios
self:activate_subscription_benefits(product_id)
-- Programar validación periódica
self:schedule_subscription_validation(product_id)
print("Subscription activated: " .. product_id)
end
function activate_subscription_benefits(self, product_id)
local product = product_catalog.get_product_by_id(product_id)
if not product or not product.reward.benefits then return end
for _, benefit in ipairs(product.reward.benefits) do
if benefit == "no_ads" then
msg.post("main:/game_state", "disable_ads")
elseif benefit == "daily_coins_500" then
msg.post("main:/game_state", "enable_daily_coins", {amount = 500})
elseif benefit == "exclusive_skins" then
msg.post("main:/game_state", "unlock_exclusive_content", {type = "skins"})
elseif benefit == "double_xp" then
msg.post("main:/game_state", "enable_xp_multiplier", {multiplier = 2.0})
end
end
end
function schedule_subscription_validation(self, product_id)
timer.delay(self.validation_interval, true, function()
self:validate_subscription_status(product_id)
end)
end
function validate_subscription_status(self, product_id)
local subscription = self.active_subscriptions[product_id]
if not subscription then return end
-- Validar con las tiendas
local receipt_validator = require "main.receipt_validator"
receipt_validator:validate_receipt(subscription.receipt_data, product_id,
function(valid, result)
if valid then
subscription.last_validated = os.time()
subscription.status = "active"
print("Subscription validated: " .. product_id)
else
subscription.status = "expired"
self:handle_subscription_expiry(product_id)
print("Subscription expired: " .. product_id)
end
end)
end
function handle_subscription_expiry(self, product_id)
-- Desactivar beneficios
self:deactivate_subscription_benefits(product_id)
-- Notificar al usuario
msg.post("main:/ui", "show_subscription_expired", {product_id = product_id})
-- Remover de activas
self.active_subscriptions[product_id] = nil
end
function deactivate_subscription_benefits(self, product_id)
local product = product_catalog.get_product_by_id(product_id)
if not product or not product.reward.benefits then return end
for _, benefit in ipairs(product.reward.benefits) do
if benefit == "no_ads" then
msg.post("main:/game_state", "enable_ads")
elseif benefit == "daily_coins_500" then
msg.post("main:/game_state", "disable_daily_coins")
elseif benefit == "double_xp" then
msg.post("main:/game_state", "disable_xp_multiplier")
end
end
end
function M.is_subscription_active(self, product_id)
local subscription = self.active_subscriptions[product_id]
return subscription and subscription.status == "active"
end
return M
Security y Anti-Fraud
1. Fraud Detection
Sistema Anti-Fraude
-- fraud_detector.lua
local M = {}
function init(self)
self.fraud_signals = {}
self.device_fingerprint = self:generate_device_fingerprint()
self.purchase_patterns = {}
self.risk_score = 0
end
function M.analyze_purchase_risk(self, product_id, price, user_data)
local risk_factors = {}
-- Factor 1: Velocidad de compras
local purchase_velocity = self:calculate_purchase_velocity()
if purchase_velocity > 5 then -- Más de 5 compras en 1 hora
table.insert(risk_factors, "high_velocity")
end
-- Factor 2: Patrón de precios
local price_pattern = self:analyze_price_pattern(price)
if price_pattern == "suspicious" then
table.insert(risk_factors, "price_pattern")
end
-- Factor 3: Geolocalización (si está disponible)
local geo_risk = self:check_geo_risk(user_data.country)
if geo_risk then
table.insert(risk_factors, "geo_risk")
end
-- Factor 4: Device fingerprint
if self:is_device_suspicious() then
table.insert(risk_factors, "device_risk")
end
-- Calcular score de riesgo
local risk_score = #risk_factors * 0.25
self.risk_score = math.min(1.0, risk_score)
return {
risk_score = self.risk_score,
risk_factors = risk_factors,
recommendation = self:get_risk_recommendation(self.risk_score)
}
end
function calculate_purchase_velocity(self)
local current_time = os.time()
local recent_purchases = 0
for _, purchase in ipairs(self.purchase_patterns) do
if current_time - purchase.timestamp < 3600 then -- Última hora
recent_purchases = recent_purchases + 1
end
end
return recent_purchases
end
function analyze_price_pattern(self, price)
-- Detectar patrones sospechosos como:
-- - Solo compras de productos de alto valor
-- - Compras inmediatamente después de install
-- - Compras repetidas del mismo producto
local high_value_threshold = 10.0
local recent_high_value = 0
for _, purchase in ipairs(self.purchase_patterns) do
if purchase.price >= high_value_threshold then
recent_high_value = recent_high_value + 1
end
end
if recent_high_value >= 3 and #self.purchase_patterns <= 5 then
return "suspicious"
end
return "normal"
end
function check_geo_risk(self, country)
-- Lista de países con alto riesgo de fraude
local high_risk_countries = {
"XX", "YY", "ZZ" -- Códigos de país de ejemplo
}
for _, risk_country in ipairs(high_risk_countries) do
if country == risk_country then
return true
end
end
return false
end
function is_device_suspicious(self)
local sys_info = sys.get_sys_info()
-- Verificar si es emulador
if sys_info.device_model and string.find(sys_info.device_model, "Emulator") then
return true
end
-- Verificar root/jailbreak (requiere extensión nativa)
if sys_info.system_name == "Android" then
-- Verificar indicadores de root
local potential_root = sys_info.device_model == "sdk" or
sys_info.manufacturer == "Genymotion"
if potential_root then
return true
end
end
return false
end
function get_risk_recommendation(self, risk_score)
if risk_score < 0.3 then
return "allow"
elseif risk_score < 0.7 then
return "review"
else
return "block"
end
end
function generate_device_fingerprint(self)
local sys_info = sys.get_sys_info()
return {
device_model = sys_info.device_model,
manufacturer = sys_info.manufacturer,
system_version = sys_info.system_version,
language = sys_info.language,
timezone = sys_info.timezone
}
end
function M.record_purchase_attempt(self, product_id, price, result)
table.insert(self.purchase_patterns, {
product_id = product_id,
price = price,
result = result,
timestamp = os.time()
})
-- Mantener solo los últimos 50 registros
if #self.purchase_patterns > 50 then
table.remove(self.purchase_patterns, 1)
end
end
return M
Testing y Debug
1. IAP Debug Tools
Debug Console
-- iap_debug.gui_script
function init(self)
self.debug_enabled = sys.get_config("project.debug", "0") == "1"
if self.debug_enabled then
self:create_debug_ui()
self:setup_test_products()
end
end
function create_debug_ui(self)
-- Panel de debug
self.debug_panel = gui.get_node("debug_panel")
self.product_list = gui.get_node("product_list")
self.test_buttons = {
purchase = gui.get_node("test_purchase"),
restore = gui.get_node("test_restore"),
validate = gui.get_node("test_validate")
}
self:update_debug_display()
end
function setup_test_products(self)
-- Productos de test para debug
self.test_products = {
"android.test.purchased",
"android.test.canceled",
"android.test.refunded",
"android.test.item_unavailable"
}
end
function on_input(self, action_id, action)
if not self.debug_enabled or not action.pressed then
return false
end
if action_id == hash("touch") then
if gui.pick_node(self.test_buttons.purchase, action.x, action.y) then
self:test_purchase()
return true
elseif gui.pick_node(self.test_buttons.restore, action.x, action.y) then
self:test_restore()
return true
elseif gui.pick_node(self.test_buttons.validate, action.x, action.y) then
self:test_validation()
return true
end
end
return false
end
function test_purchase(self)
local test_product = self.test_products[1] -- android.test.purchased
print("Testing purchase: " .. test_product)
local iap_manager = require "main.iap_manager"
iap_manager:purchase_product(test_product, function(success, error, receipt)
print("Test purchase result: " .. tostring(success))
if error then
print("Test purchase error: " .. error)
end
end)
end
function test_restore(self)
print("Testing restore purchases")
iap.restore()
end
function test_validation(self)
print("Testing receipt validation")
-- Simular validación con datos de test
local receipt_validator = require "main.receipt_validator"
receipt_validator:validate_receipt("test_receipt_data", "test_product",
function(success, result)
print("Test validation result: " .. tostring(success))
print("Test validation details: " .. (result or "none"))
end)
end
return M
Mejores Prácticas
1. User Experience
- Mostrar precios claramente en moneda local
- Explicar el valor de cada compra
- Confirmar compras antes de procesar
- Manejar errores gracefully
- Providir restore button visible
2. Security
- Validar todos los recibos en servidor
- Implementar detección de fraude
- Nunca confiar en validación local únicamente
- Usar HTTPS para todas las comunicaciones
- Proteger secrets y API keys
3. Business
- Ofrecer variety de precios
- Implementar analytics detallados
- A/B test diferentes precios
- Monitorear conversion rates
- Optimizar timing de ofertas
4. Technical
- Manejar network errors
- Implementar retry logic
- Cache product information
- Handle subscription renewals
- Test en multiple devices
Esta implementación completa te proporciona un sistema robusto de IAP que maneja todos los aspectos críticos de las compras in-app para dispositivos móviles.