diff --git a/packages/shared/__tests__/helpers/boot.tsx b/packages/shared/__tests__/helpers/boot.tsx index 679d9cc76e..8452a720e1 100644 --- a/packages/shared/__tests__/helpers/boot.tsx +++ b/packages/shared/__tests__/helpers/boot.tsx @@ -19,6 +19,7 @@ import { LazyModalElement } from '../../src/components/modals/LazyModalElement'; import LogContext from '../../src/contexts/LogContext'; import type { LogContextData } from '../../src/hooks/log/useLogContextData'; import { ChecklistViewState } from '../../src/lib/checklist'; +import { PaymentContextProvider } from '../../src/contexts/PaymentContext'; interface TestBootProviderProps { children: ReactNode; @@ -116,8 +117,10 @@ export const TestBootProvider = ({ value={{ ...defaultLogContextData, ...log }} > - {children} - + + {children} + + diff --git a/packages/shared/src/components/ProfileMenu.tsx b/packages/shared/src/components/ProfileMenu.tsx index 6b2bf11fd9..0db02eb376 100644 --- a/packages/shared/src/components/ProfileMenu.tsx +++ b/packages/shared/src/components/ProfileMenu.tsx @@ -160,7 +160,13 @@ export default function ProfileMenu({ title: 'Gift daily.dev Plus', buttonProps: { icon: , - onClick: () => openModal({ type: LazyModal.GiftPlus }), + onClick: () => { + logSubscriptionEvent({ + event_name: LogEvent.GiftSubscription, + target_id: TargetId.ProfileDropdown, + }); + openModal({ type: LazyModal.GiftPlus }); + }, }, }); @@ -174,14 +180,14 @@ export default function ProfileMenu({ return list.filter(Boolean); }, [ + user.permalink, isGdprCovered, - isDndActive, isPlus, logSubscriptionEvent, - logout, - openModal, + isDndActive, setShowDnd, - user.permalink, + openModal, + logout, ]); if (!user) { diff --git a/packages/shared/src/components/cards/squad/SquadGrid.spec.tsx b/packages/shared/src/components/cards/squad/SquadGrid.spec.tsx index e5735f80d8..1bf017679a 100644 --- a/packages/shared/src/components/cards/squad/SquadGrid.spec.tsx +++ b/packages/shared/src/components/cards/squad/SquadGrid.spec.tsx @@ -26,6 +26,7 @@ import { import { waitForNock } from '../../../../__tests__/helpers/utilities'; import { cloudinarySquadsDirectoryCardBannerDefault } from '../../../lib/image'; import { ActionType, COMPLETE_ACTION_MUTATION } from '../../../graphql/actions'; +import { PaymentContextProvider } from '../../../contexts/PaymentContext'; const routerReplace = jest.fn(); const squads = [generateTestSquad()]; @@ -76,8 +77,10 @@ const renderComponent = (): RenderResult => { loadedUserFromCache squads={squads} > - - + + + + , ); diff --git a/packages/shared/src/components/comments/CommentActionButtons.spec.tsx b/packages/shared/src/components/comments/CommentActionButtons.spec.tsx index 3707902c68..2d0e90f107 100644 --- a/packages/shared/src/components/comments/CommentActionButtons.spec.tsx +++ b/packages/shared/src/components/comments/CommentActionButtons.spec.tsx @@ -22,6 +22,7 @@ import { UserVoteEntity } from '../../hooks'; import { UserVote } from '../../graphql/posts'; import LogContext from '../../contexts/LogContext'; import { ActionType } from '../../graphql/actions'; +import { PaymentContextProvider } from '../../contexts/PaymentContext'; const showLogin = jest.fn(); const onComment = jest.fn(); @@ -76,7 +77,9 @@ const renderComponent = ( sendBeacon: jest.fn(), }} > - + + + , diff --git a/packages/shared/src/components/comments/CommentActionButtons.tsx b/packages/shared/src/components/comments/CommentActionButtons.tsx index c258866e16..a674c5ad58 100644 --- a/packages/shared/src/components/comments/CommentActionButtons.tsx +++ b/packages/shared/src/components/comments/CommentActionButtons.tsx @@ -25,7 +25,7 @@ import { } from '../buttons/Button'; import { ClickableText } from '../buttons/ClickableText'; import { SimpleTooltip } from '../tooltips/SimpleTooltip'; -import { Origin } from '../../lib/log'; +import { LogEvent, Origin, TargetId } from '../../lib/log'; import type { Post } from '../../graphql/posts'; import { UserVote } from '../../graphql/posts'; import { AuthTriggers } from '../../lib/auth'; @@ -39,7 +39,11 @@ import { useLazyModal } from '../../hooks/useLazyModal'; import { labels, largeNumberFormat } from '../../lib'; import { useToastNotification } from '../../hooks/useToastNotification'; import type { VoteEntityPayload } from '../../hooks'; -import { useVoteComment, voteMutationHandlers } from '../../hooks'; +import { + usePlusSubscription, + useVoteComment, + voteMutationHandlers, +} from '../../hooks'; import { generateQueryKey, RequestKey } from '../../lib/query'; import { useRequestProtocol } from '../../hooks/useRequestProtocol'; import { getCompanionWrapper } from '../../lib/extension'; @@ -48,6 +52,7 @@ import { ContentPreferenceType } from '../../graphql/contentPreference'; import { isFollowingContent } from '../../hooks/contentPreference/types'; import { useIsSpecialUser } from '../../hooks/auth/useIsSpecialUser'; import { GiftIcon } from '../icons/gift'; +import { usePaymentContext } from '../../contexts/PaymentContext'; export interface CommentActionProps { onComment: (comment: Comment, parentId: string | null) => void; @@ -84,6 +89,8 @@ export default function CommentActionButtons({ const { onMenuClick, isOpen, onHide } = useContextMenu({ id }); const { openModal } = useLazyModal(); const { displayToast } = useToastNotification(); + const { isPlusAvailable } = usePaymentContext(); + const { logSubscriptionEvent } = usePlusSubscription(); const [voteState, setVoteState] = useState(() => { return { id: comment.id, @@ -262,16 +269,25 @@ export default function CommentActionButtons({ }); } - if (comment.author.id !== user?.id && !comment.author.isPlus) { + if ( + isPlusAvailable && + comment.author.id !== user?.id && + !comment.author.isPlus + ) { commentOptions.push({ label: 'Gift daily.dev Plus', - action: () => + action: () => { + logSubscriptionEvent({ + event_name: LogEvent.GiftSubscription, + target_id: TargetId.ContextMenu, + }); openModal({ type: LazyModal.GiftPlus, props: { preselected: comment.author as UserShortProfile, }, - }), + }); + }, icon: , }); } diff --git a/packages/shared/src/components/comments/MainComment.spec.tsx b/packages/shared/src/components/comments/MainComment.spec.tsx index 1ba46d3e63..2fbdd6076f 100644 --- a/packages/shared/src/components/comments/MainComment.spec.tsx +++ b/packages/shared/src/components/comments/MainComment.spec.tsx @@ -11,6 +11,7 @@ import comment from '../../../__tests__/fixture/comment'; import post from '../../../__tests__/fixture/post'; import { Origin } from '../../lib/log'; import { useViewSize } from '../../hooks'; +import { PaymentContextProvider } from '../../contexts/PaymentContext'; const onDelete = jest.fn(); const mockUseViewSize = useViewSize as jest.MockedFunction; @@ -60,7 +61,9 @@ const renderLayout = ( tokenRefreshed: true, }} > - + + + , ); diff --git a/packages/shared/src/components/comments/SubComment.spec.tsx b/packages/shared/src/components/comments/SubComment.spec.tsx index 85c96bc9d7..3c22b006d9 100644 --- a/packages/shared/src/components/comments/SubComment.spec.tsx +++ b/packages/shared/src/components/comments/SubComment.spec.tsx @@ -11,6 +11,7 @@ import comment from '../../../__tests__/fixture/comment'; import { Origin } from '../../lib/log'; import post from '../../../__tests__/fixture/post'; import { useViewSize } from '../../hooks'; +import { PaymentContextProvider } from '../../contexts/PaymentContext'; const onDelete = jest.fn(); const mockUseViewSize = useViewSize as jest.MockedFunction; @@ -64,7 +65,9 @@ const renderLayout = ( tokenRefreshed: true, }} > - + + + , ); diff --git a/packages/shared/src/components/plus/PlusInfo.tsx b/packages/shared/src/components/plus/PlusInfo.tsx index e0bffacece..631892d68c 100644 --- a/packages/shared/src/components/plus/PlusInfo.tsx +++ b/packages/shared/src/components/plus/PlusInfo.tsx @@ -20,7 +20,7 @@ import { ButtonVariant, } from '../buttons/Button'; import { usePlusSubscription } from '../../hooks/usePlusSubscription'; -import { LogEvent } from '../../lib/log'; +import { LogEvent, TargetId } from '../../lib/log'; import { useGiftUserContext } from './GiftUserContext'; import { PlusOptionRadio } from './PlusOptionRadio'; import { GiftingSelectedUser } from './GiftingSelectedUser'; @@ -109,7 +109,13 @@ export const PlusInfo = ({ icon={} size={ButtonSize.XSmall} variant={ButtonVariant.Float} - onClick={() => openModal({ type: LazyModal.GiftPlus })} + onClick={() => { + logSubscriptionEvent({ + event_name: LogEvent.GiftSubscription, + target_id: TargetId.PlusPage, + }); + openModal({ type: LazyModal.GiftPlus }); + }} > Buy as a gift diff --git a/packages/shared/src/components/profile/Header.tsx b/packages/shared/src/components/profile/Header.tsx index 920f3353bb..21c8743592 100644 --- a/packages/shared/src/components/profile/Header.tsx +++ b/packages/shared/src/components/profile/Header.tsx @@ -55,6 +55,7 @@ export function Header({ entity: ContentPreferenceType.User, }); const { unblock, block } = useContentPreference(); + const { logSubscriptionEvent } = usePlusSubscription(); const onReportUser = React.useCallback( (defaultBlocked = false) => { @@ -103,11 +104,16 @@ export function Header({ options.push({ icon: , label: 'Gift daily.dev Plus', - action: () => + action: () => { + logSubscriptionEvent({ + event_name: LogEvent.GiftSubscription, + target_id: TargetId.ProfilePage, + }); openModal({ type: LazyModal.GiftPlus, props: { preselected: user as UserShortProfile }, - }), + }); + }, }); } diff --git a/packages/shared/src/components/squads/SquadMemberMenu.tsx b/packages/shared/src/components/squads/SquadMemberMenu.tsx index 8fb5260511..24e3f1879f 100644 --- a/packages/shared/src/components/squads/SquadMemberMenu.tsx +++ b/packages/shared/src/components/squads/SquadMemberMenu.tsx @@ -11,13 +11,15 @@ import { UserShortInfo } from '../profile/UserShortInfo'; import type { MenuItemProps } from '../fields/ContextMenu'; import ContextMenu from '../fields/ContextMenu'; import type { UseSquadActions } from '../../hooks'; -import { useToastNotification } from '../../hooks'; +import { usePlusSubscription, useToastNotification } from '../../hooks'; import { verifyPermission } from '../../graphql/squads'; import { ButtonColor, ButtonVariant } from '../buttons/Button'; import { ContextMenu as ContextMenuIds } from '../../hooks/constants'; import { LazyModal } from '../modals/common/types'; import { GiftIcon } from '../icons/gift'; import { useLazyModal } from '../../hooks/useLazyModal'; +import { LogEvent, TargetId } from '../../lib/log'; +import { usePaymentContext } from '../../contexts/PaymentContext'; interface SquadMemberMenuProps extends Pick { squad: Squad; @@ -127,6 +129,8 @@ export default function SquadMemberMenu({ const { user } = useContext(AuthContext); const { showPrompt } = usePrompt(); const { displayToast } = useToastNotification(); + const { isPlusAvailable } = usePaymentContext(); + const { logSubscriptionEvent } = usePlusSubscription(); const onUpdateMember = async ( role: SourceMemberRole, title: MenuItemTitle, @@ -213,14 +217,19 @@ export default function SquadMemberMenu({ }); } - if (!member.user.isPlus) { + if (!member.user.isPlus && isPlusAvailable) { menu.push({ label: 'Gift daily.dev Plus', - action: () => + action: () => { + logSubscriptionEvent({ + event_name: LogEvent.GiftSubscription, + target_id: TargetId.Squad, + }); openModal({ type: LazyModal.GiftPlus, props: { preselected: member.user }, - }), + }); + }, icon: , }); } diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index 3677d5769c..1aeb240664 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -226,6 +226,7 @@ export enum LogEvent { ReceivePayment = 'receive payment', OnboardingSkipPlus = 'skip upgrade subscription', OnboardingUpgradePlus = 'upgrade subscription', + GiftSubscription = 'gift subscription', // End Plus subscription // Clickbait Shield ToggleClickbaitShield = 'toggle clickbait shield', @@ -308,6 +309,7 @@ export enum TargetId { Ads = 'ads', MyProfile = 'my profile', PlusBadge = 'plus badge', + PlusPage = 'plus page', Onboarding = 'onboarding', BlockedWords = 'block words', CustomFeed = 'custom feed', @@ -316,6 +318,7 @@ export enum TargetId { ClickbaitShield = 'clickbait shield', StreakTimezoneLabel = 'streak timezone label', StreakTimezoneMismatchPrompt = 'streak timezone mismatch prompt', + ContextMenu = 'context', } export enum NotificationChannel { diff --git a/packages/webapp/pages/account/invite.tsx b/packages/webapp/pages/account/invite.tsx index cbc6cd5947..0a16bbc0b3 100644 --- a/packages/webapp/pages/account/invite.tsx +++ b/packages/webapp/pages/account/invite.tsx @@ -2,6 +2,7 @@ import type { ReactElement } from 'react'; import React, { useMemo, useRef } from 'react'; import { ReferralCampaignKey, + usePlusSubscription, useReferralCampaign, } from '@dailydotdev/shared/src/hooks'; import { link } from '@dailydotdev/shared/src/lib/links'; @@ -23,7 +24,6 @@ import type { UserShortProfile } from '@dailydotdev/shared/src/lib/user'; import { format } from 'date-fns'; import { IconSize } from '@dailydotdev/shared/src/components/Icon'; import { useInfiniteQuery } from '@tanstack/react-query'; -import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; import { LogEvent, TargetId, @@ -47,6 +47,8 @@ import { import { GiftIcon } from '@dailydotdev/shared/src/components/icons/gift'; import { useLazyModal } from '@dailydotdev/shared/src/hooks/useLazyModal'; import { LazyModal } from '@dailydotdev/shared/src/components/modals/common/types'; +import { usePaymentContext } from '@dailydotdev/shared/src/contexts/PaymentContext'; +import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; import AccountContentSection from '../../components/layouts/AccountLayout/AccountContentSection'; import { AccountPageContainer } from '../../components/layouts/AccountLayout/AccountPageContainer'; import { getAccountLayout } from '../../components/layouts/AccountLayout'; @@ -64,6 +66,8 @@ const AccountInvitePage = (): ReactElement => { const { url, referredUsersCount } = useReferralCampaign({ campaignKey: ReferralCampaignKey.Generic, }); + const { isPlusAvailable } = usePaymentContext(); + const { logSubscriptionEvent } = usePlusSubscription(); const { logEvent } = useLogContext(); const inviteLink = url || link.referral.defaultUrl; const [, onShareOrCopyLink] = useShareOrCopyLink({ @@ -105,39 +109,45 @@ const AccountInvitePage = (): ReactElement => { return ( -
-
-
- - Gift daily.dev Plus + {isPlusAvailable && ( +
+
+
+ + Gift daily.dev Plus + + +
+ + Gifting daily.dev Plus to a friend is the ultimate way to say, + 'I've got your back.' It unlocks an ad-free + experience, advanced content filtering and customizations, plus AI + superpowers to supercharge their daily.dev journey. -
- } + variant={ButtonVariant.Secondary} + color={ButtonColor.Bacon} + onClick={() => { + logSubscriptionEvent({ + event_name: LogEvent.GiftSubscription, + target_id: TargetId.InviteFriendsPage, + }); + openModal({ + type: LazyModal.GiftPlus, + }); + }} + className="max-w-fit border-action-plus-default text-action-plus-default" > - Gifting daily.dev Plus to a friend is the ultimate way to say, - 'I've got your back.' It unlocks an ad-free - experience, advanced content filtering and customizations, plus AI - superpowers to supercharge their daily.dev journey. - + Buy as gift +
- -
+ )}