Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: gift subscription feedback #4160

Merged
merged 9 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/shared/src/components/ConditionalWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,14 @@ const ConditionalWrapper = ({
condition ? wrapper(children) : (children as ReactElement);

export default ConditionalWrapper;

export const ConditionalRender = ({
condition,
children,
}: Pick<ConditionalWrapperProps, 'condition' | 'children'>): ReactNode => {
if (!condition) {
return null;
}

return children;
};
Comment on lines +17 to +26
Copy link
Member Author

@sshanzel sshanzel Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to avoid the ugly ternary ladder, which adds indent and confusing conditions.

For example:

{!preselected ?
        (selected ? (
          <Component1 />
        ) : (
          <Component2 />
        )
      )
  : null
}

Instead, we can just do something similar to our ConditionalWrapper by doing:

<ConditionalRender condition={isTrue}>
  {
    selected ? (
      <Component1 />
    ) : (
      <Component2 />
    )
  }
</ConditionalRender>

This will keep the ternaries to a single level.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't really though right, it's just masking it.
Technically still read it as if...else inside if...else

Not against it, but not sure it's that much clearer.
@capJavert // @omBratteng what you guys think?

I'm happy to keep it though to unblock you.

Copy link
Member Author

@sshanzel sshanzel Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I just hate excessive indents and multiple ternaries at once, especially when combined together, as it would require you to focus deeply just to understand the simple if-else-if conditions. Also a simple misplacement of the parenthesis, the result would be totally different. Hence hard to follow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably more of a cosmetic than functional change for readability.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah agreed I'd love to avoid the nested ternaries in the first place.
But to me this is just a wrapper the result is still having the ternary.

One other solution is to render 2 functions and do the inside ternary inside 1 function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that can work too, by doing early returns. I'll be merging this now but I'll update and raise another PR once we get more answers on what could fit our current setup.

Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ 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;
Expand Down Expand Up @@ -89,7 +88,7 @@ export default function CommentActionButtons({
const { onMenuClick, isOpen, onHide } = useContextMenu({ id });
const { openModal } = useLazyModal();
const { displayToast } = useToastNotification();
const { isPlusAvailable } = usePaymentContext();
const { isValidRegion: isPlusAvailable } = useAuthContext();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see we actually already just re-export it lol.
Yeah this is better.

const { logSubscriptionEvent } = usePlusSubscription();
const [voteState, setVoteState] = useState<VoteEntityPayload>(() => {
return {
Expand Down
159 changes: 108 additions & 51 deletions packages/shared/src/components/plus/GiftPlusModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReactElement } from 'react';
import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import classNames from 'classnames';
import type { ModalProps } from '../modals/common/Modal';
import { Modal } from '../modals/common/Modal';
import {
Expand Down Expand Up @@ -29,12 +30,24 @@ import { ArrowKey, KeyboardCommand } from '../../lib/element';
import { GiftingSelectedUser } from './GiftingSelectedUser';
import Link from '../utilities/Link';
import { useViewSize, ViewSize } from '../../hooks';
import { Image } from '../image/Image';
import { fallbackImages } from '../../lib/config';
import { sizeClasses } from '../ProfilePicture';
import { ConditionalRender } from '../ConditionalWrapper';
import { Separator } from '../cards/common/common';
import { IconSize } from '../Icon';
import { ReputationUserBadge } from '../ReputationUserBadge';
import classed from '../../lib/classed';
import JoinedDate from '../profile/JoinedDate';
import { webappUrl } from '../../lib/constants';

interface GiftPlusModalProps extends ModalProps {
preselected?: UserShortProfile;
onSelected?: (user: UserShortProfile) => void;
}

const UserText = classed('span', 'flex flex-row items-center justify-center');

export function GiftPlusModalComponent({
preselected,
onSelected,
Expand All @@ -54,6 +67,8 @@ export function GiftPlusModalComponent({
query,
});

setIndex(0);

return result.recommendedMentions;
},
enabled: !!query?.length,
Expand All @@ -65,6 +80,10 @@ export function GiftPlusModalComponent({
return;
}

if (!users?.length) {
return;
}

e.preventDefault();

if (e.key === ArrowKey.Down) {
Expand Down Expand Up @@ -117,7 +136,7 @@ export function GiftPlusModalComponent({
overlayRef={setOverlay}
isDrawerOnMobile
>
<Modal.Body className="gap-4 tablet:!px-4">
<Modal.Body className="gap-4 tablet:!p-4">
<div className="flex flex-row justify-between">
<PlusTitle type={TypographyType.Callout} bold />
<CloseButton
Expand All @@ -126,61 +145,99 @@ export function GiftPlusModalComponent({
onClick={onRequestClose}
/>
</div>
<Typography bold type={TypographyType.Title1}>
<Typography bold type={TypographyType.Title1} className="text-center">
Gift daily.dev Plus 🎁
</Typography>
{selected ? (
<GiftingSelectedUser
user={selected}
onClose={() => setSelected(null)}
<div className="flex flex-col items-center gap-2">
<Image
src={selected?.image ?? fallbackImages.avatar}
className={classNames(sizeClasses.xxxxlarge, 'rounded-26')}
/>
) : (
<div className="flex flex-col">
<BaseTooltip
appendTo={isTablet ? overlay : globalThis?.document?.body}
onClickOutside={() => setQuery('')}
visible={isVisible}
showArrow={false}
interactive
content={
<RecommendedMention
className="w-[24rem]"
users={users}
selected={index}
onClick={onSelect}
checkIsDisabled={(user) => user.isPlus}
disabledTooltip="This user already has daily.dev Plus"
{preselected && (
<>
<UserText>
<Typography bold type={TypographyType.Title3}>
{preselected.name}
</Typography>
<ReputationUserBadge
className="ml-0.5 !typo-footnote"
user={{ reputation: preselected.reputation }}
iconProps={{ size: IconSize.XSmall }}
disableTooltip
/>
}
container={{
className: 'shadow',
paddingClassName: 'p-0',
roundedClassName: 'rounded-16',
bgClassName: 'bg-accent-pepper-subtlest',
}}
>
<TextField
leftIcon={<UserIcon />}
inputId="search_user"
fieldType="tertiary"
autoComplete="off"
label="Select a recipient by name or handle"
onKeyDown={onKeyDown}
onChange={(e) => onSearch(e.currentTarget.value.trim())}
onFocus={(e) => setQuery(e.currentTarget.value.trim())}
/>
</BaseTooltip>
</div>
)}
<div className="flex w-full flex-row items-center gap-2 rounded-10 bg-surface-float p-2">
</UserText>
<UserText>
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Secondary}
>
@{preselected.username}
</Typography>
<Separator />
<JoinedDate
className="text-text-quaternary typo-caption2"
date={new Date(preselected.createdAt)}
/>
</UserText>
</>
)}
</div>
<ConditionalRender condition={!preselected}>
{selected ? (
<GiftingSelectedUser
user={selected}
onClose={() => setSelected(null)}
/>
) : (
<div className="flex flex-col">
<BaseTooltip
appendTo={isTablet ? overlay : globalThis?.document?.body}
onClickOutside={() => setQuery('')}
visible={isVisible}
showArrow={false}
interactive
content={
<RecommendedMention
className="w-[24rem]"
users={users}
selected={index}
onClick={onSelect}
checkIsDisabled={(user) => user.isPlus}
disabledTooltip="This user already has daily.dev Plus"
/>
}
container={{
className: 'shadow',
paddingClassName: 'p-0',
roundedClassName: 'rounded-16',
bgClassName: 'bg-accent-pepper-subtlest',
}}
>
<TextField
leftIcon={<UserIcon />}
inputId="search_user"
fieldType="tertiary"
autoComplete="off"
label="Select a recipient by name or handle"
onKeyDown={onKeyDown}
onChange={(e) => onSearch(e.currentTarget.value.trim())}
onFocus={(e) => setQuery(e.currentTarget.value.trim())}
/>
</BaseTooltip>
</div>
)}
</ConditionalRender>
<div className="flex w-full flex-row items-center gap-2 rounded-10 bg-surface-float p-2 py-3">
<Typography bold type={TypographyType.Callout}>
One-year plan
</Typography>
<PlusPlanExtraLabel
color={PlusLabelColor.Success}
label={giftOneYear?.extraLabel}
typographyProps={{ color: TypographyColor.StatusSuccess }}
/>
{giftOneYear?.extraLabel && (
<PlusPlanExtraLabel
color={PlusLabelColor.Success}
label={giftOneYear?.extraLabel}
typographyProps={{ color: TypographyColor.StatusSuccess }}
/>
)}
<Typography type={TypographyType.Body} className="ml-auto mr-1">
<strong className="mr-1">{giftOneYear?.price}</strong>
{giftOneYear?.currencyCode}
Expand All @@ -191,12 +248,12 @@ export function GiftPlusModalComponent({
payment is processed, they’ll be notified of your gift. This is a
one-time purchase, not a recurring subscription.
</Typography>
<Link href={`/plus?gift=${selected?.id}`} passHref>
<Link href={`${webappUrl}plus?gift=${selected?.id}`} passHref>
<Button
tag="a"
disabled={!selected}
variant={ButtonVariant.Primary}
onClick={() => onSelected(selected)}
onClick={() => onSelected?.(selected)}
>
Gift & Pay {giftOneYear?.price}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ProfileImageSize, ProfilePicture } from '../ProfilePicture';
import Link from '../utilities/Link';
import { useAuthContext } from '../../contexts/AuthContext';
import { Loader } from '../Loader';
import { webappUrl } from '../../lib/constants';

const GifterProfile = ({ gifter }: { gifter: UserShortProfile }) => (
<Link href={`/${gifter.username}`} passHref>
Expand Down Expand Up @@ -104,7 +105,7 @@ export function GiftReceivedPlusModal(props: ModalProps): ReactElement {
</div>
<Button
className="mt-4 w-full"
href="/squads/plus"
href={`${webappUrl}squads/plus`}
tag="a"
variant={ButtonVariant.Primary}
>{`See what's inside`}</Button>
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/components/plus/PlusTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function PlusTitle(
{...typography}
className={classnames('flex flex-row items-center gap-0.5', className)}
>
<DevPlusIcon size={IconSize.Size16} /> Plus
<DevPlusIcon size={IconSize.XSmall} /> Plus
</Typography>
);
}
5 changes: 2 additions & 3 deletions packages/shared/src/components/squads/SquadMemberMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ReactElement } from 'react';
import React, { useContext, useMemo } from 'react';
import AuthContext from '../../contexts/AuthContext';
import AuthContext, { useAuthContext } from '../../contexts/AuthContext';
import { reportSquadMember } from '../../lib/constants';
import { IconSize } from '../Icon';
import type { SourceMember, Squad } from '../../graphql/sources';
Expand All @@ -19,7 +19,6 @@ 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<UseSquadActions, 'onUpdateRole'> {
squad: Squad;
Expand Down Expand Up @@ -129,7 +128,7 @@ export default function SquadMemberMenu({
const { user } = useContext(AuthContext);
const { showPrompt } = usePrompt();
const { displayToast } = useToastNotification();
const { isPlusAvailable } = usePaymentContext();
const { isValidRegion: isPlusAvailable } = useAuthContext();
const { logSubscriptionEvent } = usePlusSubscription();
const onUpdateMember = async (
role: SourceMemberRole,
Expand Down
3 changes: 1 addition & 2 deletions packages/webapp/pages/account/invite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ 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';
Expand All @@ -66,7 +65,7 @@ const AccountInvitePage = (): ReactElement => {
const { url, referredUsersCount } = useReferralCampaign({
campaignKey: ReferralCampaignKey.Generic,
});
const { isPlusAvailable } = usePaymentContext();
const { isValidRegion: isPlusAvailable } = useAuthContext();
const { logSubscriptionEvent } = usePlusSubscription();
const { logEvent } = useLogContext();
const inviteLink = url || link.referral.defaultUrl;
Expand Down