Skip to content
Back to skills

Iblai Vibe Monetization Subscription

CSecurity

Build the user-facing PurchasesTab inside the Profile page — list, detail, and cancel for the current user's subscriptions on a Platform. Use when the user mentions purchases, my subscriptions, subscription list, cancel subscription, stripe portal, grandfathered, legacy subscription, subscription detail, or cancel-at-period-end. See /iblai-vibe-monetization for the family index + auth + RBAC, /iblai-vibe-monetization-checkout for how the user got here (paywall, checkout, callback), /iblai-vib...

  • 15 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 2, 2026
developmentrustgoshellbashexpressrefactoringgitapifrontendbackend

Works with

  • cli
  • api
  • mcp

Security analysis

C63/100
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study
  • criticalSends environment variables or credentials to an external URL

Pro scans all 2 files and shows the line behind each finding

Scanned September 20, 2026

npx -y skills add iblai/vibe --skill iblai-vibe-monetization-subscription --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Iblai Vibe Monetization Subscription?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Iblai Vibe Monetization Subscription
[![Security: C — Skills Directory](https://www.skillsdirectory.com/api/skills/iblai-iblai-vibe-monetization-subscription/badge)](https://www.skillsdirectory.com/skills/iblai-iblai-vibe-monetization-subscription)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: iblai-vibe-monetization-subscription
description: Build the user-facing PurchasesTab inside the Profile page — list, detail, and cancel for the current user's subscriptions on a Platform. Use when the user mentions purchases, my subscriptions, subscription list, cancel subscription, stripe portal, grandfathered, legacy subscription, subscription detail, or cancel-at-period-end. See /iblai-vibe-monetization for the family index + auth + RBAC, /iblai-vibe-monetization-checkout for how the user got here (paywall, checkout, callback), /iblai-vibe-monetization-analytics for the admin subscriber view, /iblai-vibe-profile for the Profile shell that hosts this tab, and /iblai-vibe-auth for the token wiring.
globs:
alwaysApply: false
metadata:
  kind: ui
---

# /iblai-vibe-monetization-subscription

Wire the post-purchase surface inside the Profile page: a paginated, searchable
list of the current user's subscriptions, a detail view per subscription, and
a cancel flow that branches between an immediate cancel and a Stripe Customer
Portal redirect. The SDK ships `PurchasesTab` — Profile renders it
automatically when the Platform has monetization enabled — so most of the work
is wiring, not UI.

> **Common setup (brand, conventions, env files, verification):** see [docs/skill-setup.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/docs/skill-setup.md).

> **Verify the API before you call it.** Fetch the live OpenAPI schema at `{dm_url}/api/docs/schema/` (browsable at `{dm_url}/api/docs/`; `{dm_url}` = `https://api.$DOMAIN/dm`, `DOMAIN` from `iblai.env`, default `iblai.app`) and confirm the URL path, method, request body, and response shape for every endpoint you reach for. The schema is the source of truth; the URLs in this skill exist for orientation and may drift between releases. See [/iblai-vibe-monetization → references/schema-validation.md](../../billing/iblai-vibe-monetization/references/schema-validation.md) for the exact fetch routine.

## Prerequisites

- Auth must be set up first (`/iblai-vibe-auth`) — reuse the same token wiring.
- `.mcp.json` configured with `@iblai/mcp` (and skills installed via
  `npx skills add iblai/vibe --all`).
- `iblai.env` populated with `PLATFORM`, `DOMAIN`, `TOKEN`. If missing:
  `curl -o iblai.env https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/iblai.env`
- The Profile page is mounted (`/iblai-vibe-profile`). PurchasesTab is rendered by
  the Profile shell, not by your app code directly.
- The Platform has `enable_monetization === true`. The Profile shell reads
  this flag from `currentTenant` (SDK identifier; that's the Platform record)
  and only renders the Purchases tab when the flag is on. The `show_paywall`
  flag governs paywall enforcement on items — it is NOT what gates the
  Purchases tab. Use `enable_monetization`.
- The caller is authenticated. There is no role restriction: any logged-in
  user can see their own subscriptions. The list view, detail view, and
  cancel view all use `IsEdxAuthenticated` and filter by `request.user`.

## What you'll build

Most of this is already implemented inside the SDK `PurchasesTab`. The
spec below documents what the user will see so you can recognize when
something is mis-wired, and so any custom subscription UI you write
mirrors the same shape.

| Surface | Content |
|---|---|
| Search bar | Debounced 300ms input; passes `search` to the list query (backend filters by `item_id__icontains`). |
| Filters | Status select (All / Active / Trialing / Canceled / Past Due) + item-type select (All / Agent / Course / Program / Pathway) + total count chip. |
| Subscription card | Item name, type badge, plan name + amount/interval, "Renews"/"Expires" date, "Since" date, cancel-at-period-end warning row, status badge, "Legacy" badge when grandfathered. |
| Detail view | Status, plan, type, renewal/expiry date, since date, grandfathered badge, "Type `cancel` to confirm" input + destructive cancel button. |
| Cancel result | Either a "Subscription Canceled" success card OR an "Open Stripe Portal" card — depends on the response branch. |
| Pagination | 8 items per page; Previous / Page N / Next controls; disabled when there's no next/previous page. |

## Step 1: Validate the API schema

Three endpoints back this surface. Confirm each one in the live OpenAPI schema
before writing any client code — the current schema has the shapes below at
fetch time, but a future release may shift fields. See
[references/cancel-flow.md](references/cancel-flow.md) for the full payloads
and the response-branching contract.

```bash
DOMAIN=$(grep -m1 '^DOMAIN=' iblai.env 2>/dev/null | cut -d= -f2-)
curl -sS "https://api.${DOMAIN:-iblai.app}/dm/api/docs/schema/" -o /tmp/iblai_schema.yaml
grep -E "my-subscriptions/|subscription/cancel/|subscription/$" /tmp/iblai_schema.yaml
```

You should see all three. Endpoints live under the **DM base**
(`{dm_url}`, e.g. `https://api.iblai.app/dm`) and require `Authorization:
Token <DM token>` — the **DM token**, not the AXD token (using the AXD
token returns `401`). Item-keyed endpoints expose both a canonical
(`unique_id`-keyed) form and the legacy composite form; the SDK still
builds composite URLs.

| Capability | Canonical (recommended) | Composite (legacy) |
|---|---|---|
| Subscription read | `GET {dm_url}/api/billing/items/{item_unique_id}/subscription/` | `GET {dm_url}/api/billing/platforms/{platform_key}/items/{item_type}/{item_id}/subscription/` |
| Subscription cancel | `POST {dm_url}/api/billing/items/{item_unique_id}/subscription/cancel/` | `POST {dm_url}/api/billing/platforms/{platform_key}/items/{item_type}/{item_id}/subscription/cancel/` |
| User's full subscription list | — (platform-scoped, no canonical) | `GET {dm_url}/api/billing/platforms/{platform_key}/my-subscriptions/` |

All three require an authenticated user; the backend scopes by `request.user`
+ `platform_key`, so there is no way to read or cancel another user's
subscription. `ItemSubscriptionCancelView` is hard-locked to `request.user`
— operators cannot cancel on behalf of another user through this API.

## Step 2: Wire the Profile integration

The Profile page shell owns this tab. Confirm `/iblai-vibe-profile` has been set
up — if it has not, do that skill first and come back.

When the Profile shell mounts it reads the current Platform record. The SDK
exposes that record under the literal identifier `currentTenant` (legacy name
— treat the field as "the active Platform"). If
`currentTenant?.enable_monetization` is truthy, the shell appends a
`Purchases` tab to the tab strip and renders `PurchasesTab` when it is
active:

```ts
// inside the SDK Profile component
const isPurchasesEnabled = currentTenant?.enable_monetization;
const tabs = [
  // …other tabs
  ...(isPurchasesEnabled
    ? [{ id: 'purchases', label: 'Purchases', renderIcon: renderLucideIcon(ShoppingBag) }]
    : []),
];

{activeTab === 'purchases' && <PurchasesTab org={tenant} username={username} />}
```

You do not need to mount `PurchasesTab` yourself. If the tab is missing,
debug in this order:

1. Is the user authenticated and on `/iblai-vibe-profile`?
2. Is `enable_monetization` actually `true` on the current Platform? Check
   the platform list endpoint or `localStorage` cache. Do NOT confuse it
   with `show_paywall` — that flag gates paywall enforcement on items and
   has no effect on this tab.
3. `PurchasesTab` is a private component owned by the `Profile` shell —
   it's NOT re-exported by `@iblai/iblai-js/web-containers`. Integrate via
   `<Profile>` (which renders `PurchasesTab` when
   `currentTenant.enable_monetization === true`); standalone use requires
   copying the component from
   `packages/web-containers/src/components/profile/purchases/purchases-tab.tsx`.

## Step 3: Customize the list query

This step is only relevant if you are building a **custom** subscriptions
surface (a billing-only page, a widget on the dashboard, an admin overlay).
If you are using the SDK `PurchasesTab`, skip ahead to Step 6 to verify
cache invalidation.

```ts
import { useGetMySubscriptionsQuery, type SubscriptionObject } from '@iblai/iblai-js/data-layer';

const { data, isLoading, isFetching } = useGetMySubscriptionsQuery(
  {
    platform_key,
    page,
    page_size: 8,
    ...(statusFilter !== 'all' ? { status: statusFilter } : {}),
    ...(typeFilter !== 'all' ? { item_type: typeFilter } : {}),
    ...(debouncedSearch.trim() ? { search: debouncedSearch.trim() } : {}),
  },
  { refetchOnMountOrArgChange: true },
);
```

Note: the SDK's `MySubscriptionsParams` type lists `item_name?` but the
backend only honors `search` (filters by `item_id__icontains`). Use
`search` for text matching — `item_name` is dropped silently by the
`SubscriptionListQPSerializer`.

The response envelope is the standard Platform pagination shape:

```ts
type MySubscriptionsResponse = {
  count: number;
  next_page: number | null;
  previous_page: number | null;
  results: SubscriptionObject[];
};
```

Note: the runtime paginator (`StandardPageNumberPagination` at
`core/utils/pagination.py`) returns `next_page`/`previous_page` as
integers (or `null`). The OpenAPI schema documents `next`/`previous` as
URI strings (DRF default) — that's schema drift. Trust the runtime
shape shown above.

`SubscriptionObject.price.amount` is a **string** (DRF decimal). Use the
SDK pattern `parseFloat(sub.price.amount)` or pass the string straight into
`Intl.NumberFormat(...).format()` — do not multiply / add the raw string.

Debounce the search input 300ms before forwarding it to the query, and
reset `page` to `1` whenever any filter or the debounced search changes.
Otherwise the user lands on an empty page-3 when the result set shrinks.

## Step 4: Detail view + cancel flow

Each card opens a detail view. Refetch the canonical record (the list payload
is summary-only on some Platforms) and wire the cancel mutation:

```ts
import {
  useGetItemSubscriptionQuery,
  useCancelSubscriptionMutation,
} from '@iblai/iblai-js/data-layer';

const { data: fetched, isLoading } = useGetItemSubscriptionQuery(
  { platform_key, item_type: sub.item_type, item_id: sub.item_id },
  { refetchOnMountOrArgChange: true },
);

const [cancelSubscription, { isLoading: isCanceling }] = useCancelSubscriptionMutation();
```

`useCancelSubscriptionMutation` returns ONE of two payload shapes — branch on
which key is present:

```ts
const result = await cancelSubscription({
  platform_key,
  item_type: sub.item_type,
  item_id: sub.item_id,
  ...(isRecurring ? { return_url: window.location.href } : {}),
}).unwrap();

if (result.portal_url) {
  // Recurring sub with a Stripe customer — open the Stripe Customer Portal.
  window.open(result.portal_url, '_blank');
} else if (result.status === 'canceled') {
  // One-time, grandfathered, or no-Stripe-customer — already canceled.
  // `result` is the FULL ItemSubscriptionSerializer record (21 fields)
  // with the new `status='canceled'` and `canceled_at` set.
  toast.success('Subscription canceled');
}
```

The three response branches the backend may return:

- **Immediate cancel** (one-time, grandfathered, or no
  `stripe_subscription_id`): full `ItemSubscriptionSerializer` response —
  21 fields — with `status='canceled'` and `canceled_at` populated.
- **Portal handoff** (recurring with a `stripe_customer_id`): single-field
  `{ portal_url }` via `PortalUrlResponseSerializer`. The actual Stripe
  cancellation happens inside the Stripe Customer Portal.
- **400** if recurring but `stripe_customer_id` is missing on the user.
- **404** if no subscription exists for the caller on this item.
- **500** if Stripe Customer Portal creation throws.

The destructive button must remain disabled until the user types the literal
string `cancel` into a confirmation input. This is what `PurchasesTab` does —
match it in any custom UI. Treat the cancel button as you would any other
irreversible action.

See [references/cancel-flow.md](references/cancel-flow.md) for the full
response shapes, the server-side branching logic, and the optional
`return_url` body field.

## Step 5: Grandfathered subscriptions

Some users are granted legacy access — historical purchases, comped seats,
admin-issued lifetime passes. The backend flags these with
`is_grandfathered: true` on `SubscriptionObject`. PurchasesTab renders a
small amber "Legacy" / "Grandfathered" badge on both the card and the
detail view.

Grandfathered subscriptions have NO Stripe customer. When the user cancels,
the backend takes the immediate-cancel branch and returns the FULL
`ItemSubscriptionSerializer` record (21 fields) with the new
`status='canceled'` and `canceled_at` populated — there is no portal URL
because there is nothing for Stripe to manage. Render the
"Subscription Canceled" success card and refresh the list.

In any custom UI, treat `is_grandfathered` as decorative — do not change
the cancel button copy. The cancel mutation handles the routing for you.

## Step 6: Cache invalidation

`useCancelSubscriptionMutation` invalidates four RTK Query tags:

- `mySubscriptions` — the Purchases list refetches and the cancelled row
  flips to the `canceled` status filter (or drops off the current page).
- `itemSubscription` — the detail view picks up the new `status` and
  `canceled_at` timestamp.
- `accessCheck` — any gated content (`useCheckAccessQuery` on a paywalled
  page, the `PaywallModal` trigger on a locked agent) flips back to
  locked. Forget this and the rest of the app shows a stale "you have
  access" UI.
- `subscribers` — the admin subscriber list (rendered by
  `/iblai-vibe-monetization-analytics`) reflects the churn immediately.

If you are building a custom cancel surface that calls a thin wrapper
around the endpoint instead of the SDK mutation, replicate this exact tag
set. Dropping `accessCheck` is the most common bug.

## Verify

After wiring:

1. Open the Profile page on a Platform with `enable_monetization === true`
   and confirm the Purchases tab is in the tab strip.
2. Log in as a user with at least one subscription and verify the list
   renders 8 cards per page with the correct status / type / Legacy badges.
3. Type into the search box; confirm the debounce kicks in after 300ms
   and the list filters by the `search` param (matches against `item_id`).
4. Open a recurring subscription detail, type `cancel` into the
   confirmation input, click the destructive button, and confirm a
   Stripe Customer Portal card renders with an "Open Stripe Portal" CTA.
5. Open a one-time or grandfathered subscription, repeat the cancel flow,
   and confirm the "Subscription Canceled" card renders instead.
6. After a cancel, navigate to a paywalled item the user previously had
   access to and confirm it is now locked (the `accessCheck` invalidation
   worked).
7. Use the pagination Previous / Next controls and confirm `page` resets
   to 1 when any filter or the debounced search changes.

## Common mistakes

- **Assuming the cancel mutation always returns `portal_url`.** It does
  not. One-time purchases, grandfathered subscriptions, and any
  subscription without a Stripe customer return `{ status: 'canceled' }`
  immediately. Branch on which key is present, not on subscription type.
- **Forgetting to invalidate `accessCheck` in a custom cancel wrapper.**
  The cancel succeeds, the Purchases list refreshes, but every gated page
  in the app keeps showing "you have access" until a hard reload. Always
  invalidate `accessCheck` (and `subscribers`) alongside the two
  subscription tags.
- **Gating the Purchases tab on `show_paywall`.** Wrong flag.
  `show_paywall` controls paywall enforcement on items. The Purchases tab
  is gated on `enable_monetization`. They are independent.
- **Building a custom `PurchasesTab` from scratch.** The Profile shell owns
  the tab; the SDK exports the component; both ship a tested UI for
  search, filters, detail, cancel, and pagination. Add a custom widget
  elsewhere if you must, but do not replace the one Profile renders.
- **Doing arithmetic on `price.amount` directly.** It is a string.
  `parseFloat` first, or pass it untouched to `Intl.NumberFormat`. Don't
  add `"19.00"` to `"5.00"` and ship `"19.005.00"` in production.
- **Hardcoding URL paths.** The SDK constants live in
  `MONETIZATION_ENDPOINTS` (`packages/data-layer/src/features/monetization/`).
  When refactoring, import from there — do not retype the path.

## MCP tools for further detail

With `@iblai/mcp` in `.mcp.json`, query the MCP tools for richer help:

- `get_hook_info("useGetMySubscriptionsQuery")`
- `get_hook_info("useGetItemSubscriptionQuery")`
- `get_hook_info("useCancelSubscriptionMutation")`
- `get_component_info("Profile")`

## Files in this skill's scope

Frontend (`iblai/ibl-web-frontend`):

- `packages/web-containers/src/components/profile/purchases/purchases-tab.tsx`
  — the SDK `PurchasesTab` component (search, filters, list, detail,
  cancel branching, pagination).
- `packages/web-containers/src/components/profile/index.tsx` — the Profile
  shell that gates `Purchases` on `currentTenant?.enable_monetization`
  and renders `<PurchasesTab org={tenant} username={username} />`.
- `packages/data-layer/src/features/monetization/custom-api-slice.ts`
  — `getMySubscriptions`, `getItemSubscription`, `cancelSubscription`
  endpoints and their `providesTags` / `invalidatesTags` wiring.
- `packages/data-layer/src/features/monetization/types.ts` —
  `SubscriptionObject`, `SubscriptionPrice`, `MySubscriptionsParams`,
  `MySubscriptionsResponse`, `CancelSubscriptionArgs`,
  `CancelSubscriptionResponse`.

Backend (`iblai/ibl-dm-pro`):

- `web/ibl-dm-core-apps/ibl-dm-billing-app/billing/views.py`
  - `ItemSubscriptionView` at line 1853 — GET detail.
  - `ItemSubscriptionCancelView` at line 1885 — POST cancel, server-side
    branching between immediate cancel and Stripe Customer Portal.
  - `UserAllSubscriptionsView` at line 2196 — GET paginated list.
- `web/ibl-dm-core-apps/ibl-dm-billing-app/billing/serializers.py`
  - `ItemSubscriptionListSerializer`, `ItemSubscriptionSerializer`,
    `SubscriptionCancelSerializer`, `PortalUrlResponseSerializer`.

## Related skills

- [/iblai-vibe-monetization](../../billing/iblai-vibe-monetization/SKILL.md) — family index, auth,
  schema validation, Platform flags, RBAC, the full monetization API slice.
- [/iblai-vibe-monetization-checkout](../../billing/iblai-vibe-monetization-checkout/SKILL.md) —
  how the user got here: PaywallModal, `useCheckAccessQuery` (402-as-success),
  checkout session creation, guest checkout, public pricing.
- [/iblai-vibe-monetization-analytics](../../billing/iblai-vibe-monetization-analytics/SKILL.md) —
  the Platform admin's view of the same data: revenue, subscribers list,
  paywalls list, admin cancel tool.
- [/iblai-vibe-monetization-configure](../../billing/iblai-vibe-monetization-configure/SKILL.md) —
  the Platform admin's MonetizationTab inside Account: paywall config,
  pricing CRUD, Item Details → Paywall → Pricing wizard.
- [/iblai-vibe-monetization-onboard](../../billing/iblai-vibe-monetization-onboard/SKILL.md) —
  Stripe Connect Express onboarding for the Platform seller.
- [/iblai-vibe-profile](../../users/iblai-vibe-profile/SKILL.md) — the Profile shell that
  hosts this tab.
- [/iblai-vibe-auth](../../start/iblai-vibe-auth/SKILL.md) — token wiring this tab depends on.
- [BRAND.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/BRAND.md)
  — colors, typography, spacing, component styles for any custom subscription
  surface you build alongside the SDK tab.

Files in this skill

  • SKILL.md20.6 KB
  • references/cancel-flow.md9.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.