feat(core): adjust subscription related mixpanel (#7536)

This commit is contained in:
CatsJuice 2024-07-26 02:49:15 +00:00
parent 549e7befed
commit a714961b20
No known key found for this signature in database
GPG Key ID: 1C1E76924FAFDDE4
13 changed files with 169 additions and 92 deletions

View File

@ -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']()}

View File

@ -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 (

View File

@ -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);
}

View File

@ -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,

View File

@ -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}>

View File

@ -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}
/>
)
) : (
<>

View File

@ -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}>

View File

@ -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);

View 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;
}

View 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;
}

View File

@ -0,0 +1,9 @@
import type { PlanChangeStartedEvent } from './plan-change-started';
/**
* Subscription plan changed successfully
*/
export type PlanChangeSucceededEvent = Pick<
PlanChangeStartedEvent,
'control' | 'type' | 'category'
>;

View File

@ -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,

View File

@ -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);
}