mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
feat(core): subscribe changed notification and typeform link (#7522)
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
import { Button, IconButton } from '@affine/component/ui/button';
|
import { Button, IconButton } from '@affine/component/ui/button';
|
||||||
import { Loading } from '@affine/component/ui/loading';
|
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 { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import type { InvoicesQuery } from '@affine/graphql';
|
import type { InvoicesQuery } from '@affine/graphql';
|
||||||
import {
|
import {
|
||||||
@@ -30,7 +31,7 @@ import {
|
|||||||
} from '../../../../../atoms';
|
} from '../../../../../atoms';
|
||||||
import { useMutation } from '../../../../../hooks/use-mutation';
|
import { useMutation } from '../../../../../hooks/use-mutation';
|
||||||
import { useQuery } from '../../../../../hooks/use-query';
|
import { useQuery } from '../../../../../hooks/use-query';
|
||||||
import { SubscriptionService } from '../../../../../modules/cloud';
|
import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
|
||||||
import { mixpanel, popupWindow } from '../../../../../utils';
|
import { mixpanel, popupWindow } from '../../../../../utils';
|
||||||
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
|
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
|
||||||
import { CancelAction, ResumeAction } from '../plans/actions';
|
import { CancelAction, ResumeAction } from '../plans/actions';
|
||||||
@@ -194,6 +195,8 @@ const SubscriptionSettings = () => {
|
|||||||
<SubscriptionSettingSkeleton />
|
<SubscriptionSettingSkeleton />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TypeFormLink />
|
||||||
|
|
||||||
{proSubscription !== null ? (
|
{proSubscription !== null ? (
|
||||||
proSubscription?.status === SubscriptionStatus.Active && (
|
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 BelieverIdentifier = ({ onOpenPlans }: { onOpenPlans?: () => void }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const subscriptionService = useService(SubscriptionService);
|
const subscriptionService = useService(SubscriptionService);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import { getDowngradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { mixpanel } from '@affine/core/utils';
|
import { mixpanel } from '@affine/core/utils';
|
||||||
|
import { SubscriptionPlan } from '@affine/graphql';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import { useState } 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';
|
import { ConfirmLoadingModal, DowngradeModal } from './modals';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,9 +27,13 @@ export const CancelAction = ({
|
|||||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||||
const [isMutating, setIsMutating] = useState(false);
|
const [isMutating, setIsMutating] = useState(false);
|
||||||
const subscription = useService(SubscriptionService).subscription;
|
const subscription = useService(SubscriptionService).subscription;
|
||||||
|
const authService = useService(AuthService);
|
||||||
|
const downgradeNotify = useDowngradeNotify();
|
||||||
|
|
||||||
const downgrade = useAsyncCallback(async () => {
|
const downgrade = useAsyncCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
const account = authService.session.account$.value;
|
||||||
|
const prevRecurring = subscription.pro$.value?.recurring;
|
||||||
setIsMutating(true);
|
setIsMutating(true);
|
||||||
await subscription.cancelSubscription(idempotencyKey);
|
await subscription.cancelSubscription(idempotencyKey);
|
||||||
subscription.revalidate();
|
subscription.revalidate();
|
||||||
@@ -41,10 +48,27 @@ export const CancelAction = ({
|
|||||||
type: subscription.pro$.value?.plan,
|
type: subscription.pro$.value?.plan,
|
||||||
category: subscription.pro$.value?.recurring,
|
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 {
|
} finally {
|
||||||
setIsMutating(false);
|
setIsMutating(false);
|
||||||
}
|
}
|
||||||
}, [subscription, idempotencyKey, onOpenChange]);
|
}, [
|
||||||
|
authService.session.account$.value,
|
||||||
|
subscription,
|
||||||
|
idempotencyKey,
|
||||||
|
onOpenChange,
|
||||||
|
downgradeNotify,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Button, type ButtonProps, useConfirmModal } from '@affine/component';
|
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 { 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 { mixpanel } from '@affine/core/utils';
|
||||||
import { SubscriptionPlan } from '@affine/graphql';
|
import { SubscriptionPlan } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -14,8 +16,10 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
|
|||||||
const [isMutating, setMutating] = useState(false);
|
const [isMutating, setMutating] = useState(false);
|
||||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||||
const subscription = useService(SubscriptionService).subscription;
|
const subscription = useService(SubscriptionService).subscription;
|
||||||
|
const authService = useService(AuthService);
|
||||||
|
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
const downgradeNotify = useDowngradeNotify();
|
||||||
|
|
||||||
const cancel = useAsyncCallback(async () => {
|
const cancel = useAsyncCallback(async () => {
|
||||||
mixpanel.track('PlanChangeStarted', {
|
mixpanel.track('PlanChangeStarted', {
|
||||||
@@ -51,12 +55,32 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
|
|||||||
segment: 'settings panel',
|
segment: 'settings panel',
|
||||||
control: 'plan cancel action',
|
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 {
|
} finally {
|
||||||
setMutating(false);
|
setMutating(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [openConfirmModal, t, subscription, idempotencyKey]);
|
}, [
|
||||||
|
subscription,
|
||||||
|
openConfirmModal,
|
||||||
|
t,
|
||||||
|
idempotencyKey,
|
||||||
|
authService.session.account$.value,
|
||||||
|
downgradeNotify,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={cancel} loading={isMutating} type="primary" {...btnProps}>
|
<Button onClick={cancel} loading={isMutating} type="primary" {...btnProps}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button, type ButtonProps, Skeleton } from '@affine/component';
|
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 { 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 { mixpanel, popupWindow } from '@affine/core/utils';
|
||||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -19,6 +20,7 @@ export const AISubscribe = ({
|
|||||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||||
const [isMutating, setMutating] = useState(false);
|
const [isMutating, setMutating] = useState(false);
|
||||||
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
|
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
|
||||||
|
const authService = useService(AuthService);
|
||||||
|
|
||||||
const subscriptionService = useService(SubscriptionService);
|
const subscriptionService = useService(SubscriptionService);
|
||||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||||
@@ -57,7 +59,11 @@ export const AISubscribe = ({
|
|||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
plan: SubscriptionPlan.AI,
|
plan: SubscriptionPlan.AI,
|
||||||
coupon: null,
|
coupon: null,
|
||||||
successCallbackLink: '/ai-upgrade-success',
|
successCallbackLink: generateSubscriptionCallbackLink(
|
||||||
|
authService.session.account$.value,
|
||||||
|
SubscriptionPlan.AI,
|
||||||
|
SubscriptionRecurring.Yearly
|
||||||
|
),
|
||||||
});
|
});
|
||||||
popupWindow(session);
|
popupWindow(session);
|
||||||
setOpenedExternalWindow(true);
|
setOpenedExternalWindow(true);
|
||||||
@@ -65,7 +71,7 @@ export const AISubscribe = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setMutating(false);
|
setMutating(false);
|
||||||
}
|
}
|
||||||
}, [idempotencyKey, subscriptionService]);
|
}, [authService, idempotencyKey, subscriptionService]);
|
||||||
|
|
||||||
if (!price || !price.yearlyAmount) {
|
if (!price || !price.yearlyAmount) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
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 { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||||
import { popupWindow } from '@affine/core/utils';
|
import { popupWindow } from '@affine/core/utils';
|
||||||
@@ -259,6 +260,7 @@ export const Upgrade = ({
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const subscriptionService = useService(SubscriptionService);
|
const subscriptionService = useService(SubscriptionService);
|
||||||
|
const authService = useService(AuthService);
|
||||||
|
|
||||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||||
|
|
||||||
@@ -293,13 +295,22 @@ export const Upgrade = ({
|
|||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
plan: SubscriptionPlan.Pro, // Only support prod plan now.
|
plan: SubscriptionPlan.Pro, // Only support prod plan now.
|
||||||
coupon: null,
|
coupon: null,
|
||||||
successCallbackLink: '/upgrade-success',
|
successCallbackLink: generateSubscriptionCallbackLink(
|
||||||
|
authService.session.account$.value,
|
||||||
|
SubscriptionPlan.Pro,
|
||||||
|
recurring
|
||||||
|
),
|
||||||
});
|
});
|
||||||
setMutating(false);
|
setMutating(false);
|
||||||
setIdempotencyKey(nanoid());
|
setIdempotencyKey(nanoid());
|
||||||
popupWindow(link);
|
popupWindow(link);
|
||||||
setOpenedExternalWindow(true);
|
setOpenedExternalWindow(true);
|
||||||
}, [subscriptionService, recurring, idempotencyKey]);
|
}, [
|
||||||
|
recurring,
|
||||||
|
authService.session.account$.value,
|
||||||
|
subscriptionService,
|
||||||
|
idempotencyKey,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { AuthPageContainer } from '@affine/component/auth-components';
|
import { AuthPageContainer } from '@affine/component/auth-components';
|
||||||
import { Button } from '@affine/component/ui/button';
|
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 { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import { SubscriptionPlan } from '@affine/graphql';
|
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import mixpanel from 'mixpanel-browser';
|
import { type ReactNode, useCallback } from 'react';
|
||||||
import { type ReactNode, useCallback, useEffect } from 'react';
|
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
@@ -58,13 +57,7 @@ const UpgradeSuccessLayout = ({
|
|||||||
|
|
||||||
export const CloudUpgradeSuccess = () => {
|
export const CloudUpgradeSuccess = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
useEffect(() => {
|
useSubscriptionNotifyWriter();
|
||||||
mixpanel.track('PlanUpgradeSucceeded', {
|
|
||||||
segment: 'settings panel',
|
|
||||||
control: 'plan upgrade action',
|
|
||||||
plan: SubscriptionPlan.Pro,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<UpgradeSuccessLayout
|
<UpgradeSuccessLayout
|
||||||
title={t['com.affine.payment.upgrade-success-page.title']()}
|
title={t['com.affine.payment.upgrade-success-page.title']()}
|
||||||
@@ -75,13 +68,7 @@ export const CloudUpgradeSuccess = () => {
|
|||||||
|
|
||||||
export const AIUpgradeSuccess = () => {
|
export const AIUpgradeSuccess = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
useEffect(() => {
|
useSubscriptionNotifyWriter();
|
||||||
mixpanel.track('PlanUpgradeSucceeded', {
|
|
||||||
segment: 'settings panel',
|
|
||||||
control: 'plan upgrade action',
|
|
||||||
plan: SubscriptionPlan.Pro,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<UpgradeSuccessLayout
|
<UpgradeSuccessLayout
|
||||||
title={t['com.affine.payment.ai-upgrade-success-page.title']()}
|
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]
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { useUpgradeNotify } from '@affine/core/components/affine/subscription-landing/notify';
|
||||||
|
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||||
|
import mixpanel from 'mixpanel-browser';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { type AuthAccountInfo } from '../../modules/cloud';
|
||||||
|
|
||||||
|
const separator = '::';
|
||||||
|
const recoverSeparator = nanoid();
|
||||||
|
const localStorageKey = 'subscription-succeed-info';
|
||||||
|
|
||||||
|
const typeFormUrl = 'https://6dxre9ihosp.typeform.com/to';
|
||||||
|
const typeFormUpgradeId = 'mUMGGQS8';
|
||||||
|
const typeFormDowngradeId = 'RvD9AoRg';
|
||||||
|
|
||||||
|
type TypeFormInfo = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
plan: string | string[];
|
||||||
|
recurring: string;
|
||||||
|
};
|
||||||
|
const getTypeFormLink = (id: string, info: TypeFormInfo) => {
|
||||||
|
const plans = Array.isArray(info.plan) ? info.plan : [info.plan];
|
||||||
|
const product_id = plans
|
||||||
|
.map(plan => (plan === SubscriptionPlan.AI ? 'ai' : 'cloud'))
|
||||||
|
.join('-');
|
||||||
|
const product_price =
|
||||||
|
info.recurring === SubscriptionRecurring.Monthly
|
||||||
|
? 'monthly'
|
||||||
|
: info.recurring === SubscriptionRecurring.Lifetime
|
||||||
|
? 'lifeTime'
|
||||||
|
: 'annually';
|
||||||
|
return `${typeFormUrl}/${id}#email=${info.email ?? ''}&name=${info.name ?? 'Unknown'}&user_id=${info.id}&product_id=${product_id}&product_price=${product_price}`;
|
||||||
|
};
|
||||||
|
export const getUpgradeQuestionnaireLink = (info: TypeFormInfo) =>
|
||||||
|
getTypeFormLink(typeFormUpgradeId, info);
|
||||||
|
export const getDowngradeQuestionnaireLink = (info: TypeFormInfo) =>
|
||||||
|
getTypeFormLink(typeFormDowngradeId, info);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate subscription callback link with account info
|
||||||
|
*/
|
||||||
|
export const generateSubscriptionCallbackLink = (
|
||||||
|
account: AuthAccountInfo | null,
|
||||||
|
plan: SubscriptionPlan,
|
||||||
|
recurring: SubscriptionRecurring
|
||||||
|
) => {
|
||||||
|
if (account === null) {
|
||||||
|
throw new Error('Account is required');
|
||||||
|
}
|
||||||
|
const baseUrl =
|
||||||
|
plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success';
|
||||||
|
|
||||||
|
let name = account?.info?.name ?? '';
|
||||||
|
if (name.includes(separator)) {
|
||||||
|
name = name.replaceAll(separator, recoverSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = [
|
||||||
|
plan,
|
||||||
|
recurring,
|
||||||
|
account.id,
|
||||||
|
account.email,
|
||||||
|
account.info?.name ?? '',
|
||||||
|
].join(separator);
|
||||||
|
|
||||||
|
return `${baseUrl}?info=${encodeURIComponent(query)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse subscription callback query.info
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const parseSubscriptionCallbackLink = (query: string) => {
|
||||||
|
const [plan, recurring, id, email, rawName] =
|
||||||
|
decodeURIComponent(query).split(separator);
|
||||||
|
const name = rawName.replaceAll(recoverSeparator, separator);
|
||||||
|
|
||||||
|
return {
|
||||||
|
plan: plan as SubscriptionPlan,
|
||||||
|
recurring: recurring as SubscriptionRecurring,
|
||||||
|
account: {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
info: {
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to parse subscription callback link, and save to local storage and delete the query
|
||||||
|
*/
|
||||||
|
export const useSubscriptionNotifyWriter = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const query = searchParams.get('info');
|
||||||
|
if (query) {
|
||||||
|
localStorage.setItem(localStorageKey, query);
|
||||||
|
searchParams.delete('info');
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to read and parse subscription info from localStorage
|
||||||
|
*/
|
||||||
|
export const useSubscriptionNotifyReader = () => {
|
||||||
|
const upgradeNotify = useUpgradeNotify();
|
||||||
|
|
||||||
|
const readAndNotify = useCallback(() => {
|
||||||
|
const query = localStorage.getItem(localStorageKey);
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { plan, recurring, account } = parseSubscriptionCallbackLink(query);
|
||||||
|
const link = getUpgradeQuestionnaireLink({
|
||||||
|
id: account.id,
|
||||||
|
email: account.email,
|
||||||
|
name: account.info?.name ?? '',
|
||||||
|
plan,
|
||||||
|
recurring,
|
||||||
|
});
|
||||||
|
upgradeNotify(link);
|
||||||
|
localStorage.removeItem(localStorageKey);
|
||||||
|
|
||||||
|
// mixpanel
|
||||||
|
mixpanel.track('PlanUpgradeSucceeded', {
|
||||||
|
segment: 'settings panel',
|
||||||
|
control: 'plan upgrade action',
|
||||||
|
plan: plan,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to parse subscription callback link', err);
|
||||||
|
}
|
||||||
|
}, [upgradeNotify]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
readAndNotify();
|
||||||
|
window.addEventListener('focus', readAndNotify);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('focus', readAndNotify);
|
||||||
|
};
|
||||||
|
}, [readAndNotify]);
|
||||||
|
};
|
||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
useGlobalDNDHelper,
|
useGlobalDNDHelper,
|
||||||
} from '../hooks/affine/use-global-dnd-helper';
|
} from '../hooks/affine/use-global-dnd-helper';
|
||||||
import { useRegisterFindInPageCommands } from '../hooks/affine/use-register-find-in-page-commands';
|
import { useRegisterFindInPageCommands } from '../hooks/affine/use-register-find-in-page-commands';
|
||||||
|
import { useSubscriptionNotifyReader } from '../hooks/affine/use-subscription-notify';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||||
import { useRegisterNavigationCommands } from '../modules/navigation/view/use-register-navigation-commands';
|
import { useRegisterNavigationCommands } from '../modules/navigation/view/use-register-navigation-commands';
|
||||||
@@ -164,6 +165,7 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
|||||||
workbench,
|
workbench,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useSubscriptionNotifyReader();
|
||||||
useRegisterWorkspaceCommands();
|
useRegisterWorkspaceCommands();
|
||||||
useRegisterNavigationCommands();
|
useRegisterNavigationCommands();
|
||||||
useRegisterFindInPageCommands();
|
useRegisterFindInPageCommands();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
||||||
|
|
||||||
|
import { generateSubscriptionCallbackLink } from '../hooks/affine/use-subscription-notify';
|
||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { AuthService, SubscriptionService } from '../modules/cloud';
|
import { AuthService, SubscriptionService } from '../modules/cloud';
|
||||||
import { mixpanel } from '../utils';
|
import { mixpanel } from '../utils';
|
||||||
@@ -58,21 +59,27 @@ export const Component = () => {
|
|||||||
category: recurring,
|
category: recurring,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
const account = authService.session.account$.value;
|
||||||
|
// should never reach
|
||||||
|
if (!account) throw new Error('No account');
|
||||||
|
const targetPlan =
|
||||||
|
plan?.toLowerCase() === 'ai'
|
||||||
|
? SubscriptionPlan.AI
|
||||||
|
: SubscriptionPlan.Pro;
|
||||||
|
const targetRecurring =
|
||||||
|
recurring?.toLowerCase() === 'monthly'
|
||||||
|
? SubscriptionRecurring.Monthly
|
||||||
|
: SubscriptionRecurring.Yearly;
|
||||||
const checkout = await subscriptionService.createCheckoutSession({
|
const checkout = await subscriptionService.createCheckoutSession({
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
plan:
|
plan: targetPlan,
|
||||||
plan?.toLowerCase() === 'ai'
|
|
||||||
? SubscriptionPlan.AI
|
|
||||||
: SubscriptionPlan.Pro,
|
|
||||||
coupon: null,
|
coupon: null,
|
||||||
recurring:
|
recurring: targetRecurring,
|
||||||
recurring?.toLowerCase() === 'monthly'
|
successCallbackLink: generateSubscriptionCallbackLink(
|
||||||
? SubscriptionRecurring.Monthly
|
account,
|
||||||
: SubscriptionRecurring.Yearly,
|
targetPlan,
|
||||||
successCallbackLink:
|
targetRecurring
|
||||||
plan?.toLowerCase() === 'ai'
|
),
|
||||||
? '/ai-upgrade-success'
|
|
||||||
: '/upgrade-success',
|
|
||||||
});
|
});
|
||||||
setMessage('Redirecting...');
|
setMessage('Redirecting...');
|
||||||
location.href = checkout;
|
location.href = checkout;
|
||||||
|
|||||||
@@ -1087,6 +1087,19 @@
|
|||||||
"com.affine.payment.upgrade-success-page.support": "If you have any questions, please contact our <1> customer support</1>.",
|
"com.affine.payment.upgrade-success-page.support": "If you have any questions, please contact our <1> customer support</1>.",
|
||||||
"com.affine.payment.upgrade-success-page.text": "Congratulations! Your AFFiNE account has been successfully upgraded to a Pro account.",
|
"com.affine.payment.upgrade-success-page.text": "Congratulations! Your AFFiNE account has been successfully upgraded to a Pro account.",
|
||||||
"com.affine.payment.upgrade-success-page.title": "Upgrade Successful!",
|
"com.affine.payment.upgrade-success-page.title": "Upgrade Successful!",
|
||||||
|
"com.affine.payment.upgrade-success-notify.title": "Thanks for subscribing!",
|
||||||
|
"com.affine.payment.upgrade-success-notify.content": "We'd like to hear more about your use case, so that we can make AFFiNE better.",
|
||||||
|
"com.affine.payment.upgrade-success-notify.later": "Later",
|
||||||
|
"com.affine.payment.upgrade-success-notify.ok-client": "Sure, Open In Browser",
|
||||||
|
"com.affine.payment.upgrade-success-notify.ok-web": "Sure, Open In New Tab",
|
||||||
|
"com.affine.payment.downgraded-notify.title": "Sorry to see you go",
|
||||||
|
"com.affine.payment.downgraded-notify.content": "We'd like to hear more about where we fall short, so that we can make AFFiNE better.",
|
||||||
|
"com.affine.payment.downgraded-notify.later": "Later",
|
||||||
|
"com.affine.payment.downgraded-notify.ok-client": "Sure, Open In Browser",
|
||||||
|
"com.affine.payment.downgraded-notify.ok-web": "Sure, Open In New Tab",
|
||||||
|
"com.affine.payment.billing-type-form.title": "Tell Us Your Use Case",
|
||||||
|
"com.affine.payment.billing-type-form.description": "Please tell us more about your use case, to make AFFiNE better.",
|
||||||
|
"com.affine.payment.billing-type-form.go": "Go",
|
||||||
"com.affine.peek-view-controls.close": "Close",
|
"com.affine.peek-view-controls.close": "Close",
|
||||||
"com.affine.peek-view-controls.open-doc": "Open this doc",
|
"com.affine.peek-view-controls.open-doc": "Open this doc",
|
||||||
"com.affine.peek-view-controls.open-doc-in-new-tab": "Open in new tab",
|
"com.affine.peek-view-controls.open-doc-in-new-tab": "Open in new tab",
|
||||||
|
|||||||
Reference in New Issue
Block a user