@@ -143,102 +85,17 @@ const SubscriptionSettings = ({
isBeliever ? (
) : (
-
-
-
-
- ),
- }}
- />
-
- >
- }
- />
-
-
-
- ${amount}
-
- /
- {currentRecurring === SubscriptionRecurring.Monthly
- ? t['com.affine.payment.billing-setting.month']()
- : t['com.affine.payment.billing-setting.year']()}
-
-
-
+
)
) : (
)}
-
+
{proSubscription !== null ? (
proSubscription?.status === SubscriptionStatus.Active && (
- <>
-
-
-
- {isBeliever || isOnetime ? null : proSubscription.end &&
- proSubscription.canceledAt ? (
-
-
-
- ) : (
-
- {
- setOpenCancelModal(true);
- }}
- className="dangerous-setting"
- name={t[
- 'com.affine.payment.billing-setting.cancel-subscription'
- ]()}
- desc={t[
- 'com.affine.payment.billing-setting.cancel-subscription.description'
- ]()}
- >
-
-
-
- )}
- >
+
)
) : (
@@ -247,362 +104,6 @@ const SubscriptionSettings = ({
);
};
-const CloudExpirationInfo = () => {
- const t = useI18n();
- const subscriptionService = useService(SubscriptionService);
- const subscription = useLiveData(subscriptionService.subscription.pro$);
-
- let text = '';
- if (subscription?.nextBillAt) {
- text = t['com.affine.payment.billing-setting.renew-date.description']({
- renewDate: i18nTime(subscription.nextBillAt, {
- absolute: { accuracy: 'day' },
- }),
- });
- } else if (subscription?.end) {
- text = t['com.affine.payment.billing-setting.due-date.description']({
- dueDate: i18nTime(subscription.end, {
- absolute: { accuracy: 'day' },
- }),
- });
- }
-
- return text ? (
- <>
-
- {text}
- >
- ) : null;
-};
-
-const TypeFormLink = () => {
- const t = useI18n();
- const subscriptionService = useService(SubscriptionService);
- const authService = useService(AuthService);
-
- const pro = useLiveData(subscriptionService.subscription.pro$);
- const ai = useLiveData(subscriptionService.subscription.ai$);
- const account = useLiveData(authService.session.account$);
-
- if (!account) return null;
- if (!pro && !ai) return null;
-
- const plan = [];
- if (pro) plan.push(SubscriptionPlan.Pro);
- if (ai) plan.push(SubscriptionPlan.AI);
-
- const link = getUpgradeQuestionnaireLink({
- name: account.info?.name,
- id: account.id,
- email: account.email,
- recurring: pro?.recurring ?? ai?.recurring ?? SubscriptionRecurring.Yearly,
- plan,
- });
-
- return (
-
-
-
-
-
- );
-};
-
-const BelieverIdentifier = ({ onOpenPlans }: { onOpenPlans?: () => void }) => {
- const t = useI18n();
- const subscriptionService = useService(SubscriptionService);
- const readableLifetimePrice = useLiveData(
- subscriptionService.prices.readableLifetimePrice$
- );
-
- if (!readableLifetimePrice) return null;
-
- return (
-
-
-
-
- );
-};
-
-const AIPlanCard = ({ onClick }: { onClick: () => void }) => {
- const t = useI18n();
- const subscriptionService = useService(SubscriptionService);
- useEffect(() => {
- subscriptionService.subscription.revalidate();
- subscriptionService.prices.revalidate();
- }, [subscriptionService]);
- const price = useLiveData(subscriptionService.prices.aiPrice$);
- const subscription = useLiveData(subscriptionService.subscription.ai$);
- const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$);
-
- const priceReadable = price?.yearlyAmount
- ? `$${(price.yearlyAmount / 100).toFixed(2)}`
- : '?';
- const priceFrequency = t['com.affine.payment.billing-setting.year']();
-
- if (subscription === null) {
- return
;
- }
-
- const billingTip =
- subscription === undefined ? (
-
- ),
- }}
- />
- ) : subscription?.nextBillAt ? (
- t['com.affine.payment.ai.billing-tip.next-bill-at']({
- due: i18nTime(subscription.nextBillAt, {
- absolute: { accuracy: 'day' },
- }),
- })
- ) : (isOnetime || subscription?.canceledAt) && subscription.end ? (
- t['com.affine.payment.ai.billing-tip.end-at']({
- end: i18nTime(subscription.end, { absolute: { accuracy: 'day' } }),
- })
- ) : null;
-
- return (
-
-
-
- {price?.yearlyAmount ? (
- subscription ? (
- isOnetime ? (
-
- ) : subscription.canceledAt ? (
-
- ) : (
-
- )
- ) : (
-
- {t['com.affine.payment.billing-setting.ai.purchase']()}
-
- )
- ) : null}
-
-
- {subscription ? priceReadable : '$0'}
- /{priceFrequency}
-
-
- );
-};
-
-const PlanAction = ({
- plan,
- gotoPlansSetting,
-}: {
- plan: string;
- gotoPlansSetting: () => void;
-}) => {
- const t = useI18n();
-
- const subscription = useService(SubscriptionService).subscription;
- const isOnetimePro = useLiveData(subscription.isOnetimePro$);
-
- if (isOnetimePro) {
- return
;
- }
-
- return (
-
- );
-};
-
-const PaymentMethodUpdater = () => {
- const { isMutating, trigger } = useMutation({
- mutation: createCustomerPortalMutation,
- });
- const urlService = useService(UrlService);
- const t = useI18n();
-
- const update = useAsyncCallback(async () => {
- await trigger(null, {
- onSuccess: data => {
- urlService.openPopupWindow(data.createCustomerPortal);
- },
- });
- }, [trigger, urlService]);
-
- return (
-
- );
-};
-
-const ResumeSubscription = () => {
- const t = useI18n();
- const [open, setOpen] = useState(false);
- const subscription = useService(SubscriptionService).subscription;
- const handleClick = useCallback(() => {
- setOpen(true);
- }, []);
-
- return (
-
-
-
- );
-};
-
-const CancelSubscription = ({ loading }: { loading?: boolean }) => {
- return (
-
-
-
- );
-};
-
-const BillingHistory = () => {
- const t = useI18n();
-
- const invoicesService = useService(InvoicesService);
- const pageInvoices = useLiveData(invoicesService.invoices.pageInvoices$);
- const invoiceCount = useLiveData(invoicesService.invoices.invoiceCount$);
- const isLoading = useLiveData(invoicesService.invoices.isLoading$);
- const error = useLiveData(invoicesService.invoices.error$);
- const pageNum = useLiveData(invoicesService.invoices.pageNum$);
-
- useEffect(() => {
- invoicesService.invoices.revalidate();
- }, [invoicesService]);
-
- const handlePageChange = useCallback(
- (_: number, pageNum: number) => {
- invoicesService.invoices.setPageNum(pageNum);
- invoicesService.invoices.revalidate();
- },
- [invoicesService]
- );
-
- if (invoiceCount === undefined) {
- if (isLoading) {
- return
;
- } else {
- return (
-
- {error
- ? UserFriendlyError.fromAny(error).message
- : 'Failed to load invoices'}
-
- );
- }
- }
-
- return (
-
-
- {invoiceCount === 0 ? (
-
- {t['com.affine.payment.billing-setting.no-invoice']()}
-
- ) : (
- pageInvoices?.map(invoice => (
-
- ))
- )}
-
-
- {invoiceCount > invoicesService.invoices.PAGE_SIZE && (
-
- )}
-
- );
-};
-
-const InvoiceLine = ({
- invoice,
-}: {
- invoice: NonNullable
['invoices'][0];
-}) => {
- const t = useI18n();
- const urlService = useService(UrlService);
-
- const open = useCallback(() => {
- if (invoice.link) {
- urlService.openPopupWindow(invoice.link);
- }
- }, [invoice.link, urlService]);
-
- return (
-
-
-
- );
-};
-
const SubscriptionSettingSkeleton = () => {
const t = useI18n();
return (
@@ -616,14 +117,3 @@ const SubscriptionSettingSkeleton = () => {
);
};
-
-const BillingHistorySkeleton = () => {
- const t = useI18n();
- return (
-
-
-
-
-
- );
-};
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/payment-method.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/payment-method.tsx
new file mode 100644
index 0000000000..ba6ab9c9e6
--- /dev/null
+++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/payment-method.tsx
@@ -0,0 +1,147 @@
+import { SettingRow } from '@affine/component/setting-components';
+import {
+ Button,
+ type ButtonProps,
+ IconButton,
+} from '@affine/component/ui/button';
+import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
+import { SubscriptionService } from '@affine/core/modules/cloud';
+import { UrlService } from '@affine/core/modules/url';
+import { createCustomerPortalMutation } from '@affine/graphql';
+import { useI18n } from '@affine/i18n';
+import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
+import { useLiveData, useService } from '@toeverything/infra';
+import { useCallback, useEffect, useState } from 'react';
+
+import { useMutation } from '../../../../../components/hooks/use-mutation';
+import { CancelAction, ResumeAction } from '../plans/actions';
+import * as styles from './style.css';
+
+export const PaymentMethod = () => {
+ const t = useI18n();
+ const subscriptionService = useService(SubscriptionService);
+ useEffect(() => {
+ subscriptionService.subscription.revalidate();
+ subscriptionService.prices.revalidate();
+ }, [subscriptionService]);
+
+ const proSubscription = useLiveData(subscriptionService.subscription.pro$);
+ const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$);
+ const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$);
+
+ const [openCancelModal, setOpenCancelModal] = useState(false);
+ return (
+ <>
+
+
+
+ {isBeliever || isOnetime ? null : proSubscription?.end &&
+ proSubscription?.canceledAt ? (
+
+
+
+ ) : (
+
+ {
+ setOpenCancelModal(true);
+ }}
+ className="dangerous-setting"
+ name={t['com.affine.payment.billing-setting.cancel-subscription']()}
+ desc={t[
+ 'com.affine.payment.billing-setting.cancel-subscription.description'
+ ]()}
+ >
+
+
+
+ )}
+ >
+ );
+};
+
+export const PaymentMethodUpdater = ({
+ inCardView,
+ className,
+ variant,
+}: {
+ inCardView?: boolean;
+ className?: string;
+ variant?: ButtonProps['variant'];
+}) => {
+ const { isMutating, trigger } = useMutation({
+ mutation: createCustomerPortalMutation,
+ });
+ const urlService = useService(UrlService);
+ const t = useI18n();
+
+ const update = useAsyncCallback(async () => {
+ await trigger(null, {
+ onSuccess: data => {
+ urlService.openPopupWindow(data.createCustomerPortal);
+ },
+ });
+ }, [trigger, urlService]);
+
+ return (
+
+ );
+};
+
+const ResumeSubscription = () => {
+ const t = useI18n();
+ const [open, setOpen] = useState(false);
+ const subscription = useService(SubscriptionService).subscription;
+ const handleClick = useCallback(() => {
+ setOpen(true);
+ }, []);
+
+ return (
+
+
+
+ );
+};
+
+const CancelSubscription = ({ loading }: { loading?: boolean }) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/pro-plan-card.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/pro-plan-card.tsx
new file mode 100644
index 0000000000..72e20ae987
--- /dev/null
+++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/pro-plan-card.tsx
@@ -0,0 +1,187 @@
+import { Button } from '@affine/component';
+import { SettingRow } from '@affine/component/setting-components';
+import { SubscriptionService } from '@affine/core/modules/cloud';
+import {
+ SubscriptionPlan,
+ SubscriptionRecurring,
+ SubscriptionStatus,
+} from '@affine/graphql';
+import { type I18nString, i18nTime, Trans, useI18n } from '@affine/i18n';
+import { useLiveData, useService } from '@toeverything/infra';
+import { useEffect } from 'react';
+
+import { RedeemCode } from '../plans/plan-card';
+import { CardNameLabelRow } from './card-name-label-row';
+import { PaymentMethodUpdater } from './payment-method';
+import * as styles from './style.css';
+
+const DescriptionI18NKey = {
+ Basic: 'com.affine.payment.billing-setting.current-plan.description',
+ Monthly:
+ 'com.affine.payment.billing-setting.current-plan.description.monthly',
+ Yearly: 'com.affine.payment.billing-setting.current-plan.description.yearly',
+ Lifetime:
+ 'com.affine.payment.billing-setting.current-plan.description.lifetime',
+} as const satisfies { [key: string]: I18nString };
+
+const getMessageKey = (
+ plan: SubscriptionPlan,
+ recurring: SubscriptionRecurring
+) => {
+ if (plan !== SubscriptionPlan.Pro) {
+ return DescriptionI18NKey.Basic;
+ }
+ return DescriptionI18NKey[recurring];
+};
+
+export const ProPlanCard = ({
+ gotoCloudPlansSetting,
+}: {
+ gotoCloudPlansSetting: () => void;
+}) => {
+ const t = useI18n();
+ const subscriptionService = useService(SubscriptionService);
+ useEffect(() => {
+ subscriptionService.subscription.revalidate();
+ subscriptionService.prices.revalidate();
+ }, [subscriptionService]);
+ const proSubscription = useLiveData(subscriptionService.subscription.pro$);
+
+ const proPrice = useLiveData(subscriptionService.prices.proPrice$);
+
+ const currentPlan = proSubscription?.plan ?? SubscriptionPlan.Free;
+ const currentRecurring =
+ proSubscription?.recurring ?? SubscriptionRecurring.Monthly;
+
+ const amount = proSubscription
+ ? proPrice
+ ? proSubscription.recurring === SubscriptionRecurring.Monthly
+ ? String((proPrice.amount ?? 0) / 100)
+ : String((proPrice.yearlyAmount ?? 0) / 100)
+ : '?'
+ : '0';
+
+ return (
+
+
+
+ }
+ desc={
+ <>
+
+ ),
+ }}
+ />
+
+ >
+ }
+ />
+
+
+
+ ${amount}
+
+ /
+ {currentRecurring === SubscriptionRecurring.Monthly
+ ? t['com.affine.payment.billing-setting.month']()
+ : t['com.affine.payment.billing-setting.year']()}
+
+
+
+ );
+};
+
+const CloudExpirationInfo = () => {
+ const t = useI18n();
+ const subscriptionService = useService(SubscriptionService);
+ const subscription = useLiveData(subscriptionService.subscription.pro$);
+
+ let text = '';
+
+ if (subscription?.status === SubscriptionStatus.PastDue) {
+ text = t['com.affine.payment.billing-tip.past-due']({
+ due: i18nTime(subscription.nextBillAt, {
+ absolute: { accuracy: 'day' },
+ }),
+ });
+ } else if (subscription?.nextBillAt) {
+ text = t['com.affine.payment.billing-setting.renew-date.description']({
+ renewDate: i18nTime(subscription.nextBillAt, {
+ absolute: { accuracy: 'day' },
+ }),
+ });
+ } else if (subscription?.end) {
+ text = t['com.affine.payment.billing-setting.due-date.description']({
+ dueDate: i18nTime(subscription.end, {
+ absolute: { accuracy: 'day' },
+ }),
+ });
+ }
+
+ return text ? (
+ <>
+
+ {text}
+ >
+ ) : null;
+};
+
+const PlanAction = ({
+ plan,
+ subscriptionStatus,
+ gotoPlansSetting,
+}: {
+ plan: string;
+ gotoPlansSetting: () => void;
+ subscriptionStatus?: SubscriptionStatus;
+}) => {
+ const t = useI18n();
+
+ const subscription = useService(SubscriptionService).subscription;
+ const isOnetimePro = useLiveData(subscription.isOnetimePro$);
+
+ if (isOnetimePro) {
+ return ;
+ }
+
+ return (
+ <>
+
+ {subscriptionStatus === SubscriptionStatus.PastDue ? (
+
+ ) : null}
+ >
+ );
+};
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/style.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/style.css.ts
index c145251a5a..b9cb852e96 100644
--- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/style.css.ts
+++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/style.css.ts
@@ -1,4 +1,5 @@
import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css';
export const subscription = style({});
export const history = style({
@@ -14,7 +15,7 @@ export const planCard = style({
display: 'flex',
justifyContent: 'space-between',
padding: '12px',
- border: `1px solid ${cssVar('borderColor')}`,
+ border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
borderRadius: '8px',
});
export const currentPlan = style({
@@ -35,10 +36,10 @@ export const paymentMethod = style({
marginTop: '24px',
});
globalStyle('.dangerous-setting .name', {
- color: cssVar('errorColor'),
+ color: cssVarV2('status/error'),
});
export const noInvoice = style({
- color: cssVar('textSecondaryColor'),
+ color: cssVarV2('text/secondary'),
fontSize: cssVar('fontXs'),
});
export const currentPlanName = style({
@@ -70,13 +71,13 @@ export const believerTitle = style({
fontSize: cssVar('fontSm'),
fontWeight: 600,
lineHeight: '22px',
- color: cssVar('textPrimaryColor'),
+ color: cssVarV2('text/primary'),
});
export const believerSubtitle = style({
fontSize: cssVar('fontXs'),
lineHeight: '20px',
fontWeight: 400,
- color: cssVar('textSecondaryColor'),
+ color: cssVarV2('text/secondary'),
});
globalStyle(`.${believerSubtitle} > a`, {
color: cssVar('brandColor'),
@@ -91,11 +92,44 @@ export const believerPrice = style({
fontSize: '18px',
fontWeight: 600,
lineHeight: '26px',
- color: cssVar('textPrimaryColor'),
+ color: cssVarV2('text/primary'),
});
export const believerPriceCaption = style({
fontSize: cssVar('fontXs'),
lineHeight: '20px',
fontWeight: 500,
- color: cssVar('textSecondaryColor'),
+ color: cssVarV2('text/secondary'),
+});
+export const cardNameLabelRow = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: '16px',
+});
+export const cardName = style({
+ fontSize: cssVar('fontSm'),
+ fontWeight: 600,
+ color: cssVarV2('text/primary'),
+ lineHeight: '22px',
+});
+export const cardLabelContainer = style({
+ display: 'flex',
+ gap: '4px',
+ color: cssVarV2('button/primary'),
+ selectors: {
+ '&.past-due': {
+ color: cssVarV2('button/error'),
+ },
+ },
+});
+export const cardLabel = style({
+ fontSize: cssVar('fontXs'),
+ fontWeight: 500,
+});
+export const cardLabelIcon = style({
+ width: '14px',
+ height: '14px',
+});
+export const manageMentInCard = style({
+ marginTop: '8px',
+ marginLeft: '12px',
});
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/typeform-link.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/typeform-link.tsx
new file mode 100644
index 0000000000..2770c756fa
--- /dev/null
+++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/typeform-link.tsx
@@ -0,0 +1,46 @@
+import { SettingRow } from '@affine/component/setting-components';
+import { Button } from '@affine/component/ui/button';
+import { getUpgradeQuestionnaireLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
+import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
+import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
+import { useI18n } from '@affine/i18n';
+import { useLiveData, useService } from '@toeverything/infra';
+
+import * as styles from './style.css';
+
+export const TypeformLink = () => {
+ const t = useI18n();
+ const subscriptionService = useService(SubscriptionService);
+ const authService = useService(AuthService);
+
+ const pro = useLiveData(subscriptionService.subscription.pro$);
+ const ai = useLiveData(subscriptionService.subscription.ai$);
+ const account = useLiveData(authService.session.account$);
+
+ if (!account) return null;
+ if (!pro && !ai) return null;
+
+ const plan = [];
+ if (pro) plan.push(SubscriptionPlan.Pro);
+ if (ai) plan.push(SubscriptionPlan.AI);
+
+ const link = getUpgradeQuestionnaireLink({
+ name: account.info?.name,
+ id: account.id,
+ email: account.email,
+ recurring: pro?.recurring ?? ai?.recurring ?? SubscriptionRecurring.Yearly,
+ plan,
+ });
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/actions/cancel.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/actions/cancel.tsx
index 7e18b648c6..a76ee587d2 100644
--- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/actions/cancel.tsx
+++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/ai/actions/cancel.tsx
@@ -86,7 +86,7 @@ export const AICancel = (btnProps: ButtonProps) => {