feat(premium): let self-hosted instances sell premium and gifts (#3025)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-28 21:21:51 +02:00
1 parent 192cec689a
commit 3736d94d73
159 files changed
+15802 -9067

No files matched your search

@@ -76,3 +76,11 @@
:global(:where(html.window-focused, html.unfocused-fully-interactive):not(.window-focus-activation-guard)) .link:hover {
text-decoration: none;
}
.linkButton {
padding: 0;
border: 0;
background: none;
font: inherit;
color: inherit;
}
@@ -4,6 +4,8 @@ import {Routes} from '@app/app/Routes';
import {PREMIUM_PRODUCT_FULL_NAME, PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {cdnUrl} from '@app/features/messaging/utils/MessagingUrlUtils';
import * as PremiumModalCommands from '@app/features/premium/commands/PremiumModalCommands';
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import {handleExternalLinkClick} from '@app/features/ui/utils/NativeUtils';
@@ -63,6 +65,7 @@ interface BaseBadge {
key: string;
tooltip: string;
url?: string;
onClick?: () => void;
}
interface IconBadge extends BaseBadge {
@@ -88,6 +91,8 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
({user, profile, isModal = false, isMobile = false}) => {
const {i18n} = useLingui();
const selfHosted = RuntimeConfig.isSelfHosted();
const showPremium = shouldShowPremiumFeatures();
const premiumInfoUrl = RuntimeConfig.premiumInfoUrl;
const badges = useMemo(() => {
const result: Array<Badge> = [];
if (user.flags & PublicUserFlags.STAFF) {
@@ -117,10 +122,11 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
url: Routes.bugs(),
});
}
if (!selfHosted && profile?.premiumType && profile.premiumType !== UserPremiumTypes.NONE) {
if (showPremium && profile?.premiumType && profile.premiumType !== UserPremiumTypes.NONE) {
let tooltipText = PREMIUM_PRODUCT_FULL_NAME;
let badgeUrl = Routes.plutonium();
if (profile.premiumType === UserPremiumTypes.LIFETIME) {
let badgeUrl: string | undefined = premiumInfoUrl ?? (selfHosted ? undefined : Routes.plutonium());
const badgeOnClick = badgeUrl ? undefined : () => PremiumModalCommands.open();
if (!selfHosted && profile.premiumType === UserPremiumTypes.LIFETIME) {
if (profile.premiumSince) {
const premiumSinceFormatted = DateUtils.getFormattedShortDate(profile.premiumSince);
tooltipText = i18n._(VISIONARY_SINCE_DESCRIPTOR, {productName: PRODUCT_NAME, premiumSinceFormatted});
@@ -141,8 +147,13 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
iconUrl: badgeAssetUrl('plutonium.svg'),
tooltip: tooltipText,
url: badgeUrl,
onClick: badgeOnClick,
});
if (profile.premiumType === UserPremiumTypes.LIFETIME && profile.premiumLifetimeSequence != null) {
if (
!selfHosted &&
profile.premiumType === UserPremiumTypes.LIFETIME &&
profile.premiumLifetimeSequence != null
) {
const visionaryIdLabel = `#${profile.premiumLifetimeSequence}`;
result.push({
type: 'text',
@@ -156,6 +167,8 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
return result;
}, [
selfHosted,
showPremium,
premiumInfoUrl,
user.flags,
profile?.premiumType,
profile?.premiumSince,
@@ -170,7 +183,23 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
: styles.containerPopout;
const badgeClassName = isModal && isMobile ? styles.badgeMobile : styles.badgeDesktop;
const isDesktopInteractions = !isMobile;
const renderInteractiveWrapper = (url: string | undefined, children: React.ReactNode) => {
const renderInteractiveWrapper = (
url: string | undefined,
onClick: (() => void) | undefined,
children: React.ReactNode,
) => {
if (onClick && isDesktopInteractions) {
return (
<button
type="button"
className={clsx(styles.link, styles.linkButton)}
onClick={onClick}
data-flx="user.user-profile-badges.render-interactive-wrapper.button"
>
{children}
</button>
);
}
if (url && isDesktopInteractions) {
return (
<a
@@ -215,7 +244,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
return (
<Tooltip key={badge.key} text={badge.tooltip} maxWidth="xl" data-flx="user.user-profile-badges.tooltip">
<FocusRing offset={-2} data-flx="user.user-profile-badges.focus-ring">
{renderInteractiveWrapper(badge.url, badgeContent)}
{renderInteractiveWrapper(badge.url, badge.onClick, badgeContent)}
</FocusRing>
</Tooltip>
);