Skip to content
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
311 changes: 311 additions & 0 deletions apps/web/src/campaigns/go-plan-content.ts

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions apps/web/src/campaigns/go-plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { DeepSeekV4FlashCampaignAudience } from './deepseek-v4-flash';

export const GO_PLAN_CAMPAIGN = {
id: 'go-plan-launch-2026',
window: {
startAt: '2026-08-20T20:00:00+08:00',
endAtExclusive: '2026-09-03T20:00:00+08:00',
},
} as const;

export const GO_PLAN_PRICING_URL = 'https://open-design.ai/pricing/';

export function isGoPlanCampaignWindowOpen(now: number): boolean {
const startAt = Date.parse(GO_PLAN_CAMPAIGN.window.startAt);
const endAtExclusive = Date.parse(GO_PLAN_CAMPAIGN.window.endAtExclusive);
return now >= startAt && now < endAtExclusive;
}

export function goPlanCampaignNextBoundary(now: number): number | null {
const startAt = Date.parse(GO_PLAN_CAMPAIGN.window.startAt);
const endAtExclusive = Date.parse(GO_PLAN_CAMPAIGN.window.endAtExclusive);
if (now < startAt) return startAt;
if (now < endAtExclusive) return endAtExclusive;
return null;
}

/** Resolve subscription state without coupling the permanent Go entry to a campaign window. */
export function resolveSubscriptionAudience(input: {
plan: string | null | undefined;
loggedIn: boolean | null | undefined;
}): DeepSeekV4FlashCampaignAudience {
const plan = input.plan?.trim().toLowerCase() ?? '';
if (plan === 'free' || input.loggedIn === false) return 'unpaid';
if (plan) return 'paid';
return 'unknown';
}
21 changes: 21 additions & 0 deletions apps/web/src/campaigns/use-go-plan-campaign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
import {
goPlanCampaignNextBoundary,
isGoPlanCampaignWindowOpen,
} from './go-plan';

export function useGoPlanCampaignVisibility(): { now: number; visible: boolean } {
const [now, setNow] = useState(() => Date.now());

useEffect(() => {
const boundary = goPlanCampaignNextBoundary(Date.now());
if (boundary === null) return;
const timer = window.setTimeout(
() => setNow(Date.now()),
Math.max(0, boundary - Date.now()) + 50,
);
return () => window.clearTimeout(timer);
}, [now]);

return { now, visible: isGoPlanCampaignWindowOpen(now) };
}
14 changes: 2 additions & 12 deletions apps/web/src/components/AmrBalanceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,8 @@ export function AmrBalanceDialog({
// resume the parked task via onResolved. Bounded so an abandoned recharge
// doesn't poll forever; guarded against double-fires.
const [watchingWallet, setWatchingWallet] = useState(false);
// Where 「升级套餐」 goes. `workspaceUpgradeUrl` is the one decision point for
// every upgrade affordance: personal workspace → B's personal plan modal
// (`billing=plan`); team → `billing=checkout` vs `billing=plan` by whether
// the team ever completed a first checkout. Getting the personal branch wrong
// opened an error-state dialog: routing a personal workspace onto the team
// `billing=checkout` deep link opened the Upgrade-to-Team dialog with "Team
// plan unavailable" / a 3-seat minimum (recvpYEiH019cD, failed acceptance
// round). B now resolves `billing=plan` against the workspace's own state, so
// the team branch's guess is a hint rather than a requirement. The profile
// fallback keeps the CTA alive after a signed-out/no-context read (but not
// while that read is pending) — same `billing=plan` deep link every other
// Upgrade affordance uses (ChatPane, AvatarMenu, InlineModelSwitcher).
// `workspaceUpgradeUrl` keeps every generic upgrade affordance on the public
// Pricing comparison surface. A concrete card there owns the Cloud handoff.
const {
context: workspaceContext,
loading: workspaceContextLoading,
Expand Down
Loading
Loading