> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encorekit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build your own button

> Draw your own gifts button or claim reminder while Encore decides what it shows, counts its views and taps, and opens the reward sheet.

<Info>
  **Requires SDK 2.4.0+** on every platform. See [Minimum SDK versions](/publishers/triggers/overview#minimum-sdk-versions).
</Info>

[Gifts Button](/publishers/triggers/gifts-button) and [Claim Reminder](/publishers/triggers/claim-reminder) are the recommended way in: one line each, and Encore can change and test their looks without an app update. When your design needs the entry drawn in your app's own style, draw it yourself instead of skipping the entry. Encore still does everything behind it: it decides when the entry should show and what it shows, counts each view and tap, opens the reward sheet, and hands you the result. After a claim, your entry can turn into a reminder for that claim (it does by default), as [Gifts Button does](/publishers/triggers/gifts-button#after-a-claim).

Everything on [Trigger components](/publishers/triggers/overview) about the reward sheet, claims, and analytics applies. What differs: you draw it, so Encore's looks, heights, copy, and colors don't apply.

## How it works

You create one claim handle per placement and read its state. The state says what to draw:

| State | When | What it carries |
| - | - | - |
| `hidden` | The reward sheet has no offers for this user at this placement (its own holdout included), the offers could not be read, the SDK isn't configured yet, the use case is the churn one, or Encore's rules hide the entry after a completion | Nothing. Draw nothing |
| `gifts` | The reward sheet has offers | The number of offers, and up to eight of their brands' logos |
| `reminder` | The user claimed an offer this placement's sheet still lists, and hasn't finished it | The brand's name and logo, the offer's perk, when it was claimed, when the reminder ends (24 hours after the claim), and how many claims are open |
| `thanks` | When Encore shows a thank you: the user finished an offer today, hasn't claimed another or tapped your entry since, and the sheet has more offers | The number of offers, and up to eight of their brands' logos, as for `gifts`. A tap opens the sheet and ends it |

You attach your view to the handle. Encore then counts a view when at least half of it is on screen for one second with no sheet over it. On iOS, Android, React Native and the web, a tap on the attached view opens the reward sheet for that placement; on Flutter, call `claim.tap()` from your view. A tap while the state is `hidden` does nothing.

The state changes on its own: by default it turns into the reminder after the user claims an offer and back into gifts when the claim is finished, its 24 hours end, or the user dismisses it, and to hidden when the offers run out. Encore can change what follows a claim and a completion (below). Redraw whenever it changes.

## Add it

<CodeGroup>
  ```swift iOS theme={null}
  @StateObject var claim = Encore.claim(placement: "streak")

  var body: some View {
    switch claim.state {
    case .hidden:
      EmptyView()
    case .gifts(let gifts):
      StreakGiftRow(count: gifts.offerCount, logos: gifts.logoURLs)
        .encoreClaim(claim)
    case .reminder(let reminder):
      MyReminder(name: reminder.advertiserName, until: reminder.deadline,
                 onOpen: { Task { await claim.tap() } },
                 onClose: { claim.dismiss() })
        .encoreClaim(claim, handlesTap: false)
    case .thanks(let gifts):
      MyThanksRow(count: gifts.offerCount)
        .encoreClaim(claim)
    }
  }
  ```

  ```kotlin Android theme={null}
  val claim = rememberEncoreClaim(placement = "streak")

  when (val state = claim.state) {
    is ClaimState.Hidden -> Unit
    is ClaimState.Gifts -> StreakGiftRow(state.offerCount, state.logoUrls,
        Modifier.encoreClaim(claim))
    is ClaimState.Reminder -> MyReminder(state.advertiserName, state.deadline,
        onOpen = { claim.tap() },
        onClose = claim::dismiss,
        modifier = Modifier.encoreClaim(claim, handlesTap = false))
    is ClaimState.Thanks -> MyThanksRow(state.offerCount, Modifier.encoreClaim(claim))
  }
  ```

  ```tsx React Native theme={null}
  const { claim, state, tap, dismiss } = useEncoreClaim({ placement: 'streak' });

  if (state.kind === 'hidden') return null;
  return (
    <EncoreClaimView claim={claim} handlesTap={state.kind !== 'reminder'}>
      {state.kind === 'reminder' ? (
        <MyReminder name={state.advertiserName} until={state.deadline}
                    onOpen={() => tap()} onClose={dismiss} />
      ) : state.kind === 'thanks' ? (
        <MyThanksRow count={state.offerCount} />
      ) : (
        <MyGiftRow count={state.offerCount} logos={state.logoUrls} />
      )}
    </EncoreClaimView>
  );
  ```

  ```dart Flutter theme={null}
  EncoreClaim(
    placement: 'streak',
    builder: (context, state, claim) => switch (state) {
      EncoreClaimHiddenState() => const SizedBox.shrink(),
      EncoreClaimGiftsState(:final offerCount) => MyGiftRow(count: offerCount, onTap: claim.tap),
      EncoreClaimReminderState(:final advertiserName, :final deadline) =>
          MyReminder(name: advertiserName, until: deadline,
              onTap: () => claim.tap(target: EncoreClaimTarget.card), onClose: claim.dismiss),
      EncoreClaimThanksState(:final offerCount) => MyThanksRow(count: offerCount, onTap: claim.tap),
    },
  )
  ```

  ```javascript Web SDK theme={null}
  const claim = Encore.claim({ placement: 'streak', onResult: (result) => {} });
  const row = document.querySelector('#my-gift-row');

  const unsubscribe = claim.subscribe((state) => {
    row.hidden = state.kind === 'hidden';
    if (state.kind === 'gifts') row.textContent = `${state.offerCount} gifts waiting`;
    if (state.kind === 'reminder') row.textContent = `Finish your ${state.advertiserName} gift`;
    if (state.kind === 'thanks') row.textContent = 'Thanks! More gifts are waiting';
  });
  const unbind = claim.bind(row);

  // When you remove the row, let go of everything above.
  function removeRow() {
    unsubscribe();
    unbind();
    claim.destroy();
    row.remove();
  }
  ```

  ```tsx Web SDK (React) theme={null}
  const { state, ref } = useEncoreClaim({ placement: 'streak' });

  if (state.kind === 'hidden') return null;
  return (
    <button ref={ref} className="my-row">
      {state.kind === 'reminder'
        ? `Finish your ${state.advertiserName} gift`
        : state.kind === 'thanks'
          ? 'Thanks! More gifts are waiting'
          : `${state.offerCount} gifts waiting`}
    </button>
  );
  ```
</CodeGroup>

The options are `placement` (required), `useCase`, `autoOpen`, `publisherReward`, and `onResult`.

**Encore decides what comes next.** As with Gifts Button, Encore sets what your entry shows after a claim and after a completion, and can test it, without an app update. After a claim the state can turn into `reminder` or stay `gifts`; after a completed claim it can be `gifts`, `hidden`, or `thanks`. Draw every state and your entry follows. When Encore's rules can't be read, a claim turns the state into `reminder` and a completion turns it back into `gifts`, as by default, so a failed rules request never hides your entry. If the offers themselves can't be read, the state is `hidden`, as for the drop-in trigger components.

**Opening by itself.** Encore may open the reward sheet by itself when a test calls for it: at most once a day per placement, only after your view was really on screen, only while your app is in the foreground, and never over another Encore sheet. Set `autoOpen` to off to never: `autoOpen: .off` on iOS, `AutoOpen.OFF` on Android, `AutoOpen.off` on React Native, `EncoreAutoOpen.off` on Flutter, and `'off'` on the web.

On Android, `claim.state` is a Compose state, so a Compose view that reads it redraws on every change; outside Compose, collect `claim.stateFlow`. For Views or code outside Compose, make the handle with `Encore.claim(placement = "streak")`, attach a view with `claim.bind(view)`, and call `claim.close()` when you no longer need it. `rememberEncoreClaim` closes its handle when it leaves the composition. The reminder state also carries `perk`, the offer's perk as the sheet showed it.

## A view with its own buttons

When your view has buttons of its own, such as a close button on your reminder, don't let the whole view open the sheet. Report what's visible and call the tap yourself:

* **iOS and Android:** pass `handlesTap: false` to the modifier (on an Android View, `claim.bind(view, handlesTap = false)`), and call `claim.tap()` from your own button. On iOS, `tap()` returns the sheet's result; on Android, pass `onDone` to get it, as in `claim.tap(onDone = { result -> })`. To grant a prize, use `onResult` instead (see [Pass your prize and read the result](#pass-your-prize-and-read-the-result)).
* **React Native:** pass `handlesTap={false}` to `EncoreClaimView`, and call `tap()` from `useEncoreClaim` in your own button.
* **Flutter:** `EncoreClaim` never opens the sheet from a tap by itself; call `claim.tap()` from your own buttons, with `target: EncoreClaimTarget.card` for a reminder card. It returns the sheet's result.
* **Web:** `bind(element, { tap: false })` counts the whole element's visibility and leaves taps to you; call `claim.tap()` from your button. In React, pass `tap: false` to `useEncoreClaim` and put `ref` on the element. Even with taps on, a click that starts on a control inside the bound element, such as your close button, never opens the sheet.

`claim.dismiss()` hides the reminder, every claim it was showing, on this device. Call it from your close button. It does nothing while the state isn't `reminder`.

## Pass your prize and read the result

Pass `publisherReward` and `onResult` when you create the handle, as for [Gifts Button](/publishers/triggers/gifts-button#pass-your-prize-and-read-the-result). `onResult` is called once per sheet the handle opens, after the sheet closes, whether the tap came from your attached view or from your own `claim.tap()`. The result `tap()` returns is the same one, so grant your prize in `onResult` only, or an app that grants from both pays twice.

The grant rule is the same as for Gifts Button. Everywhere but the web, grant when `userConfirmedCompletion` is true on the result's claim, keyed on the transaction ID. On the web, keep the claim's `transactionId` and grant when the `offer_completed` webhook confirms it.

<CodeGroup>
  ```swift iOS theme={null}
  @StateObject var claim = Encore.claim(
      placement: "streak",
      publisherReward: PublisherReward(id: "coins_100", title: "100 coins"),
      onResult: { result in
          if let claim = result.claim, claim.userConfirmedCompletion {
              // Grant once per claim: key on the transaction, else on this user and campaign.
              grantCoins(100, dedupeKey: claim.transactionId ?? "\(userId):\(claim.campaignId)")
          }
      }
  )
  ```

  ```kotlin Android theme={null}
  val claim = rememberEncoreClaim(
      placement = "streak",
      publisherReward = PublisherReward(id = "coins_100", title = "100 coins"),
      onResult = { result ->
          val done = result.claim
          if (done != null && done.userConfirmedCompletion) {
              // Grant once per claim: key on the transaction, else on this user and campaign.
              grantCoins(100, dedupeKey = done.transactionId ?: "$userId:${done.campaignId}")
          }
      },
  )
  ```

  ```tsx React Native theme={null}
  const { claim, state, tap, dismiss } = useEncoreClaim({
    placement: 'streak',
    publisherReward: { id: 'coins_100', title: '100 coins' },
    onResult: (result) => {
      if (result.status === 'presented' && result.claim?.userConfirmedCompletion) {
        // Grant once per claim: key on the transaction, else on this user and campaign.
        grantCoins(100, result.claim.transactionId ?? `${userId}:${result.claim.campaignId}`);
      }
    },
  });
  ```

  ```dart Flutter theme={null}
  EncoreClaim(
    placement: 'streak',
    publisherReward: const EncorePublisherReward(id: 'coins_100', title: '100 coins'),
    onResult: (result) {
      final claim = result.claim;
      if (claim != null && claim.userConfirmedCompletion) {
        // Grant once per claim: key on the transaction, else on this user and campaign.
        grantCoins(100, claim.transactionId ?? '$userId:${claim.campaignId}');
      }
    },
    builder: (context, state, claim) => MyEntry(state: state, claim: claim),
  )
  ```

  ```javascript Web SDK theme={null}
  const claim = Encore.claim({
    placement: 'streak',
    publisherReward: { id: 'coins_100', title: '100 coins' },
    onResult: (result) => {
      // A claim isn't a completion: keep its transaction ID and grant on the offer_completed webhook.
      if (result.status === 'claimed' && result.transactionId) saveClaim(result.transactionId);
    },
  });
  ```
</CodeGroup>

## Accessibility

You own the labels. When the handle takes the tap, each platform makes your view one control, and you give it a label that says what a tap does:

* **iOS:** `.encoreClaim(claim)` adds the button trait to the view it modifies. On a stack with several children, SwiftUI passes that trait to each child, so VoiceOver reads every text and image as a separate button. Combine the children into one element and label it, before the modifier:

  ```swift theme={null}
  StreakGiftRow(count: gifts.offerCount, logos: gifts.logoURLs)
      .accessibilityElement(children: .combine)
      .accessibilityLabel("\(gifts.offerCount) gifts waiting")
      .encoreClaim(claim)
  ```

  With `handlesTap: false` the modifier adds no trait, so your own buttons, such as a close button, stay separate elements.
* **Android:** `Modifier.encoreClaim(claim)` makes the view one clickable element with the button role, and TalkBack reads its children's text as one label. When that text doesn't say what a tap does, add a `contentDescription` with `Modifier.semantics`. On an Android View, `claim.bind(view)` only sets a click listener, so set the view's `contentDescription` yourself.
* **React Native:** `EncoreClaimView` wraps your children in one `Pressable` with the button role. Pass it `accessibilityLabel`.
* **Flutter:** `EncoreClaim` adds no semantics, because your own widgets call `claim.tap()`. Wrap an entry made of several widgets in `MergeSemantics`, or in `Semantics(button: true, label: ...)`.
* **Web:** `bind(element)` makes an element that isn't already a button or link behave like one: it adds `role="button"`, lets it take keyboard focus, and opens the sheet on Enter and Space. Give it an `aria-label` when its text isn't enough.

## Testing your states

Draw every state, then check each one on a debug build with your test API key (`pk_test_…`), which serves a demo catalog:

* **`gifts`:** shows once the reward sheet at your placement has offers. On the test key it does.
* **`hidden`:** shows at a placement whose sheet has no offers, and before `configure()`. Check that your screen looks right without the entry.
* **`reminder`:** open the sheet from your entry and claim an offer. The test key lets the claim go through without a live campaign. Back in your app, the state turns into `reminder` for 24 hours. Your close button's `claim.dismiss()` turns it back into `gifts`.
* **`thanks`:** opt-in on Encore's side. After a completion the default is `gifts`, so you see `thanks` only once Encore turns it on for your app. Ask your Encore contact to turn it on for your test app if you want to check it.

Claims are kept per user on the device. To start over, `identify()` a new test user. The SDKs have no public hooks for adding or clearing claims.

## Experiments and analytics

Encore can test what your entry does after a claim and after a completion, as it tests the drop-in trigger components. The reward sheet it opens takes part in its own experiments too: when the sheet's experiment holds a user out, the state is `hidden`.

Encore reports your entry's views, taps, and dismissals as it does for its own trigger components, marked as your own button. You don't send any events.

<Warning>
  Don't also mount a Gifts Button or Claim Reminder at the same placement and screen as your own entry: the user would see two entries for one sheet.
</Warning>

## Related

* [Gifts Button](/publishers/triggers/gifts-button): Encore's own gifts entry, and what happens after a claim.
* [Claim Reminder](/publishers/triggers/claim-reminder): what the reminder shows, and when.
* [Trigger components](/publishers/triggers/overview): what every trigger component shares.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.