diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/ai-usage-panel.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/ai-usage-panel.tsx index 325aa4217a..f53db45a41 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/account-setting/ai-usage-panel.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/ai-usage-panel.tsx @@ -104,7 +104,7 @@ export const AIUsagePanel = () => { > {copilotActionLimit === 'unlimited' ? ( hasPaymentFeature && aiSubscription?.canceledAt ? ( - + ) : ( {t['com.affine.payment.ai.usage.change-button-label']()} diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx index 22d2a05dfb..179ea88bc0 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx @@ -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 = () => { ) : ( { - 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 ? ( - + ) : ( - + ) ) : ( @@ -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 ( diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx index 1184aab4c2..f801ed0052 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx @@ -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); } diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/cancel.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/cancel.tsx index bb3ee1b80e..34224144f6 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/cancel.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/cancel.tsx @@ -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, diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/resume.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/resume.tsx index ce5ecbd8bc..0edb3dcbef 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/resume.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/actions/resume.tsx @@ -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: , iconColor: cssVar('processingColor'), @@ -66,7 +76,7 @@ export const AIResume = ({ ...btnProps }: AIResumeProps) => { setIdempotencyKey(nanoid()); }, }); - }, [openConfirmModal, t, subscription, idempotencyKey]); + }, [subscription, openConfirmModal, t, module, idempotencyKey]); return ( diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx index d7679864dd..51b616bcf0 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx @@ -53,9 +53,15 @@ export const AIPlan = () => { isLoggedIn ? ( subscription ? ( subscription.canceledAt ? ( - + ) : ( - + ) ) : ( <> diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx index 10cb551d6e..b3445d0cf0 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx @@ -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 ( - + { - 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 ( diff --git a/packages/frontend/core/src/hooks/affine/use-subscription-notify.tsx b/packages/frontend/core/src/hooks/affine/use-subscription-notify.tsx index 5e0f30b5fb..f9d06c26b1 100644 --- a/packages/frontend/core/src/hooks/affine/use-subscription-notify.tsx +++ b/packages/frontend/core/src/hooks/affine/use-subscription-notify.tsx @@ -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); diff --git a/packages/frontend/core/src/mixpanel/index.ts b/packages/frontend/core/src/mixpanel/index.ts new file mode 100644 index 0000000000..07b53a423f --- /dev/null +++ b/packages/frontend/core/src/mixpanel/index.ts @@ -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; +} diff --git a/packages/frontend/core/src/mixpanel/plan-change-started.ts b/packages/frontend/core/src/mixpanel/plan-change-started.ts new file mode 100644 index 0000000000..697c937a1e --- /dev/null +++ b/packages/frontend/core/src/mixpanel/plan-change-started.ts @@ -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; +} diff --git a/packages/frontend/core/src/mixpanel/plan-change-succeed.ts b/packages/frontend/core/src/mixpanel/plan-change-succeed.ts new file mode 100644 index 0000000000..4487f77d75 --- /dev/null +++ b/packages/frontend/core/src/mixpanel/plan-change-succeed.ts @@ -0,0 +1,9 @@ +import type { PlanChangeStartedEvent } from './plan-change-started'; + +/** + * Subscription plan changed successfully + */ +export type PlanChangeSucceededEvent = Pick< + PlanChangeStartedEvent, + 'control' | 'type' | 'category' +>; diff --git a/packages/frontend/core/src/pages/subscribe.tsx b/packages/frontend/core/src/pages/subscribe.tsx index bf1db523e3..280a01cf53 100644 --- a/packages/frontend/core/src/pages/subscribe.tsx +++ b/packages/frontend/core/src/pages/subscribe.tsx @@ -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, diff --git a/packages/frontend/core/src/utils/mixpanel.ts b/packages/frontend/core/src/utils/mixpanel.ts index 20bb74a26d..7e0bf980e7 100644 --- a/packages/frontend/core/src/utils/mixpanel.ts +++ b/packages/frontend/core/src/utils/mixpanel.ts @@ -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; return handler; } + +export function mixpanelTrack( + event: T, + properties?: MixpanelEvents[T] +) { + return mixpanel.track(event, properties); +}