# Protect a feature

Suppose exporting a report requires a paid plan. The user clicks **Export**; your app checks access, opens an `upgrade` offer if needed, refreshes access after checkout, and lets the server make the final decision about the export.

## 1. Prepare the offer and access check

1. [Publish an upgrade offer](https://plandalf.com/docs/product-guides/checkouts/create) with the slug `upgrade`. Test its price and checkout flow.
2. In your own authenticated app, expose a read endpoint such as `/api/access/export` that returns `{ "canExport": true }` or `{ "canExport": false }` for the current user. That route is **your application code**, not a Plandalf endpoint.
3. Protect the actual export endpoint on your server. It must check the current customer's entitlement even if the browser says access is allowed. Arrange for confirmed purchases to update that entitlement record.

The examples below assume your access endpoint and purchase handoff are working. Replace `https://your-org.plandalf.dev` with your organization host.

## 2. Connect the feature button

<scalar-tabs default="HTML">
<scalar-tab title="HTML">

If you can edit only HTML, have your **server** decide whether to render this upgrade button. The attribute opens checkout; it cannot check entitlement by itself.

```html title="upgrade.html"
<script defer src="https://your-org.plandalf.dev/js/plandalf-sdk.js"></script>
<button data-plandalf-present="upgrade">
  Unlock export
</button>
```

Keep the export endpoint protected server-side, and refresh the page or access state after a confirmed purchase.

</scalar-tab>
<scalar-tab title="JavaScript">

Register the named gate once, then trigger it when the customer asks to export. The sync callback refreshes your app's entitlement record after checkout.

```javascript title="export.js"
import { Plandalf } from '@plandalf/sdk'

async function readAccess() {
  const response = await fetch('/api/access/export')
  if (!response.ok) throw new Error('Could not check export access')
  return response.json()
}

const sdk = new Plandalf({ apiBase: 'https://your-org.plandalf.dev' })
let current = await readAccess()
sdk.addGate('export', 'upgrade', (fresh) =>
  fresh?.canExport ?? current.canExport,
)

document.querySelector('#export').addEventListener('click', async () => {
  const result = await sdk.gate('export', async () => {
    current = await readAccess()
    return current
  })

  if (result.access) {
    // Call your protected export endpoint; it checks access again.
  } else if (result.status === 'error') {
    // Show a retry or pending message, not the protected file.
  }
})
```

```html title="feature.html"
<button id="export">Export report</button>
```

</scalar-tab>
<scalar-tab title="React">

`useGate()` registers the gate and previews access. `trigger()` runs the interactive check and fallback checkout on click.

```tsx title="ExportFeature.tsx"
import { useCallback } from 'react'
import { PlandalfProvider, useGate } from '@plandalf/react'

async function readAccess() {
  const response = await fetch('/api/access/export')
  if (!response.ok) throw new Error('Could not check export access')
  return response.json() as Promise<{ canExport: boolean }>
}

function ExportButton() {
  const check = useCallback(async (fresh?: { canExport: boolean }) =>
    fresh?.canExport ?? (await readAccess()).canExport, [])
  const { access, checking, trigger } = useGate('export', 'upgrade', check)

  async function exportReport() {
    const result = await trigger(readAccess)
    if (result.access) {
      // Call your protected export endpoint; it checks access again.
    }
  }

  return <button disabled={checking} onClick={exportReport}>
    {access ? 'Export report' : 'Unlock export'}
  </button>
}

export function ExportFeature() {
  return <PlandalfProvider apiBase="https://your-org.plandalf.dev">
    <ExportButton />
  </PlandalfProvider>
}
```

</scalar-tab>
</scalar-tabs>

## 3. Verify denied, dismissed, and paid states

1. As a user without access, click Export. The `upgrade` checkout should open. Dismiss it: export stays blocked.
2. After confirming that the offer and integration are explicitly configured for sandbox use, complete a provider-supported test checkout. A `mode: 'test'` override can fall back to live for public visitors. If your confirmed-purchase handoff has not yet updated entitlement, keep access pending; do not hand over the report because checkout returned `complete`.
3. After your server records access, click again. The gate should allow the action without reopening checkout, and the export endpoint must independently allow it.
4. Test a failed access request. It should deny the action and show an error or retry state.

The checked-out SDK source reruns the gate check after sync; verify the installed package version before relying on that client behavior. The server authorization check is still required. A denied gate has already opened its fallback offer, so do not call `present()` again. See the [complete feature example](https://plandalf.com/docs/guides/gate-a-feature), [`gate()`](https://plandalf.com/docs/sdk/gate), and [`GateResult`](https://plandalf.com/docs/packages/sdk/interfaces/GateResult) for the exact outcomes.

Source: https://plandalf.com/docs/start/protect-a-feature
