feat(core): pricing plans actions (#4724)

This commit is contained in:
Cats Juice
2023-10-26 22:00:14 +08:00
committed by GitHub
parent 9334a363c7
commit edb6e0fd69
8 changed files with 607 additions and 178 deletions

View File

@@ -1,10 +1,14 @@
import { RadioButton, RadioButtonGroup } from '@affine/component';
import { pushNotificationAtom } from '@affine/component/notification-center';
import {
pricesQuery,
SubscriptionPlan,
SubscriptionRecurring,
} from '@affine/graphql';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useQuery } from '@affine/workspace/affine/gql';
import { useSetAtom } from 'jotai';
import { Suspense, useEffect, useRef, useState } from 'react';
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
@@ -14,10 +18,25 @@ import { type FixedPrice, getPlanDetail, PlanCard } from './plan-card';
import { PlansSkeleton } from './skeleton';
import * as styles from './style.css';
const getRecurringLabel = ({
recurring,
t,
}: {
recurring: SubscriptionRecurring;
t: ReturnType<typeof useAFFiNEI18N>;
}) => {
return recurring === SubscriptionRecurring.Monthly
? t['com.affine.payment.recurring-monthly']()
: t['com.affine.payment.recurring-yearly']();
};
const Settings = () => {
const t = useAFFiNEI18N();
const [subscription, mutateSubscription] = useUserSubscription();
const pushNotification = useSetAtom(pushNotificationAtom);
const loggedIn = useCurrentLoginStatus() === 'authenticated';
const planDetail = getPlanDetail();
const planDetail = getPlanDetail(t);
const scrollWrapper = useRef<HTMLDivElement>(null);
const {
@@ -44,6 +63,9 @@ const Settings = () => {
);
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
const isCanceled = !!subscription?.canceledAt;
const currentRecurring =
subscription?.recurring ?? SubscriptionRecurring.Monthly;
const yearlyDiscount = (
planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined
@@ -76,23 +98,39 @@ const Settings = () => {
}, [recurring]);
const subtitle = loggedIn ? (
<p>
You are current on the {currentPlan} plan. If you have any questions,
please contact our <span>{/*TODO: add action*/}customer support</span>.
</p>
isCanceled ? (
<p>
{t['com.affine.payment.subtitle-canceled']({
plan: `${getRecurringLabel({
recurring: currentRecurring,
t,
})} ${currentPlan}`,
})}
</p>
) : (
<p>
<Trans
plan={currentPlan}
i18nKey="com.affine.payment.subtitle-active"
values={{ currentPlan }}
>
You are current on the {{ currentPlan }} plan. If you have any
questions, please contact our&nbsp;
<a
href="#"
target="_blank"
style={{ color: 'var(--affine-link-color)' }}
>
customer support
</a>
.
</Trans>
</p>
)
) : (
<p>
This is the Pricing plans of AFFiNE Cloud. You can sign up or sign in to
your account first.
</p>
<p>{t['com.affine.payment.subtitle-not-signed-in']()}</p>
);
const getRecurringLabel = (recurring: SubscriptionRecurring) =>
({
[SubscriptionRecurring.Monthly]: 'Monthly',
[SubscriptionRecurring.Yearly]: 'Annually',
})[recurring];
const tabs = (
<RadioButtonGroup
className={styles.recurringRadioGroup}
@@ -101,10 +139,12 @@ const Settings = () => {
>
{Object.values(SubscriptionRecurring).map(recurring => (
<RadioButton key={recurring} value={recurring}>
{getRecurringLabel(recurring)}
{getRecurringLabel({ recurring, t })}
{recurring === SubscriptionRecurring.Yearly && yearlyDiscount && (
<span className={styles.radioButtonDiscount}>
{yearlyDiscount}% off
{t['com.affine.payment.discount-amount']({
amount: yearlyDiscount,
})}
</span>
)}
</RadioButton>
@@ -119,6 +159,21 @@ const Settings = () => {
<PlanCard
key={detail.plan}
onSubscriptionUpdate={mutateSubscription}
onNotify={({ detail, recurring }) => {
pushNotification({
type: 'success',
title: t['com.affine.payment.updated-notify-title'](),
message: t['com.affine.payment.updated-notify-msg']({
plan:
detail.plan === SubscriptionPlan.Free
? SubscriptionPlan.Free
: getRecurringLabel({
recurring: recurring as SubscriptionRecurring,
t,
}),
}),
});
}}
{...{ detail, subscription, recurring }}
/>
);

View File

@@ -1,4 +1,5 @@
import { SettingHeader } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon } from '@blocksuite/icons';
import type { HtmlHTMLAttributes, ReactNode } from 'react';
@@ -14,32 +15,37 @@ export interface PlanLayoutProps
scrollRef?: React.RefObject<HTMLDivElement>;
}
const SeeAllLink = () => (
<a
className={styles.allPlansLink}
href="https://affine.pro/pricing"
target="_blank"
rel="noopener noreferrer"
>
See all plans
{<ArrowRightBigIcon width="16" height="16" />}
</a>
);
const SeeAllLink = () => {
const t = useAFFiNEI18N();
return (
<a
className={styles.allPlansLink}
href="https://affine.pro/pricing"
target="_blank"
rel="noopener noreferrer"
>
{t['com.affine.payment.see-all-plans']()}
{<ArrowRightBigIcon width="16" height="16" />}
</a>
);
};
export const PlanLayout = ({
subtitle,
tabs,
scroll,
title = 'Pricing Plans',
title,
footer = <SeeAllLink />,
scrollRef,
}: PlanLayoutProps) => {
const t = useAFFiNEI18N();
return (
<div className={styles.plansLayoutRoot}>
{/* TODO: SettingHeader component shouldn't have margin itself */}
<SettingHeader
style={{ marginBottom: '0px' }}
title={title}
title={title ?? t['com.affine.payment.title']()}
subtitle={subtitle}
/>
{tabs}

View File

@@ -0,0 +1,119 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DialogTrigger } from '@radix-ui/react-dialog';
import { Button } from '@toeverything/components/button';
import {
ConfirmModal,
type ConfirmModalProps,
Modal,
} from '@toeverything/components/modal';
import { type ReactNode, useEffect, useRef } from 'react';
import * as styles from './style.css';
/**
*
* @param param0
* @returns
*/
export const ConfirmLoadingModal = ({
type,
loading,
open,
content,
onOpenChange,
onConfirm,
...props
}: {
type: 'resume' | 'change';
loading?: boolean;
content?: ReactNode;
} & ConfirmModalProps) => {
const t = useAFFiNEI18N();
const confirmed = useRef(false);
const title = t[`com.affine.payment.modal.${type}.title`]();
const confirmText = t[`com.affine.payment.modal.${type}.confirm`]();
const cancelText = t[`com.affine.payment.modal.${type}.cancel`]();
const contentText =
type !== 'change' ? t[`com.affine.payment.modal.${type}.content`]() : '';
useEffect(() => {
if (!loading && open && confirmed.current) {
onOpenChange?.(false);
confirmed.current = false;
}
}, [loading, open, onOpenChange]);
return (
<ConfirmModal
title={title}
cancelText={cancelText}
confirmButtonOptions={{
type: 'primary',
children: confirmText,
loading,
}}
open={open}
onOpenChange={onOpenChange}
onConfirm={() => {
confirmed.current = true;
onConfirm?.();
}}
{...props}
>
{content ?? contentText}
</ConfirmModal>
);
};
/**
* Downgrade modal, confirm & cancel button are reversed
* @param param0
*/
export const DowngradeModal = ({
open,
onOpenChange,
onCancel,
}: {
loading?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
onCancel?: () => void;
}) => {
const t = useAFFiNEI18N();
return (
<Modal
title={t['com.affine.payment.modal.downgrade.title']()}
open={open}
contentOptions={{}}
width={480}
onOpenChange={onOpenChange}
>
<div className={styles.downgradeContentWrapper}>
<p className={styles.downgradeContent}>
{t['com.affine.payment.modal.downgrade.content']()}
</p>
<p className={styles.downgradeCaption}>
{t['com.affine.payment.modal.downgrade.caption']()}
</p>
</div>
<footer className={styles.downgradeFooter}>
<Button
onClick={() => {
onOpenChange?.(false);
onCancel?.();
}}
>
{t['com.affine.payment.modal.downgrade.cancel']()}
</Button>
<DialogTrigger asChild>
<Button onClick={() => onOpenChange?.(false)} type="primary">
{t['com.affine.payment.modal.downgrade.confirm']()}
</Button>
</DialogTrigger>
</footer>
</Modal>
);
};

View File

@@ -5,19 +5,32 @@ import type {
import {
cancelSubscriptionMutation,
checkoutMutation,
resumeSubscriptionMutation,
SubscriptionPlan,
SubscriptionRecurring,
updateSubscriptionMutation,
} from '@affine/graphql';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { DoneIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { useSetAtom } from 'jotai';
import { useAtom } from 'jotai';
import { type PropsWithChildren, useCallback, useEffect, useRef } from 'react';
import {
type PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { openPaymentDisableAtom } from '../../../../../atoms';
import { authAtom } from '../../../../../atoms/index';
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
import { BulledListIcon } from './icons/bulled-list';
import { ConfirmLoadingModal, DowngradeModal } from './modals';
import * as styles from './style.css';
export interface FixedPrice {
@@ -41,12 +54,13 @@ interface PlanCardProps {
subscription?: Subscription | null;
recurring: string;
onSubscriptionUpdate: SubscriptionMutator;
onNotify: (info: {
detail: FixedPrice | DynamicPrice;
recurring: string;
}) => void;
}
export function getPlanDetail() {
// const t = useAFFiNEI18N();
// TODO: i18n all things
export function getPlanDetail(t: ReturnType<typeof useAFFiNEI18N>) {
return new Map<SubscriptionPlan, FixedPrice | DynamicPrice>([
[
SubscriptionPlan.Free,
@@ -56,12 +70,12 @@ export function getPlanDetail() {
price: '0',
yearlyPrice: '0',
benefits: [
'Unlimited local workspace',
'Unlimited login devices',
'Unlimited blocks',
'AFFiNE Cloud Storage 10GB',
'The maximum file size is 10M',
'Number of members per Workspace ≤ 3',
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' }),
],
},
],
@@ -73,12 +87,12 @@ export function getPlanDetail() {
price: '1',
yearlyPrice: '1',
benefits: [
'Unlimited local workspace',
'Unlimited login devices',
'Unlimited blocks',
'AFFiNE Cloud Storage 100GB',
'The maximum file size is 500M',
'Number of members per Workspace ≤ 10',
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: '500M' }),
t['com.affine.payment.benefit-6']({ capacity: '10' }),
],
},
],
@@ -89,9 +103,9 @@ export function getPlanDetail() {
plan: SubscriptionPlan.Team,
contact: true,
benefits: [
'Best team workspace for collaboration and knowledge distilling.',
'Focusing on what really matters with team project management and automation.',
'Pay for seats, fits all team size.',
t['com.affine.payment.dynamic-benefit-1'](),
t['com.affine.payment.dynamic-benefit-2'](),
t['com.affine.payment.dynamic-benefit-3'](),
],
},
],
@@ -102,20 +116,16 @@ export function getPlanDetail() {
plan: SubscriptionPlan.Enterprise,
contact: true,
benefits: [
'Solutions & best practices for dedicated needs.',
'Embedable & interrogations with IT support.',
t['com.affine.payment.dynamic-benefit-4'](),
t['com.affine.payment.dynamic-benefit-5'](),
],
},
],
]);
}
export const PlanCard = ({
detail,
subscription,
recurring,
onSubscriptionUpdate,
}: PlanCardProps) => {
export const PlanCard = (props: PlanCardProps) => {
const { detail, subscription, recurring } = props;
const loggedIn = useCurrentLoginStatus() === 'authenticated';
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
const currentRecurring = subscription?.recurring;
@@ -160,49 +170,7 @@ export const PlanCard = ({
)}
</p>
</div>
{
// branches:
// if contact => 'Contact Sales'
// if not signed in:
// if free => 'Sign up free'
// else => 'Buy Pro'
// else
// if isCurrent => 'Current Plan'
// else if free => 'Downgrade'
// else if currentRecurring !== recurring => 'Change to {recurring} Billing'
// else => 'Upgrade'
// TODO: should replace with components with proper actions
detail.type === 'dynamic' ? (
<ContactSales />
) : loggedIn ? (
detail.plan === currentPlan &&
(currentRecurring === recurring ||
(!currentRecurring && detail.plan === SubscriptionPlan.Free)) ? (
<CurrentPlan />
) : detail.plan === SubscriptionPlan.Free ? (
<Downgrade onSubscriptionUpdate={onSubscriptionUpdate} />
) : currentRecurring !== recurring &&
currentPlan === detail.plan ? (
<ChangeRecurring
// @ts-expect-error must exist
from={currentRecurring}
to={recurring as SubscriptionRecurring}
onSubscriptionUpdate={onSubscriptionUpdate}
/>
) : (
<Upgrade
recurring={recurring as SubscriptionRecurring}
onSubscriptionUpdate={onSubscriptionUpdate}
/>
)
) : (
<SignupAction>
{detail.plan === SubscriptionPlan.Free
? 'Sign up free'
: 'Buy Pro'}
</SignupAction>
)
}
<ActionButton {...props} />
</div>
<div className={styles.planBenefits}>
{detail.benefits.map((content, i) => (
@@ -226,15 +194,111 @@ export const PlanCard = ({
);
};
const ActionButton = ({
detail,
subscription,
recurring,
onSubscriptionUpdate,
onNotify,
}: PlanCardProps) => {
const t = useAFFiNEI18N();
const loggedIn = useCurrentLoginStatus() === 'authenticated';
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
const currentRecurring = subscription?.recurring;
const mutateAndNotify = useCallback(
(sub: Parameters<SubscriptionMutator>[0]) => {
onSubscriptionUpdate?.(sub);
onNotify?.({ detail, recurring });
},
[onSubscriptionUpdate, onNotify, detail, recurring]
);
// branches:
// if contact => 'Contact Sales'
// if not signed in:
// if free => 'Sign up free'
// else => 'Buy Pro'
// else
// if isCurrent
// if canceled => 'Resume'
// else => 'Current Plan'
// if isCurrent => 'Current Plan'
// else if free => 'Downgrade'
// else if currentRecurring !== recurring => 'Change to {recurring} Billing'
// else => 'Upgrade'
// contact
if (detail.type === 'dynamic') {
return <ContactSales />;
}
// not signed in
if (!loggedIn) {
return (
<SignUpAction>
{detail.plan === SubscriptionPlan.Free
? t['com.affine.payment.sign-up-free']()
: t['com.affine.payment.buy-pro']()}
</SignUpAction>
);
}
const isCanceled = !!subscription?.canceledAt;
const isFree = detail.plan === SubscriptionPlan.Free;
const isCurrent =
detail.plan === currentPlan &&
(isFree ? true : currentRecurring === recurring);
// is current
if (isCurrent) {
return isCanceled ? (
<ResumeAction onSubscriptionUpdate={mutateAndNotify} />
) : (
<CurrentPlan />
);
}
if (isFree) {
return (
<Downgrade disabled={isCanceled} onSubscriptionUpdate={mutateAndNotify} />
);
}
return currentPlan === detail.plan ? (
<ChangeRecurring
from={currentRecurring as SubscriptionRecurring}
to={recurring as SubscriptionRecurring}
due={subscription?.nextBillAt || ''}
onSubscriptionUpdate={mutateAndNotify}
disabled={isCanceled}
/>
) : (
<Upgrade
recurring={recurring as SubscriptionRecurring}
onSubscriptionUpdate={mutateAndNotify}
/>
);
};
const CurrentPlan = () => {
return <Button className={styles.planAction}>Current Plan</Button>;
const t = useAFFiNEI18N();
return (
<Button className={styles.planAction}>
{t['com.affine.payment.current-plan']()}
</Button>
);
};
const Downgrade = ({
disabled,
onSubscriptionUpdate,
}: {
disabled?: boolean;
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
const { isMutating, trigger } = useMutation({
mutation: cancelSubscriptionMutation,
});
@@ -247,25 +311,43 @@ const Downgrade = ({
});
}, [trigger, onSubscriptionUpdate]);
const tooltipContent = disabled
? t['com.affine.payment.downgraded-tooltip']()
: null;
return (
<Button
className={styles.planAction}
type="primary"
onClick={downgrade /* TODO: poppup confirmation modal instead */}
disabled={isMutating}
loading={isMutating}
>
Downgrade
</Button>
<>
<Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}>
<div className={styles.planAction}>
<Button
className={styles.planAction}
type="primary"
onClick={() => setOpen(true)}
disabled={disabled || isMutating}
loading={isMutating}
>
{t['com.affine.payment.downgrade']()}
</Button>
</div>
</Tooltip>
<DowngradeModal open={open} onCancel={downgrade} onOpenChange={setOpen} />
</>
);
};
const ContactSales = () => {
const t = useAFFiNEI18N();
return (
// TODO: add action
<Button className={styles.planAction} type="primary">
Contact Sales
</Button>
<a
className={styles.planAction}
href="https://6dxre9ihosp.typeform.com/to/uZeBtpPm"
target="_blank"
rel="noreferrer"
>
<Button className={styles.planAction} type="primary">
{t['com.affine.payment.contact-sales']()}
</Button>
</a>
);
};
@@ -276,6 +358,7 @@ const Upgrade = ({
recurring: SubscriptionRecurring;
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const t = useAFFiNEI18N();
const { isMutating, trigger } = useMutation({
mutation: checkoutMutation,
});
@@ -330,27 +413,35 @@ const Upgrade = ({
}, [onClose]);
return (
<Button
className={styles.planAction}
type="primary"
onClick={upgrade}
disabled={isMutating}
loading={isMutating}
>
Upgrade
</Button>
<>
<Button
className={styles.planAction}
type="primary"
onClick={upgrade}
disabled={isMutating}
loading={isMutating}
>
{t['com.affine.payment.upgrade']()}
</Button>
</>
);
};
const ChangeRecurring = ({
from: _from /* TODO: from can be useful when showing confirmation modal */,
from,
to,
disabled,
due,
onSubscriptionUpdate,
}: {
from: SubscriptionRecurring;
to: SubscriptionRecurring;
disabled?: boolean;
due: string;
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
const { isMutating, trigger } = useMutation({
mutation: updateSubscriptionMutation,
});
@@ -366,24 +457,102 @@ const ChangeRecurring = ({
);
}, [trigger, onSubscriptionUpdate, to]);
const changeCurringContent = (
<Trans values={{ from, to, due }} className={styles.downgradeContent}>
You are changing your <span className={styles.textEmphasis}>{from}</span>{' '}
subscription to <span className={styles.textEmphasis}>{to}</span>{' '}
subscription. This change will take effect in the next billing cycle, with
an effective date of <span className={styles.textEmphasis}>{due}</span>.
</Trans>
);
return (
<Button
className={styles.planAction}
type="primary"
onClick={change /* TODO: popup confirmation modal instead */}
disabled={isMutating}
loading={isMutating}
>
Change to {to} Billing
</Button>
<>
<Button
className={styles.planAction}
type="primary"
onClick={() => setOpen(true)}
disabled={disabled || isMutating}
loading={isMutating}
>
{t['com.affine.payment.change-to']({ to })}
</Button>
<ConfirmLoadingModal
type={'change'}
loading={isMutating}
open={open}
onConfirm={change}
onOpenChange={setOpen}
content={changeCurringContent}
/>
</>
);
};
const SignupAction = ({ children }: PropsWithChildren) => {
// TODO: add login action
const SignUpAction = ({ children }: PropsWithChildren) => {
const setOpen = useSetAtom(authAtom);
const onClickSignIn = useCallback(async () => {
setOpen(state => ({
...state,
openModal: true,
}));
}, [setOpen]);
return (
<Button className={styles.planAction} type="primary">
<Button
onClick={onClickSignIn}
className={styles.planAction}
type="primary"
>
{children}
</Button>
);
};
const ResumeAction = ({
onSubscriptionUpdate,
}: {
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
const [hovered, setHovered] = useState(false);
const { isMutating, trigger } = useMutation({
mutation: resumeSubscriptionMutation,
});
const resume = useCallback(() => {
trigger(null, {
onSuccess: data => {
onSubscriptionUpdate(data.resumeSubscription);
},
});
}, [trigger, onSubscriptionUpdate]);
return (
<>
<Button
className={styles.planAction}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={() => setOpen(true)}
loading={isMutating}
disabled={isMutating}
>
{hovered
? t['com.affine.payment.resume-renewal']()
: t['com.affine.payment.current-plan']()}
</Button>
<ConfirmLoadingModal
type={'resume'}
open={open}
onConfirm={resume}
onOpenChange={setOpen}
loading={isMutating}
/>
</>
);
};

View File

@@ -14,7 +14,7 @@ export const radioButtonDiscount = style({
});
export const planCardsWrapper = style({
paddingRight: 'calc(var(--setting-modal-gap-x))',
paddingRight: 'calc(var(--setting-modal-gap-x) + 30px)',
display: 'flex',
gap: '16px',
width: 'fit-content',
@@ -116,3 +116,34 @@ export const planBenefitText = style({
flexDirection: 'column',
alignItems: 'center',
});
export const downgradeContentWrapper = style({
padding: '12px 0 20px 0px',
display: 'flex',
flexDirection: 'column',
gap: '12px',
});
export const downgradeContent = style({
fontSize: '15px',
lineHeight: '24px',
fontWeight: 400,
color: 'var(--affine-text-primary-color)',
});
export const downgradeCaption = style({
fontSize: '14px',
lineHeight: '22px',
color: 'var(--affine-text-secondary-color)',
});
export const downgradeFooter = style({
display: 'flex',
justifyContent: 'flex-end',
gap: '20px',
paddingTop: '20px',
});
export const textEmphasis = style({
color: 'var(--affine-text-emphasis-color)',
});

View File

@@ -39,6 +39,7 @@ export const SettingModal = ({
const generalSettingList = useGeneralSettingList();
const modalContentRef = useRef<HTMLDivElement>(null);
const modalContentWrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!modalProps.open) return;
@@ -46,19 +47,18 @@ export const SettingModal = ({
const onResize = debounce(() => {
cancelAnimationFrame(animationFrameId);
animationFrameId = requestAnimationFrame(() => {
if (!modalContentRef.current) return;
if (!modalContentRef.current || !modalContentWrapperRef.current) return;
const wrapperWidth = modalContentWrapperRef.current.offsetWidth;
const contentWidth = modalContentRef.current.offsetWidth;
const computedStyle = window.getComputedStyle(modalContentRef.current);
const marginX = parseInt(computedStyle.marginLeft, 10);
const paddingX = parseInt(computedStyle.paddingLeft, 10);
modalContentRef.current?.style.setProperty(
'--setting-modal-width',
`${contentWidth + marginX * 2}px`
`${wrapperWidth}px`
);
modalContentRef.current?.style.setProperty(
'--setting-modal-gap-x',
`${marginX + paddingX}px`
`${(wrapperWidth - contentWidth) / 2}px`
);
});
}, 200);
@@ -121,33 +121,35 @@ export const SettingModal = ({
<div
data-testid="setting-modal-content"
className={style.wrapper}
ref={modalContentRef}
ref={modalContentWrapperRef}
>
<div className={style.content}>
{activeTab === 'workspace' && workspaceId ? (
<Suspense fallback={<WorkspaceDetailSkeleton />}>
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
</Suspense>
) : null}
{generalSettingList.find(v => v.key === activeTab) ? (
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
) : null}
{activeTab === 'account' && loginStatus === 'authenticated' ? (
<AccountSetting />
) : null}
</div>
<div className="footer">
<a
href="https://community.affine.pro/home"
target="_blank"
rel="noreferrer"
className={style.suggestionLink}
>
<span className={style.suggestionLinkIcon}>
<ContactWithUsIcon />
</span>
{t['com.affine.settings.suggestion']()}
</a>
<div ref={modalContentRef} className={style.centerContainer}>
<div className={style.content}>
{activeTab === 'workspace' && workspaceId ? (
<Suspense fallback={<WorkspaceDetailSkeleton />}>
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
</Suspense>
) : null}
{generalSettingList.find(v => v.key === activeTab) ? (
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
) : null}
{activeTab === 'account' && loginStatus === 'authenticated' ? (
<AccountSetting />
) : null}
</div>
<div className="footer">
<a
href="https://community.affine.pro/home"
target="_blank"
rel="noreferrer"
className={style.suggestionLink}
>
<span className={style.suggestionLinkIcon}>
<ContactWithUsIcon />
</span>
{t['com.affine.settings.suggestion']()}
</a>
</div>
</div>
</div>
</Modal>

View File

@@ -3,21 +3,25 @@ import { style } from '@vanilla-extract/css';
export const wrapper = style({
flexGrow: '1',
height: '100%',
maxWidth: '560px',
margin: '0 auto',
padding: '40px 15px 20px 15px',
// children
// margin: '0 auto',
padding: '40px 15px 20px 15px',
overflowX: 'hidden',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
justifyContent: 'center',
'::-webkit-scrollbar': {
display: 'none',
},
});
export const centerContainer = style({
width: '100%',
maxWidth: '560px',
});
export const content = style({
width: '100%',
marginBottom: '24px',

View File

@@ -642,9 +642,52 @@
"com.affine.auth.sign-out.confirm-modal.description": "After signing out, the Cloud Workspaces associated with this account will be removed from the current device, and signing in again will add them back.",
"com.affine.auth.sign-out.confirm-modal.cancel": "Cancel",
"com.affine.auth.sign-out.confirm-modal.confirm": "Sign Out",
"com.affine.payment.recurring-yearly": "Annually",
"com.affine.payment.recurring-monthly": "Monthly",
"com.affine.payment.title": "Pricing Plans",
"com.affine.payment.subtitle-not-signed-in": "This is the Pricing plans of AFFiNE Cloud. You can sign up or sign in to your account first.",
"com.affine.payment.subtitle-active": "You are current on the {{currentPlan}} plan. If you have any questions, please contact our <3>customer support</3>.",
"com.affine.payment.subtitle-canceled": "You are currently on the {{plan}} plan. After the current billing period ends, your account will automatically switch to the Free plan.",
"com.affine.payment.discount-amount": "{{amount}}% off",
"com.affine.payment.sign-up-free": "Sign up free",
"com.affine.payment.buy-pro": "Buy Pro",
"com.affine.payment.current-plan": "Current Plan",
"com.affine.payment.downgrade": "Downgrade",
"com.affine.payment.upgrade": "Upgrade",
"com.affine.payment.downgraded-tooltip": "You have successfully downgraded. After the current billing period ends, your account will automatically switch to the Free plan.",
"com.affine.payment.contact-sales": "Contact Sales",
"com.affine.payment.change-to": "Change to {{to}} Billing",
"com.affine.payment.resume": "Resume",
"com.affine.payment.resume-renewal": "Resume Auto-renewal",
"com.affine.payment.benefit-1": "Unlimited local workspace",
"com.affine.payment.benefit-2": "Unlimited login devices",
"com.affine.payment.benefit-3": "Unlimited blocks",
"com.affine.payment.benefit-4": "AFFiNE Cloud Storage {{capacity}}",
"com.affine.payment.benefit-5": "The maximum file size is {{capacity}}",
"com.affine.payment.benefit-6": "Number of members per Workspace ≤ {{capacity}}",
"com.affine.payment.dynamic-benefit-1": "Best team workspace for collaboration and knowledge distilling.",
"com.affine.payment.dynamic-benefit-2": "Focusing on what really matters with team project management and automation.",
"com.affine.payment.dynamic-benefit-3": "Pay for seats, fits all team size.",
"com.affine.payment.dynamic-benefit-4": "Solutions & best practices for dedicated needs.",
"com.affine.payment.dynamic-benefit-5": "Embedable & interrogations with IT support.",
"com.affine.payment.see-all-plans": "See all plans",
"com.affine.payment.modal.resume.title": "Resume Auto-Renewal?",
"com.affine.payment.modal.resume.content": "Are you sure you want to resume the subscription for your pro account? This means your payment method will be charged automatically at the end of each billing cycle, starting from the next billing cycle.",
"com.affine.payment.modal.resume.cancel": "Cancel",
"com.affine.payment.modal.resume.confirm": "Confirm",
"com.affine.payment.modal.downgrade.title": "Are you sure?",
"com.affine.payment.modal.downgrade.content": "We're sorry to see you go, but we're always working to improve, and your feedback is welcome. We hope to see you return in the future.",
"com.affine.payment.modal.downgrade.caption": "You can still use AFFiNE Cloud Pro until the end of this billing period :)",
"com.affine.payment.modal.downgrade.cancel": "Cancel Subscription",
"com.affine.payment.modal.downgrade.confirm": "Keep AFFiNE Cloud Pro",
"com.affine.payment.modal.change.title": "Change your subscription",
"com.affine.payment.modal.change.content": "You are changing your <0>from</0> subscription to <1>to</1> subscription. This change will take effect in the next billing cycle, with an effective date of <2>due</2>.",
"com.affine.payment.modal.change.cancel": "Cancel",
"com.affine.payment.modal.change.confirm": "Change",
"com.affine.payment.updated-notify-title": "Subscription updated",
"com.affine.payment.updated-notify-msg": "You have changed your plan to {{plan}} billing.",
"com.affine.storage.maximum-tips": "You have reached the maximum capacity limit for your current account",
"com.affine.payment.tag-tooltips": "See all plans",
"com.affine.payment.title": "Pricing Plans",
"com.affine.payment.billing-setting.title": "Billing",
"com.affine.payment.billing-setting.subtitle": "Manage your billing information and invoices.",
"com.affine.payment.billing-setting.information": "Information",