Implementing Special Offers (Limited-Time Deals) for Mobile Games
Special Offers — time-limited deals with high value, shown at right moment to right player. Not "20% off everything", but targeted: new player on day 3 gets starter pack, stuck player on level 15 gets needed booster, churning user before deletion gets 50% winback.
Conversion difference between "show everyone" and "show right segment at right time" — 3–5x.
Offers System Architecture
Server config. All offers on server. Client requests "active offers for this player" on launch and shop open. Server returns only matching conditions:
{
"offerId": "starter_pack_d3",
"title": "Starter Pack",
"products": [
{"type": "gems", "amount": 500},
{"type": "chest", "itemId": "epic_chest", "amount": 3},
{"type": "resource", "itemId": "gold", "amount": 10000}
],
"iapProductId": "com.mygame.starter_pack_d3",
"originalPrice": 9.99,
"discountPercent": 70,
"validUntil": "2025-03-28T23:59:59Z",
"triggerConditions": {
"daysSinceInstall": {"min": 2, "max": 4},
"hasNeverPurchased": true,
"minLevel": 5
},
"maxPurchases": 1
}
triggerConditions checked server-side — client doesn't see segmentation logic, only ready offer list.
Display Triggers
Offer doesn't just sit in shop — it pops up contextually. Triggers:
Contextual trigger. Player failed level 3 times — show booster offer for exactly this level. Server knows current level and attempt count.
Time trigger. Day 3 after install — best moment for starter pack: player already invested, understood mechanics, ready to spend first dollar.
Re-engagement trigger. Player absent 5 days — on return show winback offer. Via push with deeplink to offer screen.
Achievement trigger. Player just beat cool level or unlocked new content — emotional peak, best moment to offer.
Timer and Urgency
struct OfferTimerView: View {
let expiresAt: Date
@State private var timeRemaining: String = ""
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("Time left: \(timeRemaining)")
.onReceive(timer) { _ in
let remaining = expiresAt.timeIntervalSinceNow
if remaining > 0 {
timeRemaining = formatDuration(remaining)
} else {
// Offer expired — hide
NotificationCenter.default.post(name: .offerExpired, object: nil)
}
}
}
}
Expiration checked server-side on "Buy" click — timer on client for UI only. Can't buy expired offer even if client didn't update state.
A/B Testing Offers
Firebase Remote Config lets test different offer variants:
- Group A: 500 gems + 3 chest for $1.99
- Group B: 300 gems + 5 chest + 1 skin for $1.99
Metric: conversion rate (bought / showed). Test on 1000+ players per group, statistical significance >95%.
For complex personalization use Firebase ML or custom recommendation engine based on segments: newcomers, mid-spenders, whales, churning users.
Purchase Limits and Anti-Abuse
maxPurchases: 1 at server level — offer can be bought once. Check: before purchase server looks purchase_history by offerId + userId.
Some offers "one-time-ever" (starter pack — never show again), others repeat with time limit (flash sale weekly). Logic in offer config, not client.
IAP Integration
Special Offer is separate product in App Store/Google Play with unique productId. Can't dynamically change price of existing IAP — platform limitation. So each price tier = separate registered product.
Apple allows Promotional Offers (subscription discount for existing buyers) and Introductory Offers (new subscriber discount) — built-in, don't need separate product IDs.
Timeline: basic system with 3–5 offer types, server config and timer — 2–3 days. Full system with personalization, A/B tests, contextual triggers and analytics — 1–1.5 weeks.







