diff --git a/fluxer_app/src/components/modals/tabs/developer_options_tab/AccountPremiumTab.tsx b/fluxer_app/src/components/modals/tabs/developer_options_tab/AccountPremiumTab.tsx index 61dc4642..3531eb9d 100644 --- a/fluxer_app/src/components/modals/tabs/developer_options_tab/AccountPremiumTab.tsx +++ b/fluxer_app/src/components/modals/tabs/developer_options_tab/AccountPremiumTab.tsx @@ -19,6 +19,7 @@ import * as DeveloperOptionsActionCreators from '@app/actions/DeveloperOptionsActionCreators'; import * as UserActionCreators from '@app/actions/UserActionCreators'; +import {Input} from '@app/components/form/Input'; import {Select} from '@app/components/form/Select'; import {Switch} from '@app/components/form/Switch'; import {SettingsTabSection} from '@app/components/modals/shared/SettingsTabLayout'; @@ -36,6 +37,7 @@ import type {MessageDescriptor} from '@lingui/core'; import {Trans, useLingui} from '@lingui/react/macro'; import {observer} from 'mobx-react-lite'; import type React from 'react'; +import {useCallback} from 'react'; interface AccountPremiumTabContentProps { user: UserRecord; @@ -132,6 +134,24 @@ export const AccountPremiumTabContent: React.FC = /> + Visionary Badge Override}> + ) => { + const value = event.target.value; + const parsed = value === '' ? null : Number.parseInt(value, 10); + DeveloperOptionsActionCreators.updateOption( + 'premiumLifetimeSequenceOverride', + parsed != null && !Number.isNaN(parsed) ? parsed : null, + ); + }, [])} + /> + + Premium Subscription Scenarios}> label={t`Test Subscription State`} diff --git a/fluxer_app/src/components/popouts/UserProfileBadges.module.css b/fluxer_app/src/components/popouts/UserProfileBadges.module.css index b23f5780..a249ead0 100644 --- a/fluxer_app/src/components/popouts/UserProfileBadges.module.css +++ b/fluxer_app/src/components/popouts/UserProfileBadges.module.css @@ -52,41 +52,37 @@ width: 20px; } -.virtualBadge { +.sequenceBadge { display: inline-flex; align-items: center; justify-content: center; - padding-top: 1px; + color: var(--brand-primary-light); font-weight: 700; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; - color: #4641d9; + font-family: var(--font-primary); line-height: 1; - letter-spacing: 0.05em; + font-variant-numeric: tabular-nums; white-space: nowrap; - text-decoration: none; - touch-action: manipulation; - user-select: none; - -webkit-user-select: none; - cursor: pointer; + pointer-events: none; + text-shadow: + 0 0 6px var(--glow-color), + 0 0 14px color-mix(in srgb, var(--glow-color) 50%, transparent); } -.virtualBadgeMobile { +:global(.theme-light) .sequenceBadge { + color: var(--brand-primary); + text-shadow: + 0 0 5px color-mix(in srgb, var(--glow-color) 60%, transparent), + 0 0 12px color-mix(in srgb, var(--glow-color) 30%, transparent); +} + +.sequenceBadgeMobile { height: 28px; - min-width: 28px; - font-size: 18px; - line-height: 24px; + font-size: 24px; } -.virtualBadgeDesktop { +.sequenceBadgeDesktop { height: 20px; - min-width: 20px; - font-size: 14px; - line-height: 20px; -} - -.virtualBadge:hover, -.virtualBadge:active { - text-decoration: none; + font-size: 17px; } .link { diff --git a/fluxer_app/src/components/popouts/UserProfileBadges.tsx b/fluxer_app/src/components/popouts/UserProfileBadges.tsx index 35b75ed7..e663c87a 100644 --- a/fluxer_app/src/components/popouts/UserProfileBadges.tsx +++ b/fluxer_app/src/components/popouts/UserProfileBadges.tsx @@ -33,12 +33,34 @@ import {observer} from 'mobx-react-lite'; import type React from 'react'; import {useMemo} from 'react'; -interface Badge { +interface BaseBadge { key: string; - iconUrl: string; tooltip: string; url: string; } +interface IconBadge extends BaseBadge { + type: 'icon'; + iconUrl: string; +} +interface TextBadge extends BaseBadge { + type: 'text'; + text: string; + glowColor: string; +} +type Badge = IconBadge | TextBadge; + +const GLOW_COLORS = [ + '#ff6b9d', // rose + '#c084fc', // violet + '#60a5fa', // sky blue + '#34d399', // emerald + '#fbbf24', // amber + '#f472b6', // pink + '#818cf8', // indigo + '#2dd4bf', // teal + '#fb923c', // orange + '#a78bfa', // purple +] as const; interface UserProfileBadgesProps { user: UserRecord; @@ -57,6 +79,7 @@ export const UserProfileBadges: React.FC = observer( if (user.flags & PublicUserFlags.STAFF) { result.push({ + type: 'icon', key: 'staff', iconUrl: cdnUrl('badges/staff.svg'), tooltip: t`Fluxer Staff`, @@ -66,6 +89,7 @@ export const UserProfileBadges: React.FC = observer( if (!selfHosted && user.flags & PublicUserFlags.CTP_MEMBER) { result.push({ + type: 'icon', key: 'ctp', iconUrl: cdnUrl('badges/ctp.svg'), tooltip: t`Fluxer Community Team`, @@ -75,6 +99,7 @@ export const UserProfileBadges: React.FC = observer( if (!selfHosted && user.flags & PublicUserFlags.PARTNER) { result.push({ + type: 'icon', key: 'partner', iconUrl: cdnUrl('badges/partner.svg'), tooltip: t`Fluxer Partner`, @@ -84,6 +109,7 @@ export const UserProfileBadges: React.FC = observer( if (!selfHosted && user.flags & PublicUserFlags.BUG_HUNTER) { result.push({ + type: 'icon', key: 'bug_hunter', iconUrl: cdnUrl('badges/bug-hunter.svg'), tooltip: t`Fluxer Bug Hunter`, @@ -109,15 +135,27 @@ export const UserProfileBadges: React.FC = observer( } result.push({ + type: 'icon', key: 'premium', iconUrl: cdnUrl('badges/plutonium.svg'), tooltip: tooltipText, url: badgeUrl, }); + + if (profile.premiumType === UserPremiumTypes.LIFETIME && profile.premiumLifetimeSequence != null) { + result.push({ + type: 'text', + key: 'premium_sequence', + text: `#${profile.premiumLifetimeSequence}`, + tooltip: t`Visionary ID #${profile.premiumLifetimeSequence}`, + url: badgeUrl, + glowColor: GLOW_COLORS[profile.premiumLifetimeSequence % GLOW_COLORS.length], + }); + } } return result; - }, [selfHosted, user.flags, profile?.premiumType, profile?.premiumSince]); + }, [selfHosted, user.flags, profile?.premiumType, profile?.premiumSince, profile?.premiumLifetimeSequence]); if (badges.length === 0) { return null; @@ -145,7 +183,19 @@ export const UserProfileBadges: React.FC = observer( return (
{badges.map((badge) => { - const badgeContent = {badge.tooltip}; + const sequenceClassName = isModal && isMobile ? styles.sequenceBadgeMobile : styles.sequenceBadgeDesktop; + const badgeContent = + badge.type === 'icon' ? ( + {badge.tooltip} + ) : ( + + ); return ( diff --git a/fluxer_app/src/records/UserRecord.tsx b/fluxer_app/src/records/UserRecord.tsx index f5eee953..338225d4 100644 --- a/fluxer_app/src/records/UserRecord.tsx +++ b/fluxer_app/src/records/UserRecord.tsx @@ -75,7 +75,7 @@ export class UserRecord { private readonly _premiumUntil?: Date | null; private readonly _premiumWillCancel?: boolean; private readonly _premiumBillingCycle?: string | null; - readonly premiumLifetimeSequence?: number | null; + private readonly _premiumLifetimeSequence?: number | null; readonly premiumBadgeHidden?: boolean; readonly premiumBadgeMasked?: boolean; readonly premiumBadgeTimestampHidden?: boolean; @@ -121,7 +121,7 @@ export class UserRecord { if ('premium_until' in user) this._premiumUntil = user.premium_until ? new Date(user.premium_until) : null; if ('premium_will_cancel' in user) this._premiumWillCancel = user.premium_will_cancel; if ('premium_billing_cycle' in user) this._premiumBillingCycle = user.premium_billing_cycle; - if ('premium_lifetime_sequence' in user) this.premiumLifetimeSequence = user.premium_lifetime_sequence; + if ('premium_lifetime_sequence' in user) this._premiumLifetimeSequence = user.premium_lifetime_sequence; if ('premium_badge_hidden' in user) this.premiumBadgeHidden = user.premium_badge_hidden; if ('premium_badge_masked' in user) this.premiumBadgeMasked = user.premium_badge_masked; if ('premium_badge_timestamp_hidden' in user) @@ -197,6 +197,11 @@ export class UserRecord { return override != null ? override : this._premiumBillingCycle; } + get premiumLifetimeSequence(): number | null | undefined { + const override = DeveloperOptionsStore.premiumLifetimeSequenceOverride; + return override != null ? override : this._premiumLifetimeSequence; + } + get premiumWillCancel(): boolean | undefined { const override = DeveloperOptionsStore.premiumWillCancelOverride; return override != null ? override : this._premiumWillCancel; @@ -339,8 +344,8 @@ export class UserRecord { ...(this._premiumBillingCycle !== undefined || updates.premium_billing_cycle !== undefined ? {premium_billing_cycle: updates.premium_billing_cycle ?? this._premiumBillingCycle} : {}), - ...(this.premiumLifetimeSequence !== undefined || updates.premium_lifetime_sequence !== undefined - ? {premium_lifetime_sequence: updates.premium_lifetime_sequence ?? this.premiumLifetimeSequence} + ...(this._premiumLifetimeSequence !== undefined || updates.premium_lifetime_sequence !== undefined + ? {premium_lifetime_sequence: updates.premium_lifetime_sequence ?? this._premiumLifetimeSequence} : {}), ...(this.premiumBadgeHidden !== undefined || updates.premium_badge_hidden !== undefined ? {premium_badge_hidden: updates.premium_badge_hidden ?? this.premiumBadgeHidden} @@ -520,7 +525,7 @@ export class UserRecord { this.premiumUntil?.getTime() === other.premiumUntil?.getTime() && this.premiumWillCancel === other.premiumWillCancel && this._premiumBillingCycle === other._premiumBillingCycle && - this.premiumLifetimeSequence === other.premiumLifetimeSequence && + this._premiumLifetimeSequence === other._premiumLifetimeSequence && this.premiumBadgeHidden === other.premiumBadgeHidden && this.premiumBadgeMasked === other.premiumBadgeMasked && this.premiumBadgeTimestampHidden === other.premiumBadgeTimestampHidden && @@ -582,7 +587,9 @@ export class UserRecord { ...(this._premiumUntil !== undefined ? {premium_until: normalizeDate(this._premiumUntil)} : {}), ...(this._premiumWillCancel !== undefined ? {premium_will_cancel: this._premiumWillCancel} : {}), ...(this._premiumBillingCycle !== undefined ? {premium_billing_cycle: this._premiumBillingCycle} : {}), - ...(this.premiumLifetimeSequence !== undefined ? {premium_lifetime_sequence: this.premiumLifetimeSequence} : {}), + ...(this._premiumLifetimeSequence !== undefined + ? {premium_lifetime_sequence: this._premiumLifetimeSequence} + : {}), ...(this.premiumBadgeHidden !== undefined ? {premium_badge_hidden: this.premiumBadgeHidden} : {}), ...(this.premiumBadgeMasked !== undefined ? {premium_badge_masked: this.premiumBadgeMasked} : {}), ...(this.premiumBadgeTimestampHidden !== undefined diff --git a/fluxer_app/src/stores/DeveloperOptionsStore.tsx b/fluxer_app/src/stores/DeveloperOptionsStore.tsx index 6822893f..ce45dd6a 100644 --- a/fluxer_app/src/stores/DeveloperOptionsStore.tsx +++ b/fluxer_app/src/stores/DeveloperOptionsStore.tsx @@ -47,6 +47,7 @@ export type DeveloperOptionsState = Readonly<{ forceShowVanityURLDisclaimer: boolean; forceShowVoiceConnection: boolean; premiumTypeOverride: number | null; + premiumLifetimeSequenceOverride: number | null; premiumSinceOverride: Date | null; premiumUntilOverride: Date | null; premiumBillingCycleOverride: string | null; @@ -122,6 +123,7 @@ class DeveloperOptionsStore implements DeveloperOptionsState { forceShowVanityURLDisclaimer = false; forceShowVoiceConnection = false; premiumTypeOverride: number | null = null; + premiumLifetimeSequenceOverride: number | null = null; premiumSinceOverride: Date | null = null; premiumUntilOverride: Date | null = null; premiumBillingCycleOverride: string | null = null; @@ -200,6 +202,7 @@ class DeveloperOptionsStore implements DeveloperOptionsState { 'forceShowVanityURLDisclaimer', 'forceShowVoiceConnection', 'premiumTypeOverride', + 'premiumLifetimeSequenceOverride', 'premiumSinceOverride', 'premiumUntilOverride', 'premiumBillingCycleOverride',