mirror of
https://github.com/toeverything/AFFiNE.git
synced 2025-01-05 07:53:46 +03:00
feat(core): adjust subscription related mixpanel (#7536)
This commit is contained in:
parent
549e7befed
commit
a714961b20
@ -104,7 +104,7 @@ export const AIUsagePanel = () => {
|
||||
>
|
||||
{copilotActionLimit === 'unlimited' ? (
|
||||
hasPaymentFeature && aiSubscription?.canceledAt ? (
|
||||
<AIResume />
|
||||
<AIResume module="billing subscription list" />
|
||||
) : (
|
||||
<Button onClick={openBilling}>
|
||||
{t['com.affine.payment.ai.usage.change-button-label']()}
|
||||
|
@ -32,7 +32,7 @@ import {
|
||||
import { useMutation } from '../../../../../hooks/use-mutation';
|
||||
import { useQuery } from '../../../../../hooks/use-query';
|
||||
import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { mixpanel, popupWindow } from '../../../../../utils';
|
||||
import { mixpanel, mixpanelTrack, popupWindow } from '../../../../../utils';
|
||||
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
|
||||
import { CancelAction, ResumeAction } from '../plans/actions';
|
||||
import { AICancel, AIResume, AISubscribe } from '../plans/ai/actions';
|
||||
@ -237,19 +237,13 @@ const SubscriptionSettings = () => {
|
||||
</SettingRow>
|
||||
) : (
|
||||
<CancelAction
|
||||
module="billing subscription list"
|
||||
open={openCancelModal}
|
||||
onOpenChange={setOpenCancelModal}
|
||||
>
|
||||
<SettingRow
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mixpanel.track('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
module: 'billing subscription list',
|
||||
control: 'plan cancel action',
|
||||
type: proSubscription.plan,
|
||||
category: proSubscription.recurring,
|
||||
});
|
||||
setOpenCancelModal(true);
|
||||
}}
|
||||
className="dangerous-setting"
|
||||
@ -403,9 +397,15 @@ const AIPlanCard = ({ onClick }: { onClick: () => void }) => {
|
||||
{price?.yearlyAmount ? (
|
||||
subscription ? (
|
||||
subscription.canceledAt ? (
|
||||
<AIResume className={styles.planAction} />
|
||||
<AIResume
|
||||
module="billing subscription list"
|
||||
className={styles.planAction}
|
||||
/>
|
||||
) : (
|
||||
<AICancel className={styles.planAction} />
|
||||
<AICancel
|
||||
module="billing subscription list"
|
||||
className={styles.planAction}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<AISubscribe className={styles.planAction}>
|
||||
@ -476,13 +476,17 @@ const ResumeSubscription = () => {
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
const handleClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
mixpanel.track('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
module: 'pricing plan list',
|
||||
control: 'plan resume action',
|
||||
type: subscription.pro$.value?.plan,
|
||||
category: subscription.pro$.value?.recurring,
|
||||
});
|
||||
const type = subscription.pro$.value?.plan;
|
||||
const category = subscription.pro$.value?.recurring;
|
||||
if (type && category) {
|
||||
mixpanelTrack('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
module: 'pricing plan list',
|
||||
control: 'paying',
|
||||
type,
|
||||
category,
|
||||
});
|
||||
}
|
||||
}, [subscription.pro$.value?.plan, subscription.pro$.value?.recurring]);
|
||||
|
||||
return (
|
||||
|
@ -1,11 +1,12 @@
|
||||
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 type { MixpanelEvents } from '@affine/core/mixpanel';
|
||||
import { mixpanelTrack } from '@affine/core/utils';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { useDowngradeNotify } from '../../../subscription-landing/notify';
|
||||
@ -20,16 +21,30 @@ export const CancelAction = ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
module,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
module: MixpanelEvents['PlanChangeStarted']['module'];
|
||||
} & PropsWithChildren) => {
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
const proSubscription = useLiveData(subscription.pro$);
|
||||
const authService = useService(AuthService);
|
||||
const downgradeNotify = useDowngradeNotify();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !proSubscription) return;
|
||||
mixpanelTrack('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
module,
|
||||
control: 'cancel',
|
||||
type: proSubscription.plan,
|
||||
category: proSubscription.recurring,
|
||||
});
|
||||
}, [module, open, proSubscription]);
|
||||
|
||||
const downgrade = useAsyncCallback(async () => {
|
||||
try {
|
||||
const account = authService.session.account$.value;
|
||||
@ -41,13 +56,14 @@ export const CancelAction = ({
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onOpenChange(false);
|
||||
mixpanel.track('ChangePlanSucceeded', {
|
||||
segment: 'settings panel',
|
||||
module: 'pricing plan list',
|
||||
control: 'plan cancel action',
|
||||
type: subscription.pro$.value?.plan,
|
||||
category: subscription.pro$.value?.recurring,
|
||||
});
|
||||
const proSubscription = subscription.pro$.value;
|
||||
if (proSubscription) {
|
||||
mixpanelTrack('PlanChangeSucceeded', {
|
||||
control: 'cancel',
|
||||
type: proSubscription.plan,
|
||||
category: proSubscription.recurring,
|
||||
});
|
||||
}
|
||||
if (account && prevRecurring) {
|
||||
downgradeNotify(
|
||||
getDowngradeQuestionnaireLink({
|
||||
@ -110,13 +126,14 @@ export const ResumeAction = ({
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onOpenChange(false);
|
||||
mixpanel.track('ChangePlanSucceeded', {
|
||||
segment: 'settings panel',
|
||||
module: 'pricing plan list',
|
||||
control: 'plan resume action',
|
||||
type: subscription.pro$.value?.plan,
|
||||
category: subscription.pro$.value?.recurring,
|
||||
});
|
||||
const proSubscription = subscription.pro$.value;
|
||||
if (proSubscription) {
|
||||
mixpanelTrack('PlanChangeSucceeded', {
|
||||
control: 'paying',
|
||||
type: proSubscription.plan,
|
||||
category: proSubscription.recurring,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
|
@ -2,16 +2,19 @@ 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 type { MixpanelEvents } from '@affine/core/mixpanel';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { mixpanel, mixpanelTrack } from '@affine/core/utils';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface AICancelProps extends ButtonProps {}
|
||||
export const AICancel = ({ ...btnProps }: AICancelProps) => {
|
||||
export interface AICancelProps extends ButtonProps {
|
||||
module: MixpanelEvents['PlanChangeStarted']['module'];
|
||||
}
|
||||
export const AICancel = ({ module, ...btnProps }: AICancelProps) => {
|
||||
const t = useI18n();
|
||||
const [isMutating, setMutating] = useState(false);
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
@ -22,12 +25,16 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
|
||||
const downgradeNotify = useDowngradeNotify();
|
||||
|
||||
const cancel = useAsyncCallback(async () => {
|
||||
mixpanel.track('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
control: 'plan cancel action',
|
||||
type: subscription.ai$.value?.plan,
|
||||
category: subscription.ai$.value?.recurring,
|
||||
});
|
||||
const aiSubscription = subscription.ai$.value;
|
||||
if (aiSubscription) {
|
||||
mixpanelTrack('PlanChangeStarted', {
|
||||
module,
|
||||
segment: 'settings panel',
|
||||
control: 'cancel',
|
||||
type: SubscriptionPlan.AI,
|
||||
category: aiSubscription.recurring,
|
||||
});
|
||||
}
|
||||
openConfirmModal({
|
||||
title: t['com.affine.payment.ai.action.cancel.confirm.title'](),
|
||||
description:
|
||||
@ -51,9 +58,10 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
|
||||
SubscriptionPlan.AI
|
||||
);
|
||||
setIdempotencyKey(nanoid());
|
||||
mixpanel.track('ChangePlanSucceeded', {
|
||||
mixpanel.track('PlanChangeSucceeded', {
|
||||
segment: 'settings panel',
|
||||
control: 'plan cancel action',
|
||||
type: subscription.ai$.value?.plan,
|
||||
});
|
||||
const account = authService.session.account$.value;
|
||||
const prevRecurring = subscription.ai$.value?.recurring;
|
||||
@ -74,6 +82,7 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
|
||||
},
|
||||
});
|
||||
}, [
|
||||
module,
|
||||
subscription,
|
||||
openConfirmModal,
|
||||
t,
|
||||
|
@ -5,8 +5,9 @@ import {
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import type { MixpanelEvents } from '@affine/core/mixpanel';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { mixpanelTrack } from '@affine/core/utils';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons/rc';
|
||||
@ -15,9 +16,11 @@ import { cssVar } from '@toeverything/theme';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface AIResumeProps extends ButtonProps {}
|
||||
export interface AIResumeProps extends ButtonProps {
|
||||
module: MixpanelEvents['PlanChangeStarted']['module'];
|
||||
}
|
||||
|
||||
export const AIResume = ({ ...btnProps }: AIResumeProps) => {
|
||||
export const AIResume = ({ module, ...btnProps }: AIResumeProps) => {
|
||||
const t = useI18n();
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
@ -27,12 +30,16 @@ export const AIResume = ({ ...btnProps }: AIResumeProps) => {
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const resume = useAsyncCallback(async () => {
|
||||
mixpanel.track('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
control: 'plan resume action',
|
||||
type: subscription.ai$.value?.plan,
|
||||
category: subscription.ai$.value?.recurring,
|
||||
});
|
||||
const aiSubscription = subscription.ai$.value;
|
||||
if (aiSubscription) {
|
||||
mixpanelTrack('PlanChangeStarted', {
|
||||
module,
|
||||
segment: 'settings panel',
|
||||
control: 'paying',
|
||||
type: aiSubscription.plan,
|
||||
category: aiSubscription.recurring,
|
||||
});
|
||||
}
|
||||
|
||||
openConfirmModal({
|
||||
title: t['com.affine.payment.ai.action.resume.confirm.title'](),
|
||||
@ -51,10 +58,13 @@ export const AIResume = ({ ...btnProps }: AIResumeProps) => {
|
||||
idempotencyKey,
|
||||
SubscriptionPlan.AI
|
||||
);
|
||||
mixpanel.track('ChangePlanSucceeded', {
|
||||
segment: 'settings panel',
|
||||
control: 'plan resume action',
|
||||
});
|
||||
if (aiSubscription) {
|
||||
mixpanelTrack('PlanChangeSucceeded', {
|
||||
category: aiSubscription.recurring,
|
||||
control: 'paying',
|
||||
type: aiSubscription.plan,
|
||||
});
|
||||
}
|
||||
notify({
|
||||
icon: <SingleSelectSelectSolidIcon />,
|
||||
iconColor: cssVar('processingColor'),
|
||||
@ -66,7 +76,7 @@ export const AIResume = ({ ...btnProps }: AIResumeProps) => {
|
||||
setIdempotencyKey(nanoid());
|
||||
},
|
||||
});
|
||||
}, [openConfirmModal, t, subscription, idempotencyKey]);
|
||||
}, [subscription, openConfirmModal, t, module, idempotencyKey]);
|
||||
|
||||
return (
|
||||
<Button loading={isMutating} onClick={resume} type="primary" {...btnProps}>
|
||||
|
@ -53,9 +53,15 @@ export const AIPlan = () => {
|
||||
isLoggedIn ? (
|
||||
subscription ? (
|
||||
subscription.canceledAt ? (
|
||||
<AIResume className={styles.purchaseButton} />
|
||||
<AIResume
|
||||
module="pricing plan list"
|
||||
className={styles.purchaseButton}
|
||||
/>
|
||||
) : (
|
||||
<AICancel className={styles.purchaseButton} />
|
||||
<AICancel
|
||||
module="pricing plan list"
|
||||
className={styles.purchaseButton}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
|
@ -3,7 +3,7 @@ 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';
|
||||
import { mixpanelTrack, popupWindow } from '@affine/core/utils';
|
||||
import type { SubscriptionRecurring } from '@affine/graphql';
|
||||
import { SubscriptionPlan, SubscriptionStatus } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
@ -179,7 +179,6 @@ const CurrentPlan = () => {
|
||||
const Downgrade = ({ disabled }: { disabled?: boolean }) => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const tooltipContent = disabled
|
||||
? t['com.affine.payment.downgraded-tooltip']()
|
||||
@ -187,17 +186,10 @@ const Downgrade = ({ disabled }: { disabled?: boolean }) => {
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
mixpanel.track('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
module: 'pricing plan list',
|
||||
control: 'billing cancel action',
|
||||
type: subscription.pro$.value?.plan,
|
||||
category: subscription.pro$.value?.recurring,
|
||||
});
|
||||
}, [subscription.pro$.value?.plan, subscription.pro$.value?.recurring]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CancelAction open={open} onOpenChange={setOpen}>
|
||||
<CancelAction module="pricing plan list" open={open} onOpenChange={setOpen}>
|
||||
<Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}>
|
||||
<div className={styles.planAction}>
|
||||
<Button
|
||||
@ -345,11 +337,11 @@ const ChangeRecurring = ({
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const onStartChange = useCallback(() => {
|
||||
mixpanel.track('PlanChangeStarted', {
|
||||
mixpanelTrack('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
module: 'pricing plan list',
|
||||
control: 'plan resume action',
|
||||
type: 'cloud pro subscription',
|
||||
control: 'paying',
|
||||
type: SubscriptionPlan.Pro,
|
||||
category: to,
|
||||
});
|
||||
setOpen(true);
|
||||
@ -428,14 +420,17 @@ const ResumeButton = () => {
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
mixpanel.track('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
module: 'pricing plan list',
|
||||
control: 'pricing plan action',
|
||||
type: 'cloud pro subscription',
|
||||
category: subscription.pro$.value?.recurring,
|
||||
});
|
||||
}, [subscription.pro$.value?.recurring]);
|
||||
const pro = subscription.pro$.value;
|
||||
if (pro) {
|
||||
mixpanelTrack('PlanChangeStarted', {
|
||||
segment: 'settings panel',
|
||||
module: 'pricing plan list',
|
||||
control: 'paying',
|
||||
type: SubscriptionPlan.Pro,
|
||||
category: pro.recurring,
|
||||
});
|
||||
}
|
||||
}, [subscription.pro$.value]);
|
||||
|
||||
return (
|
||||
<ResumeAction open={open} onOpenChange={setOpen}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useUpgradeNotify } from '@affine/core/components/affine/subscription-landing/notify';
|
||||
import { mixpanelTrack } from '@affine/core/utils';
|
||||
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';
|
||||
@ -130,10 +130,10 @@ export const useSubscriptionNotifyReader = () => {
|
||||
localStorage.removeItem(localStorageKey);
|
||||
|
||||
// mixpanel
|
||||
mixpanel.track('PlanUpgradeSucceeded', {
|
||||
segment: 'settings panel',
|
||||
control: 'plan upgrade action',
|
||||
plan: plan,
|
||||
mixpanelTrack('PlanChangeSucceeded', {
|
||||
category: recurring,
|
||||
type: plan,
|
||||
control: 'new subscription',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to parse subscription callback link', err);
|
||||
|
7
packages/frontend/core/src/mixpanel/index.ts
Normal file
7
packages/frontend/core/src/mixpanel/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { PlanChangeStartedEvent } from './plan-change-started';
|
||||
import type { PlanChangeSucceededEvent } from './plan-change-succeed';
|
||||
|
||||
export interface MixpanelEvents {
|
||||
PlanChangeStarted: PlanChangeStartedEvent;
|
||||
PlanChangeSucceeded: PlanChangeSucceededEvent;
|
||||
}
|
15
packages/frontend/core/src/mixpanel/plan-change-started.ts
Normal file
15
packages/frontend/core/src/mixpanel/plan-change-started.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
|
||||
/**
|
||||
* Before subscription plan changed
|
||||
*/
|
||||
export interface PlanChangeStartedEvent {
|
||||
segment: 'settings panel';
|
||||
module: 'pricing plan list' | 'billing subscription list';
|
||||
control:
|
||||
| 'new subscription' // no subscription before
|
||||
| 'cancel'
|
||||
| 'paying'; // resume: subscribed before
|
||||
type: SubscriptionPlan;
|
||||
category: SubscriptionRecurring;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import type { PlanChangeStartedEvent } from './plan-change-started';
|
||||
|
||||
/**
|
||||
* Subscription plan changed successfully
|
||||
*/
|
||||
export type PlanChangeSucceededEvent = Pick<
|
||||
PlanChangeStartedEvent,
|
||||
'control' | 'type' | 'category'
|
||||
>;
|
@ -83,10 +83,6 @@ export const Component = () => {
|
||||
});
|
||||
setMessage('Redirecting...');
|
||||
location.href = checkout;
|
||||
mixpanel.track('PlanChangeSucceeded', {
|
||||
type: plan,
|
||||
category: recurring,
|
||||
});
|
||||
if (plan) {
|
||||
mixpanel.people.set({
|
||||
[SubscriptionPlan.AI === plan ? 'ai plan' : plan]: plan,
|
||||
|
@ -2,6 +2,8 @@ import { DebugLogger } from '@affine/debug';
|
||||
import type { OverridedMixpanel } from 'mixpanel-browser';
|
||||
import mixpanelBrowser from 'mixpanel-browser';
|
||||
|
||||
import type { MixpanelEvents } from '../mixpanel';
|
||||
|
||||
const logger = new DebugLogger('affine:mixpanel');
|
||||
|
||||
export const mixpanel = process.env.MIXPANEL_TOKEN
|
||||
@ -31,3 +33,10 @@ function createProxyHandler(property?: string | symbol) {
|
||||
} as ProxyHandler<OverridedMixpanel>;
|
||||
return handler;
|
||||
}
|
||||
|
||||
export function mixpanelTrack<T extends keyof MixpanelEvents>(
|
||||
event: T,
|
||||
properties?: MixpanelEvents[T]
|
||||
) {
|
||||
return mixpanel.track(event, properties);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user