> For the complete documentation index, see [llms.txt](https://docs.amply.tools/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.amply.tools/developer-guide/gating-actions.md).

# Gating actions

A **gate** lets a campaign pause the user's flow at a moment you choose, run an action (e.g. a rewarded ad), and resume — or back out — based on the outcome. For the product picture and when to reach for this, read [Gating an action](/concepts/gating-an-action.md) first. This page is the wiring.

There are two halves, and they live in different places:

1. **At the call site** — instead of firing an event and moving on, you `trackGated` the event and `await` a decision before doing the next step. This is opt-in per call site: a moment is gate-able only where you do this.
2. **At app startup** — you `registerGate` for the URLs that are outcome-bearing (the rewarded ad, the survey), supplying a presenter that runs the action and reports how it ended. Everything you don't register stays an ordinary fire-and-forget [deeplink](/developer-guide/handling-deeplinks.md).

If no campaign gates the event, or the SDK isn't ready, or anything fails, the decision is **proceed** — the gate is fail-open and never traps the user.

## 1. Gate the call site

{% tabs %}
{% tab title="iOS (Swift)" %}

```swift
func handleExport() async {
    let decision = await amply.trackGated(event: "ExportTapped", properties: [:])
    guard case .proceed = decision else { return }   // user backed out — don't export
    exporter.export(currentDocument)                  // your code, your live context
}
```

`trackGated` returns a `GateDecision` — `.proceed(reason:)` or `.cancelled`. Use the plain `guard case .proceed` form unless you specifically want the reason (`.completed` vs `.failOpen`, e.g. to grant a reward only on a real completion).
{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
lifecycleScope.launch {
    val decision = amply.trackGated("ExportTapped")
    if (decision is GateDecision.Proceed) {
        exporter.export(currentDocument)   // your code, your live context
    } // GateDecision.Cancelled -> user backed out, don't export
}
```

`trackGated` is a `suspend` function — call it from a coroutine. Match `GateDecision.Proceed(reason)` if you need `ProceedReason.Completed` vs `FailOpen`.
{% endtab %}

{% tab title="React Native (TS)" %}

```ts
async function handleExport() {
  const decision = await amply.trackGated('ExportTapped');
  if (decision.outcome !== 'proceed') return;   // user backed out
  await exporter.export(currentDocument);
}
```

The promise resolves to `{ outcome: 'proceed', reason: 'completed' | 'failOpen' }` or `{ outcome: 'cancelled' }`. It **never rejects** — any failure resolves to `proceed`.
{% endtab %}
{% endtabs %}

Track the same event with plain `track` elsewhere and it won't gate there. Use `trackGated` only where the call site can honor a wait.

## 2. Register what runs at the gate

Register once, at startup, for each outcome-bearing URL. The presenter runs the action and reports how it ended: completed, dismissed by the user, or unavailable (no fill, failed to load, not ready).

{% tabs %}
{% tab title="iOS (Swift)" %}

```swift
final class RewardedAdPresenter: CampaignPresenter {
    func present(params: [String: String], info: [String: Any], resolution: CampaignResolution) {
        let reward = Int(params["reward"] ?? "") ?? 0   // params come from the campaign URL query
        rewardedAd.show(reward: reward) { result in
            switch result {
            case .earned:  resolution.resolve(result: .completed)
            case .closed:  resolution.resolve(result: .dismissed)     // user backed out
            case .noFill, .failed: resolution.resolve(result: .unavailable)  // -> proceeds (fail-open)
            }
        }
    }
    func dismiss() { rewardedAd.tearDown() }   // SDK calls this if the gate is abandoned
}

// Keep the presenter and the token alive for as long as the gate should exist.
let adPresenter = RewardedAdPresenter()
let adGateToken = amply.registerGate(baseUrl: "yourapp://ad", presenter: adPresenter,
                                     onAbort: .cancel, timeoutMs: 60_000)
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
class RewardedAdPresenter : CampaignPresenter {
    override fun present(params: Map<String, String>, info: Map<String, Any>, resolution: CampaignResolution) {
        val reward = params["reward"]?.toIntOrNull() ?: 0
        rewardedAd.show(reward) { result ->
            when (result) {
                Result.EARNED -> resolution.resolve(CampaignResult.Completed)
                Result.CLOSED -> resolution.resolve(CampaignResult.Dismissed)
                else          -> resolution.resolve(CampaignResult.Unavailable)
            }
        }
    }
    override fun dismiss() { rewardedAd.tearDown() }
}

// Keep the presenter and the token alive for as long as the gate should exist.
val adPresenter = RewardedAdPresenter()
val adGateToken = amply.registerGate("yourapp://ad", adPresenter, onAbort = AbortPolicy.Cancel, timeoutMs = 60_000)
```

{% endtab %}

{% tab title="React Native (TS)" %}

```ts
await amply.registerGate(
  'yourapp://ad',
  (params, info, resolution) => {
    showRewardedAd(Number(params.reward ?? 0))
      .then(r => resolution.completed())   // or .dismissed() / .unavailable()
      .catch(() => resolution.unavailable());
  },
  { onAbort: 'cancel', timeoutMs: 60_000 },
);
```

{% endtab %}
{% endtabs %}

* **Match by base URL.** Register `yourapp://ad`; a campaign firing `yourapp://ad?type=rewarded&reward=10` matches it, and the query (`type`, `reward`, …) arrives as `params`. One registration serves many campaign variants — the dashboard tunes the parameters with no app release.
* **`onAbort`** decides what a user dismissal means: `cancel` (the gate's `.cancelled` — the flow backs out) or `proceed` (continue anyway). It **defaults to `cancel`**, which is what you want for value-exchange gates like rewarded ads; pass `proceed` for gates that should never hold the flow back.
* **`dismiss()` is required** on the presenter (there is no default). The SDK calls it when the gate is abandoned — caller cancelled, or the timeout fired — so you can tear down any UI you put on screen. Implement it even if it's a no-op for your action.
* **`timeoutMs`** is the fail-open deadline (default 60s, paused while the app is backgrounded). If the presenter never reports, the gate proceeds.
* **Outcomes, strictly:** `dismissed` is a *user-initiated* close only. Every infrastructure failure — no fill, failed to show, not ready — is `unavailable`, which proceeds. Never map an infra failure to `dismissed`, or you'd cancel the user's action for your own outage.

## Consent and permissions are not gates

### Withdrawing a gate

`registerGate` returns a token; `unregisterGate(token)` withdraws it.

A gate registered at launch and kept for the life of the app needs no withdrawal. One registered by something shorter-lived does: the registry outlives whatever registered into it, so a gate left behind by a screen that has gone away makes the next gated call on that URL wait out its full timeout with nothing on screen. A withdrawn gate resolves immediately instead — the way it was registered to resolve, per its `onAbort` — and costs the campaign no impression.

React Native holds the tokens for you; there is nothing to do at the JavaScript layer.

Don't `registerGate` ATT / privacy-consent / push-permission URLs. Route them as ordinary [deeplinks](/developer-guide/handling-deeplinks.md): present the prompt, record the answer as a [user attribute](/developer-guide/user-attributes.md), and let the flow continue. See [Gating an action — When not to gate](/concepts/gating-an-action.md#when-not-to-gate).

## Related

* [Gating an action](/concepts/gating-an-action.md) — the product model, use cases, and the safety promise
* [Handling deeplinks](/developer-guide/handling-deeplinks.md) — fire-and-forget actions (everything you don't gate)
* [Tracking events](/developer-guide/tracking-events.md) — `track` vs `trackGated`
