From 4eb31444a939a47638296b202e63651761158afa Mon Sep 17 00:00:00 2001 From: JimmFly Date: Wed, 11 Dec 2024 17:59:46 +0800 Subject: [PATCH] fix(core): optimize upgrade to team page (#9099) --- .../invite-team-modal/index.tsx | 7 +- .../setting/general-setting/plans/actions.tsx | 63 +++++++++- .../setting/general-setting/plans/modals.tsx | 59 +++++++++ .../general-setting/plans/plan-card.tsx | 7 +- .../workspace-setting/billing/index.tsx | 43 +++++-- .../members/member-option.tsx | 118 +++++++++--------- .../desktop/pages/upgrade-to-team/index.tsx | 29 ++++- .../cloud/entities/workspace-subscription.ts | 21 +++- .../src/modules/cloud/stores/subscription.ts | 12 +- .../src/graphql/cancel-subscription.gql | 7 +- .../frontend/graphql/src/graphql/index.ts | 18 +-- .../src/graphql/resume-subscription.gql | 7 +- .../graphql/update-subscription-billing.gql | 7 +- packages/frontend/graphql/src/schema.ts | 3 + .../i18n/src/i18n-completenesses.json | 8 +- packages/frontend/i18n/src/resources/en.json | 5 + 16 files changed, 307 insertions(+), 107 deletions(-) diff --git a/packages/frontend/component/src/components/member-components/invite-team-modal/index.tsx b/packages/frontend/component/src/components/member-components/invite-team-modal/index.tsx index d2903bebfd..41e1bf686a 100644 --- a/packages/frontend/component/src/components/member-components/invite-team-modal/index.tsx +++ b/packages/frontend/component/src/components/member-components/invite-team-modal/index.tsx @@ -4,7 +4,6 @@ import { useI18n } from '@affine/i18n'; import { useCallback, useEffect, useState } from 'react'; import { ConfirmModal } from '../../../ui/modal'; -import { notify } from '../../../ui/notification'; import { type InviteMethodType, ModalContent } from './modal-content'; import * as styles from './styles.css'; @@ -61,11 +60,7 @@ export const InviteTeamMemberModal = ({ onConfirm({ emails: inviteEmailsArray, }); - notify.success({ - title: t['com.affine.payment.member.team.invite.notify.title'](), - message: t['com.affine.payment.member.team.invite.notify.message'](), - }); - }, [inviteEmails, inviteMethod, onConfirm, setOpen, t]); + }, [inviteEmails, inviteMethod, onConfirm, setOpen]); useEffect(() => { if (!open) { diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/actions.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/actions.tsx index 4990205ef0..2f15f5f8b6 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/actions.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/actions.tsx @@ -13,7 +13,11 @@ import { SubscriptionService, WorkspaceSubscriptionService, } from '../../../../../modules/cloud'; -import { ConfirmLoadingModal, DowngradeModal } from './modals'; +import { + ConfirmLoadingModal, + DowngradeModal, + DowngradeTeamModal, +} from './modals'; /** * Cancel action with modal & request @@ -115,7 +119,10 @@ export const CancelTeamAction = ({ const account = authService.session.account$.value; const prevRecurring = workspaceSubscription?.recurring; setIsMutating(true); - await subscription.cancelSubscription(idempotencyKey); + await subscription.cancelSubscription( + idempotencyKey, + SubscriptionPlan.Team + ); await subscription.waitForRevalidation(); // refresh idempotency key setIdempotencyKey(nanoid()); @@ -136,18 +143,22 @@ export const CancelTeamAction = ({ setIsMutating(false); } }, [ - authService.session.account$.value, - workspaceSubscription, + authService, + workspaceSubscription?.recurring, subscription, idempotencyKey, onOpenChange, downgradeNotify, ]); + if (workspaceSubscription?.canceledAt) { + return null; + } + return ( <> {children} - ); }; +export const TeamResumeAction = ({ + children, + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; +} & PropsWithChildren) => { + // allow replay request on network error until component unmount or success + const [idempotencyKey, setIdempotencyKey] = useState(nanoid()); + const [isMutating, setIsMutating] = useState(false); + const subscription = useService(WorkspaceSubscriptionService).subscription; + + const resume = useAsyncCallback(async () => { + try { + setIsMutating(true); + await subscription.resumeSubscription( + idempotencyKey, + SubscriptionPlan.Team + ); + await subscription.waitForRevalidation(); + // refresh idempotency key + setIdempotencyKey(nanoid()); + onOpenChange(false); + } finally { + setIsMutating(false); + } + }, [subscription, idempotencyKey, onOpenChange]); + + return ( + <> + {children} + + + ); +}; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/modals.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/modals.tsx index 8aac457a35..01baf51154 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/modals.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/modals.tsx @@ -129,3 +129,62 @@ export const DowngradeModal = ({ ); }; + +export const DowngradeTeamModal = ({ + open, + loading, + onOpenChange, + onCancel, +}: { + loading?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + onCancel?: () => void; +}) => { + const t = useI18n(); + const canceled = useRef(false); + + useEffect(() => { + if (!loading && open && canceled.current) { + onOpenChange?.(false); + canceled.current = false; + } + }, [loading, open, onOpenChange]); + + return ( + +
+

+ {t['com.affine.payment.modal.downgrade.content']()} +

+
+ +
+ + + + +
+
+ ); +}; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx index f2325f29fb..2c9de66078 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx @@ -153,7 +153,7 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { // team if (detail.plan === SubscriptionPlan.Team) { - return ; + return ; } // lifetime @@ -247,10 +247,11 @@ const Downgrade = ({ disabled }: { disabled?: boolean }) => { ); }; -const UpgradeToTeam = () => { +const UpgradeToTeam = ({ recurring }: { recurring: SubscriptionRecurring }) => { const t = useI18n(); const serverService = useService(ServerService); - const url = `${serverService.server.baseUrl}/upgrade-to-team`; + const url = `${serverService.server.baseUrl}/upgrade-to-team?recurring=${recurring}`; + return ( { subscriptionService?.subscription.revalidate(); - }, [subscriptionService]); + }, [subscriptionService?.subscription]); if (workspace === null) { return null; @@ -97,8 +98,10 @@ export const WorkspaceSettingBilling = ({ const TeamCard = () => { const t = useI18n(); const workspaceSubscriptionService = useService(WorkspaceSubscriptionService); + const workspaceQuotaService = useService(WorkspaceQuotaService); const subscriptionService = useService(SubscriptionService); - + const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$); + const workspaceMemberCount = workspaceQuota?.memberCount; const teamSubscription = useLiveData( workspaceSubscriptionService.subscription.subscription$ ); @@ -108,7 +111,13 @@ const TeamCard = () => { useEffect(() => { workspaceSubscriptionService.subscription.revalidate(); - }, [workspaceSubscriptionService.subscription]); + workspaceQuotaService.quota.revalidate(); + subscriptionService.prices.revalidate(); + }, [ + subscriptionService, + workspaceQuotaService, + workspaceSubscriptionService, + ]); const expiration = teamSubscription?.end; const nextBillingDate = teamSubscription?.nextBillAt; @@ -147,12 +156,22 @@ const TeamCard = () => { }, [expiration, nextBillingDate, t]); const amount = teamSubscription - ? teamPrices + ? teamPrices && workspaceMemberCount ? teamSubscription.recurring === SubscriptionRecurring.Monthly - ? String((teamPrices.amount ?? 0) / 100) - : String((teamPrices.yearlyAmount ?? 0) / 100) + ? String( + (teamPrices.amount ? teamPrices.amount * workspaceMemberCount : 0) / + 100 + ) + : String( + (teamPrices.yearlyAmount + ? teamPrices.yearlyAmount * workspaceMemberCount + : 0) / 100 + ) : '?' : '0'; + const handleClick = useCallback(() => { + setOpenCancelModal(true); + }, []); return (
@@ -171,7 +190,11 @@ const TeamCard = () => { open={openCancelModal} onOpenChange={setOpenCancelModal} > - - + ); }; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/members/member-option.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/members/member-option.tsx index 0640e00bb0..19109c5dd9 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/members/member-option.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/members/member-option.tsx @@ -1,4 +1,4 @@ -import { MenuItem, notify } from '@affine/component'; +import { MenuItem, notify, useConfirmModal } from '@affine/component'; import { type Member, WorkspacePermissionService, @@ -21,31 +21,55 @@ export const MemberOptions = ({ }) => { const t = useI18n(); const permission = useService(WorkspacePermissionService).permission; + const { openConfirmModal } = useConfirmModal(); + + const openRemoveConfirmModal = useCallback( + (successNotify: { title: string; message: string }) => { + openConfirmModal({ + title: t['com.affine.payment.member.team.remove.confirm.title'](), + description: + t['com.affine.payment.member.team.remove.confirm.description'](), + confirmText: + t['com.affine.payment.member.team.remove.confirm.confirm-button'](), + cancelText: t['com.affine.payment.member.team.remove.confirm.cancel'](), + confirmButtonOptions: { + variant: 'error', + }, + onConfirm: () => + permission + .revokeMember(member.id) + .then(result => { + if (result) { + notify.success({ + title: successNotify.title, + message: successNotify.message, + }); + } + }) + .catch(error => { + notify.error({ + title: 'Operation failed', + message: error.message, + }); + }), + }); + }, + [member.id, openConfirmModal, permission, t] + ); const handleAssignOwner = useCallback(() => { openAssignModal(); }, [openAssignModal]); const handleRevoke = useCallback(() => { - permission - .revokeMember(member.id) - .then(result => { - if (result) { - notify.success({ - title: t['com.affine.payment.member.team.revoke.notify.title'](), - message: t['com.affine.payment.member.team.revoke.notify.message']({ - name: member.name || member.email || member.id, - }), - }); - } - }) - .catch(error => { - notify.error({ - title: 'Operation failed', - message: error.message, - }); - }); - }, [permission, member, t]); + openRemoveConfirmModal({ + title: t['com.affine.payment.member.team.revoke.notify.title'](), + message: t['com.affine.payment.member.team.revoke.notify.message']({ + name: member.name || member.email || member.id, + }), + }); + }, [openRemoveConfirmModal, member, t]); + const handleApprove = useCallback(() => { permission .approveMember(member.id) @@ -70,48 +94,22 @@ export const MemberOptions = ({ }, [member, permission, t]); const handleDecline = useCallback(() => { - permission - .revokeMember(member.id) - .then(result => { - if (result) { - notify.success({ - title: t['com.affine.payment.member.team.decline.notify.title'](), - message: t['com.affine.payment.member.team.decline.notify.message']( - { - name: member.name || member.email || member.id, - } - ), - }); - } - }) - .catch(error => { - notify.error({ - title: 'Operation failed', - message: error.message, - }); - }); - }, [member, permission, t]); + openRemoveConfirmModal({ + title: t['com.affine.payment.member.team.decline.notify.title'](), + message: t['com.affine.payment.member.team.decline.notify.message']({ + name: member.name || member.email || member.id, + }), + }); + }, [member, openRemoveConfirmModal, t]); const handleRemove = useCallback(() => { - permission - .revokeMember(member.id) - .then(result => { - if (result) { - notify.success({ - title: t['com.affine.payment.member.team.remove.notify.title'](), - message: t['com.affine.payment.member.team.remove.notify.message']({ - name: member.name || member.email || member.id, - }), - }); - } - }) - .catch(error => { - notify.error({ - title: 'Operation failed', - message: error.message, - }); - }); - }, [member, permission, t]); + openRemoveConfirmModal({ + title: t['com.affine.payment.member.team.remove.notify.title'](), + message: t['com.affine.payment.member.team.remove.notify.message']({ + name: member.name || member.email || member.id, + }), + }); + }, [member, openRemoveConfirmModal, t]); const handleChangeToAdmin = useCallback(() => { permission diff --git a/packages/frontend/core/src/desktop/pages/upgrade-to-team/index.tsx b/packages/frontend/core/src/desktop/pages/upgrade-to-team/index.tsx index b8153d7979..3b475dec6d 100644 --- a/packages/frontend/core/src/desktop/pages/upgrade-to-team/index.tsx +++ b/packages/frontend/core/src/desktop/pages/upgrade-to-team/index.tsx @@ -12,6 +12,7 @@ import { AuthPageContainer } from '@affine/component/auth-components'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info'; import { PureWorkspaceCard } from '@affine/core/components/workspace-selector/workspace-card'; +import { AuthService } from '@affine/core/modules/cloud'; import { buildShowcaseWorkspace } from '@affine/core/utils/first-app-data'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant'; import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql'; @@ -24,8 +25,10 @@ import { WorkspacesService, } from '@toeverything/infra'; import { useCallback, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { Upgrade } from '../../dialogs/setting/general-setting/plans/plan-card'; +import { PageNotFound } from '../404'; import * as styles from './styles.css'; const benefitList: I18nString[] = [ @@ -36,6 +39,20 @@ const benefitList: I18nString[] = [ ]; export const Component = () => { + const authService = useService(AuthService); + const authStatus = useLiveData(authService.session.status$); + + const [params] = useSearchParams(); + const recurring = params.get('recurring'); + + const authIsRevalidating = useLiveData(authService.session.isRevalidating$); + if (authStatus === 'unauthenticated' && !authIsRevalidating) { + return ; + } + return ; +}; + +export const UpgradeToTeam = ({ recurring }: { recurring: string | null }) => { const t = useI18n(); const workspacesList = useService(WorkspacesService).list; const workspaces = useLiveData(workspacesList.workspaces$); @@ -111,6 +128,7 @@ export const Component = () => { {t['com.affine.upgrade-to-team-page.benefit.description']()}
void; }) => { const t = useI18n(); const onClose = useCallback(() => { onOpenChange(false); }, [onOpenChange]); + + const currentRecurring = + recurring && + recurring.toLowerCase() === SubscriptionRecurring.Yearly.toLowerCase() + ? SubscriptionRecurring.Yearly + : SubscriptionRecurring.Monthly; + return (
@@ -163,7 +190,7 @@ const UpgradeDialog = ({ ; + workspaceId?: InputMaybe; }>; export type CancelSubscriptionMutation = { @@ -2256,6 +2257,7 @@ export type RemoveAvatarMutation = { export type ResumeSubscriptionMutationVariables = Exact<{ plan?: InputMaybe; + workspaceId?: InputMaybe; }>; export type ResumeSubscriptionMutation = { @@ -2467,6 +2469,7 @@ export type UpdateServerRuntimeConfigsMutation = { export type UpdateSubscriptionMutationVariables = Exact<{ plan?: InputMaybe; recurring: SubscriptionRecurring; + workspaceId?: InputMaybe; }>; export type UpdateSubscriptionMutation = { diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index be151c0f1d..65b5bddf3c 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -1,5 +1,5 @@ { - "ar": 69, + "ar": 68, "ca": 5, "da": 5, "de": 26, @@ -12,13 +12,13 @@ "hi": 2, "it-IT": 1, "it": 1, - "ja": 92, + "ja": 91, "ko": 73, "pl": 0, - "pt-BR": 79, + "pt-BR": 78, "ru": 67, "sv-SE": 4, "ur": 2, "zh-Hans": 92, - "zh-Hant": 92 + "zh-Hant": 91 } \ No newline at end of file diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index c0c0cb5e23..c35e36f9ad 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -982,12 +982,17 @@ "com.affine.payment.member.team.assign.confirm.description-4": "To confirm this transfer, please type the workspace name", "com.affine.payment.member.team.assign.confirm.placeholder": "Type workspace name to confirm", "com.affine.payment.member.team.assign.confirm.button": "Transfer Ownership", + "com.affine.payment.member.team.remove.confirm.title": "Remove member from workspace?", + "com.affine.payment.member.team.remove.confirm.description": "This action will revoke their access to all workspace resources immediately.", + "com.affine.payment.member.team.remove.confirm.confirm-button": "Remove Member", + "com.affine.payment.member.team.remove.confirm.cancel": "Cancel", "com.affine.payment.modal.change.cancel": "Cancel", "com.affine.payment.modal.change.confirm": "Change", "com.affine.payment.modal.change.title": "Change your subscription", "com.affine.payment.modal.downgrade.cancel": "Cancel subscription", "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.confirm": "Keep AFFiNE Cloud Pro", + "com.affine.payment.modal.downgrade.team-confirm": "Keep Team plan", "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.title": "Are you sure?", "com.affine.payment.modal.resume.cancel": "Cancel",