mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 10:06:17 +08:00
feat(core): subscribe changed notification and typeform link (#7522)
This commit is contained in:
+43
-1
@@ -7,6 +7,7 @@ import {
|
||||
} from '@affine/component/setting-components';
|
||||
import { Button, IconButton } from '@affine/component/ui/button';
|
||||
import { Loading } from '@affine/component/ui/loading';
|
||||
import { getUpgradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import type { InvoicesQuery } from '@affine/graphql';
|
||||
import {
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
} from '../../../../../atoms';
|
||||
import { useMutation } from '../../../../../hooks/use-mutation';
|
||||
import { useQuery } from '../../../../../hooks/use-query';
|
||||
import { SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { mixpanel, popupWindow } from '../../../../../utils';
|
||||
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
|
||||
import { CancelAction, ResumeAction } from '../plans/actions';
|
||||
@@ -194,6 +195,8 @@ const SubscriptionSettings = () => {
|
||||
<SubscriptionSettingSkeleton />
|
||||
)}
|
||||
|
||||
<TypeFormLink />
|
||||
|
||||
{proSubscription !== null ? (
|
||||
proSubscription?.status === SubscriptionStatus.Active && (
|
||||
<>
|
||||
@@ -269,6 +272,45 @@ const SubscriptionSettings = () => {
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name={t['com.affine.payment.billing-type-form.title']()}
|
||||
desc={t['com.affine.payment.billing-type-form.description']()}
|
||||
>
|
||||
<a target="_blank" href={link} rel="noreferrer">
|
||||
<Button style={{ padding: '4px 12px' }}>
|
||||
{t['com.affine.payment.billing-type-form.go']()}
|
||||
</Button>
|
||||
</a>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
const BelieverIdentifier = ({ onOpenPlans }: { onOpenPlans?: () => void }) => {
|
||||
const t = useI18n();
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
|
||||
+26
-2
@@ -1,11 +1,14 @@
|
||||
import { getDowngradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { useDowngradeNotify } from '../../../subscription-landing/notify';
|
||||
import { ConfirmLoadingModal, DowngradeModal } from './modals';
|
||||
|
||||
/**
|
||||
@@ -24,9 +27,13 @@ export const CancelAction = ({
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
const authService = useService(AuthService);
|
||||
const downgradeNotify = useDowngradeNotify();
|
||||
|
||||
const downgrade = useAsyncCallback(async () => {
|
||||
try {
|
||||
const account = authService.session.account$.value;
|
||||
const prevRecurring = subscription.pro$.value?.recurring;
|
||||
setIsMutating(true);
|
||||
await subscription.cancelSubscription(idempotencyKey);
|
||||
subscription.revalidate();
|
||||
@@ -41,10 +48,27 @@ export const CancelAction = ({
|
||||
type: subscription.pro$.value?.plan,
|
||||
category: subscription.pro$.value?.recurring,
|
||||
});
|
||||
if (account && prevRecurring) {
|
||||
downgradeNotify(
|
||||
getDowngradeQuestionnaireLink({
|
||||
email: account.email ?? '',
|
||||
id: account.id,
|
||||
name: account.info?.name ?? '',
|
||||
plan: SubscriptionPlan.Pro,
|
||||
recurring: prevRecurring,
|
||||
})
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
}, [subscription, idempotencyKey, onOpenChange]);
|
||||
}, [
|
||||
authService.session.account$.value,
|
||||
subscription,
|
||||
idempotencyKey,
|
||||
onOpenChange,
|
||||
downgradeNotify,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+26
-2
@@ -1,6 +1,8 @@
|
||||
import { Button, type ButtonProps, useConfirmModal } from '@affine/component';
|
||||
import { useDowngradeNotify } from '@affine/core/components/affine/subscription-landing/notify';
|
||||
import { getDowngradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -14,8 +16,10 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
|
||||
const [isMutating, setMutating] = useState(false);
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const downgradeNotify = useDowngradeNotify();
|
||||
|
||||
const cancel = useAsyncCallback(async () => {
|
||||
mixpanel.track('PlanChangeStarted', {
|
||||
@@ -51,12 +55,32 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
|
||||
segment: 'settings panel',
|
||||
control: 'plan cancel action',
|
||||
});
|
||||
const account = authService.session.account$.value;
|
||||
const prevRecurring = subscription.ai$.value?.recurring;
|
||||
if (account && prevRecurring) {
|
||||
downgradeNotify(
|
||||
getDowngradeQuestionnaireLink({
|
||||
email: account.email,
|
||||
name: account.info?.name,
|
||||
id: account.id,
|
||||
plan: SubscriptionPlan.AI,
|
||||
recurring: prevRecurring,
|
||||
})
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setMutating(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [openConfirmModal, t, subscription, idempotencyKey]);
|
||||
}, [
|
||||
subscription,
|
||||
openConfirmModal,
|
||||
t,
|
||||
idempotencyKey,
|
||||
authService.session.account$.value,
|
||||
downgradeNotify,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Button onClick={cancel} loading={isMutating} type="primary" {...btnProps}>
|
||||
|
||||
+9
-3
@@ -1,6 +1,7 @@
|
||||
import { Button, type ButtonProps, Skeleton } from '@affine/component';
|
||||
import { generateSubscriptionCallbackLink } from '@affine/core/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { mixpanel, popupWindow } from '@affine/core/utils';
|
||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -19,6 +20,7 @@ export const AISubscribe = ({
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const [isMutating, setMutating] = useState(false);
|
||||
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||
@@ -57,7 +59,11 @@ export const AISubscribe = ({
|
||||
idempotencyKey,
|
||||
plan: SubscriptionPlan.AI,
|
||||
coupon: null,
|
||||
successCallbackLink: '/ai-upgrade-success',
|
||||
successCallbackLink: generateSubscriptionCallbackLink(
|
||||
authService.session.account$.value,
|
||||
SubscriptionPlan.AI,
|
||||
SubscriptionRecurring.Yearly
|
||||
),
|
||||
});
|
||||
popupWindow(session);
|
||||
setOpenedExternalWindow(true);
|
||||
@@ -65,7 +71,7 @@ export const AISubscribe = ({
|
||||
} finally {
|
||||
setMutating(false);
|
||||
}
|
||||
}, [idempotencyKey, subscriptionService]);
|
||||
}, [authService, idempotencyKey, subscriptionService]);
|
||||
|
||||
if (!price || !price.yearlyAmount) {
|
||||
return (
|
||||
|
||||
+13
-2
@@ -1,5 +1,6 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { generateSubscriptionCallbackLink } from '@affine/core/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { popupWindow } from '@affine/core/utils';
|
||||
@@ -259,6 +260,7 @@ export const Upgrade = ({
|
||||
const t = useI18n();
|
||||
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
|
||||
@@ -293,13 +295,22 @@ export const Upgrade = ({
|
||||
idempotencyKey,
|
||||
plan: SubscriptionPlan.Pro, // Only support prod plan now.
|
||||
coupon: null,
|
||||
successCallbackLink: '/upgrade-success',
|
||||
successCallbackLink: generateSubscriptionCallbackLink(
|
||||
authService.session.account$.value,
|
||||
SubscriptionPlan.Pro,
|
||||
recurring
|
||||
),
|
||||
});
|
||||
setMutating(false);
|
||||
setIdempotencyKey(nanoid());
|
||||
popupWindow(link);
|
||||
setOpenedExternalWindow(true);
|
||||
}, [subscriptionService, recurring, idempotencyKey]);
|
||||
}, [
|
||||
recurring,
|
||||
authService.session.account$.value,
|
||||
subscriptionService,
|
||||
idempotencyKey,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { AuthPageContainer } from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useSubscriptionNotifyWriter } from '@affine/core/hooks/affine/use-subscription-notify';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import mixpanel from 'mixpanel-browser';
|
||||
import { type ReactNode, useCallback, useEffect } from 'react';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
@@ -58,13 +57,7 @@ const UpgradeSuccessLayout = ({
|
||||
|
||||
export const CloudUpgradeSuccess = () => {
|
||||
const t = useI18n();
|
||||
useEffect(() => {
|
||||
mixpanel.track('PlanUpgradeSucceeded', {
|
||||
segment: 'settings panel',
|
||||
control: 'plan upgrade action',
|
||||
plan: SubscriptionPlan.Pro,
|
||||
});
|
||||
}, []);
|
||||
useSubscriptionNotifyWriter();
|
||||
return (
|
||||
<UpgradeSuccessLayout
|
||||
title={t['com.affine.payment.upgrade-success-page.title']()}
|
||||
@@ -75,13 +68,7 @@ export const CloudUpgradeSuccess = () => {
|
||||
|
||||
export const AIUpgradeSuccess = () => {
|
||||
const t = useI18n();
|
||||
useEffect(() => {
|
||||
mixpanel.track('PlanUpgradeSucceeded', {
|
||||
segment: 'settings panel',
|
||||
control: 'plan upgrade action',
|
||||
plan: SubscriptionPlan.Pro,
|
||||
});
|
||||
}, []);
|
||||
useSubscriptionNotifyWriter();
|
||||
return (
|
||||
<UpgradeSuccessLayout
|
||||
title={t['com.affine.payment.ai-upgrade-success-page.title']()}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const notifyHeader = style({
|
||||
fontWeight: 500,
|
||||
fontSize: 15,
|
||||
});
|
||||
|
||||
export const notifyFooter = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
gap: 12,
|
||||
paddingTop: 8,
|
||||
});
|
||||
|
||||
export const actionButton = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 500,
|
||||
lineHeight: '22px',
|
||||
});
|
||||
export const confirmButton = style({
|
||||
selectors: {
|
||||
'&.plain': {
|
||||
color: cssVar('brandColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const cancelButton = style({
|
||||
selectors: {
|
||||
'&.plain': {
|
||||
color: cssVar('textPrimaryColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Button, notify } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import {
|
||||
actionButton,
|
||||
cancelButton,
|
||||
confirmButton,
|
||||
notifyFooter,
|
||||
notifyHeader,
|
||||
} from './notify.css';
|
||||
|
||||
interface SubscriptionChangedNotifyFooterProps {
|
||||
onCancel: () => void;
|
||||
onConfirm?: () => void;
|
||||
to: string;
|
||||
okText: string;
|
||||
cancelText: string;
|
||||
}
|
||||
|
||||
const SubscriptionChangedNotifyFooter = ({
|
||||
to,
|
||||
okText,
|
||||
cancelText,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: SubscriptionChangedNotifyFooterProps) => {
|
||||
return (
|
||||
<div className={notifyFooter}>
|
||||
<Button
|
||||
className={clsx(actionButton, cancelButton)}
|
||||
size={'default'}
|
||||
onClick={onCancel}
|
||||
type="plain"
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<a href={to} target="_blank" rel="noreferrer">
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
className={clsx(actionButton, confirmButton)}
|
||||
type="plain"
|
||||
>
|
||||
{okText}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isDesktop = environment.isDesktop;
|
||||
export const useUpgradeNotify = () => {
|
||||
const t = useI18n();
|
||||
const prevNotifyIdRef = useRef<string | number | null>(null);
|
||||
|
||||
return useCallback(
|
||||
(link: string) => {
|
||||
prevNotifyIdRef.current && notify.dismiss(prevNotifyIdRef.current);
|
||||
const id = notify(
|
||||
{
|
||||
title: (
|
||||
<span className={notifyHeader}>
|
||||
{t['com.affine.payment.upgrade-success-notify.title']()}
|
||||
</span>
|
||||
),
|
||||
message: t['com.affine.payment.upgrade-success-notify.content'](),
|
||||
alignMessage: 'title',
|
||||
icon: null,
|
||||
footer: (
|
||||
<SubscriptionChangedNotifyFooter
|
||||
to={link}
|
||||
okText={
|
||||
isDesktop
|
||||
? t['com.affine.payment.upgrade-success-notify.ok-client']()
|
||||
: t['com.affine.payment.upgrade-success-notify.ok-web']()
|
||||
}
|
||||
cancelText={t[
|
||||
'com.affine.payment.upgrade-success-notify.later'
|
||||
]()}
|
||||
onCancel={() => notify.dismiss(id)}
|
||||
onConfirm={() => notify.dismiss(id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ duration: 24 * 60 * 60 * 1000 }
|
||||
);
|
||||
prevNotifyIdRef.current = id;
|
||||
},
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useDowngradeNotify = () => {
|
||||
const t = useI18n();
|
||||
const prevNotifyIdRef = useRef<string | number | null>(null);
|
||||
|
||||
return useCallback(
|
||||
(link: string) => {
|
||||
prevNotifyIdRef.current && notify.dismiss(prevNotifyIdRef.current);
|
||||
const id = notify(
|
||||
{
|
||||
title: (
|
||||
<span className={notifyHeader}>
|
||||
{t['com.affine.payment.downgraded-notify.title']()}
|
||||
</span>
|
||||
),
|
||||
message: t['com.affine.payment.downgraded-notify.content'](),
|
||||
alignMessage: 'title',
|
||||
icon: null,
|
||||
footer: (
|
||||
<SubscriptionChangedNotifyFooter
|
||||
to={link}
|
||||
okText={
|
||||
isDesktop
|
||||
? t['com.affine.payment.downgraded-notify.ok-client']()
|
||||
: t['com.affine.payment.downgraded-notify.ok-web']()
|
||||
}
|
||||
cancelText={t['com.affine.payment.downgraded-notify.later']()}
|
||||
onCancel={() => notify.dismiss(id)}
|
||||
onConfirm={() => notify.dismiss(id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ duration: 24 * 60 * 60 * 1000 }
|
||||
);
|
||||
prevNotifyIdRef.current = id;
|
||||
},
|
||||
[t]
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user