mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-11 01:28:03 +00:00
## Summary Adds an in-app deep link that opens the pricing table directly, for driving pilot users straight to subscribe (request from nav/Alex). Resolves [FE-1104](https://linear.app/comfyorg/issue/FE-1104). - `/?pricing=1` — on app load, open the pricing table. - `/?pricing=team` / `/?pricing=personal` — open it on the Team / Personal plan tab (via the existing `UnifiedPricingTable` `initialPlanMode`). - Gated to the **original owner** via `useWorkspaceUI().permissions.canManageSubscriptionLifecycle` (personal user, or a team workspace's original owner). A member or a promoted owner is a **silent no-op**: the app loads normally, the param is stripped, no 404 / error / toast. - Off-cloud (OSS): the loader isn't instantiated, so the param is ignored. - Survives the login redirect via the preserved-query system, same as `?invite` / `?create_workspace`. ## How Mirrors the established URL-loader pattern (`useInviteUrlLoader` / `useCreateWorkspaceUrlLoader`): - `preservedQueryNamespaces.ts` / `router.ts` — register the `pricing` namespace + tracker key. - New `usePricingTableUrlLoader.ts` — hydrate preserved query, read `pricing`, strip the param + `clearPreservedQuery` in a single replace before any await, then `await fetchMembers()` (resolves the original-owner gate; no-ops for personal) and open the table only when the gate allows. - `GraphCanvas.vue` — call the loader in `onMounted` after the create-workspace loader (cloud only; not gated on the team-workspaces flag so it also drives personal/legacy users). - `useSubscriptionDialog.ts` — new `'deep_link'` value on `SubscriptionDialogReason`. ## Telemetry Eligible opens emit the existing `subscription_required_modal_opened` PostHog event with the new `reason: 'deep_link'`. Ineligible-click bounce rate is derivable from the autocaptured pageview URL (`?pricing=…`), so no new event plumbing. ## Stacking / dependencies This feature needs two sibling stacks off `main`: - **FE-934** (`#12666`, base of this PR) — the `UnifiedPricingTable` + `showPricingTable({ planMode })`. - **FE-770** (`#12829`) — the `canManageSubscriptionLifecycle` gate. **Merged into this branch**, so the diff against the FE-934 base includes FE-770's changes until it lands. Review the single `feat(billing): deep link…` commit. Once both land on `main`, rebase onto `main` and the diff collapses to just this feature. Do not merge before FE-770 and FE-934. Post-Billing-V1 follow-up. End-state: swap the FE original-owner heuristic for the BE workspace-level `is_original_owner` flag when it lands (removes the members-fetch). ## Tests - Unit (`usePricingTableUrlLoader.test.ts`, 12 cases): opens for an original owner; `team`/`personal` tab preselect; silent no-op + param-strip for a member/promoted owner; proves the gate is read only after `fetchMembers` resolves; preserved-query restore; empty/non-string/absent/unrecognized param; members-fetch failure strips+clears without opening. - E2E (`browser_tests/tests/dialogs/pricingTableDeepLink.spec.ts`, `@cloud`, 4 cases, verified locally): personal owner opens + URL stripped; `?pricing=team` lands on the active Team tab; team original owner opens (real `is_original_owner` + email gate); team member is a silent no-op + URL stripped. - Typecheck + related unit suites (`useSubscriptionDialog`, `useWorkspaceUI`, `teamWorkspaceStore`) green. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com>
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
import type { Page } from '@playwright/test'
|
|
|
|
import type { RemoteConfig } from '@/platform/remoteConfig/types'
|
|
|
|
import { mockSystemStats } from '@e2e/fixtures/data/systemStats'
|
|
import { CloudAuthHelper } from '@e2e/fixtures/helpers/CloudAuthHelper'
|
|
import { jsonRoute } from '@e2e/fixtures/utils/jsonRoute'
|
|
|
|
interface CloudBootOptions {
|
|
/** Remote-config payload for `/api/features` (enables the flags under test). */
|
|
features: RemoteConfig
|
|
/** Body for `/api/settings` (defaults to `{}`). */
|
|
settings?: unknown
|
|
}
|
|
|
|
/**
|
|
* Stub the core endpoints the cloud app hits on boot so a raw `page` reaches the
|
|
* working app without falling through to the OSS devtools backend. Specs layer
|
|
* their own feature- or flow-specific routes on top.
|
|
*/
|
|
export async function mockCloudBoot(
|
|
page: Page,
|
|
{ features, settings = {} }: CloudBootOptions
|
|
) {
|
|
await page.route('**/api/features', (r) => r.fulfill(jsonRoute(features)))
|
|
await page.route('**/api/system_stats', (r) =>
|
|
r.fulfill(jsonRoute(mockSystemStats))
|
|
)
|
|
await page.route('**/api/users', (r) =>
|
|
r.fulfill(
|
|
jsonRoute({
|
|
storage: 'server',
|
|
migrated: true,
|
|
users: { 'test-user-e2e': 'E2E Test User' }
|
|
})
|
|
)
|
|
)
|
|
await page.route('**/api/user', (r) =>
|
|
r.fulfill(jsonRoute({ status: 'active' }))
|
|
)
|
|
await page.route('**/api/settings', (r) => r.fulfill(jsonRoute(settings)))
|
|
await page.route('**/api/userdata**', (r) => r.fulfill(jsonRoute([])))
|
|
await page.route('**/api/extensions', (r) => r.fulfill(jsonRoute([])))
|
|
await page.route('**/api/object_info', (r) => r.fulfill(jsonRoute({})))
|
|
await page.route('**/api/global_subgraphs', (r) => r.fulfill(jsonRoute({})))
|
|
await page.route('**/api/i18n', (r) => r.fulfill(jsonRoute({})))
|
|
await page.route('**/api/auth/session', (r) =>
|
|
r.fulfill(jsonRoute({ token: 'mock-workspace-token' }))
|
|
)
|
|
await page.route('**/releases**', (r) => r.fulfill(jsonRoute([])))
|
|
}
|
|
|
|
/**
|
|
* Mock Firebase auth and pre-select the e2e user so the cloud app boots
|
|
* signed-in. The signed-in email (`e2e@test.comfy.org`) is what the
|
|
* original-owner gate matches against the members self-row.
|
|
*/
|
|
export async function bootCloud(page: Page) {
|
|
const auth = new CloudAuthHelper(page)
|
|
await auth.mockAuth()
|
|
await page.addInitScript(() => {
|
|
localStorage.setItem('Comfy.userId', 'test-user-e2e')
|
|
})
|
|
}
|