← Volver al listado de tecnologías
Push Notifications y Notificaciones Locales
Push Notifications y Notificaciones Locales
Las notificaciones son fundamentales para mantener el engagement y la retención de usuarios. Esta guía te enseñará a implementar un sistema completo de notificaciones para móviles.
Configuración Base de Notificaciones
1. Extensions y Dependencies
game.project Setup
[project]
dependencies = https://github.com/defold/extension-push/archive/main.zip
[android]
gcm_sender_id = 123456789012
package = com.miestudio.mijuego
[ios]
bundle_identifier = com.miestudio.mijuego
Permisos Android (AndroidManifest.xml)
<!-- Push Notifications -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission
android:name="com.miestudio.mijuego.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="com.miestudio.mijuego.permission.C2D_MESSAGE" />
<application>
<!-- Firebase Cloud Messaging -->
<service android:name="com.defold.push.PushService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- Default notification icon -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/notification_icon" />
<!-- Default notification color -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/notification_color" />
<!-- Notification channels -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
</application>
iOS Configuration (Info.plist)
<!-- Push Notifications -->
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<!-- Firebase Configuration -->
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
2. Notification Manager Base
Sistema de Gestión de Notificaciones
-- notification_manager.script
local M = {}
function init(self)
self.push_token = nil
self.notification_enabled = false
self.local_notifications = {}
self.scheduled_notifications = {}
-- Configuración
self.config = {
request_permission_on_start = true,
default_badge_count = 0,
max_local_notifications = 64,
notification_channels = {
default = {id = "default", name = "General", importance = "high"},
gameplay = {id = "gameplay", name = "Gameplay", importance = "default"},
social = {id = "social", name = "Social", importance = "high"},
promotions = {id = "promotions", name = "Promotions", importance = "low"}
}
}
-- Estado
self.stats = {
notifications_sent = 0,
notifications_opened = 0,
push_registered = false,
permission_granted = false
}
print("Notification Manager initialized")
end
function M.initialize(self, callback)
-- Configurar callback de push
push.set_callback(function(self, payload, origin, activated)
M.handle_push_message(self, payload, origin, activated)
end)
-- Solicitar permisos de notificación
if self.config.request_permission_on_start then
self:request_notification_permission(callback)
end
-- Configurar canales de notificación (Android)
self:setup_notification_channels()
-- Registrar para push notifications
self:register_for_push_notifications()
end
function M.request_notification_permission(self, callback)
print("Requesting notification permission...")
push.request_permission(function(self, result)
self.stats.permission_granted = result
if result then
print("Notification permission granted")
self.notification_enabled = true
else
print("Notification permission denied")
end
if callback then callback(result) end
end)
end
function M.register_for_push_notifications(self)
push.register({}, function(self, token, error)
if token then
self.push_token = token
self.stats.push_registered = true
print("Push token received: " .. token)
-- Enviar token al servidor
self:send_token_to_server(token)
else
print("Push registration failed: " .. (error or "Unknown error"))
end
end)
end
function M.handle_push_message(self, payload, origin, activated)
print("Push notification received:")
print("Origin: " .. tostring(origin))
print("Activated: " .. tostring(activated))
self.stats.notifications_opened = self.stats.notifications_opened + 1
-- Procesar payload
if payload then
local notification_data = self:parse_notification_payload(payload)
self:process_notification_action(notification_data, activated)
end
end
function M.parse_notification_payload(self, payload)
-- Estructura esperada del payload:
-- {
-- "title": "Título",
-- "body": "Mensaje",
-- "action": "open_store",
-- "data": {...}
-- }
return {
title = payload.title or "",
body = payload.body or payload.alert or "",
action = payload.action or "default",
data = payload.data or payload.custom or {},
badge = tonumber(payload.badge) or 0
}
end
function M.process_notification_action(self, notification_data, activated)
if not activated then
-- Solo registrar si no se activó la app
return
end
local action = notification_data.action
if action == "open_store" then
msg.post("main:/ui", "open_store")
elseif action == "claim_reward" then
msg.post("main:/rewards", "claim_daily_reward")
elseif action == "join_event" then
msg.post("main:/events", "open_event", notification_data.data)
elseif action == "continue_level" then
msg.post("main:/game", "continue_level", notification_data.data)
elseif action == "social_update" then
msg.post("main:/social", "open_friends")
else
-- Acción por defecto: abrir pantalla principal
msg.post("main:/ui", "show_main_menu")
end
-- Trackear apertura
self:track_notification_opened(notification_data)
end
return M
Notificaciones Locales
1. Local Notification System
Notificaciones Programadas
-- local_notifications.lua
local M = {}
function init(self)
self.scheduled = {}
self.templates = {}
self.next_id = 1
-- Configurar templates de notificaciones
self:setup_notification_templates()
end
function M.setup_notification_templates(self)
self.templates = {
daily_login = {
title = "¡Tu recompensa diaria te espera!",
body = "Entra y reclama tus monedas gratis",
action = "claim_reward",
icon = "coin_icon",
channel = "gameplay"
},
energy_full = {
title = "¡Energía completamente recargada!",
body = "Tu energía está al máximo. ¡Es hora de jugar!",
action = "continue_level",
icon = "energy_icon",
channel = "gameplay"
},
limited_offer = {
title = "¡Oferta por tiempo limitado!",
body = "50% de descuento en monedas. ¡Solo por hoy!",
action = "open_store",
icon = "offer_icon",
channel = "promotions"
},
friend_request = {
title = "Nueva solicitud de amistad",
body = "{{friend_name}} quiere ser tu amigo",
action = "social_update",
icon = "friend_icon",
channel = "social"
},
level_complete_reminder = {
title = "¡Casi terminas el nivel!",
body = "Regresa y completa el nivel {{level_number}}",
action = "continue_level",
icon = "level_icon",
channel = "gameplay"
},
comeback_3days = {
title = "¡Te extrañamos!",
body = "Han pasado 3 días. Regresa por tu regalo especial",
action = "claim_reward",
icon = "gift_icon",
channel = "default"
},
comeback_7days = {
title = "¡Recompensa especial!",
body = "7 días sin jugar = recompensa extra grande",
action = "claim_reward",
icon = "big_gift_icon",
channel = "default"
}
}
end
function M.schedule_notification(self, template_id, delay_seconds, data)
local template = self.templates[template_id]
if not template then
print("Unknown notification template: " .. template_id)
return nil
end
local notification_id = self.next_id
self.next_id = self.next_id + 1
-- Procesar template con datos
local notification = self:build_notification(template, data)
notification.id = notification_id
notification.scheduled_time = os.time() + delay_seconds
-- Programar notificación
push.schedule(notification_id, notification.title, notification.body, delay_seconds, {
action = notification.action,
icon = notification.icon,
badge = 1,
channel = notification.channel,
data = data or {}
})
-- Guardar referencia
self.scheduled[notification_id] = {
template_id = template_id,
notification = notification,
data = data
}
print(string.format("Scheduled notification %d: %s (in %d seconds)",
notification_id, template_id, delay_seconds))
return notification_id
end
function M.build_notification(self, template, data)
local notification = {
title = template.title,
body = template.body,
action = template.action,
icon = template.icon,
channel = template.channel
}
-- Reemplazar placeholders con datos
if data then
for key, value in pairs(data) do
local placeholder = "{{" .. key .. "}}"
notification.title = string.gsub(notification.title, placeholder, tostring(value))
notification.body = string.gsub(notification.body, placeholder, tostring(value))
end
end
return notification
end
function M.cancel_notification(self, notification_id)
if self.scheduled[notification_id] then
push.cancel(notification_id)
self.scheduled[notification_id] = nil
print("Cancelled notification: " .. notification_id)
return true
end
return false
end
function M.cancel_all_notifications(self)
for notification_id, _ in pairs(self.scheduled) do
push.cancel(notification_id)
end
self.scheduled = {}
print("Cancelled all scheduled notifications")
end
function M.schedule_daily_login_reminder(self)
-- Cancelar notificación diaria anterior
self:cancel_notifications_by_template("daily_login")
-- Programar para mañana a las 10:00 AM
local tomorrow_10am = self:get_next_10am()
local delay = tomorrow_10am - os.time()
return self:schedule_notification("daily_login", delay)
end
function M.schedule_energy_notification(self, current_energy, max_energy, recharge_time)
if current_energy >= max_energy then
return nil -- Energía ya llena
end
local time_to_full = (max_energy - current_energy) * recharge_time
return self:schedule_notification("energy_full", time_to_full)
end
function M.schedule_comeback_notifications(self)
-- Cancelar notificaciones de comeback anteriores
self:cancel_notifications_by_template("comeback_3days")
self:cancel_notifications_by_template("comeback_7days")
-- Programar para 3 y 7 días
local three_days = 3 * 24 * 60 * 60
local seven_days = 7 * 24 * 60 * 60
local id_3days = self:schedule_notification("comeback_3days", three_days)
local id_7days = self:schedule_notification("comeback_7days", seven_days)
return {id_3days, id_7days}
end
function M.cancel_notifications_by_template(self, template_id)
local to_cancel = {}
for notification_id, scheduled in pairs(self.scheduled) do
if scheduled.template_id == template_id then
table.insert(to_cancel, notification_id)
end
end
for _, notification_id in ipairs(to_cancel) do
self:cancel_notification(notification_id)
end
end
function M.get_next_10am(self)
local current_time = os.time()
local current_date = os.date("*t", current_time)
-- Configurar para las 10:00 AM del día siguiente
local target_date = {
year = current_date.year,
month = current_date.month,
day = current_date.day + 1,
hour = 10,
min = 0,
sec = 0
}
return os.time(target_date)
end
return M
2. Smart Scheduling System
Sistema de Programación Inteligente
-- smart_scheduler.lua
local M = {}
function init(self)
self.user_patterns = {
typical_play_times = {}, -- Horarios frecuentes de juego
session_lengths = {}, -- Duración típica de sesiones
last_activity = 0, -- Última actividad
timezone_offset = 0, -- Offset de zona horaria
preferred_language = "en" -- Idioma preferido
}
self.scheduling_rules = {
respect_sleep_hours = true,
sleep_start = 22, -- 10 PM
sleep_end = 8, -- 8 AM
max_daily_notifications = 3,
min_interval_hours = 4
}
end
function M.analyze_user_patterns(self, session_data)
-- Analizar horarios de juego
local play_hour = tonumber(os.date("%H", session_data.start_time))
table.insert(self.user_patterns.typical_play_times, play_hour)
-- Mantener solo los últimos 30 registros
if #self.user_patterns.typical_play_times > 30 then
table.remove(self.user_patterns.typical_play_times, 1)
end
-- Analizar duración de sesiones
local session_length = session_data.end_time - session_data.start_time
table.insert(self.user_patterns.session_lengths, session_length)
if #self.user_patterns.session_lengths > 20 then
table.remove(self.user_patterns.session_lengths, 1)
end
self.user_patterns.last_activity = session_data.end_time
end
function M.get_optimal_notification_time(self, base_delay)
local current_time = os.time()
local target_time = current_time + base_delay
-- Ajustar basado en patrones del usuario
target_time = self:adjust_for_user_patterns(target_time)
-- Respetar horas de sueño
target_time = self:adjust_for_sleep_hours(target_time)
-- Evitar sobrecargar al usuario
target_time = self:adjust_for_notification_limits(target_time)
return target_time - current_time
end
function adjust_for_user_patterns(self, target_time)
if #self.user_patterns.typical_play_times == 0 then
return target_time -- No hay datos suficientes
end
-- Calcular hora más frecuente de juego
local hour_counts = {}
for _, hour in ipairs(self.user_patterns.typical_play_times) do
hour_counts[hour] = (hour_counts[hour] or 0) + 1
end
local most_frequent_hour = 0
local max_count = 0
for hour, count in pairs(hour_counts) do
if count > max_count then
max_count = count
most_frequent_hour = hour
end
end
-- Ajustar hacia la hora más frecuente si es razonable
local target_date = os.date("*t", target_time)
local current_hour = target_date.hour
if math.abs(current_hour - most_frequent_hour) > 3 then
target_date.hour = most_frequent_hour
target_date.min = math.random(0, 59) -- Añadir algo de variación
target_time = os.time(target_date)
end
return target_time
end
function adjust_for_sleep_hours(self, target_time)
if not self.scheduling_rules.respect_sleep_hours then
return target_time
end
local target_date = os.date("*t", target_time)
local hour = target_date.hour
-- Si cae en horas de sueño, mover a la mañana siguiente
if hour >= self.scheduling_rules.sleep_start or hour < self.scheduling_rules.sleep_end then
target_date.hour = self.scheduling_rules.sleep_end
target_date.min = math.random(0, 30) -- Entre 8:00 y 8:30 AM
-- Si ya pasó las 8 AM hoy, mover a mañana
if hour < self.scheduling_rules.sleep_end then
-- Ya es mañana
else
-- Mover al día siguiente
target_date.day = target_date.day + 1
end
target_time = os.time(target_date)
end
return target_time
end
function adjust_for_notification_limits(self, target_time)
-- Verificar límite diario de notificaciones
local notifications_today = self:count_notifications_for_date(target_time)
if notifications_today >= self.scheduling_rules.max_daily_notifications then
-- Mover al día siguiente
local target_date = os.date("*t", target_time)
target_date.day = target_date.day + 1
target_date.hour = 9 -- 9 AM del día siguiente
target_time = os.time(target_date)
end
return target_time
end
function count_notifications_for_date(self, timestamp)
local target_date = os.date("*t", timestamp)
local day_start = os.time({
year = target_date.year,
month = target_date.month,
day = target_date.day,
hour = 0, min = 0, sec = 0
})
local day_end = day_start + 24 * 60 * 60
local count = 0
local local_notifications = require "main.local_notifications"
for _, scheduled in pairs(local_notifications.scheduled) do
if scheduled.notification.scheduled_time >= day_start and
scheduled.notification.scheduled_time < day_end then
count = count + 1
end
end
return count
end
function M.should_send_notification(self, notification_type)
-- Verificar si el usuario ha estado inactivo lo suficiente
local time_since_last_activity = os.time() - self.user_patterns.last_activity
-- Reglas específicas por tipo de notificación
if notification_type == "energy_full" then
return time_since_last_activity > 30 * 60 -- 30 minutos de inactividad
elseif notification_type == "daily_login" then
return time_since_last_activity > 20 * 60 * 60 -- 20 horas
elseif notification_type == "limited_offer" then
return time_since_last_activity > 2 * 60 * 60 -- 2 horas
elseif notification_type == "comeback_3days" then
return time_since_last_activity > 3 * 24 * 60 * 60 -- 3 días
end
return true
end
return M
Push Notifications Server
1. Server-Side Implementation
Push Notification Server (Node.js)
// push_server.js
const admin = require('firebase-admin');
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
// Inicializar Firebase Admin
const serviceAccount = require('./firebase-service-account.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
// Base de datos de tokens (en producción usar base de datos real)
const userTokens = new Map();
const notificationTemplates = new Map();
// Configurar templates de notificaciones
function setupNotificationTemplates() {
notificationTemplates.set('daily_reward', {
title: {
en: 'Daily Reward Available!',
es: '¡Recompensa Diaria Disponible!',
fr: 'Récompense Quotidienne Disponible!'
},
body: {
en: 'Come back and claim your free coins',
es: 'Regresa y reclama tus monedas gratis',
fr: 'Revenez et réclamez vos pièces gratuites'
},
action: 'claim_reward',
icon: 'coin_icon'
});
notificationTemplates.set('friend_request', {
title: {
en: 'New Friend Request',
es: 'Nueva Solicitud de Amistad',
fr: 'Nouvelle Demande d\'Ami'
},
body: {
en: '{{friend_name}} wants to be your friend',
es: '{{friend_name}} quiere ser tu amigo',
fr: '{{friend_name}} veut être votre ami'
},
action: 'social_update',
icon: 'friend_icon'
});
notificationTemplates.set('limited_offer', {
title: {
en: 'Limited Time Offer!',
es: '¡Oferta por Tiempo Limitado!',
fr: 'Offre à Durée Limitée!'
},
body: {
en: '50% off coins - today only!',
es: '50% de descuento en monedas - ¡solo hoy!',
fr: '50% de réduction sur les pièces - aujourd\'hui seulement!'
},
action: 'open_store',
icon: 'offer_icon'
});
}
// Registrar token de usuario
app.post('/register-token', (req, res) => {
const { userId, token, platform, language } = req.body;
if (!userId || !token) {
return res.status(400).json({ error: 'Missing userId or token' });
}
userTokens.set(userId, {
token: token,
platform: platform,
language: language || 'en',
registered_at: new Date(),
last_seen: new Date()
});
console.log(`Token registered for user ${userId}: ${token}`);
res.json({ success: true });
});
// Enviar notificación a usuario específico
app.post('/send-notification', async (req, res) => {
const { userId, templateId, data, customMessage } = req.body;
try {
const userToken = userTokens.get(userId);
if (!userToken) {
return res.status(404).json({ error: 'User token not found' });
}
let notification;
if (customMessage) {
notification = customMessage;
} else {
const template = notificationTemplates.get(templateId);
if (!template) {
return res.status(400).json({ error: 'Template not found' });
}
notification = buildNotificationFromTemplate(template, userToken.language, data);
}
const message = {
token: userToken.token,
notification: {
title: notification.title,
body: notification.body,
image: notification.image
},
data: {
action: notification.action || 'default',
...data
},
android: {
notification: {
icon: notification.icon,
color: '#FF6B35',
sound: 'default',
channelId: 'default'
}
},
apns: {
payload: {
aps: {
badge: 1,
sound: 'default'
}
}
}
};
const response = await admin.messaging().send(message);
console.log('Notification sent successfully:', response);
res.json({ success: true, messageId: response });
} catch (error) {
console.error('Error sending notification:', error);
res.status(500).json({ error: error.message });
}
});
// Enviar notificación a múltiples usuarios
app.post('/send-bulk-notification', async (req, res) => {
const { userIds, templateId, data } = req.body;
try {
const messages = [];
for (const userId of userIds) {
const userToken = userTokens.get(userId);
if (!userToken) continue;
const template = notificationTemplates.get(templateId);
if (!template) continue;
const notification = buildNotificationFromTemplate(template, userToken.language, data);
messages.push({
token: userToken.token,
notification: {
title: notification.title,
body: notification.body
},
data: {
action: notification.action || 'default',
...data
}
});
}
if (messages.length === 0) {
return res.status(400).json({ error: 'No valid tokens found' });
}
const response = await admin.messaging().sendAll(messages);
console.log(`Bulk notification sent to ${response.successCount} users`);
res.json({
success: true,
successCount: response.successCount,
failureCount: response.failureCount
});
} catch (error) {
console.error('Error sending bulk notification:', error);
res.status(500).json({ error: error.message });
}
});
// Programar notificación
app.post('/schedule-notification', (req, res) => {
const { userId, templateId, data, scheduledTime } = req.body;
const delay = new Date(scheduledTime) - new Date();
if (delay <= 0) {
return res.status(400).json({ error: 'Scheduled time must be in the future' });
}
setTimeout(() => {
// Enviar notificación después del delay
sendScheduledNotification(userId, templateId, data);
}, delay);
res.json({ success: true, scheduledAt: scheduledTime });
});
function buildNotificationFromTemplate(template, language, data) {
const notification = {
title: template.title[language] || template.title.en,
body: template.body[language] || template.body.en,
action: template.action,
icon: template.icon
};
// Reemplazar placeholders
if (data) {
for (const [key, value] of Object.entries(data)) {
const placeholder = `{{${key}}}`;
notification.title = notification.title.replace(placeholder, value);
notification.body = notification.body.replace(placeholder, value);
}
}
return notification;
}
async function sendScheduledNotification(userId, templateId, data) {
try {
const userToken = userTokens.get(userId);
if (!userToken) return;
const template = notificationTemplates.get(templateId);
if (!template) return;
const notification = buildNotificationFromTemplate(template, userToken.language, data);
const message = {
token: userToken.token,
notification: {
title: notification.title,
body: notification.body
},
data: {
action: notification.action,
...data
}
};
await admin.messaging().send(message);
console.log('Scheduled notification sent to user:', userId);
} catch (error) {
console.error('Error sending scheduled notification:', error);
}
}
// Inicializar templates y servidor
setupNotificationTemplates();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Push notification server running on port ${PORT}`);
});
2. Server Communication
Cliente para Comunicación con Servidor
-- push_server_client.lua
local M = {}
function init(self)
self.server_url = sys.get_config("push.server_url", "")
self.api_key = sys.get_config("push.api_key", "")
self.user_id = nil
self.registered = false
end
function M.register_token(self, user_id, token, callback)
if not self.server_url or self.server_url == "" then
print("No push server URL configured")
if callback then callback(false, "no_server_url") end
return
end
self.user_id = user_id
local platform = sys.get_sys_info().system_name == "iPhone OS" and "ios" or "android"
local language = sys.get_sys_info().language
local data = {
userId = user_id,
token = token,
platform = platform,
language = language
}
local headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. self.api_key
}
http.request(self.server_url .. "/register-token", "POST",
function(self, id, response)
local success = response.status == 200
if success then
self.registered = true
print("Token registered successfully on server")
else
print("Failed to register token on server: " .. response.status)
end
if callback then callback(success, response) end
end,
headers, json.encode(data))
end
function M.send_user_activity(self, activity_data)
if not self.registered then return end
local data = {
userId = self.user_id,
activity = activity_data,
timestamp = os.time()
}
local headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. self.api_key
}
http.request(self.server_url .. "/user-activity", "POST",
function(self, id, response)
if response.status ~= 200 then
print("Failed to send user activity: " .. response.status)
end
end,
headers, json.encode(data))
end
function M.request_notification(self, template_id, data, callback)
if not self.registered then
if callback then callback(false, "not_registered") end
return
end
local request_data = {
userId = self.user_id,
templateId = template_id,
data = data or {}
}
local headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. self.api_key
}
http.request(self.server_url .. "/send-notification", "POST",
function(self, id, response)
local success = response.status == 200
if success then
print("Notification request sent successfully")
else
print("Failed to request notification: " .. response.status)
end
if callback then callback(success, response) end
end,
headers, json.encode(request_data))
end
return M
Engagement Strategy
1. Notification Campaigns
Campaign Manager
-- notification_campaigns.lua
local M = {}
function init(self)
self.campaigns = {}
self.user_segments = {}
self.campaign_stats = {}
-- Configurar campañas
self:setup_campaigns()
end
function M.setup_campaigns(self)
self.campaigns = {
onboarding = {
name = "Onboarding Flow",
target_segment = "new_users",
notifications = {
{template = "welcome", delay = 0},
{template = "tutorial_reminder", delay = 24 * 60 * 60}, -- 1 día
{template = "first_purchase_offer", delay = 3 * 24 * 60 * 60} -- 3 días
}
},
retention = {
name = "Retention Campaign",
target_segment = "at_risk_users",
notifications = {
{template = "comeback_reminder", delay = 24 * 60 * 60}, -- 1 día
{template = "special_offer", delay = 3 * 24 * 60 * 60}, -- 3 días
{template = "friend_invite", delay = 7 * 24 * 60 * 60} -- 7 días
}
},
engagement = {
name = "Daily Engagement",
target_segment = "active_users",
notifications = {
{template = "daily_reward", delay = 20 * 60 * 60}, -- 20 horas
{template = "energy_reminder", delay = 6 * 60 * 60}, -- 6 horas
{template = "limited_event", delay = 12 * 60 * 60} -- 12 horas
}
},
monetization = {
name = "Monetization Push",
target_segment = "potential_payers",
notifications = {
{template = "coin_offer", delay = 2 * 60 * 60}, -- 2 horas
{template = "premium_benefits", delay = 24 * 60 * 60}, -- 1 día
{template = "limited_bundle", delay = 48 * 60 * 60} -- 2 días
}
}
}
end
function M.segment_user(self, user_id, user_data)
local segment = "active_users" -- Default
-- Segmentar basado en datos del usuario
if user_data.days_since_install <= 7 then
segment = "new_users"
elseif user_data.days_since_last_play > 3 then
segment = "at_risk_users"
elseif user_data.total_purchases > 0 and user_data.days_since_last_purchase <= 30 then
segment = "paying_users"
elseif user_data.total_sessions > 10 and user_data.total_purchases == 0 then
segment = "potential_payers"
end
self.user_segments[user_id] = segment
return segment
end
function M.start_campaign_for_user(self, user_id, campaign_id, user_data)
local campaign = self.campaigns[campaign_id]
if not campaign then
print("Campaign not found: " .. campaign_id)
return false
end
-- Verificar si el usuario pertenece al segmento objetivo
local user_segment = self:segment_user(user_id, user_data)
if campaign.target_segment ~= "all" and user_segment ~= campaign.target_segment then
print(string.format("User %s (segment: %s) doesn't match campaign target: %s",
user_id, user_segment, campaign.target_segment))
return false
end
-- Programar notificaciones de la campaña
local scheduled_notifications = {}
for i, notification in ipairs(campaign.notifications) do
local notification_id = self:schedule_campaign_notification(
user_id, campaign_id, notification.template, notification.delay, user_data
)
if notification_id then
table.insert(scheduled_notifications, notification_id)
end
end
-- Trackear inicio de campaña
self:track_campaign_start(user_id, campaign_id, #scheduled_notifications)
print(string.format("Started campaign '%s' for user %s with %d notifications",
campaign.name, user_id, #scheduled_notifications))
return true
end
function schedule_campaign_notification(self, user_id, campaign_id, template, delay, user_data)
local smart_scheduler = require "main.smart_scheduler"
local optimal_delay = smart_scheduler:get_optimal_notification_time(delay)
local local_notifications = require "main.local_notifications"
return local_notifications:schedule_notification(template, optimal_delay, user_data)
end
function track_campaign_start(self, user_id, campaign_id, notification_count)
if not self.campaign_stats[campaign_id] then
self.campaign_stats[campaign_id] = {
started = 0,
completed = 0,
notifications_sent = 0,
notifications_opened = 0
}
end
local stats = self.campaign_stats[campaign_id]
stats.started = stats.started + 1
stats.notifications_sent = stats.notifications_sent + notification_count
-- Enviar a analytics
msg.post("main:/analytics", "track_event", {
event = "campaign_started",
properties = {
campaign_id = campaign_id,
user_id = user_id,
notification_count = notification_count
}
})
end
function M.track_campaign_notification_opened(self, campaign_id, template_id)
if self.campaign_stats[campaign_id] then
self.campaign_stats[campaign_id].notifications_opened =
self.campaign_stats[campaign_id].notifications_opened + 1
end
msg.post("main:/analytics", "track_event", {
event = "campaign_notification_opened",
properties = {
campaign_id = campaign_id,
template_id = template_id
}
})
end
function M.get_campaign_performance(self, campaign_id)
local stats = self.campaign_stats[campaign_id]
if not stats then return nil end
return {
open_rate = stats.notifications_opened / math.max(1, stats.notifications_sent),
completion_rate = stats.completed / math.max(1, stats.started),
total_started = stats.started,
total_notifications = stats.notifications_sent
}
end
return M
Analytics y Optimización
1. Notification Analytics
Sistema de Métricas
-- notification_analytics.lua
local M = {}
function init(self)
self.metrics = {
sent = 0,
delivered = 0,
opened = 0,
clicked = 0,
opt_outs = 0
}
self.template_metrics = {}
self.time_metrics = {}
self.segment_metrics = {}
end
function M.track_notification_sent(self, template_id, user_segment)
self.metrics.sent = self.metrics.sent + 1
-- Trackear por template
if not self.template_metrics[template_id] then
self.template_metrics[template_id] = {sent = 0, opened = 0, clicked = 0}
end
self.template_metrics[template_id].sent = self.template_metrics[template_id].sent + 1
-- Trackear por segmento
if not self.segment_metrics[user_segment] then
self.segment_metrics[user_segment] = {sent = 0, opened = 0, clicked = 0}
end
self.segment_metrics[user_segment].sent = self.segment_metrics[user_segment].sent + 1
-- Trackear por hora del día
local hour = tonumber(os.date("%H"))
if not self.time_metrics[hour] then
self.time_metrics[hour] = {sent = 0, opened = 0}
end
self.time_metrics[hour].sent = self.time_metrics[hour].sent + 1
-- Enviar a analytics externos
self:send_analytics_event("notification_sent", {
template_id = template_id,
user_segment = user_segment,
hour = hour
})
end
function M.track_notification_opened(self, template_id, user_segment, activated)
self.metrics.opened = self.metrics.opened + 1
if activated then
self.metrics.clicked = self.metrics.clicked + 1
end
-- Actualizar métricas por template
if self.template_metrics[template_id] then
self.template_metrics[template_id].opened = self.template_metrics[template_id].opened + 1
if activated then
self.template_metrics[template_id].clicked = self.template_metrics[template_id].clicked + 1
end
end
-- Actualizar métricas por segmento
if self.segment_metrics[user_segment] then
self.segment_metrics[user_segment].opened = self.segment_metrics[user_segment].opened + 1
if activated then
self.segment_metrics[user_segment].clicked = self.segment_metrics[user_segment].clicked + 1
end
end
-- Actualizar métricas por hora
local hour = tonumber(os.date("%H"))
if self.time_metrics[hour] then
self.time_metrics[hour].opened = self.time_metrics[hour].opened + 1
end
self:send_analytics_event("notification_opened", {
template_id = template_id,
user_segment = user_segment,
activated = activated,
hour = hour
})
end
function M.get_performance_report(self)
return {
overall = {
open_rate = self.metrics.opened / math.max(1, self.metrics.sent),
click_rate = self.metrics.clicked / math.max(1, self.metrics.sent),
ctr = self.metrics.clicked / math.max(1, self.metrics.opened),
opt_out_rate = self.metrics.opt_outs / math.max(1, self.metrics.sent)
},
by_template = self:calculate_template_performance(),
by_segment = self:calculate_segment_performance(),
by_hour = self:calculate_time_performance()
}
end
function calculate_template_performance(self)
local performance = {}
for template_id, metrics in pairs(self.template_metrics) do
performance[template_id] = {
open_rate = metrics.opened / math.max(1, metrics.sent),
click_rate = metrics.clicked / math.max(1, metrics.sent),
total_sent = metrics.sent
}
end
return performance
end
function calculate_segment_performance(self)
local performance = {}
for segment, metrics in pairs(self.segment_metrics) do
performance[segment] = {
open_rate = metrics.opened / math.max(1, metrics.sent),
click_rate = metrics.clicked / math.max(1, metrics.sent),
total_sent = metrics.sent
}
end
return performance
end
function calculate_time_performance(self)
local performance = {}
for hour, metrics in pairs(self.time_metrics) do
performance[hour] = {
open_rate = metrics.opened / math.max(1, metrics.sent),
total_sent = metrics.sent
}
end
return performance
end
function send_analytics_event(self, event_name, properties)
msg.post("main:/analytics", "track_event", {
event = event_name,
properties = properties
})
end
return M
Mejores Prácticas
1. User Experience
- Solicitar permisos en momento apropiado
- Personalizar mensajes según comportamiento del usuario
- Respetar horas de sueño y preferencias
- Ofrecer control granular de notificaciones
- No sobrecargar al usuario
2. Technical
- Validar tokens regularmente
- Manejar errores de envío gracefully
- Implementar retry logic
- Usar analytics para optimizar
- Testear en múltiples dispositivos
3. Content Strategy
- Crear mensajes valiosos y relevantes
- Usar localización apropiada
- A/B test diferentes mensajes
- Optimizar timing basado en datos
- Segmentar usuarios efectivamente
4. Compliance
- Seguir políticas de cada plataforma
- Implementar opt-out fácil
- Respetar GDPR y regulaciones locales
- Documentar consent del usuario
- Mantener data privacy
Esta implementación completa te proporciona un sistema sofisticado de notificaciones que maximiza el engagement mientras respeta la experiencia del usuario.