Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions packages/shared/src/components/MainFeedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { buildPersonalizedCategories } from './feeds/exploreCategories';
import { useFeedTagsList } from '../hooks/useFeedTagsList';
import ReadingReminderHero from './marketing/banners/ReadingReminderHero';
import { WebappShortcutsRow } from '../features/shortcuts/components/WebappShortcutsRow';
import { InviteLedgerStrip } from '../features/inviteLedger/components/InviteLedgerStrip';
import { AskSearchBanner } from './marketing/banners/AskSearchBanner';
import AuthContext from '../contexts/AuthContext';
import type { LoggedUser } from '../lib/user';
Expand Down Expand Up @@ -717,6 +718,7 @@ export default function MainFeedLayout({
{!isExtension && isHomePage && (
<WebappShortcutsRow className="px-4 pb-2" />
)}
{isHomePage && <InviteLedgerStrip />}
{shouldUseCommentFeedLayout ? (
<CommentFeed
isMainFeed
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { SearchProvider } from '../contexts/search/SearchContext';
import { SpotlightProvider } from './spotlight/SpotlightContext';
import { SpotlightHost } from './spotlight/SpotlightHost';
import { FeedbackWidget } from './feedback';
import { InviteLedgerNavigator } from '../features/inviteLedger/components/InviteLedgerNavigator';
import { isExtension } from '../lib/func';

const GoBackHeaderMobile = dynamic(
Expand Down Expand Up @@ -226,6 +227,7 @@ function MainLayoutComponent({
{children}
</main>
{!hideFeedbackWidget && <FeedbackWidget />}
<InviteLedgerNavigator />
</div>
);
}
Expand Down
37 changes: 35 additions & 2 deletions packages/shared/src/components/modals/BootPopups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import { isNullOrUndefined } from '../../lib/func';
import useProfileForm from '../../hooks/useProfileForm';
import { useConditionalFeature } from '../../hooks/useConditionalFeature';
import { featureGenericReferralPopupV2 } from '../../lib/featureManagement';
import { useInviteLedgerEnabled } from '../../features/inviteLedger/useInviteLedgerEnabled';
import {
hasSeenInviteLedgerPromoThisSession,
isInviteLedgerPromoDismissed,
} from '../../features/inviteLedger/debug';

const REP_TRESHOLD = 250;

Expand Down Expand Up @@ -195,14 +200,16 @@ export const BootPopups = (): ReactElement => {
}
}, [marketingCtaPopoverSmall]);

const shouldShowGenericReferral = alerts?.showGenericReferral === true;
const isInviteLedgerEnabled = useInviteLedgerEnabled();
const shouldShowGenericReferral =
alerts?.showGenericReferral === true && !isInviteLedgerEnabled;
const { value: isGenericReferralV2 } = useConditionalFeature({
feature: featureGenericReferralPopupV2,
shouldEvaluate: shouldShowGenericReferral,
});

/** *
* Boot popup for generic referral campaign
* Boot popup for generic referral campaign (legacy — suppressed when invite ledger is on)
*/
useEffect(() => {
if (!shouldShowGenericReferral) {
Expand All @@ -222,6 +229,32 @@ export const BootPopups = (): ReactElement => {
});
}, [shouldShowGenericReferral, isGenericReferralV2, updateLastBootPopup]);

/**
* Boot popup for the Invite Ledger promo (replaces legacy referral popup when enabled).
* Shows once per session, suppressed forever after explicit "don't show again".
*/
useEffect(() => {
if (!isInviteLedgerEnabled) {
return;
}
if (isInviteLedgerPromoDismissed()) {
return;
}
if (hasSeenInviteLedgerPromoThisSession()) {
return;
}

addBootPopup({
type: LazyModal.InviteLedgerPromo,
props: {
onAfterOpen: () => {
updateLastBootPopup();
},
isDrawerOnMobile: true,
},
});
}, [isInviteLedgerEnabled, updateLastBootPopup]);

/**
* Streak recovery modal
*/
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 @@ -98,6 +98,13 @@ const GenericReferralModalV2 = dynamic(
),
);

const InviteLedgerPromoModal = dynamic(
() =>
import(
/* webpackChunkName: "inviteLedgerPromoModal" */ '../../features/inviteLedger/components/InviteLedgerPromoModal'
),
);

const NewStreakModal = dynamic(
() =>
import(/* webpackChunkName: "newStreakModal" */ './streaks/NewStreakModal'),
Expand Down Expand Up @@ -502,6 +509,7 @@ export const modals = {
[LazyModal.VerifySession]: VerifySession,
[LazyModal.GenericReferral]: GenericReferralModal,
[LazyModal.GenericReferralV2]: GenericReferralModalV2,
[LazyModal.InviteLedgerPromo]: InviteLedgerPromoModal,
[LazyModal.Video]: VideoModal,
[LazyModal.NewStreak]: NewStreakModal,
[LazyModal.ReputationPrivileges]: ReputationPrivilegesModal,
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 @@ -40,6 +40,7 @@ export enum LazyModal {
VerifySession = 'verifySession',
GenericReferral = 'genericReferral',
GenericReferralV2 = 'genericReferralV2',
InviteLedgerPromo = 'inviteLedgerPromo',
Video = 'video',
NewStreak = 'newStreak',
RecoverStreak = 'recoverStreak',
Expand Down
13 changes: 10 additions & 3 deletions packages/shared/src/components/profile/ProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { VerifiedCompanyUserBadge } from '../VerifiedCompanyUserBadge';
import { locationToString } from '../../lib/utils';
import { IconSize } from '../Icon';
import { fallbackImages } from '../../lib/config';
import { InviteLedgerCounter } from '../../features/inviteLedger/components/InviteLedgerCounter';

import { ElementPlaceholder } from '../ElementPlaceholder';

Expand Down Expand Up @@ -99,7 +100,7 @@ const ProfileHeader = ({
<div className="flex flex-col gap-2">
{bio && <Typography type={TypographyType.Body}>{bio}</Typography>}
<div className="flex items-center">
{user?.companies?.length > 0 && (
{(user?.companies?.length ?? 0) > 0 && (
<VerifiedCompanyUserBadge
size={ProfileImageSize.XSmall}
user={user}
Expand All @@ -110,7 +111,7 @@ const ProfileHeader = ({
}}
/>
)}
{user?.companies?.length > 0 && user?.location && (
{(user?.companies?.length ?? 0) > 0 && user?.location && (
<Separator className="text-text-secondary" />
)}
{user?.location && (
Expand All @@ -122,7 +123,7 @@ const ProfileHeader = ({
</Typography>
)}
</div>
<div className="flex items-center">
<div className="flex flex-wrap items-center gap-y-1">
<Typography
type={TypographyType.Subhead}
color={TypographyColor.Secondary}
Expand All @@ -135,6 +136,12 @@ const ProfileHeader = ({
date={new Date(user.createdAt)}
dateFormat="MMM d. yyyy"
/>
{isSameUser && (
<>
<Separator className="text-text-secondary" />
<InviteLedgerCounter />
</>
)}
</div>
{!isSameUser && (
<ProfileActions user={user} isPreviewMode={isPreviewMode} />
Expand Down
11 changes: 10 additions & 1 deletion packages/shared/src/components/profile/ProfileSettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { ProfileImageSize } from '../ProfilePicture';
import { useViewSize, ViewSize } from '../../hooks';
import { TypographyColor, TypographyType } from '../typography/Typography';
import { useHasAccessToCores } from '../../hooks/useCoresFeature';
import { useInviteLedgerEnabled } from '../../features/inviteLedger/useInviteLedgerEnabled';
import { useLazyModal } from '../../hooks/useLazyModal';
import { LazyModal } from '../modals/common/types';
import { useLogContext } from '../../contexts/LogContext';
Expand All @@ -84,6 +85,7 @@ const useAccountPageItems = ({ onClose }: { onClose?: () => void } = {}) => {
const { openModal } = useLazyModal();
const { logEvent } = useLogContext();
const { user } = useAuthContext();
const isInviteLedgerEnabled = useInviteLedgerEnabled();

const items = useMemo(
() =>
Expand Down Expand Up @@ -132,6 +134,13 @@ const useAccountPageItems = ({ onClose }: { onClose?: () => void } = {}) => {
icon: InviteIcon,
href: `${settingsUrl}/invite`,
},
...(isInviteLedgerEnabled && {
referrals: {
title: 'Referrals',
icon: InviteIcon,
href: `${settingsUrl}/referrals`,
},
}),
},
},
feed: {
Expand Down Expand Up @@ -337,7 +346,7 @@ const useAccountPageItems = ({ onClose }: { onClose?: () => void } = {}) => {
},
},
}),
[logEvent, onClose, openModal, user?.username],
[logEvent, onClose, openModal, user?.username, isInviteLedgerEnabled],
);

return { items };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { ReactElement } from 'react';
import React, { useContext } from 'react';
import classNames from 'classnames';
import Link from '../../../components/utilities/Link';
import { useLogContext } from '../../../contexts/LogContext';
import { LogEvent, TargetType } from '../../../lib/log';
import AuthContext from '../../../contexts/AuthContext';
import { useInviteLedgerEnabled } from '../useInviteLedgerEnabled';
import { useInviteLedger } from '../useInviteLedger';
import { formatStep, getCurrentInviteTier } from '../milestones';

/**
* The Stamp.
*
* A tiny monospace pill that lives next to the user's handle in their
* own profile header. Reads like a stamp on a field report:
*
* LEDGER №3 · 5 IN
*/
export const InviteLedgerCounter = (): ReactElement | null => {
const { user } = useContext(AuthContext);
const isEnabled = useInviteLedgerEnabled();
const ledger = useInviteLedger();
const { logEvent } = useLogContext();

if (!user?.id || !isEnabled) {
return null;
}

const tier = getCurrentInviteTier(ledger.invitesAccepted);

const handleClick = () => {
logEvent({
event_name: LogEvent.InviteLedgerCounterClick,
target_type: TargetType.InviteLedgerCounter,
extra: JSON.stringify({
invites: ledger.invitesAccepted,
tier: tier?.step ?? 0,
}),
});
};

return (
<Link href="/settings/referrals" passHref>
<a
href="/settings/referrals"
onClick={handleClick}
aria-label={
tier
? `Invite ledger: tier ${tier.step} (${tier.title}), ${ledger.invitesAccepted} bring-ins`
: `Invite ledger: ${ledger.invitesAccepted} bring-ins`
}
className={classNames(
'inline-flex items-center gap-1.5 rounded-6 border border-border-subtlest-secondary bg-surface-float px-1.5 py-0.5',
'font-mono uppercase tracking-[0.14em] text-text-secondary typo-caption2',
'transition-colors hover:border-border-subtlest-primary hover:bg-surface-hover hover:text-text-primary',
)}
>
<span className="font-semibold">Ledger</span>
{tier && (
<>
<span aria-hidden className="text-text-quaternary">
·
</span>
<span className="tabular-nums">№{formatStep(tier.step)}</span>
</>
)}
<span aria-hidden className="text-text-quaternary">
·
</span>
<span className="tabular-nums text-text-primary">
{ledger.invitesAccepted} in
</span>
</a>
</Link>
);
};
Loading
Loading