Skip to content

Commit

Permalink
Merge branch 'MI-746-gifting-plus' into feat-gift-tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
sshanzel authored Feb 4, 2025
2 parents ece4046 + cef485d commit 83d4d11
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 37 deletions.
26 changes: 26 additions & 0 deletions packages/shared/src/components/modals/BootPopups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import InteractivePopup, {
import { MarketingCtaPopoverSmall } from '../marketingCta/MarketingCtaPopoverSmall';
import { ButtonVariant } from '../buttons/common';
import { isNullOrUndefined } from '../../lib/func';
import useProfileForm from '../../hooks/useProfileForm';

const REP_TRESHOLD = 250;

Expand All @@ -32,6 +33,7 @@ export const BootPopups = (): ReactElement => {
const { checkHasCompleted, isActionsFetched, completeAction } = useActions();
const { openModal } = useLazyModal();
const { user } = useAuthContext();
const { updateUserProfile } = useProfileForm();
const { alerts, loadedAlerts, updateAlerts, updateLastBootPopup } =
useContext(AlertContext);
const [bootPopups, setBootPopups] = useState(() => new Map());
Expand Down Expand Up @@ -242,6 +244,30 @@ export const BootPopups = (): ReactElement => {
user,
]);

/**
* Received gift plus modal
*/
useEffect(() => {
if (!user?.flags?.showPlusGift || !user.isPlus) {
return;
}

addBootPopup({
type: LazyModal.GiftPlusReceived,
props: {
user,
onAfterClose: () => {
updateUserProfile({
flags: { showPlusGift: false },
});
},
},
});
}, [updateUserProfile, user]);

/**
* Top reader badge modal
*/
useEffect(() => {
if (!alerts?.showTopReader) {
return;
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/components/modals/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,13 @@ const GiftPlusModal = dynamic(
() => import(/* webpackChunkName: "giftPlusModal" */ '../plus/GiftPlusModal'),
);

const GiftReceivedPlusModal = dynamic(
() =>
import(
/* webpackChunkName: "giftReceivedPlusModal" */ '../plus/GiftReceivedPlusModal'
),
);

export const modals = {
[LazyModal.SquadMember]: SquadMemberModal,
[LazyModal.UpvotedPopup]: UpvotedPopupModal,
Expand Down Expand Up @@ -271,6 +278,7 @@ export const modals = {
[LazyModal.CookieConsent]: CookieConsentModal,
[LazyModal.ReportUser]: ReportUserModal,
[LazyModal.GiftPlus]: GiftPlusModal,
[LazyModal.GiftPlusReceived]: GiftReceivedPlusModal,
};

type GetComponentProps<T> = T extends
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/components/modals/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export enum LazyModal {
CookieConsent = 'cookieConsent',
ReportUser = 'reportUser',
GiftPlus = 'giftPlus',
GiftPlusReceived = 'giftPlusReceived',
}

export type ModalTabItem = {
Expand Down
116 changes: 116 additions & 0 deletions packages/shared/src/components/plus/GiftReceivedPlusModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { ReactElement } from 'react';
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import type { ModalProps } from '../modals/common/Modal';
import { Modal } from '../modals/common/Modal';

import type { UserShortProfile } from '../../lib/user';
import { PlusTitle } from './PlusTitle';
import {
Typography,
TypographyColor,
TypographyType,
} from '../typography/Typography';
import CloseButton from '../CloseButton';
import { ButtonSize, ButtonVariant } from '../buttons/common';
import { cloudinaryGiftedPlusModalImage } from '../../lib/image';
import { PlusList } from './PlusList';
import { Button } from '../buttons/Button';
import { getPlusGifterUser } from '../../graphql/users';
import { generateQueryKey, RequestKey } from '../../lib/query';
import { ProfileImageSize, ProfilePicture } from '../ProfilePicture';
import Link from '../utilities/Link';
import { useAuthContext } from '../../contexts/AuthContext';
import { Loader } from '../Loader';

const GifterProfile = ({ gifter }: { gifter: UserShortProfile }) => (
<Link href={`/${gifter.username}`} passHref>
<a
className="flex items-center gap-2"
title={`View ${gifter.username}'s profile`}
>
<ProfilePicture user={gifter} size={ProfileImageSize.Medium} />
<Typography
bold
color={TypographyColor.Primary}
type={TypographyType.Callout}
>
{gifter.name}
</Typography>
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Secondary}
>
@{gifter.username}
</Typography>
</a>
</Link>
);

export function GiftReceivedPlusModal(props: ModalProps): ReactElement {
const { onRequestClose } = props;
const { user } = useAuthContext();
const { data: gifter, isLoading } = useQuery({
queryKey: generateQueryKey(RequestKey.GifterUser),
queryFn: getPlusGifterUser,
enabled: Boolean(user?.isPlus),
});

if (!gifter || isLoading) {
return (
<Modal
{...props}
isDrawerOnMobile
kind={Modal.Kind.FixedCenter}
size={Modal.Size.Small}
>
<Modal.Body className="flex flex-1 tablet:!px-4">
<Loader />
</Modal.Body>
</Modal>
);
}

return (
<Modal
{...props}
isDrawerOnMobile
kind={Modal.Kind.FixedCenter}
size={Modal.Size.Small}
>
<Modal.Body className="flex flex-1 tablet:!px-4">
<div className="flex flex-1 flex-col gap-4">
<div className="flex flex-row justify-between">
<PlusTitle type={TypographyType.Callout} bold />
<CloseButton
type="button"
size={ButtonSize.Small}
onClick={onRequestClose}
/>
</div>
<GifterProfile gifter={gifter} />
<Typography bold type={TypographyType.Title1}>
Surprise! 🎁 {gifter.username} thought of you and gifted you a
one-year daily.dev Plus membership!
</Typography>
<img
src={cloudinaryGiftedPlusModalImage}
alt="gift pack with daily.dev logo"
height={140}
width={386}
className="h-auto w-full"
/>
<PlusList className="overflow-clip !py-0" />
</div>
<Button
className="mt-4 w-full"
href="/squads/plus"
tag="a"
variant={ButtonVariant.Primary}
>{`See what's inside`}</Button>
</Modal.Body>
</Modal>
);
}

export default GiftReceivedPlusModal;
29 changes: 2 additions & 27 deletions packages/shared/src/components/plus/PlusList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { ReactElement } from 'react';
import React, { useMemo } from 'react';
import React from 'react';
import classNames from 'classnames';
import type { WithClassNameProps } from '../utilities';
import type { PlusItem, PlusListItemProps } from './PlusListItem';
import { PlusItemStatus, PlusListItem } from './PlusListItem';
import { usePaymentContext } from '../../contexts/PaymentContext';

export const defaultFeatureList: Array<PlusItem> = [
{
Expand Down Expand Up @@ -88,35 +87,11 @@ export const PlusList = ({
items = plusFeatureList,
...props
}: PlusListProps & WithClassNameProps): ReactElement => {
const { earlyAdopterPlanId } = usePaymentContext();
const isEarlyAdopterExperiment = !!earlyAdopterPlanId;

const list = useMemo(
() =>
items.filter(
(item) =>
isEarlyAdopterExperiment || item.status !== PlusItemStatus.ComingSoon,
),
[items, isEarlyAdopterExperiment],
);
const hasFilteredComingSoon =
!isEarlyAdopterExperiment && list.length !== items.length;

return (
<ul className={classNames('flex flex-col gap-0.5 py-6', className)}>
{list.map((item) => (
{items.map((item) => (
<PlusListItem key={item.label} item={item} {...props} />
))}
{/* On cleanup: remove this additional item if ComingSoon experiment won */}
{hasFilteredComingSoon && (
<PlusListItem
item={{
label: 'And so much more coming soon...',
status: PlusItemStatus.Ready,
}}
{...props}
/>
)}
</ul>
);
};
11 changes: 2 additions & 9 deletions packages/shared/src/components/plus/PlusListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
TypographyTag,
TypographyType,
} from '../typography/Typography';
import { usePaymentContext } from '../../contexts/PaymentContext';

export enum PlusItemStatus {
Ready = 'done',
Expand All @@ -40,11 +39,6 @@ export const PlusListItem = ({
item,
typographyProps,
}: PlusListItemProps): ReactElement => {
const { earlyAdopterPlanId } = usePaymentContext();
const isEarlyAdopterExperiment = !!earlyAdopterPlanId;
const isComingSoonVisible =
isEarlyAdopterExperiment && item.status === PlusItemStatus.ComingSoon;

return (
<ConditionalWrapper
condition={!!item.tooltip}
Expand Down Expand Up @@ -72,8 +66,7 @@ export const PlusListItem = ({
size={IconSize.XSmall}
{...iconProps}
className={classNames(
'mr-1 inline-block text-text-quaternary',
isComingSoonVisible && 'mt-px',
'mr-1 mt-px inline-block text-text-quaternary',
iconProps?.className,
)}
/>
Expand All @@ -88,7 +81,7 @@ export const PlusListItem = ({
)}
>
{item.label}
{isComingSoonVisible && (
{item.status === PlusItemStatus.ComingSoon && (
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption1}
Expand Down
16 changes: 16 additions & 0 deletions packages/shared/src/graphql/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,19 @@ export const CLEAR_IMAGE_MUTATION = gql`
export const clearImage = async (presets: string[]): Promise<void> => {
await gqlClient.request(CLEAR_IMAGE_MUTATION, { presets });
};

export const GET_PLUS_GIFTER_USER = gql`
query PlusGifterUser {
plusGifterUser {
id
name
image
username
}
}
`;
export const getPlusGifterUser = async (): Promise<UserShortProfile> => {
const res = await gqlClient.request(GET_PLUS_GIFTER_USER);

return res.plusGifterUser;
};
3 changes: 2 additions & 1 deletion packages/shared/src/hooks/useProfileForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
socialHandleRegex,
UPDATE_USER_PROFILE_MUTATION,
} from '../graphql/users';
import type { LoggedUser, UserProfile } from '../lib/user';
import type { LoggedUser, UserFlagsPublic, UserProfile } from '../lib/user';
import { useToastNotification } from './useToastNotification';
import type { ResponseError } from '../graphql/common';
import { errorMessage, gqlClient } from '../graphql/common';
Expand All @@ -33,6 +33,7 @@ export interface ProfileFormHint {
export interface UpdateProfileParameters extends Partial<UserProfile> {
image?: File;
onUpdateSuccess?: () => void;
flags?: UserFlagsPublic;
}

interface UseProfileForm {
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/lib/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ export const cloudinaryPWADesktopSafari =
export const clickbaitShieldModalImage =
'https://daily-now-res.cloudinary.com/image/upload/s--GWqpMG8r--/f_auto/v1732802237/Streak_together_with_a_friend_1_1_pwoill';

export const cloudinaryGiftedPlusModalImage = `https://daily-now-res.cloudinary.com/image/upload/s--JNm5gqXz--/f_auto/v1733838699/daily-dev-plus-gift_qosjrm`;

export const smallPostImage = (url: string): string => {
if (!url) {
return cloudinaryPostImageCoverPlaceholder;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/lib/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export enum RequestKey {
UserShortById = 'user_short_by_id',
BookmarkFolders = 'bookmark_folders',
FetchedOriginalTitle = 'fetched_original_title',
GifterUser = 'gifter_user',
}

export const getPostByIdKey = (id: string): QueryKey => [RequestKey.Post, id];
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/lib/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export interface UserShortProfile
topReader?: Partial<TopReader>;
}

export type UserFlagsPublic = Partial<{
showPlusGift: boolean;
}>;

export interface LoggedUser extends UserProfile, AnonymousUser {
image: string;
infoConfirmed?: boolean;
Expand All @@ -136,6 +140,7 @@ export interface LoggedUser extends UserProfile, AnonymousUser {
companies?: Company[];
contentPreference?: ContentPreference;
defaultFeedId?: string;
flags?: UserFlagsPublic;
}

interface BaseError {
Expand Down

0 comments on commit 83d4d11

Please sign in to comment.