diff --git a/packages/backend/src/adapters/connector-setup.service.spec.ts b/packages/backend/src/adapters/connector-setup.service.spec.ts
index 757e688e..5b5fd892 100644
--- a/packages/backend/src/adapters/connector-setup.service.spec.ts
+++ b/packages/backend/src/adapters/connector-setup.service.spec.ts
@@ -145,6 +145,34 @@ describe('ConnectorSetupService — install', () => {
expect(out.isError).toBe(true);
expect(adapters.importAdapter).not.toHaveBeenCalled();
});
+
+ it('at the limit, offers an admin on the free trial the card trial', async () => {
+ const { service, ctx, licenseGuard } = build({ role: 'ADMIN' });
+ licenseGuard.checkCanCreateConnector.mockRejectedValueOnce(new Error('Trial limit reached (2 connectors).'));
+ licenseGuard.getUsage.mockResolvedValueOnce({ plan: 'trial', connectors: { current: 2, max: 2 } });
+ const out: any = await service.install(ctx, { adapter: 'openplz' });
+ expect(out.body).toMatchObject({
+ error: 'Trial limit reached (2 connectors).',
+ upgradeUrl: 'https://cloud.example.com/start-trial',
+ });
+ expect(out.body.whatTheUserCanDo).toMatch(/nothing is charged before the trial ends/);
+ });
+
+ it('at the limit, sends an admin on a paid plan to the licence page', async () => {
+ const { service, ctx, licenseGuard } = build({ role: 'ADMIN' });
+ licenseGuard.checkCanCreateConnector.mockRejectedValueOnce(new Error('Connector limit reached.'));
+ licenseGuard.getUsage.mockResolvedValueOnce({ plan: 'cloud_starter', connectors: { current: 5, max: 5 } });
+ const out: any = await service.install(ctx, { adapter: 'openplz' });
+ expect(out.body.upgradeUrl).toBe('https://cloud.example.com/settings/license');
+ });
+
+ it('at the limit, tells an editor who can lift it, without a billing link', async () => {
+ const { service, ctx, licenseGuard } = build({ role: 'EDITOR' });
+ licenseGuard.checkCanCreateConnector.mockRejectedValueOnce(new Error('Trial limit reached (2 connectors).'));
+ const out: any = await service.install(ctx, { adapter: 'openplz' });
+ expect(out.body.upgradeUrl).toBeUndefined();
+ expect(out.body.whatTheUserCanDo).toMatch(/administrator/);
+ });
});
describe('ConnectorSetupService — links', () => {
diff --git a/packages/backend/src/adapters/connector-setup.service.ts b/packages/backend/src/adapters/connector-setup.service.ts
index b4a70f8b..79e985a6 100644
--- a/packages/backend/src/adapters/connector-setup.service.ts
+++ b/packages/backend/src/adapters/connector-setup.service.ts
@@ -171,7 +171,7 @@ export class ConnectorSetupService implements SharedSetupProvider, OnModuleInit
try {
await this.licenseGuard.checkCanCreateConnector(ctx.userId, ctx.organizationId);
} catch (err: any) {
- return { isError: true, body: { error: String(err?.message ?? err), dashboard: `${ctx.dashboardBase}/settings/license` } };
+ return { isError: true, body: await this.planLimitAnswer(ctx, String(err?.message ?? err)) };
}
let imported: Awaited>;
@@ -227,6 +227,34 @@ export class ConnectorSetupService implements SharedSetupProvider, OnModuleInit
};
}
+ /**
+ * The plan's connector limit stopped an install. Billing is mentioned here
+ * and only here: the answer is the reason the request failed, with the one
+ * page that lifts the limit. An admin on the free trial gets the card-trial
+ * offer (nothing charged before the trial ends); other admins the licence
+ * page; members are told who can do it.
+ */
+ private async planLimitAnswer(ctx: SetupContext, reason: string): Promise> {
+ const member = await this.prisma.organizationMember
+ .findFirst({
+ where: { userId: ctx.userId, organizationId: ctx.organizationId, deactivatedAt: null },
+ select: { role: true },
+ })
+ .catch(() => null);
+ if (member?.role !== 'ADMIN') {
+ return { error: reason, whatTheUserCanDo: 'Ask a workspace administrator to upgrade the plan, or remove a connector they no longer need.' };
+ }
+ const usage = await this.licenseGuard.getUsage(ctx.userId, ctx.organizationId).catch(() => null);
+ const onTrial = usage?.plan === 'trial';
+ return {
+ error: reason,
+ whatTheUserCanDo: onTrial
+ ? 'Add a card to continue the trial on the full plan (nothing is charged before the trial ends), or remove a connector.'
+ : 'Choose a plan with more connectors, or remove a connector.',
+ upgradeUrl: `${ctx.dashboardBase}${onTrial ? '/start-trial' : '/settings/license'}`,
+ };
+ }
+
// ── Status ────────────────────────────────────────────────────────────
async status(ctx: SetupContext): Promise {
diff --git a/packages/frontend/src/app/login/page.tsx b/packages/frontend/src/app/login/page.tsx
index ed6ec476..34b12601 100644
--- a/packages/frontend/src/app/login/page.tsx
+++ b/packages/frontend/src/app/login/page.tsx
@@ -19,6 +19,7 @@ import {
cardTrialEligible,
parsePlanIntent,
readCardTrialPrompt,
+ saveAuthorizationReturn,
savePlanIntent,
parsePromoCode,
savePromoCode,
@@ -177,6 +178,31 @@ function LoginForm() {
return true;
};
+ /**
+ * Cloud only: someone who signed up from "Connect" in an AI client (the
+ * redirect is the pending authorization) is offered the card trial before
+ * approving, once. Every way out of /start-trial (skip, a cancelled or a
+ * completed checkout) returns to this authorization, which the backend
+ * keeps for 30 minutes. Resolves true when it has navigated.
+ */
+ const offerCardTrialBeforeAuthorization = async (
+ u: { id?: string; role?: string } | null | undefined,
+ token: string,
+ ): Promise => {
+ if (!isCloudMode || !returningToAuthorization || !u?.id || u.role !== 'ADMIN') return false;
+ if (readCardTrialPrompt(u.id) !== null) return false;
+ try {
+ const lic = await license.getStatus(token);
+ if (!cardTrialEligible({ isCloud: true, role: u.role, license: lic })) return false;
+ } catch {
+ return false;
+ }
+ writeCardTrialPrompt(u.id, 'shown');
+ saveAuthorizationReturn(redirectTo);
+ router.push('/start-trial');
+ return true;
+ };
+
// Surface a failure the SSO callback redirected back with.
useEffect(() => {
if (errorParam) setError(errorParam);
@@ -305,6 +331,7 @@ function LoginForm() {
login(result.accessToken, result.user);
if (isCloudMode && needsLicenseSetup && returningToAuthorization) {
await license.activateTrial(result.accessToken).catch(() => undefined);
+ if (await offerCardTrialBeforeAuthorization(result.user, result.accessToken)) return;
goTo(redirectTo);
} else if (isCloudMode && needsLicenseSetup) {
// Cloud mode: auto-activate trial for verified users
@@ -352,7 +379,9 @@ function LoginForm() {
if (isCloudMode && returningToAuthorization) {
// Verification already created the trial; the user is in the middle
- // of connecting an AI client, so take them straight back to approve.
+ // of connecting an AI client: offer the card trial once, then back
+ // to approve.
+ if (await offerCardTrialBeforeAuthorization(verifiedUser, authToken)) return;
goTo(redirectTo);
} else if (isCloudMode) {
// Cloud mode: auto-activate trial
diff --git a/packages/frontend/src/app/settings/license/activate/page.tsx b/packages/frontend/src/app/settings/license/activate/page.tsx
index 6f616eee..70f4a11a 100644
--- a/packages/frontend/src/app/settings/license/activate/page.tsx
+++ b/packages/frontend/src/app/settings/license/activate/page.tsx
@@ -9,6 +9,7 @@ import { buttonVariants } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { sessionStore } from '@/lib/storage';
+import { takeAuthorizationReturn } from '@/lib/card-trial';
const KEY_RE = /^AMCP-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/;
const PENDING_KEY = 'amcp_pending_license_key';
@@ -45,6 +46,7 @@ function LicenseActivateInner() {
const key = rawKey.toUpperCase();
const [phase, setPhase] = useState('loading');
const [message, setMessage] = useState('');
+ const [redirectLabel, setRedirectLabel] = useState('Redirecting to settings…');
const ran = useRef(false);
useEffect(() => {
@@ -85,10 +87,14 @@ function LicenseActivateInner() {
sessionStore.remove(PENDING_KEY);
setPhase('success');
setMessage(res.message || 'License activated successfully.');
+ // Started the card trial while connecting an AI client: finish that
+ // authorization (it waits 30 minutes) instead of opening settings.
+ const next = takeAuthorizationReturn('/settings/license');
+ setRedirectLabel(next.startsWith('/auth/') ? 'Taking you back to finish connecting your AI client…' : 'Redirecting to settings…');
// A full load rather than a client-side route change: the app shell
// (trial banner, licence wall) read the licence before activation and
// would keep showing the trial until the next reload.
- setTimeout(() => window.location.replace('/settings/license'), 1500);
+ setTimeout(() => window.location.replace(next), 1500);
})
.catch((err: any) => {
setPhase('error');
@@ -112,7 +118,7 @@ function LicenseActivateInner() {
{phase === 'success' && (
{message}
-
Redirecting to settings…
+
{redirectLabel}
)}
diff --git a/packages/frontend/src/app/start-trial/page.tsx b/packages/frontend/src/app/start-trial/page.tsx
index 95372849..39d2d8e9 100644
--- a/packages/frontend/src/app/start-trial/page.tsx
+++ b/packages/frontend/src/app/start-trial/page.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth-context';
import { license } from '@/lib/api';
@@ -14,8 +14,10 @@ import {
cardTrialEligible,
formatTrialEnd,
planById,
+ readAuthorizationReturn,
readPlanIntent,
readPromoCode,
+ takeAuthorizationReturn,
writeCardTrialPrompt,
type PlanSelection,
} from '@/lib/card-trial';
@@ -39,6 +41,19 @@ export default function StartTrialPage() {
const [ready, setReady] = useState(false);
const [selection, setSelection] = useState(DEFAULT_SELECTION);
const [promo, setPromo] = useState(null);
+ // Set when the user came from "Connect" in an AI client: every way out of
+ // this page goes back to that authorization.
+ const [authorizationReturn, setAuthorizationReturn] = useState(null);
+
+ /** Leave for the waiting authorization (a backend page) or for `fallback`. */
+ const leave = useCallback(
+ (fallback: string) => {
+ const target = takeAuthorizationReturn(fallback);
+ if (target.startsWith('/auth/')) window.location.assign(target);
+ else router.replace(target);
+ },
+ [router],
+ );
useEffect(() => {
if (isLoading || !deploymentModeLoaded) return;
@@ -47,7 +62,7 @@ export default function StartTrialPage() {
return;
}
if (deploymentMode !== 'cloud' || user.role !== 'ADMIN') {
- router.replace('/');
+ leave('/');
return;
}
let live = true;
@@ -56,21 +71,22 @@ export default function StartTrialPage() {
.then((lic) => {
if (!live) return;
if (!cardTrialEligible({ isCloud: true, role: user.role, license: lic })) {
- router.replace('/');
+ leave('/');
return;
}
+ setAuthorizationReturn(readAuthorizationReturn());
setTrialEnd(cardTrialDisplayEnd(lic.expiresAt));
setSelection(readPlanIntent() ?? DEFAULT_SELECTION);
setPromo(readPromoCode());
setReady(true);
})
.catch(() => {
- if (live) router.replace('/');
+ if (live) leave('/');
});
return () => {
live = false;
};
- }, [isLoading, deploymentModeLoaded, deploymentMode, token, user, router]);
+ }, [isLoading, deploymentModeLoaded, deploymentMode, token, user, router, leave]);
const handleStart = async () => {
if (!user) return;
@@ -80,8 +96,9 @@ export default function StartTrialPage() {
const handleSkip = () => {
if (user) writeCardTrialPrompt(user.id, 'skipped');
- // The dashboard decides what comes next (usually the /welcome wizard).
- router.replace('/');
+ // Back to the AI client's authorization if one is waiting; otherwise the
+ // dashboard decides what comes next (usually the /welcome wizard).
+ leave('/');
};
if (!ready) {
@@ -132,6 +149,11 @@ export default function StartTrialPage() {
lower connector limit, and you can add a card
any time to unlock your full plan.
+ {authorizationReturn && (
+
+ Either way, you go straight back to finish connecting your AI client.
+
+ )}
diff --git a/packages/frontend/src/lib/card-trial.ts b/packages/frontend/src/lib/card-trial.ts
index 14591689..5b8ebcb0 100644
--- a/packages/frontend/src/lib/card-trial.ts
+++ b/packages/frontend/src/lib/card-trial.ts
@@ -1,4 +1,5 @@
import { storage } from './storage';
+import { safeRedirect } from './safe-redirect';
/**
* The card trial on AnythingMCP Cloud: right after sign-up an admin is offered
@@ -260,3 +261,64 @@ export function formatTrialEnd(
...(timeZone && { timeZone }),
});
}
+
+// ── Back to an AI client's authorization ────────────────────────────────────
+
+/**
+ * Someone who signs up from "Connect" in Claude is offered the card trial
+ * before approving the connection. The authorization waits for them (the
+ * backend keeps it 30 minutes), so the way back is remembered across the
+ * detour: skipping, a cancelled checkout (Stripe returns to /start-trial) and
+ * a completed one (the licence site returns to /settings/license/activate)
+ * all end on the same consent page.
+ *
+ * localStorage, not sessionStorage: Stripe can open in another tab on some
+ * mobile browsers. Only the backend's own /auth/ pages qualify.
+ */
+const AUTH_RETURN_KEY = 'amcp_card_trial_return';
+export const AUTH_RETURN_TTL_MS = 30 * 60 * 1000;
+
+export function isAuthorizationPath(path: string | null | undefined): path is string {
+ return typeof path === 'string' && path.startsWith('/auth/') && safeRedirect(path, '') === path;
+}
+
+export function saveAuthorizationReturn(path: string, now: number = Date.now()): void {
+ if (!isAuthorizationPath(path)) return;
+ storage.set(AUTH_RETURN_KEY, JSON.stringify({ path, savedAt: now }));
+}
+
+/** The consent page to go back to, or null when none is waiting (or it expired). */
+export function readAuthorizationReturn(now: number = Date.now()): string | null {
+ const raw = storage.get(AUTH_RETURN_KEY);
+ if (!raw) return null;
+ try {
+ const data = JSON.parse(raw);
+ const savedAt = Number(data?.savedAt);
+ if (
+ isAuthorizationPath(data?.path) &&
+ Number.isFinite(savedAt) &&
+ savedAt <= now &&
+ now - savedAt <= AUTH_RETURN_TTL_MS
+ ) {
+ return data.path;
+ }
+ } catch {
+ /* fall through */
+ }
+ storage.remove(AUTH_RETURN_KEY);
+ return null;
+}
+
+export function clearAuthorizationReturn(): void {
+ storage.remove(AUTH_RETURN_KEY);
+}
+
+/**
+ * Where to go after the card-trial offer: the waiting authorization if there
+ * is one (spent here, so it is followed once), otherwise `fallback`.
+ */
+export function takeAuthorizationReturn(fallback: string): string {
+ const path = readAuthorizationReturn();
+ clearAuthorizationReturn();
+ return path ?? fallback;
+}
diff --git a/packages/frontend/tests/e2e/card-trial-claude-signup.spec.ts b/packages/frontend/tests/e2e/card-trial-claude-signup.spec.ts
new file mode 100644
index 00000000..34bea59f
--- /dev/null
+++ b/packages/frontend/tests/e2e/card-trial-claude-signup.spec.ts
@@ -0,0 +1,149 @@
+import { expect, test, type Page } from '@playwright/test';
+
+/**
+ * Sign-up from "Connect" in Claude: after verifying the email, a new admin on
+ * a fresh free trial is offered the card trial once, then always returns to
+ * the pending authorization (/auth/login, served by the backend), whichever
+ * way they leave the offer:
+ * - "Continue without payment details" → back to approve;
+ * - a completed checkout → the licence site returns to
+ * /settings/license/activate#key=…, which activates and goes back;
+ * - a cancelled checkout → Stripe returns to /start-trial, skip → back.
+ * The authorization must never be stranded, which is what an earlier version
+ * of the trial offer did in this flow.
+ */
+
+const CLOUD_INFO = { deploymentMode: 'cloud', mcpAuthMode: 'oauth2', hasUsers: true, registrationEnabled: true, ssoProviders: [] };
+const NEUTRAL = { verificationRequired: true, message: 'Check your inbox.' };
+const TRIAL_END = new Date(Date.now() + 7 * 86_400_000).toISOString();
+const KEY = 'AMCP-AB12-CD34-EF56-0789';
+
+interface Calls {
+ paths: string[];
+ checkout: unknown[];
+ setKey: unknown[];
+}
+
+async function mockCloud(page: Page, opts: { role?: string; license?: Record } = {}): Promise {
+ const calls: Calls = { paths: [], checkout: [], setKey: [] };
+ const user = { id: 'u-claude', email: 'claude@example.test', name: 'Jane', role: opts.role ?? 'ADMIN', organizationId: 'o1' };
+ await page.route(/\/(api|health)\//, async (route) => {
+ const req = route.request();
+ const p = new URL(req.url()).pathname;
+ calls.paths.push(p);
+ const json = (body: unknown, status = 200) => route.fulfill({ status, json: body });
+ if (p === '/health/server-info') return json(CLOUD_INFO);
+ if (p === '/api/auth/register') return json(NEUTRAL, 201);
+ if (p === '/api/auth/login') return json({ accessToken: 't', user: { ...user, emailVerified: false }, needsLicenseSetup: true });
+ if (p === '/api/auth/verify-email') return json({ message: 'ok', emailVerified: true });
+ if (p === '/api/license/status') {
+ return json(opts.license ?? { plan: 'trial', status: 'active', expiresAt: TRIAL_END, trialDaysLeft: 7, features: {} });
+ }
+ if (p === '/api/license/checkout-link') {
+ calls.checkout.push(req.postDataJSON());
+ return json({ url: 'https://checkout.example.test/start?intent=i1' });
+ }
+ if (p === '/api/license/key' || p === '/api/license/activate') {
+ calls.setKey.push(req.postDataJSON());
+ return json({ message: 'License activated successfully.' });
+ }
+ if (p.endsWith('/users/me')) return json({ ...user, emailVerified: true });
+ return json({});
+ });
+ // The backend's authorization page and Stripe Checkout, stubbed.
+ await page.route((url) => url.pathname === '/auth/login', (route) =>
+ route.fulfill({ status: 200, contentType: 'text/html', body: 'Authorize AnythingMCP
' }),
+ );
+ await page.route('https://checkout.example.test/**', (route) =>
+ route.fulfill({ status: 200, contentType: 'text/html', body: 'Stripe Checkout
' }),
+ );
+ return calls;
+}
+
+async function signUpFromClaude(page: Page) {
+ await page.goto('/login?mode=register&redirect=%2Fauth%2Flogin');
+ await page.locator('#auth-name').fill('Jane');
+ await page.locator('#auth-email').fill('claude@example.test');
+ await page.locator('#auth-password').fill('Str0ng#Passw0rd');
+ await page.locator('#auth-confirm-password').fill('Str0ng#Passw0rd');
+ await page.getByRole('checkbox').check();
+ await page.getByRole('button', { name: 'Create Account' }).click();
+ await expect(page.getByRole('heading', { name: 'Verify Your Email' })).toBeVisible();
+ await page.locator('input[autocomplete="one-time-code"]').fill('123456');
+ await page.getByRole('button', { name: 'Verify Email' }).click();
+}
+
+test.describe('card trial when signing up from Claude', () => {
+ test('is offered once after verifying, and skipping goes back to approve', async ({ page }) => {
+ const calls = await mockCloud(page);
+ await signUpFromClaude(page);
+
+ await expect(page).toHaveURL(/\/start-trial$/, { timeout: 15_000 });
+ await expect(page.getByText('you go straight back to finish connecting your AI client')).toBeVisible();
+ await page.getByRole('button', { name: 'Try free without payment details' }).click();
+
+ await expect(page).toHaveURL(/\/auth\/login$/, { timeout: 15_000 });
+ await expect(page.getByRole('heading', { name: 'Authorize AnythingMCP' })).toBeVisible();
+ expect(calls.checkout).toHaveLength(0);
+ // The way back is spent: a later visit to /start-trial does not detour again.
+ expect(await page.evaluate(() => localStorage.getItem('amcp_card_trial_return'))).toBeNull();
+ });
+
+ test('a completed checkout activates the licence and goes back to approve', async ({ page }) => {
+ const calls = await mockCloud(page);
+ await signUpFromClaude(page);
+ await expect(page).toHaveURL(/\/start-trial$/, { timeout: 15_000 });
+
+ await page.getByRole('button', { name: 'Start free trial with card' }).click();
+ await expect(page.getByRole('heading', { name: 'Stripe Checkout' })).toBeVisible();
+ expect(calls.checkout).toEqual([expect.objectContaining({ trial: true })]);
+
+ // The licence site's success page hands the key over in the fragment.
+ await page.goto(`/settings/license/activate#key=${KEY}`);
+ await expect(page.getByText('Taking you back to finish connecting your AI client')).toBeVisible();
+ await expect(page).toHaveURL(/\/auth\/login$/, { timeout: 15_000 });
+ expect(JSON.stringify(calls.setKey)).toContain(KEY);
+ });
+
+ test('a cancelled checkout comes back to the offer, and skipping still goes back to approve', async ({ page }) => {
+ await mockCloud(page);
+ await signUpFromClaude(page);
+ await expect(page).toHaveURL(/\/start-trial$/, { timeout: 15_000 });
+ await page.getByRole('button', { name: 'Start free trial with card' }).click();
+ await expect(page.getByRole('heading', { name: 'Stripe Checkout' })).toBeVisible();
+
+ // Stripe's cancel URL for a card trial.
+ await page.goto('/start-trial');
+ await page.getByRole('button', { name: 'Continue without payment details' }).click();
+ await expect(page).toHaveURL(/\/auth\/login$/, { timeout: 15_000 });
+ });
+
+ test('a member who cannot subscribe goes straight back to approve', async ({ page }) => {
+ const calls = await mockCloud(page, { role: 'EDITOR' });
+ await signUpFromClaude(page);
+ await expect(page).toHaveURL(/\/auth\/login$/, { timeout: 15_000 });
+ expect(calls.paths).not.toContain('/api/license/checkout-link');
+ });
+
+ test('without a running trial to convert, goes straight back to approve', async ({ page }) => {
+ await mockCloud(page, { license: { plan: 'cloud_team', status: 'active', expiresAt: TRIAL_END } });
+ await signUpFromClaude(page);
+ await expect(page).toHaveURL(/\/auth\/login$/, { timeout: 15_000 });
+ });
+
+ test('an expired way back is not followed', async ({ page }) => {
+ // A stale entry (older than the 30 minutes the backend keeps an
+ // authorization) must not send a later visitor to a dead consent page.
+ await mockCloud(page);
+ await page.context().addCookies([{ name: 'amcp_token', value: 't', url: test.info().project.use.baseURL ?? 'http://localhost:3100' }]);
+ await page.addInitScript(() => {
+ localStorage.setItem('amcp_token', 't');
+ localStorage.setItem('amcp_user', JSON.stringify({ id: 'u-claude', email: 'claude@example.test', role: 'ADMIN', organizationId: 'o1', emailVerified: true }));
+ localStorage.setItem('amcp_card_trial_return', JSON.stringify({ path: '/auth/login', savedAt: Date.now() - 31 * 60 * 1000 }));
+ });
+ await page.goto('/start-trial');
+ await expect(page.getByText('you go straight back to finish connecting your AI client')).toHaveCount(0);
+ await page.getByRole('button', { name: 'Continue without payment details' }).click();
+ await expect(page).not.toHaveURL(/\/auth\/login/);
+ });
+});
diff --git a/packages/frontend/tests/e2e/neutral-signup-and-billing.spec.ts b/packages/frontend/tests/e2e/neutral-signup-and-billing.spec.ts
index 2e3fee8c..4213ff9c 100644
--- a/packages/frontend/tests/e2e/neutral-signup-and-billing.spec.ts
+++ b/packages/frontend/tests/e2e/neutral-signup-and-billing.spec.ts
@@ -83,8 +83,9 @@ test.describe('sign-up from an AI client (Claude "Connect")', () => {
// The MCP authorization page links "Create an account" with
// redirect=/auth/login. After verifying, the new user must land back on that
// page (still inside the pending authorization) to approve with one click,
- // not on the trial offer or the welcome wizard, which used to strand the
- // connection half way.
+ // not on the welcome wizard, which used to strand the connection half way.
+ // The card-trial offer may come first, but only with a guaranteed way back
+ // (card-trial-claude-signup.spec.ts); here there is no trial to convert.
test('after verifying the email, goes straight back to the authorization page', async ({ page }) => {
const calls: string[] = [];
await page.route(/\/(api|health)\//, async (route) => {