Skip to content

Commit

Permalink
fix(core): cancel and resume team subscription
Browse files Browse the repository at this point in the history
  • Loading branch information
JimmFly committed Dec 11, 2024
1 parent bcb4b9c commit bfd4eb0
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -99,8 +103,10 @@ export const CancelTeamAction = ({
children,
open,
onOpenChange,
workspaceId,
}: {
open: boolean;
workspaceId: string;
onOpenChange: (open: boolean) => void;
} & PropsWithChildren) => {
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
Expand All @@ -115,7 +121,11 @@ 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,
workspaceId
);
await subscription.waitForRevalidation();
// refresh idempotency key
setIdempotencyKey(nanoid());
Expand All @@ -136,18 +146,23 @@ export const CancelTeamAction = ({
setIsMutating(false);
}
}, [
authService.session.account$.value,
workspaceSubscription,
authService,
workspaceSubscription?.recurring,
subscription,
idempotencyKey,
workspaceId,
onOpenChange,
downgradeNotify,
]);

if (workspaceSubscription?.canceledAt) {
return null;
}

return (
<>
{children}
<DowngradeModal
<DowngradeTeamModal
open={open}
onCancel={downgrade}
onOpenChange={onOpenChange}
Expand Down Expand Up @@ -208,3 +223,48 @@ export const ResumeAction = ({
</>
);
};
export const TeamResumeAction = ({
children,
open,
onOpenChange,
workspaceId,
}: {
open: boolean;
workspaceId: string;
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,
workspaceId
);
await subscription.waitForRevalidation();
// refresh idempotency key
setIdempotencyKey(nanoid());
onOpenChange(false);
} finally {
setIsMutating(false);
}
}, [subscription, idempotencyKey, workspaceId, onOpenChange]);

return (
<>
{children}
<ConfirmLoadingModal
type={'resume'}
open={open}
onConfirm={resume}
onOpenChange={onOpenChange}
loading={isMutating}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,62 @@ export const DowngradeModal = ({
</Modal>
);
};

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 (
<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>
</div>

<footer className={styles.downgradeFooter}>
<Button
onClick={() => {
canceled.current = true;
onCancel?.();
}}
loading={loading}
>
{t['com.affine.payment.modal.downgrade.cancel']()}
</Button>
<DialogTrigger asChild>
<Button
disabled={loading}
onClick={() => onOpenChange?.(false)}
variant="primary"
>
{t['com.affine.payment.modal.downgrade.team-confirm']()}
</Button>
</DialogTrigger>
</footer>
</Modal>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
WorkspaceInvoicesService,
WorkspaceSubscriptionService,
} from '@affine/core/modules/cloud';
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
import { UrlService } from '@affine/core/modules/url';
import {
createCustomerPortalMutation,
Expand All @@ -36,7 +37,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';

import {
CancelTeamAction,
ResumeAction,
TeamResumeAction,
} from '../../general-setting/plans/actions';
import * as styles from './styles.css';

Expand All @@ -60,7 +61,7 @@ export const WorkspaceSettingBilling = ({

useEffect(() => {
subscriptionService?.subscription.revalidate();
}, [subscriptionService]);
}, [subscriptionService?.subscription]);

if (workspace === null) {
return null;
Expand All @@ -79,11 +80,14 @@ export const WorkspaceSettingBilling = ({
<SettingWrapper
title={t['com.affine.payment.billing-setting.information']()}
>
<TeamCard />
<TeamCard workspaceId={workspaceMetadata.id} />
<TypeFormLink />
<PaymentMethodUpdater />
{subscription?.end && subscription.canceledAt ? (
<ResumeSubscription expirationDate={subscription.end} />
<ResumeSubscription
expirationDate={subscription.end}
workspaceId={workspaceMetadata.id}
/>
) : null}
</SettingWrapper>

Expand All @@ -94,11 +98,13 @@ export const WorkspaceSettingBilling = ({
);
};

const TeamCard = () => {
const TeamCard = ({ workspaceId }: { workspaceId: string }) => {
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$
);
Expand All @@ -108,7 +114,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;
Expand Down Expand Up @@ -147,12 +159,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 (
<div className={styles.planCard}>
Expand All @@ -170,8 +192,13 @@ const TeamCard = () => {
<CancelTeamAction
open={openCancelModal}
onOpenChange={setOpenCancelModal}
workspaceId={workspaceId}
>
<Button variant="primary" className={styles.cancelPlanButton}>
<Button
variant="primary"
className={styles.cancelPlanButton}
onClick={handleClick}
>
{t[
'com.affine.settings.workspace.billing.team-workspace.cancel-plan'
]()}
Expand All @@ -191,7 +218,13 @@ const TeamCard = () => {
);
};

const ResumeSubscription = ({ expirationDate }: { expirationDate: string }) => {
const ResumeSubscription = ({
expirationDate,
workspaceId,
}: {
expirationDate: string;
workspaceId: string;
}) => {
const t = useI18n();
const [open, setOpen] = useState(false);
const handleClick = useCallback(() => {
Expand All @@ -207,11 +240,15 @@ const ResumeSubscription = ({ expirationDate }: { expirationDate: string }) => {
}
)}
>
<ResumeAction open={open} onOpenChange={setOpen}>
<TeamResumeAction
open={open}
onOpenChange={setOpen}
workspaceId={workspaceId}
>
<Button onClick={handleClick}>
{t['com.affine.payment.billing-setting.resume-subscription']()}
</Button>
</ResumeAction>
</TeamResumeAction>
</SettingRow>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,37 @@ export class WorkspaceSubscription extends Entity {
server = this.workspaceServerService.server;
store = this.workspaceServerService.server?.scope.get(SubscriptionStore);

async resumeSubscription(idempotencyKey: string, plan?: SubscriptionPlan) {
async resumeSubscription(
idempotencyKey: string,
plan?: SubscriptionPlan,
workspaceId?: string
) {
if (!this.store) {
throw new Error('Subscription store not available');
}
await this.store.mutateResumeSubscription(idempotencyKey, plan);
await this.store.mutateResumeSubscription(
idempotencyKey,
plan,
undefined,
workspaceId
);
await this.waitForRevalidation();
}

async cancelSubscription(idempotencyKey: string, plan?: SubscriptionPlan) {
async cancelSubscription(
idempotencyKey: string,
plan?: SubscriptionPlan,
workspaceId?: string
) {
if (!this.store) {
throw new Error('Subscription store not available');
}
await this.store.mutateCancelSubscription(idempotencyKey, plan);
await this.store.mutateCancelSubscription(
idempotencyKey,
plan,
undefined,
workspaceId
);
await this.waitForRevalidation();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,14 @@ export class SubscriptionStore extends Store {
async mutateResumeSubscription(
idempotencyKey: string,
plan?: SubscriptionPlan,
abortSignal?: AbortSignal
abortSignal?: AbortSignal,
workspaceId?: string
) {
const data = await this.gqlService.gql({
query: resumeSubscriptionMutation,
variables: {
plan,
workspaceId,
},
context: {
signal: abortSignal,
Expand All @@ -116,12 +118,14 @@ export class SubscriptionStore extends Store {
async mutateCancelSubscription(
idempotencyKey: string,
plan?: SubscriptionPlan,
abortSignal?: AbortSignal
abortSignal?: AbortSignal,
workspaceId?: string
) {
const data = await this.gqlService.gql({
query: cancelSubscriptionMutation,
variables: {
plan,
workspaceId,
},
context: {
signal: abortSignal,
Expand Down
Loading

0 comments on commit bfd4eb0

Please sign in to comment.