Campaigns

Campaigns are the core mechanism the SDK uses to respond to application events. When you fire an event, the SDK evaluates all configured campaigns and returns the best matching one, if any is available.

Before requesting campaigns, make sure you have called initSdk and initialSetup on your Magify instance.

Requesting a campaign for an event

Call campaign(event, params, silent) at any point where you want to trigger campaign evaluation. The method returns Campaign?null means no campaign matched under current conditions.

// Basic request — triggers impression tracking and conversion events
val campaign = magify.campaign("level_finished")
if (campaign != null) {
    // handle campaign
}

// With optional parameters
val params = mapOf<String, Any>("difficulty" to "hard", "score" to 1500)
val campaign = magify.campaign("level_finished", params)

// Silent request — evaluates without counting an impression or firing analytics
val preview = magify.campaign("level_finished", silent = true)

Checking availability without triggering a campaign

Use isCampaignAvailableFor(campaignName, event, params?) to test whether a specific named campaign would fire for an event. This does not count as an impression.

val isAvailable = magify.isCampaignAvailableFor(
    campaignName = "summer_sale",
    event = "shop_opened"
)

if (isAvailable) {
    // show a teaser badge in the UI
}

Campaign types

CampaignType is an enum. The values available are:

LTO variants (see Limited-Time Offers below):

Campaign is a sealed class. Check the concrete subclass to access type-specific data:

when (campaign) {
    is SubscriptionCampaign -> {
        val products = campaign.products  // List<CampaignSubscriptionProduct>
    }
    is InAppCampaign -> {
        val products = campaign.products  // List<CampaignInAppProduct>
    }
    is InterstitialCampaign -> {
        val splash = campaign.splash
    }
    is RewardedVideoCampaign -> {
        val products = campaign.products  // List<CampaignRewardedProduct>
    }
    is BonusCampaign -> {
        val products = campaign.products  // List<CampaignBonusProduct>
    }
    is BannerCampaign -> {
        val position = campaign.position  // BannerPosition
    }
    is PromoCampaign -> {
        val destination = campaign.destination  // PromoDestination
    }
    is MixedCampaign -> {
        val products = campaign.products  // List<CampaignProduct>
    }
    is RateReviewCampaign -> { /* prompt rating */ }
    is NotificationCampaign -> { /* handle notification */ }
    else -> { /* UnsupportedCampaign — ignore */ }
}

All Campaign subclasses expose a common name property:

val name = campaign.name   // String — campaign name from the dashboard
val type = campaign.type   // CampaignType

Subscribing to campaign updates

Use subscribeCampaignUpdates(campaignName) to receive an Observable<Campaign> (RxJava2, io.reactivex.Observable) that emits whenever the resolved output for a named campaign changes — for example, after a remote config refresh or a status change that makes a different variant eligible.

val disposable = magify
    .subscribeCampaignUpdates("summer_sale")
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { campaign ->
        // update your UI with the new campaign variant
    }

// When the observer is no longer needed
magify.unsubscribeCampaignUpdates("summer_sale")

subscribeCampaignUpdates returns a hot Observable backed by the SDK's internal state. Disposing the returned Disposable stops the emission but does not remove the underlying subscription — call unsubscribeCampaignUpdates to release resources on the SDK side.

Limits

magify.limits returns the current server-side throttling configuration as a Limits object.

magify.initSdk {
    val limits = magify.limits

    // Minimum seconds between any two campaign impressions
    limits.globalIntervalSeconds?.let { println("Global interval: $it s") }

    // Per-type caps
    limits.inAppLimits?.let { cap ->
        println("In-app per session: ${cap.sessionLimit ?: "unlimited"}")
        println("In-app per day:     ${cap.dailyLimit ?: "unlimited"}")
        println("In-app global:      ${cap.globalLimit ?: "unlimited"}")
    }
}

Key Limits fields (all nullable — null means no limit configured):

ImpressionLimit has nullable integer fields: sessionLimit, dailyLimit, globalLimit (and the server-configured period cap periodLimit with periodInMinutes).

Limited-Time Offers

Limited-Time Offers (LTO) are time-bounded campaigns managed by the SDK. Your app observes changes through RxJava2 Observable streams (all are io.reactivex.Observable).

Reading active offers

magify.limitedOffers returns the current List<LtoInfo> synchronously. Each LtoInfo describes one active offer:

for (offer in magify.limitedOffers) {
    println("Campaign:    ${offer.campaignName}")
    println("Spot:        ${offer.spot}")            // UI placement identifier
    println("Start (ms):  ${offer.startTimeMillis}")
    println("End (ms):    ${offer.endTimeMillis}")   // derived from duration
}

Key LtoInfo properties:

Observing offer changes

Subscribe to granular streams to react to individual offer lifecycle events:

// Full list refresh — emits the complete current list whenever any offer changes
magify.observeLimitedOffers()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { offers -> refreshOfferUI(offers) }

// Individual offer events
magify.observeOfferAdded()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { offer -> showOfferBadge(offer) }

magify.observeOfferUpdated()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { offer -> updateOfferBadge(offer) }

magify.observeOfferRemoved()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { offer -> hideOfferBadge(offer) }

// Fired when the SDK has finished processing a completed offer
magify.observeCompleteOfferEvents()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { offer -> onOfferFinished(offer) }

All streams are hot Observables. Observe them as long as your UI is visible and dispose the Disposable when the screen is destroyed.

Completing an offer

Call completeOffer(campaignName) when the user converts (purchases the associated product, redeems the reward, etc.). This ends the offer lifecycle and emits on observeCompleteOfferEvents().

// After a successful purchase tied to the LTO
magify.completeOffer("summer_sale")

Presenting campaigns

After campaign(event) returns a non-null Campaign, use SimpleCampaignPresenter to display it. The class lives in com.magify.sdk.presenter and manages a single in-flight presentation.

SimpleCampaignPresenter

import com.magify.sdk.presenter.SimpleCampaignPresenter
import com.magify.sdk.presenter.CampaignPresenterCallback

val presenter = SimpleCampaignPresenter(
    popupCreativeStyle = null,      // optional @StyleRes Int for popup customization
    isCreativesCancelable = false,  // whether image creatives can be dismissed by back-press
    animator = null                 // optional DefaultCreativeAnimator for enter/exit transitions
)

All present* methods are @MainThread and must be called from the main thread. They take an Activity, the typed Campaign subclass, an optional timeout: Int? (seconds to wait for image load; null = no timeout), and a CampaignPresenterCallback.

CampaignPresenterCallback

val callback = object : CampaignPresenterCallback {
    override fun onCampaignImpression(type: CampaignType) {
        // impression counted — creative is visible
    }
    override fun onCampaignImpressionFail(campaignType: CampaignType, reason: String) {
        // creative could not be shown (e.g. "Timeout")
    }
    override fun onCampaignClick(type: CampaignType) {
        // user tapped the action button
    }
    override fun onCampaignClose(type: CampaignType) {
        // user dismissed the creative via the close button
    }
    override fun onDialogClose() {
        // underlying Dialog dismissed (covers both close and click-to-dismiss)
    }
}

Presenting by campaign type

In-app purchase campaign

val campaign = magify.campaign("shop_opened")
if (campaign is InAppCampaign) {
    presenter.presentInAppCampaign(
        activity = this,
        campaign = campaign,
        timeout = 5,
        callback = callback,
        productClickListener = { product ->  // CampaignInAppProduct
            // initiate purchase with product.productId
        }
    )
}

Subscription campaign

if (campaign is SubscriptionCampaign) {
    presenter.presentSubscriptionCampaign(
        activity = this,
        campaign = campaign,
        timeout = 5,
        callback = callback,
        productClickListener = { product ->  // CampaignProduct
            // initiate subscription purchase
        }
    )
}

Bonus campaign

if (campaign is BonusCampaign) {
    presenter.presentBonusCampaign(
        activity = this,
        campaign = campaign,
        timeout = 5,
        callback = callback,
        productClickListener = { product ->  // CampaignProduct
            // grant the bonus
        }
    )
}

Mixed campaign

if (campaign is MixedCampaign) {
    presenter.presentMixedCampaign(
        activity = this,
        campaign = campaign,
        timeout = 5,
        callback = callback,
        productClickListener = { product ->  // CampaignProduct
            // handle mixed product
        }
    )
}

Rewarded video campaign

if (campaign is RewardedVideoCampaign) {
    presenter.presentRewardedCampaign(
        activity = this,
        campaign = campaign,
        timeout = 5,
        callback = callback,
        productClickListener = { product ->  // CampaignRewardedProduct
            // grant the reward for product
        }
    )
}

Promo (cross-promo / external link) campaign

if (campaign is PromoCampaign) {
    presenter.presentPromoCampaign(
        activity = this,
        campaign = campaign,
        timeout = 5,
        callback = callback,
        redirection = { destination ->  // PromoDestination
            // open destination.url or navigate to the promoted app
        }
    )
}

Notification campaign

presentNotificationCampaign has no productClickListener — notification campaigns carry no product. The timeout parameter controls image load time.

if (campaign is NotificationCampaign) {
    presenter.presentNotificationCampaign(
        activity = this,
        campaign = campaign,
        callback = callback,
        timeout = 5
    )
}

Rate & review campaign

if (campaign is RateReviewCampaign) {
    presenter.presentRateReviewCampaign(
        activity = this,
        campaign = campaign,
        callback = callback,
        timeout = null
    )
}

Closing the current creative

presenter.closePresentedDialog()

Rendering LTO badge images

LtoBadgePresenter is a companion-object helper — no instance needed. Call it from the view that hosts your LTO badge.

import com.magify.sdk.presenter.LtoBadgePresenter

LtoBadgePresenter.presentBadgeImage(
    context = requireContext(),
    imageView = badgeImageView,
    imageUrl = offer.badgeCreative.imageUrl,   // nullable String
    placeholder = "badge_placeholder.png"       // file name in assets/, nullable
)

The method loads the image with Glide (disk-cached), falling back to the asset placeholder on error. It does not support Lottie animations — use badgeCreative.imageUrl for static images.

Next step

For tracking campaign impressions and clicks, see the Analytics section.

Related articles

Magify Service

LevelPlay / IronSource

Advertisement

Purchases

Conversion Tracking

App Features