From f653e029a18acb9a7ab0af36b3ef09d8a24f525b Mon Sep 17 00:00:00 2001 From: Ahmad Tash <144666528+AhmadTash@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:58:27 +0300 Subject: [PATCH 1/4] feat(web): gate paid features at the action instead of the page (#15562) --- .../engineering/ci-pr-review-hygiene.md | 3 + .../engineering/web-feature-anatomy.md | 9 +- .../ee-platform-plans-billing.md | 3 + .../web/public/locales/en/translation.json | 150 ++++++++++- .../web/src/app/components/feature-teaser.tsx | 138 ++++++++++ .../app/components/locked-feature-guard.tsx | 97 ++----- .../project-settings/pieces/index.tsx | 2 +- .../routes/mcp-server/pieces/pieces-tab.tsx | 2 +- .../routes/platform/infra/workers/index.tsx | 4 +- .../routes/platform/setup/pieces/index.tsx | 2 +- .../src/app/routes/platform/users/index.tsx | 31 ++- .../components/custom/data-table/index.tsx | 11 +- .../billing}/components/request-trial.tsx | 0 .../billing/hooks/use-feature-gate.tsx | 128 +++++++++ .../billing/hooks/use-seat-limit-guard.tsx | 23 +- .../hooks/use-team-project-limit-guard.tsx | 157 ++++++----- packages/web/src/features/billing/index.ts | 7 + .../features/billing/utils/feature-tier.ts | 6 + .../billing/utils/platform-features.ts | 145 +++++++++++ .../platform-admin/hooks/api-key-hooks.ts | 11 +- .../platform-admin/hooks/signing-key-hooks.ts | 11 +- .../components/create-project-button.tsx | 64 +++-- .../components/new-project-dialog.tsx | 77 ++++-- .../test/app/billing/feature-gating.test.tsx | 244 ++++++++++++++++++ 24 files changed, 1108 insertions(+), 217 deletions(-) create mode 100644 packages/web/src/app/components/feature-teaser.tsx rename packages/web/src/{app => features/billing}/components/request-trial.tsx (100%) create mode 100644 packages/web/src/features/billing/hooks/use-feature-gate.tsx create mode 100644 packages/web/src/features/billing/utils/feature-tier.ts create mode 100644 packages/web/src/features/billing/utils/platform-features.ts create mode 100644 packages/web/test/app/billing/feature-gating.test.tsx diff --git a/brain/knowledge/engineering/ci-pr-review-hygiene.md b/brain/knowledge/engineering/ci-pr-review-hygiene.md index 00a16b5ebcef..e8a0e431fc7c 100644 --- a/brain/knowledge/engineering/ci-pr-review-hygiene.md +++ b/brain/knowledge/engineering/ci-pr-review-hygiene.md @@ -48,6 +48,7 @@ Enforcement is the **`Codeowners review` repository ruleset** (active on the def - **`@activepieces/shared` re-exports from `@activepieces/core-execution`, so a partial rebuild produces phantom "has no exported member" errors in unrelated files.** Rebuilding `core/shared` against a stale `core/execution` dist drops those re-exports, and the API typecheck then fails in `ee/agent/*` on symbols like `GetPersonalizationConfigRequest` — which live in `core/execution/src/lib/workers/worker-contract.ts`, not in shared at all. It reads exactly like a bad merge. The dependency order that actually works is `core/utils` → `core/piece-types` → `core/formula` → `core/execution` → `core/shared` → `server/utils` → `pieces/framework` → `core/ai-providers`; skipping a link silently poisons everything downstream of it. The same staleness makes an editor report missing enum members that exist in the source. **There is no api typecheck "error baseline" — rebuild first, then trust it.** A stale `core/shared/dist` reliably yields a handful of `ee/agent` errors that look permanent enough to be waved off as known, which is how a real error hides among them; after `npx turbo run build --filter=@activepieces/shared`, `tsc --noEmit -p packages/server/api/tsconfig.app.json` comes back completely clean. The same staleness invents errors in files a rebase just touched, so during a conflict resolution rebuild before concluding you resolved it wrong. - **To pull a file back out of a PR, restore it from the merge-base, never from `origin/main`.** A PR's diff is computed against the merge-base, so `git checkout origin/main -- ` does not "revert" the file — it imports every change `main` made to it since the fork and attributes them to you. Dropping one web file from [#15001](https://github.com/activepieces/activepieces/pull/15001) that way would have silently added 82 insertions / 44 deletions of somebody else's work. `git checkout $(git merge-base origin/main HEAD) -- ` makes it byte-identical to where the branch started, so it leaves the diff entirely and merges cleanly instead of conflicting. Verify with `git diff --quiet $(git merge-base origin/main HEAD) -- ` before committing, and read `git status` first — a `bun.lock` left dirty by an earlier `bun install` loves to ride along on a commit like this. - **Retargeting a stacked PR to `main` does not drop its base branch — it merges the whole thing.** A PR opened against a long-lived feature branch shows a small diff *relative to that base*, but `gh pr edit --base main` only moves the target; the branch still contains every commit of its old base. [#14593](https://github.com/activepieces/activepieces/pull/14593) read as 2 docs files against `feat/autumn-billing-integration` and as 198 commits / 211 files / +12k lines against `main`. Check with `git diff --stat origin/main...` **before** retargeting, and if it disagrees with the PR page, cherry-pick that PR's own commits onto `main` and force-push instead. A "conflict" on such a PR is often against the feature base only — those same commits can apply to `main` cleanly. +- **Each link of a stack runs CI against its own base, so verifying the top proves nothing about the ones below it.** A refactor that lands on the bottom branch and is repaired two branches up is green where you tested and red on the PR that actually carries it, and the failure reads as unrelated because the file it names was fine when you last looked at it. Before pushing a stack, check out each branch in turn and run the package's typecheck, lint and full test suite there, not just at the tip; the suites legitimately differ (this stack's tip runs two more web tests than its base). Do the same after any rebase, since a conflict resolved in one replay can leave a lower branch importing something only a higher one defines. - **A deliberately stacked PR is charged only for its own delta, but each link pays its own `@activepieces/shared` bump.** `pr-size-check.ts` diffs `HEAD^1...HEAD^2` on the `pull_request` merge commit precisely so an ancestor PR is not re-counted — a stack member measures the same whether reviewed against `main` or against the PR below it, so splitting a large change into a stack really does buy budget rather than just moving it. Two things the split still costs you. Every PR in the stack that touches `packages/core/shared` needs its **own** version increment (0.158.0 then 0.159.0, not the same bump twice), and `bun.lock` records each workspace's version, so edit that one line **by hand** in each — regenerating sweeps `main`'s pending drift into your diff, per the regenerator trap above. And prove the split is faithful before pushing: tag the pre-split commit, then `git diff ..` must come back empty apart from those intentional version lines. A file that lands partially in one PR and grows in the next (a service that gains its read side, a test file that gains its endpoint cases) is exactly where a hand-split silently drops a hunk. - **A migration-check failure in a stack names the PR that *introduced* the file, which is usually not the branch you are sitting on.** `check-migration-rollback.ts` lists candidates with `git diff --name-only --diff-filter=A origin/$GITHUB_BASE_REF...HEAD`, so a migration is only ever scanned on the stack member that **adds** it; every branch above inherits the file and its CI stays green on that check. So the pasted failure belongs to the lowest link, and editing the file on the tip you happen to have checked out fixes nothing the gate looks at. Confirm ownership before touching anything — `gh pr view --json headRefName` for the branch the number really points at, then `git cat-file -e :` across the stack for the first branch holding the file — fix it there, and rebase upward. Two things make that cheap: the file is usually byte-identical across the stack (`git diff -- ` comes back empty), and `git rebase` reports `skipped previously applied commit` for the lower branch's pre-rebase commits, which is the expected signal rather than a lost commit. Verify each link the same way the split rule above does — tag the old tip, then `git diff ` must show only the intended delta. Hit on PR 15240 (`feat/mcp-activity-recording`) while its own migration read as a failure on `feat/mcp-activity-serve` two links up. - **A decision authored on a long-lived branch will collide on its number.** `brain/decisions/` numbers are assigned once and never reused, but the next free number is only knowable against `main` — two branches in flight both grab it. #14593 carried a `000024` that `main` had since filled, and `000025` too, so it landed as `000026`. Renumber against `main` at merge time and update every referring link; nothing in CI catches a duplicate number or a dead decision link. @@ -75,3 +76,5 @@ Enforcement is the **`Codeowners review` repository ruleset** (active on the def - **Merging up a stacked chain, a parent's rename reaches a child's call sites as a clean auto-merge — only the typecheck finds it.** Git conflicts on lines both sides edited, so a symbol renamed on the parent merges silently into a child that merely *calls* the old name: `feat/barrier-service` renamed `barrierService.receive` → `receiveSignal`, and `feat/barrier-edges`' `resume-controller.ts` kept calling `.receive(...)` with no conflict marker. Worse, both sides can each be right and still merge wrong — the same commit renamed `findPreCompletedByFlowRunId` while the child had rewritten that function's *body* to fix a real bug, so the conflict exposed only the signature and taking either side whole silently dropped work. Two habits: typecheck **every** branch in the chain after its merge rather than trusting a conflict-free result, and before picking a side run `git log --oneline $(git merge-base HEAD MERGE_HEAD)..HEAD -- ` on both to read what each side was actually doing. Stacks here run deep (the waitpoints → process-in-batches chain was 17 PRs), so one missed rename propagates through every branch above it. - **Docs videos belong on `cdn.activepieces.com/videos/docs/`, never in the repo — and `docs/resources/` is the legacy path that suggests otherwise.** Every `