diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql
index 982b4fa9d2..9cb954954c 100644
--- a/packages/backend/server/src/schema.gql
+++ b/packages/backend/server/src/schema.gql
@@ -248,6 +248,9 @@ type ServerConfigType {
"""credentials requirement"""
credentialsRequirement: CredentialsRequirementType!
+ """enable telemetry"""
+ enableTelemetry: Boolean!
+
"""enabled server features"""
features: [ServerFeature!]!
diff --git a/packages/frontend/component/src/components/setting-components/setting-header.tsx b/packages/frontend/component/src/components/setting-components/setting-header.tsx
index 77ea5b27a1..635e2776f5 100644
--- a/packages/frontend/component/src/components/setting-components/setting-header.tsx
+++ b/packages/frontend/component/src/components/setting-components/setting-header.tsx
@@ -16,7 +16,7 @@ export const SettingHeader = ({
return (
{title}
-
{subtitle}
+ {subtitle ?
{subtitle}
: null}
);
};
diff --git a/packages/frontend/component/src/components/setting-components/share.css.ts b/packages/frontend/component/src/components/setting-components/share.css.ts
index 223d5b7933..c00efbcecf 100644
--- a/packages/frontend/component/src/components/setting-components/share.css.ts
+++ b/packages/frontend/component/src/components/setting-components/share.css.ts
@@ -2,16 +2,17 @@ import { cssVar } from '@toeverything/theme';
import { globalStyle, style } from '@vanilla-extract/css';
export const settingHeader = style({
borderBottom: `1px solid ${cssVar('borderColor')}`,
- paddingBottom: '24px',
+ paddingBottom: '16px',
marginBottom: '24px',
});
globalStyle(`${settingHeader} .title`, {
fontSize: cssVar('fontBase'),
fontWeight: 600,
lineHeight: '24px',
- marginBottom: '4px',
});
globalStyle(`${settingHeader} .subtitle`, {
+ paddingTop: '4px',
+ paddingBottom: '8px',
fontSize: cssVar('fontXs'),
lineHeight: '16px',
color: cssVar('textSecondaryColor'),
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx
index 029ffed410..3fedab293c 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx
@@ -13,7 +13,7 @@ import { AboutAffine } from './about';
import { AppearanceSettings } from './appearance';
import { BillingSettings } from './billing';
import { PaymentIcon, UpgradeIcon } from './icons';
-import { AFFiNECloudPlans } from './plans';
+import { AFFiNEPricingPlans } from './plans';
import { Shortcuts } from './shortcuts';
interface GeneralSettingListItem {
@@ -84,7 +84,7 @@ export const GeneralSetting = ({ generalKey }: GeneralSettingProps) => {
case 'about':
return ;
case 'plans':
- return ;
+ return ;
case 'billing':
return ;
default:
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.css.ts b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.css.ts
new file mode 100644
index 0000000000..b6113186c5
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.css.ts
@@ -0,0 +1,109 @@
+import { cssVar } from '@toeverything/theme';
+import { globalStyle, style } from '@vanilla-extract/css';
+
+export const card = style({
+ border: `1px solid ${cssVar('borderColor')}`,
+ borderRadius: 16,
+ padding: 36,
+});
+
+export const titleBlock = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+ marginBottom: 24,
+});
+export const titleCaption1 = style({
+ fontWeight: 500,
+ fontSize: cssVar('fontSm'),
+ lineHeight: '14px',
+ color: cssVar('brandColor'),
+});
+export const titleCaption2 = style({
+ fontWeight: 500,
+ fontSize: cssVar('fontSm'),
+ lineHeight: '20px',
+ color: cssVar('textPrimaryColor'),
+ letterSpacing: '-2%',
+});
+export const title = style({
+ fontWeight: 600,
+ fontSize: '30px',
+ lineHeight: '36px',
+ letterSpacing: '-2%',
+});
+
+// action button
+export const actionBlock = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 12,
+ alignItems: 'start',
+ marginBottom: 24,
+});
+export const purchaseButton = style({
+ minWidth: 160,
+ height: 37,
+ borderRadius: 18,
+ fontWeight: 500,
+ fontSize: cssVar('fontSm'),
+ lineHeight: '14px',
+ letterSpacing: '-1%',
+});
+export const agreement = style({
+ fontSize: cssVar('fontXs'),
+ fontWeight: 400,
+ lineHeight: '20px',
+ color: cssVar('textSecondaryColor'),
+});
+globalStyle(`.${agreement} > a`, {
+ color: cssVar('textPrimaryColor'),
+ textDecoration: 'underline',
+});
+
+// benefits
+export const benefits = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 12,
+});
+export const benefitGroup = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 12,
+});
+export const benefitTitle = style({
+ fontWeight: 500,
+ fontSize: cssVar('fontSm'),
+ lineHeight: '20px',
+ color: cssVar('textPrimaryColor'),
+ letterSpacing: '-2%',
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+});
+globalStyle(`.${benefitTitle} > svg`, {
+ color: cssVar('brandColor'),
+});
+export const benefitList = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+});
+export const benefitItem = style({
+ fontWeight: 400,
+ fontSize: cssVar('fontXs'),
+ lineHeight: '24px',
+ paddingLeft: 22,
+ position: 'relative',
+ '::before': {
+ content: '""',
+ width: 4,
+ height: 4,
+ borderRadius: 2,
+ background: 'currentColor',
+ position: 'absolute',
+ left: '10px',
+ top: '10px',
+ },
+});
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx
new file mode 100644
index 0000000000..715aafb355
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx
@@ -0,0 +1,89 @@
+import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
+import {
+ type SubscriptionMutator,
+ useUserSubscription,
+} from '@affine/core/hooks/use-subscription';
+import { timestampToLocalDate } from '@affine/core/utils';
+import {
+ type PricesQuery,
+ SubscriptionPlan,
+ SubscriptionRecurring,
+} from '@affine/graphql';
+
+import { AIPlanLayout } from '../layout';
+import * as styles from './ai-plan.css';
+import { AIBenefits } from './benefits';
+import { AICancel } from './cancel';
+import { AILogin } from './login';
+import { AIResume } from './resume';
+import { AISubscribe } from './subscribe';
+import type { BaseActionProps } from './types';
+
+interface AIPlanProps {
+ price?: PricesQuery['prices'][number];
+ onSubscriptionUpdate: SubscriptionMutator;
+}
+export const AIPlan = ({ price, onSubscriptionUpdate }: AIPlanProps) => {
+ const plan = SubscriptionPlan.AI;
+ const recurring = SubscriptionRecurring.Yearly;
+
+ const loggedIn = useCurrentLoginStatus() === 'authenticated';
+
+ const [subscription] = useUserSubscription(plan);
+
+ // yearly subscription should always be available
+ if (!price?.yearlyAmount) return null;
+
+ const baseActionProps: BaseActionProps = {
+ plan,
+ price,
+ recurring,
+ onSubscriptionUpdate,
+ };
+ const isCancelled = !!subscription?.canceledAt;
+
+ const Action = !loggedIn
+ ? AILogin
+ : !subscription
+ ? AISubscribe
+ : isCancelled
+ ? AIResume
+ : AICancel;
+
+ return (
+
+
+
+
+ Turn all your ideas into reality
+
+
+
+ A true multimodal AI copilot.
+
+
+
+
+
+ {subscription?.nextBillAt ? (
+
+ ) : null}
+
+
+
+
+
+ );
+};
+
+const PurchasedTip = ({ due }: { due: string }) => (
+
+ You have purchased AFFiNE AI. The next payment date is {due}.
+
+);
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/benefits.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/benefits.tsx
new file mode 100644
index 0000000000..0fb0337187
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/benefits.tsx
@@ -0,0 +1,59 @@
+import { ShapeIcon } from '@blocksuite/icons';
+
+import * as styles from './ai-plan.css';
+
+const benefits = [
+ {
+ name: 'Write with you',
+ icon: ,
+ items: [
+ 'Create quality content from sentences to articles on topics you need',
+ 'Rewrite like the professionals',
+ 'Change the tones / fix spelling & grammar',
+ ],
+ },
+ {
+ name: 'Draw with you',
+ icon: ,
+ items: [
+ 'Visualize your mind, magically',
+ 'Turn your outline into beautiful, engaging presentations',
+ 'Summarize your content into structured mind-map',
+ ],
+ },
+ {
+ name: 'Plan with you',
+ icon: ,
+ items: [
+ 'Memorize and tidy up your knowledge',
+ 'Auto-sorting and auto-tagging',
+ 'Open source & Privacy ensured',
+ ],
+ },
+];
+
+export const AIBenefits = () => {
+ // TODO: responsive
+ return (
+
+ {benefits.map(({ name, icon, items }) => {
+ return (
+
+
+ {icon}
+ {name}
+
+
+
+ {items.map(item => (
+ -
+ {item}
+
+ ))}
+
+
+ );
+ })}
+
+ );
+};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/cancel.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/cancel.tsx
new file mode 100644
index 0000000000..5d068b6f5b
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/cancel.tsx
@@ -0,0 +1,58 @@
+import { Button, useConfirmModal } from '@affine/component';
+import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
+import { useMutation } from '@affine/core/hooks/use-mutation';
+import { cancelSubscriptionMutation } from '@affine/graphql';
+import { nanoid } from 'nanoid';
+import { useState } from 'react';
+
+import { purchaseButton } from './ai-plan.css';
+import type { BaseActionProps } from './types';
+
+interface AICancelProps extends BaseActionProps {}
+export const AICancel = ({ plan, onSubscriptionUpdate }: AICancelProps) => {
+ const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
+ const { trigger, isMutating } = useMutation({
+ mutation: cancelSubscriptionMutation,
+ });
+ const { openConfirmModal } = useConfirmModal();
+
+ const cancel = useAsyncCallback(async () => {
+ openConfirmModal({
+ title: 'Cancel Subscription',
+ description:
+ 'If you end your subscription now, you can still use AFFiNE AI until the end of this billing period.',
+ reverseFooter: true,
+ confirmButtonOptions: {
+ children: 'Cancel Subscription',
+ type: 'default',
+ },
+ cancelText: 'Keep AFFiNE AI',
+ cancelButtonOptions: {
+ type: 'primary',
+ },
+ onConfirm: async () => {
+ await trigger(
+ { idempotencyKey, plan },
+ {
+ onSuccess: data => {
+ // refresh idempotency key
+ setIdempotencyKey(nanoid());
+ onSubscriptionUpdate(data.cancelSubscription);
+ },
+ }
+ );
+ },
+ });
+ }, [openConfirmModal, trigger, idempotencyKey, plan, onSubscriptionUpdate]);
+
+ return (
+
+ );
+};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/login.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/login.tsx
new file mode 100644
index 0000000000..37471f1b08
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/login.tsx
@@ -0,0 +1,17 @@
+import { Button } from '@affine/component';
+import { authAtom } from '@affine/core/atoms';
+import { useSetAtom } from 'jotai';
+import { useCallback } from 'react';
+
+export const AILogin = () => {
+ const setOpen = useSetAtom(authAtom);
+
+ const onClickSignIn = useCallback(() => {
+ setOpen(state => ({
+ ...state,
+ openModal: true,
+ }));
+ }, [setOpen]);
+
+ return ;
+};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/resume.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/resume.tsx
new file mode 100644
index 0000000000..69d8fa5e5a
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/resume.tsx
@@ -0,0 +1,66 @@
+import { Button, notify, useConfirmModal } from '@affine/component';
+import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
+import { useMutation } from '@affine/core/hooks/use-mutation';
+import { resumeSubscriptionMutation } from '@affine/graphql';
+import { SingleSelectSelectSolidIcon } from '@blocksuite/icons';
+import { cssVar } from '@toeverything/theme';
+import { nanoid } from 'nanoid';
+import { useState } from 'react';
+
+import { purchaseButton } from './ai-plan.css';
+import type { BaseActionProps } from './types';
+
+interface AIResumeProps extends BaseActionProps {}
+
+export const AIResume = ({ plan, onSubscriptionUpdate }: AIResumeProps) => {
+ const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
+
+ const { isMutating, trigger } = useMutation({
+ mutation: resumeSubscriptionMutation,
+ });
+ const { openConfirmModal } = useConfirmModal();
+
+ const resume = useAsyncCallback(async () => {
+ openConfirmModal({
+ title: 'Resume Auto-Renewal?',
+ description:
+ 'Are you sure you want to resume the subscription for AFFiNE AI? This means your payment method will be charged automatically at the end of each billing cycle, starting from the next billing cycle.',
+ confirmButtonOptions: {
+ children: 'Confirm',
+ type: 'primary',
+ },
+ onConfirm: async () => {
+ await trigger(
+ { idempotencyKey, plan },
+ {
+ onSuccess: data => {
+ // refresh idempotency key
+ setIdempotencyKey(nanoid());
+ onSubscriptionUpdate(data.resumeSubscription);
+ notify({
+ icon: (
+
+ ),
+ title: 'Subscription Updated',
+ message: 'You will be charged in the next billing cycle.',
+ });
+ },
+ }
+ );
+ },
+ });
+ }, [openConfirmModal, trigger, idempotencyKey, plan, onSubscriptionUpdate]);
+
+ return (
+
+ );
+};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/subscribe.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/subscribe.tsx
new file mode 100644
index 0000000000..cdad60b361
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/subscribe.tsx
@@ -0,0 +1,80 @@
+import { Button } from '@affine/component';
+import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
+import { useMutation } from '@affine/core/hooks/use-mutation';
+import { createCheckoutSessionMutation } from '@affine/graphql';
+import { nanoid } from 'nanoid';
+import { useCallback, useEffect, useMemo, useRef } from 'react';
+
+import { purchaseButton } from './ai-plan.css';
+import type { BaseActionProps } from './types';
+
+interface AISubscribeProps extends BaseActionProps {}
+
+export const AISubscribe = ({
+ price,
+ plan,
+ recurring,
+ onSubscriptionUpdate,
+}: AISubscribeProps) => {
+ const idempotencyKey = useMemo(() => `${nanoid()}-${recurring}`, [recurring]);
+
+ const newTabRef = useRef(null);
+
+ const { isMutating, trigger } = useMutation({
+ mutation: createCheckoutSessionMutation,
+ });
+
+ const onClose = useCallback(() => {
+ newTabRef.current = null;
+ onSubscriptionUpdate();
+ }, [onSubscriptionUpdate]);
+
+ useEffect(() => {
+ return () => {
+ if (newTabRef.current) {
+ newTabRef.current.removeEventListener('close', onClose);
+ newTabRef.current = null;
+ }
+ };
+ }, [onClose]);
+
+ const subscribe = useAsyncCallback(async () => {
+ await trigger(
+ {
+ input: {
+ recurring,
+ idempotencyKey,
+ plan,
+ coupon: null,
+ successCallbackLink: null,
+ },
+ },
+ {
+ onSuccess: data => {
+ const newTab = window.open(
+ data.createCheckoutSession,
+ '_blank',
+ 'noopener noreferrer'
+ );
+ if (newTab) {
+ newTabRef.current = newTab;
+ newTab.addEventListener('close', onClose);
+ }
+ },
+ }
+ );
+ }, [idempotencyKey, onClose, plan, recurring, trigger]);
+
+ if (!price.yearlyAmount) return null;
+
+ return (
+
+ );
+};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/types.ts b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/types.ts
new file mode 100644
index 0000000000..68668635c7
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/types.ts
@@ -0,0 +1,13 @@
+import type { SubscriptionMutator } from '@affine/core/hooks/use-subscription';
+import type {
+ PricesQuery,
+ SubscriptionPlan,
+ SubscriptionRecurring,
+} from '@affine/graphql';
+
+export interface BaseActionProps {
+ price: PricesQuery['prices'][number];
+ recurring: SubscriptionRecurring;
+ plan: SubscriptionPlan;
+ onSubscriptionUpdate: SubscriptionMutator;
+}
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/cloud-plans.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/cloud-plans.tsx
new file mode 100644
index 0000000000..6359a7adb6
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/cloud-plans.tsx
@@ -0,0 +1,129 @@
+// TODO: we don't handle i18n for now
+// it's better to manage all equity at server side
+import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
+import { AfFiNeIcon } from '@blocksuite/icons';
+import type { ReactNode } from 'react';
+
+export type Benefits = Record<
+ string,
+ Array<{
+ icon?: ReactNode;
+ title: ReactNode;
+ }>
+>;
+interface BasePrice {
+ plan: SubscriptionPlan;
+ name: string;
+ description: string;
+ benefits: Benefits;
+}
+export interface FixedPrice extends BasePrice {
+ type: 'fixed';
+ price: string;
+ yearlyPrice: string;
+ discount?: string;
+ titleRenderer: (
+ recurring: SubscriptionRecurring,
+ detail: FixedPrice
+ ) => ReactNode;
+}
+
+export interface DynamicPrice extends BasePrice {
+ type: 'dynamic';
+ contact: boolean;
+ titleRenderer: (
+ recurring: SubscriptionRecurring,
+ detail: DynamicPrice
+ ) => ReactNode;
+}
+
+const freeBenefits: Benefits = {
+ 'Include in FOSS': [
+ { title: 'Unlimited Local Workspaces.' },
+ { title: 'Unlimited use and Customization.' },
+ { title: 'Unlimited Doc and Edgeless editing.' },
+ ],
+ 'Include in Basic': [
+ { title: '10 GB of Cloud Storage.' },
+ { title: '10 MB of Maximum file size.' },
+ { title: 'Up to 3 members per Workspace.' },
+ { title: '7-days Cloud Time Machine file version history.' },
+ { title: 'Up to 3 login devices.' },
+ ],
+};
+
+const proBenefits: Benefits = {
+ 'Include in Pro': [
+ { title: 'Everything in AFFiNE FOSS & Basic.', icon: },
+ { title: '100 GB of Cloud Storage.' },
+ { title: '100 MB of Maximum file size.' },
+ { title: 'Up to 10 members per Workspace.' },
+ { title: '30-days Cloud Time Machine file version history.' },
+ { title: 'Add comments on Doc and Edgeless.' },
+ { title: 'Community Support.' },
+ { title: 'Real-time Syncing & Collaboration for more people.' },
+ ],
+};
+
+const teamBenefits: Benefits = {
+ 'Both in Team & Enterprise': [
+ { title: 'Everything in AFFiNE Pro.', icon: },
+ { title: 'Advanced Permission control, Page history and Review mode.' },
+ { title: 'Pay for seats, fits all team size.' },
+ { title: 'Email & Slack Support.' },
+ ],
+ 'Enterprise only': [
+ { title: 'SSO Authorization.' },
+ { title: 'Solutions & Best Practices for Dedicated needs.' },
+ { title: 'Embed-able & Integrations with IT support.' },
+ ],
+};
+
+export function getPlanDetail() {
+ return new Map([
+ [
+ SubscriptionPlan.Free,
+ {
+ type: 'fixed',
+ plan: SubscriptionPlan.Free,
+ price: '0',
+ yearlyPrice: '0',
+ name: 'FOSS + Basic',
+ description: 'Open-Source under MIT license.',
+ titleRenderer: () => 'Free forever',
+ benefits: freeBenefits,
+ },
+ ],
+ [
+ SubscriptionPlan.Pro,
+ {
+ type: 'fixed',
+ plan: SubscriptionPlan.Pro,
+ price: '1',
+ yearlyPrice: '1',
+ name: 'Pro',
+ description: 'For family and small teams.',
+ titleRenderer: (recurring, detail) => {
+ const price =
+ recurring === SubscriptionRecurring.Yearly
+ ? detail.yearlyPrice
+ : detail.price;
+ return `$${price} per month`;
+ },
+ benefits: proBenefits,
+ },
+ ],
+ [
+ SubscriptionPlan.Team,
+ {
+ type: 'dynamic',
+ plan: SubscriptionPlan.Team,
+ contact: true,
+ name: 'Team / Enterprise',
+ description: 'Best for scalable teams.',
+ titleRenderer: () => 'Contact Sales',
+ benefits: teamBenefits,
+ },
+ ],
+ ]);
+}
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx
index a81a393133..e576e9c0c9 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx
@@ -1,4 +1,4 @@
-import { notify, RadioButton, RadioButtonGroup } from '@affine/component';
+import { notify, Switch } from '@affine/component';
import {
pricesQuery,
SubscriptionPlan,
@@ -15,9 +15,10 @@ import { SWRErrorBoundary } from '../../../../../components/pure/swr-error-bunda
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
import { useQuery } from '../../../../../hooks/use-query';
import { useUserSubscription } from '../../../../../hooks/use-subscription';
-import { PlanLayout } from './layout';
-import type { FixedPrice } from './plan-card';
-import { getPlanDetail, PlanCard } from './plan-card';
+import { AIPlan } from './ai/ai-plan';
+import { type FixedPrice, getPlanDetail } from './cloud-plans';
+import { CloudPlanLayout, PlanLayout } from './layout';
+import { PlanCard } from './plan-card';
import { PlansSkeleton } from './skeleton';
import * as styles from './style.css';
@@ -38,7 +39,7 @@ const Settings = () => {
const [subscription, mutateSubscription] = useUserSubscription();
const loggedIn = useCurrentLoginStatus() === 'authenticated';
- const planDetail = getPlanDetail(t);
+ const planDetail = getPlanDetail();
const scrollWrapper = useRef(null);
const {
@@ -62,7 +63,7 @@ const Settings = () => {
}
});
- const [recurring, setRecurring] = useState(
+ const [recurring, setRecurring] = useState(
subscription?.recurring ?? SubscriptionRecurring.Yearly
);
@@ -100,7 +101,7 @@ const Settings = () => {
};
}, [recurring]);
- const subtitle = loggedIn ? (
+ const cloudCaption = loggedIn ? (
isCanceled ? (
{t['com.affine.payment.subtitle-canceled']({
@@ -133,30 +134,38 @@ const Settings = () => {
{t['com.affine.payment.subtitle-not-signed-in']()}
);
- const tabs = (
-
- {Object.values(SubscriptionRecurring).map(recurring => (
-
-
- {getRecurringLabel({ recurring, t })}
-
- {recurring === SubscriptionRecurring.Yearly && yearlyDiscount && (
-
- {t['com.affine.payment.discount-amount']({
- amount: yearlyDiscount,
- })}
-
- )}
-
- ))}
-
+ const cloudToggle = (
+
+
+ {recurring === SubscriptionRecurring.Yearly ? (
+
Yearly
+ ) : (
+ <>
+
+ Billed Yearly
+
+ {yearlyDiscount ? (
+
+ Saving {yearlyDiscount}%
+
+ ) : null}
+ >
+ )}
+
+
+ setRecurring(
+ checked
+ ? SubscriptionRecurring.Yearly
+ : SubscriptionRecurring.Monthly
+ )
+ }
+ />
+
);
- const scroll = (
+ const cloudScroll = (
{Array.from(planDetail.values()).map(detail => {
return (
@@ -190,12 +199,35 @@ const Settings = () => {
);
+ const cloudSelect = (
+
+ Hosted by AFFiNE.Pro
+ We host, no technical setup required.
+
+ );
+
return (
-
+
+ }
+ ai={
+ p.plan === SubscriptionPlan.AI)}
+ onSubscriptionUpdate={mutateSubscription}
+ />
+ }
+ />
);
};
-export const AFFiNECloudPlans = () => {
+export const AFFiNEPricingPlans = () => {
return (
}>
@@ -208,11 +240,6 @@ export const AFFiNECloudPlans = () => {
const PlansErrorBoundary = ({ resetErrorBoundary }: FallbackProps) => {
const t = useAFFiNEI18N();
- const title = t['com.affine.payment.title']();
- const subtitle = '';
- const tabs = '';
- const footer = '';
-
const scroll = (
{t['com.affine.payment.plans-error-tip']()}
@@ -222,5 +249,5 @@ const PlansErrorBoundary = ({ resetErrorBoundary }: FallbackProps) => {
);
- return ;
+ return } />;
};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.css.ts b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.css.ts
index 899b617375..69fac2448a 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.css.ts
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.css.ts
@@ -43,3 +43,30 @@ export const allPlansLink = style({
borderColor: 'transparent',
fontSize: cssVar('fontXs'),
});
+
+export const collapsibleHeader = style({
+ display: 'flex',
+ marginBottom: 8,
+});
+export const collapsibleHeaderContent = style({
+ width: 0,
+ flex: 1,
+});
+export const collapsibleHeaderTitle = style({
+ fontWeight: 600,
+ fontSize: cssVar('fontBase'),
+ lineHeight: '22px',
+});
+export const collapsibleHeaderCaption = style({
+ fontWeight: 400,
+ fontSize: cssVar('fontXs'),
+ lineHeight: '20px',
+ color: cssVar('textSecondaryColor'),
+});
+
+export const affineCloudHeader = style({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 24,
+});
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.tsx
index bbb946db07..b3d3add429 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.tsx
@@ -1,22 +1,20 @@
+import { Divider, IconButton } from '@affine/component';
import { SettingHeader } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
-import { ArrowRightBigIcon } from '@blocksuite/icons';
+import { ArrowRightBigIcon, ArrowUpSmallIcon } from '@blocksuite/icons';
+import * as Collapsible from '@radix-ui/react-collapsible';
import * as ScrollArea from '@radix-ui/react-scroll-area';
-import type { HtmlHTMLAttributes, ReactNode } from 'react';
+import {
+ type HtmlHTMLAttributes,
+ type PropsWithChildren,
+ type ReactNode,
+ useCallback,
+ useState,
+} from 'react';
import * as styles from './layout.css';
-export interface PlanLayoutProps
- extends Omit, 'title'> {
- title?: ReactNode;
- subtitle: ReactNode;
- tabs: ReactNode;
- scroll: ReactNode;
- footer?: ReactNode;
- scrollRef?: React.RefObject;
-}
-
-const SeeAllLink = () => {
+export const SeeAllLink = () => {
const t = useAFFiNEI18N();
return (
@@ -32,24 +30,86 @@ const SeeAllLink = () => {
);
};
-export const PlanLayout = ({
- subtitle,
- tabs,
- scroll,
+interface PricingCollapsibleProps
+ extends Omit, 'title'> {
+ title?: ReactNode;
+ caption?: ReactNode;
+}
+const PricingCollapsible = ({
title,
- footer = ,
- scrollRef,
-}: PlanLayoutProps) => {
+ caption,
+ children,
+}: PricingCollapsibleProps) => {
+ const [open, setOpen] = useState(true);
+ const toggle = useCallback(() => setOpen(prev => !prev), []);
+ return (
+
+
+ {children}
+
+ );
+};
+
+export interface PlanLayoutProps {
+ cloud?: ReactNode;
+ ai?: ReactNode;
+}
+
+export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
const t = useAFFiNEI18N();
return (
{/* TODO: SettingHeader component shouldn't have margin itself */}
- {tabs}
+ {cloud}
+ {ai ? (
+ <>
+
+ {ai}
+ >
+ ) : null}
+
+ );
+};
+
+export interface PlanCardProps {
+ title?: ReactNode;
+ caption?: ReactNode;
+ select?: ReactNode;
+ toggle?: ReactNode;
+ scroll?: ReactNode;
+ scrollRef?: React.RefObject;
+}
+export const CloudPlanLayout = ({
+ title = 'AFFiNE Cloud',
+ caption,
+ select,
+ toggle,
+ scroll,
+ scrollRef,
+}: PlanCardProps) => {
+ return (
+
+
{scroll}
@@ -62,7 +122,22 @@ export const PlanLayout = ({
- {footer}
-
+
+ );
+};
+
+export interface AIPlanLayoutProps {
+ title?: ReactNode;
+ caption?: ReactNode;
+}
+export const AIPlanLayout = ({
+ title = 'AFFiNE AI',
+ caption,
+ children,
+}: PropsWithChildren) => {
+ return (
+
+ {children}
+
);
};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx
index aecd730acd..469a88c855 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx
@@ -5,10 +5,10 @@ import type {
Subscription,
SubscriptionMutator,
} from '@affine/core/hooks/use-subscription';
+import type { SubscriptionRecurring } from '@affine/graphql';
import {
createCheckoutSessionMutation,
SubscriptionPlan,
- SubscriptionRecurring,
SubscriptionStatus,
updateSubscriptionMutation,
} from '@affine/graphql';
@@ -26,30 +26,14 @@ import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-l
import { useMutation } from '../../../../../hooks/use-mutation';
import { mixpanel } from '../../../../../utils';
import { CancelAction, ResumeAction } from './actions';
-import { BulledListIcon } from './icons/bulled-list';
+import type { DynamicPrice, FixedPrice } from './cloud-plans';
import { ConfirmLoadingModal } from './modals';
import * as styles from './style.css';
-export interface FixedPrice {
- type: 'fixed';
- plan: SubscriptionPlan;
- price: string;
- yearlyPrice: string;
- discount?: string;
- benefits: string[];
-}
-
-export interface DynamicPrice {
- type: 'dynamic';
- plan: SubscriptionPlan;
- contact: boolean;
- benefits: string[];
-}
-
interface PlanCardProps {
detail: FixedPrice | DynamicPrice;
subscription?: Subscription | null;
- recurring: string;
+ recurring: SubscriptionRecurring;
onSubscriptionUpdate: SubscriptionMutator;
onNotify: (info: {
detail: FixedPrice | DynamicPrice;
@@ -57,79 +41,15 @@ interface PlanCardProps {
}) => void;
}
-export function getPlanDetail(t: ReturnType) {
- return new Map([
- [
- SubscriptionPlan.Free,
- {
- type: 'fixed',
- plan: SubscriptionPlan.Free,
- price: '0',
- yearlyPrice: '0',
- benefits: [
- t['com.affine.payment.benefit-1'](),
- t['com.affine.payment.benefit-2'](),
- t['com.affine.payment.benefit-3'](),
- t['com.affine.payment.benefit-4']({ capacity: '10GB' }),
- t['com.affine.payment.benefit-5']({ capacity: '10M' }),
- t['com.affine.payment.benefit-6']({ capacity: '3' }),
- t['com.affine.payment.benefit-7']({ capacity: '7' }),
- ],
- },
- ],
- [
- SubscriptionPlan.Pro,
- {
- type: 'fixed',
- plan: SubscriptionPlan.Pro,
- price: '1',
- yearlyPrice: '1',
- benefits: [
- t['com.affine.payment.benefit-1'](),
- t['com.affine.payment.benefit-2'](),
- t['com.affine.payment.benefit-3'](),
- t['com.affine.payment.benefit-4']({ capacity: '100GB' }),
- t['com.affine.payment.benefit-5']({ capacity: '100M' }),
- t['com.affine.payment.benefit-6']({ capacity: '10' }),
- t['com.affine.payment.benefit-7']({ capacity: '30' }),
- ],
- },
- ],
- [
- SubscriptionPlan.Team,
- {
- type: 'dynamic',
- plan: SubscriptionPlan.Team,
- contact: true,
- benefits: [
- t['com.affine.payment.dynamic-benefit-1'](),
- t['com.affine.payment.dynamic-benefit-2'](),
- t['com.affine.payment.dynamic-benefit-3'](),
- ],
- },
- ],
- [
- SubscriptionPlan.Enterprise,
- {
- type: 'dynamic',
- plan: SubscriptionPlan.Enterprise,
- contact: true,
- benefits: [
- t['com.affine.payment.dynamic-benefit-4'](),
- t['com.affine.payment.dynamic-benefit-5'](),
- ],
- },
- ],
- ]);
-}
-
export const PlanCard = (props: PlanCardProps) => {
- const t = useAFFiNEI18N();
const { detail, subscription, recurring } = props;
const loggedIn = useCurrentLoginStatus() === 'authenticated';
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
- const isCurrent = loggedIn && detail.plan === currentPlan;
+ const isCurrent =
+ loggedIn &&
+ detail.plan === currentPlan &&
+ recurring === subscription?.recurring;
const isPro = detail.plan === SubscriptionPlan.Pro;
return (
@@ -138,56 +58,39 @@ export const PlanCard = (props: PlanCardProps) => {
key={detail.plan}
className={isPro ? styles.proPlanCard : styles.planCard}
>
+
-
-
- {detail.plan}
- {' '}
- {'discount' in detail &&
- recurring === SubscriptionRecurring.Yearly && (
-
- {detail.discount}% off
-
- )}
-
-
-
- {detail.type === 'dynamic' ? (
- Coming soon...
- ) : (
- <>
-
- $
- {recurring === SubscriptionRecurring.Monthly
- ? detail.price
- : detail.yearlyPrice}
-
-
- {t['com.affine.payment.price-description.per-month']()}
-
- >
- )}
-
+
+
+
+
+ {detail.titleRenderer(recurring, detail as any)}
+
- {detail.benefits.map((content, i) => (
-
-
- {detail.type === 'dynamic' ? (
-
- ) : (
-
- )}
-
-
{content}
-
- ))}
+ {Object.entries(detail.benefits).map(([groupName, benefitList]) => {
+ return (
+
+
+ {benefitList.map(({ icon, title }, index) => {
+ return (
+ -
+
+ {icon ?? }
+
+ {title}
+
+ );
+ })}
+
+ );
+ })}
);
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/skeleton.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/skeleton.tsx
index 6d437a594c..c8f052115d 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/skeleton.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/skeleton.tsx
@@ -1,6 +1,6 @@
import { Skeleton } from '@affine/component';
-import { PlanLayout } from './layout';
+import { CloudPlanLayout, PlanLayout } from './layout';
import * as styles from './skeleton.css';
/**
@@ -17,10 +17,6 @@ const RoundedSkeleton = ({
);
-const SubtitleSkeleton = () => (
-
-);
-
const TabsSkeleton = () => (
// TODO: height should be `32px` by design
// but the RadioGroup component is not matching with the design currently
@@ -52,9 +48,15 @@ const ScrollSkeleton = () => (
export const PlansSkeleton = () => {
return (
}
- tabs={}
- scroll={}
+ cloud={
+
+ }
+ select={}
+ scroll={}
+ />
+ }
/>
);
};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts
index e347c3941f..f14fe13b00 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts
@@ -1,10 +1,26 @@
import { cssVar } from '@toeverything/theme';
-import { style } from '@vanilla-extract/css';
+import { globalStyle, style } from '@vanilla-extract/css';
export const wrapper = style({
width: '100%',
});
-export const recurringRadioGroup = style({
- width: '256px',
+export const recurringToggleWrapper = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: 16,
+ minHeight: 40,
+});
+// export const recurringToggleLabel = style({});
+export const recurringToggleRecurring = style({
+ fontWeight: 400,
+ fontSize: cssVar('fontXs'),
+ lineHeight: '20px',
+ color: cssVar('textSecondaryColor'),
+});
+export const recurringToggleDiscount = style({
+ fontWeight: 600,
+ fontSize: cssVar('fontXs'),
+ lineHeight: '20px',
+ color: cssVar('brandColor'),
});
export const radioButtonDiscount = style({
marginLeft: '4px',
@@ -18,6 +34,13 @@ export const radioButtonText = style({
},
},
});
+export const cloudSelect = style({
+ fontSize: cssVar('fontXs'),
+ lineHeight: '20px',
+ display: 'flex',
+ gap: 8,
+});
+globalStyle(`.${cloudSelect} > span`, { color: cssVar('textSecondaryColor') });
export const planCardsWrapper = style({
paddingRight: 'calc(var(--setting-modal-gap-x) + 30px)',
display: 'flex',
@@ -29,9 +52,10 @@ export const planCard = style({
minHeight: '426px',
minWidth: '258px',
borderRadius: '16px',
- padding: '20px',
border: `1px solid ${cssVar('borderColor')}`,
position: 'relative',
+ userSelect: 'none',
+ transition: 'all 0.23s ease',
selectors: {
'&::before': {
content: '',
@@ -39,27 +63,43 @@ export const planCard = style({
right: 'calc(100% + var(--setting-modal-gap-x))',
scrollSnapAlign: 'start',
},
- },
-});
-export const proPlanCard = style([
- planCard,
- {
- borderWidth: '1px',
- borderColor: cssVar('brandColor'),
- boxShadow: cssVar('shadow2'),
- position: 'relative',
- '::after': {
- content: '',
- position: 'absolute',
- inset: '-1px',
- borderRadius: 'inherit',
- boxShadow: `0px 0px 0px 2px ${cssVar('brandColor')}`,
- opacity: 0.3,
- zIndex: 1,
- pointerEvents: 'none',
+ '&[data-current="true"]': {
+ borderColor: 'transparent',
},
},
-]);
+});
+export const planCardBorderMock = style({
+ position: 'absolute',
+ inset: 0,
+ borderRadius: 'inherit',
+ pointerEvents: 'none',
+ zIndex: 1,
+
+ '::after': {
+ content: '""',
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ borderRadius: 'inherit',
+ border: `2px solid transparent`,
+ // TODO: brandColor with opacity, dark mode compatibility needed
+ background: `linear-gradient(180deg, ${cssVar('brandColor')}, #1E96EB33) border-box`,
+ ['WebkitMask']: `linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0)`,
+ [`WebkitMaskComposite`]: `destination-out`,
+ maskComposite: `exclude`,
+ opacity: 0,
+ transition: 'opacity 0.23s ease',
+ },
+
+ selectors: {
+ [`.${planCard}[data-current="true"] &::after`]: {
+ opacity: 1,
+ },
+ },
+});
+export const proPlanCard = style([planCard, {}]);
export const proPlanTitle = style({
backgroundColor: cssVar('brandColor'),
color: cssVar('white'),
@@ -82,8 +122,36 @@ export const planTitle = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
- gap: '10px',
+ padding: '12px 16px',
+ background: cssVar('backgroundOverlayPanelColor'),
+ borderRadius: 'inherit',
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: 0,
+ borderBottom: '1px solid ' + cssVar('borderColor'),
fontWeight: 600,
+ overflow: 'hidden',
+ position: 'relative',
+});
+export const planTitleSpotlight = style({});
+globalStyle(`.${planTitle} > :not(.${planTitleSpotlight})`, {
+ position: 'relative',
+});
+export const planTitleName = style({
+ fontWeight: 600,
+ fontSize: cssVar('fontXs'),
+ lineHeight: '20px',
+});
+export const planTitleDescription = style({
+ fontWeight: 400,
+ fontSize: cssVar('fontXs'),
+ lineHeight: '20px',
+ color: cssVar('textSecondaryColor'),
+ marginBottom: 8,
+});
+export const planTitleTitle = style({
+ fontWeight: 600,
+ fontSize: cssVar('fontBase'),
+ lineHeight: '20px',
});
export const planPriceWrapper = style({
minHeight: '28px',
@@ -103,28 +171,43 @@ export const planAction = style({
width: '100%',
});
export const planBenefits = style({
- marginTop: '20px',
fontSize: cssVar('fontXs'),
display: 'flex',
flexDirection: 'column',
gap: '8px',
+ padding: '12px 16px',
+});
+export const planBenefitGroup = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 4,
+});
+export const planBenefitGroupTitle = style({
+ fontWeight: 500,
+ fontSize: cssVar('fontXs'),
+ lineHeight: '20px',
+ color: cssVar('textSecondaryColor'),
});
export const planBenefit = style({
display: 'flex',
gap: '8px',
lineHeight: '20px',
alignItems: 'normal',
- fontSize: '12px',
});
export const planBenefitIcon = style({
display: 'flex',
alignItems: 'center',
height: '20px',
});
+globalStyle(`.${planBenefitIcon} > svg`, {
+ color: cssVar('brandColor'),
+});
export const planBenefitText = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
+ color: cssVar('textPrimaryColor'),
+ fontSize: cssVar('fontXs'),
});
export const downgradeContentWrapper = style({
padding: '12px 0 20px 0px',
diff --git a/packages/frontend/graphql/src/graphql/cancel-subscription.gql b/packages/frontend/graphql/src/graphql/cancel-subscription.gql
index 6c791d909c..3d2361c04a 100644
--- a/packages/frontend/graphql/src/graphql/cancel-subscription.gql
+++ b/packages/frontend/graphql/src/graphql/cancel-subscription.gql
@@ -1,5 +1,8 @@
-mutation cancelSubscription($idempotencyKey: String!) {
- cancelSubscription(idempotencyKey: $idempotencyKey) {
+mutation cancelSubscription(
+ $idempotencyKey: String!
+ $plan: SubscriptionPlan = Pro
+) {
+ cancelSubscription(idempotencyKey: $idempotencyKey, plan: $plan) {
id
status
nextBillAt
diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts
index 014afe243f..4169577125 100644
--- a/packages/frontend/graphql/src/graphql/index.ts
+++ b/packages/frontend/graphql/src/graphql/index.ts
@@ -96,8 +96,8 @@ export const cancelSubscriptionMutation = {
definitionName: 'cancelSubscription',
containsFile: false,
query: `
-mutation cancelSubscription($idempotencyKey: String!) {
- cancelSubscription(idempotencyKey: $idempotencyKey) {
+mutation cancelSubscription($idempotencyKey: String!, $plan: SubscriptionPlan = Pro) {
+ cancelSubscription(idempotencyKey: $idempotencyKey, plan: $plan) {
id
status
nextBillAt
@@ -614,8 +614,8 @@ export const resumeSubscriptionMutation = {
definitionName: 'resumeSubscription',
containsFile: false,
query: `
-mutation resumeSubscription($idempotencyKey: String!) {
- resumeSubscription(idempotencyKey: $idempotencyKey) {
+mutation resumeSubscription($idempotencyKey: String!, $plan: SubscriptionPlan = Pro) {
+ resumeSubscription(idempotencyKey: $idempotencyKey, plan: $plan) {
id
status
nextBillAt
@@ -768,10 +768,11 @@ export const updateSubscriptionMutation = {
definitionName: 'updateSubscriptionRecurring',
containsFile: false,
query: `
-mutation updateSubscription($recurring: SubscriptionRecurring!, $idempotencyKey: String!) {
+mutation updateSubscription($idempotencyKey: String!, $plan: SubscriptionPlan = Pro, $recurring: SubscriptionRecurring!) {
updateSubscriptionRecurring(
- recurring: $recurring
idempotencyKey: $idempotencyKey
+ plan: $plan
+ recurring: $recurring
) {
id
plan
diff --git a/packages/frontend/graphql/src/graphql/resume-subscription.gql b/packages/frontend/graphql/src/graphql/resume-subscription.gql
index c060e25059..6d40d38232 100644
--- a/packages/frontend/graphql/src/graphql/resume-subscription.gql
+++ b/packages/frontend/graphql/src/graphql/resume-subscription.gql
@@ -1,5 +1,8 @@
-mutation resumeSubscription($idempotencyKey: String!) {
- resumeSubscription(idempotencyKey: $idempotencyKey) {
+mutation resumeSubscription(
+ $idempotencyKey: String!
+ $plan: SubscriptionPlan = Pro
+) {
+ resumeSubscription(idempotencyKey: $idempotencyKey, plan: $plan) {
id
status
nextBillAt
diff --git a/packages/frontend/graphql/src/graphql/update-subscription-billing.gql b/packages/frontend/graphql/src/graphql/update-subscription-billing.gql
index 1957efbfa3..cefdb89277 100644
--- a/packages/frontend/graphql/src/graphql/update-subscription-billing.gql
+++ b/packages/frontend/graphql/src/graphql/update-subscription-billing.gql
@@ -1,10 +1,12 @@
mutation updateSubscription(
- $recurring: SubscriptionRecurring!
$idempotencyKey: String!
+ $plan: SubscriptionPlan = Pro
+ $recurring: SubscriptionRecurring!
) {
updateSubscriptionRecurring(
- recurring: $recurring
idempotencyKey: $idempotencyKey
+ plan: $plan
+ recurring: $recurring
) {
id
plan
diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts
index a0f408e969..fc01dd81fc 100644
--- a/packages/frontend/graphql/src/schema.ts
+++ b/packages/frontend/graphql/src/schema.ts
@@ -173,6 +173,7 @@ export type AllBlobSizesQuery = {
export type CancelSubscriptionMutationVariables = Exact<{
idempotencyKey: Scalars['String']['input'];
+ plan?: InputMaybe;
}>;
export type CancelSubscriptionMutation = {
@@ -619,6 +620,7 @@ export type RemoveAvatarMutation = {
export type ResumeSubscriptionMutationVariables = Exact<{
idempotencyKey: Scalars['String']['input'];
+ plan?: InputMaybe;
}>;
export type ResumeSubscriptionMutation = {
@@ -758,8 +760,9 @@ export type SubscriptionQuery = {
};
export type UpdateSubscriptionMutationVariables = Exact<{
- recurring: SubscriptionRecurring;
idempotencyKey: Scalars['String']['input'];
+ plan?: InputMaybe;
+ recurring: SubscriptionRecurring;
}>;
export type UpdateSubscriptionMutation = {