initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.sliderContainer {
margin-left: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.sliderLabel {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.labelText {
display: block;
font-weight: 500;
font-size: 0.875rem;
}
.labelDescription {
margin-bottom: 0.5rem;
color: var(--text-primary-muted);
font-size: 0.875rem;
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Plural, Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import * as DeveloperOptionsActionCreators from '~/actions/DeveloperOptionsActionCreators';
import * as UserActionCreators from '~/actions/UserActionCreators';
import {UserPremiumTypes} from '~/Constants';
import {Select} from '~/components/form/Select';
import {Switch} from '~/components/form/Switch';
import {SettingsTabSection} from '~/components/modals/shared/SettingsTabLayout';
import {Slider} from '~/components/uikit/Slider';
import type {UserRecord} from '~/records/UserRecord';
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
import styles from './AccountPremiumTab.module.css';
import {
applyPremiumScenarioOption,
PREMIUM_SCENARIO_OPTIONS,
type PremiumScenarioOption,
} from './premiumScenarioOptions';
interface AccountPremiumTabContentProps {
user: UserRecord;
}
export const AccountPremiumTabContent: React.FC<AccountPremiumTabContentProps> = observer(({user}) => {
const {t} = useLingui();
const premiumTypeOptions = [
{value: '', label: t`Use Actual Premium Type`},
{value: UserPremiumTypes.NONE.toString(), label: t`None`},
{value: UserPremiumTypes.SUBSCRIPTION.toString(), label: t`Subscription`},
{value: UserPremiumTypes.LIFETIME.toString(), label: t`Lifetime`},
];
return (
<>
<SettingsTabSection title={<Trans>Account State Overrides</Trans>}>
<Switch
label={t`Email Verified Override`}
value={DeveloperOptionsStore.emailVerifiedOverride ?? false}
description={t`Override email verification status`}
onChange={(value) =>
DeveloperOptionsActionCreators.updateOption('emailVerifiedOverride', value ? true : null)
}
/>
<Switch
label={t`Unclaimed Account Override`}
value={DeveloperOptionsStore.unclaimedAccountOverride ?? false}
description={t`Override unclaimed account status`}
onChange={(value) =>
DeveloperOptionsActionCreators.updateOption('unclaimedAccountOverride', value ? true : null)
}
/>
<Switch
label={t`Unread Gift Inventory Override`}
value={DeveloperOptionsStore.hasUnreadGiftInventoryOverride ?? false}
description={t`Override unread gift inventory status`}
onChange={(value) => {
DeveloperOptionsActionCreators.updateOption('hasUnreadGiftInventoryOverride', value ? true : null);
if (!value) DeveloperOptionsActionCreators.updateOption('unreadGiftInventoryCountOverride', null);
}}
/>
{DeveloperOptionsStore.hasUnreadGiftInventoryOverride && (
<div className={styles.sliderContainer}>
<div className={styles.sliderLabel}>
<span className={styles.labelText}>
<Trans>Unread Gift Count</Trans>
</span>
<p className={styles.labelDescription}>
<Trans>Set the number of unread gifts in inventory.</Trans>
</p>
</div>
<Slider
defaultValue={DeveloperOptionsStore.unreadGiftInventoryCountOverride ?? 1}
factoryDefaultValue={1}
minValue={0}
maxValue={99}
step={1}
markers={[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 99]}
stickToMarkers={false}
onMarkerRender={(v) => `${v}`}
onValueRender={(v) => <Plural value={v} one="# gift" other="# gifts" />}
onValueChange={(v) => DeveloperOptionsActionCreators.updateOption('unreadGiftInventoryCountOverride', v)}
/>
</div>
)}
</SettingsTabSection>
<SettingsTabSection title={<Trans>Premium Type Override</Trans>}>
<Select
label={t`Override Premium Type`}
value={DeveloperOptionsStore.premiumTypeOverride?.toString() ?? ''}
options={premiumTypeOptions}
onChange={(value) => {
const premiumTypeOverride = value === '' ? null : Number.parseInt(value, 10);
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', premiumTypeOverride);
}}
/>
<Switch
label={t`Backend Premium Override`}
value={user.premiumEnabledOverride ?? false}
description={t`Toggle premium_enabled_override on the backend`}
onChange={async (value) => {
await UserActionCreators.update({premium_enabled_override: value});
}}
/>
<Switch
label={t`Has Ever Purchased Override`}
value={DeveloperOptionsStore.hasEverPurchasedOverride ?? false}
description={t`Simulates having a Stripe customer ID (for testing purchase history access)`}
onChange={(value) =>
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', value ? true : null)
}
/>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Premium Subscription Scenarios</Trans>}>
<Select<PremiumScenarioOption>
label={t`Test Subscription State`}
value="none"
options={PREMIUM_SCENARIO_OPTIONS.map(({value, label}) => ({
value,
label: t(label),
}))}
onChange={applyPremiumScenarioOption}
/>
</SettingsTabSection>
</>
);
});

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.toggleGroup {
border-top: 1px solid var(--background-header-secondary);
padding-top: 1rem;
}
.toggleGroupFirst {
padding-top: 0;
border-top: none;
}
.groupTitle {
margin-top: 0.25rem;
margin-bottom: 0.75rem;
font-weight: 600;
font-size: 0.75rem;
color: var(--text-tertiary-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.toggleList {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.buttonGroup {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import * as DeveloperOptionsActionCreators from '~/actions/DeveloperOptionsActionCreators';
import {Switch} from '~/components/form/Switch';
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
import styles from './GeneralTab.module.css';
import {getToggleGroups} from './shared';
export const GeneralTabContent: React.FC = observer(() => {
const {t} = useLingui();
const toggleGroups = getToggleGroups();
return (
<>
{toggleGroups.map((group, gi) => (
<div
key={group.title.id ?? `toggle-group-${gi}`}
className={gi > 0 ? styles.toggleGroup : styles.toggleGroupFirst}
>
<div className={styles.groupTitle}>{t(group.title)}</div>
<div className={styles.toggleList}>
{group.items.map(({key, label, description}) => (
<Switch
key={String(key)}
label={t(label)}
description={description ? t(description) : undefined}
value={Boolean(DeveloperOptionsStore[key])}
onChange={(value) => {
DeveloperOptionsActionCreators.updateOption(key, value);
if (key === 'selfHostedModeOverride') {
window.location.reload();
}
}}
/>
))}
</div>
</div>
))}
</>
);
});

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.container {
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {SettingsSection} from '~/components/modals/shared/SettingsSection';
import ConnectionStore from '~/stores/ConnectionStore';
import NagbarStore from '~/stores/NagbarStore';
import UserStore from '~/stores/UserStore';
import {AccountPremiumTabContent} from './AccountPremiumTab';
import {GeneralTabContent} from './GeneralTab';
import styles from './Inline.module.css';
import {MockingTabContent} from './MockingTab';
import {NagbarsTabContent} from './NagbarsTab';
import {ToolsTabContent} from './ToolsTab';
export const DeveloperOptionsInlineTab: React.FC = observer(() => {
const {t} = useLingui();
const socket = ConnectionStore.socket;
const nagbarState = NagbarStore;
const user = UserStore.currentUser;
if (!(user && socket)) return null;
return (
<div className={styles.container}>
<SettingsSection id="dev-general" title={t`General`}>
<GeneralTabContent />
</SettingsSection>
<SettingsSection id="dev-account-premium" title={t`Account & Premium`}>
<AccountPremiumTabContent user={user} />
</SettingsSection>
<SettingsSection id="dev-mocking" title={t`Mocking`}>
<MockingTabContent />
</SettingsSection>
<SettingsSection id="dev-nagbars" title={t`Nagbars`}>
<NagbarsTabContent nagbarState={nagbarState} />
</SettingsSection>
<SettingsSection id="dev-tools" title={t`Tools`}>
<ToolsTabContent socket={socket} />
</SettingsSection>
</div>
);
});

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.headerTitle {
margin-bottom: 0;
font-weight: 600;
font-size: 1rem;
}
.sliderContainer {
margin-left: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.sliderLabel {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.labelText {
display: block;
font-weight: 500;
font-size: 0.875rem;
}
.labelDescription {
margin-bottom: 0.5rem;
color: var(--text-primary-muted);
font-size: 0.875rem;
}
.note {
color: var(--text-primary-muted);
font-size: 0.75rem;
}
.buttonRow {
display: flex;
align-items: flex-start;
}

View File

@@ -0,0 +1,388 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import * as DeveloperOptionsActionCreators from '~/actions/DeveloperOptionsActionCreators';
import * as ModalActionCreators from '~/actions/ModalActionCreators';
import {modal} from '~/actions/ModalActionCreators';
import {Select} from '~/components/form/Select';
import {Switch} from '~/components/form/Switch';
import RequiredActionModal from '~/components/modals/RequiredActionModal';
import {SettingsTabSection} from '~/components/modals/shared/SettingsTabLayout';
import {Button} from '~/components/uikit/Button/Button';
import {Slider} from '~/components/uikit/Slider';
import type {DeveloperOptionsState} from '~/stores/DeveloperOptionsStore';
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
import UserStore from '~/stores/UserStore';
import styles from './MockingTab.module.css';
export const MockingTabContent: React.FC = observer(() => {
const {t} = useLingui();
const handleClearAttachmentMocks = () => {
DeveloperOptionsActionCreators.clearAllAttachmentMocks();
};
return (
<>
<SettingsTabSection title={<Trans>Verification Barriers</Trans>}>
<Select
label={t`Mock Verification Barrier`}
value={DeveloperOptionsStore.mockVerificationBarrier}
options={[
{value: 'none', label: t`None (Normal Behavior)`},
{value: 'unclaimed_account', label: t`Unclaimed Account`},
{value: 'unverified_email', label: t`Unverified Email`},
{value: 'account_too_new', label: t`Account Too New`},
{value: 'not_member_long', label: t`Not Member Long Enough`},
{value: 'no_phone', label: t`No Phone Number`},
{value: 'send_message_disabled', label: t`Send Message Disabled`},
]}
onChange={(value) => {
DeveloperOptionsActionCreators.updateOption(
'mockVerificationBarrier',
value as DeveloperOptionsState['mockVerificationBarrier'],
);
}}
/>
{(DeveloperOptionsStore.mockVerificationBarrier === 'account_too_new' ||
DeveloperOptionsStore.mockVerificationBarrier === 'not_member_long') && (
<Select<string>
label={t`Countdown Timer`}
value={DeveloperOptionsStore.mockBarrierTimeRemaining?.toString() ?? '300000'}
options={[
{value: '0', label: t`No Timer`},
{value: '10000', label: t`10 seconds`},
{value: '30000', label: t`30 seconds`},
{value: '60000', label: t`1 minute`},
{value: '120000', label: t`2 minutes`},
{value: '300000', label: t`5 minutes`},
{value: '600000', label: t`10 minutes`},
]}
onChange={(v) =>
DeveloperOptionsActionCreators.updateOption('mockBarrierTimeRemaining', Number.parseInt(v, 10))
}
/>
)}
</SettingsTabSection>
<SettingsTabSection title={<Trans>Channel Permissions</Trans>}>
<Switch
label={t`Force No SEND_MESSAGES Permission`}
value={DeveloperOptionsStore.forceNoSendMessages}
description={t`Removes SEND_MESSAGES permission in all channels (disables textarea and all buttons)`}
onChange={(value) => DeveloperOptionsActionCreators.updateOption('forceNoSendMessages', value)}
/>
<Switch
label={t`Force No ATTACH_FILES Permission`}
value={DeveloperOptionsStore.forceNoAttachFiles}
description={t`Removes ATTACH_FILES permission in all channels (hides upload button)`}
onChange={(value) => DeveloperOptionsActionCreators.updateOption('forceNoAttachFiles', value)}
/>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Attachments</Trans>}>
<Button variant="secondary" onClick={handleClearAttachmentMocks}>
<Trans>Clear attachment mocks</Trans>
</Button>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Voice Calls</Trans>}>
<Button
variant="secondary"
onClick={() => DeveloperOptionsActionCreators.triggerMockIncomingCall()}
disabled={!UserStore.currentUser}
>
<Trans>Trigger Mock Incoming Call</Trans>
</Button>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Slowmode</Trans>}>
<Switch
label={t`Force Slowmode Active`}
value={DeveloperOptionsStore.mockSlowmodeActive}
description={t`Forces slowmode to be active in all channels`}
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockSlowmodeActive', value)}
/>
{DeveloperOptionsStore.mockSlowmodeActive && (
<div className={styles.sliderContainer}>
<div className={styles.sliderLabel}>
<span className={styles.labelText}>
<Trans>Slowmode Time Remaining (ms)</Trans>
</span>
<p className={styles.labelDescription}>
<Trans>Set how much time remains before the user can send another message.</Trans>
</p>
</div>
<Slider
defaultValue={DeveloperOptionsStore.mockSlowmodeRemaining}
factoryDefaultValue={10000}
minValue={0}
maxValue={60000}
step={1000}
markers={[0, 5000, 10000, 15000, 20000, 30000, 45000, 60000]}
stickToMarkers={false}
onMarkerRender={(v) => `${v / 1000}s`}
onValueRender={(v) => <Trans>{Math.floor(v / 1000)} seconds</Trans>}
onValueChange={(v) => DeveloperOptionsActionCreators.updateOption('mockSlowmodeRemaining', v)}
/>
</div>
)}
</SettingsTabSection>
<SettingsTabSection
title={
<div className={styles.header}>
<h3 className={styles.headerTitle}>
<Trans>Visionary</Trans>
</h3>
<Button
variant="secondary"
small={true}
onClick={() => {
DeveloperOptionsActionCreators.updateOption('mockVisionarySoldOut', false);
DeveloperOptionsActionCreators.updateOption('mockVisionaryRemaining', null);
}}
>
<Trans>Reset to Real Data</Trans>
</Button>
</div>
}
>
<Switch
label={t`Mock Visionary Sold Out`}
value={DeveloperOptionsStore.mockVisionarySoldOut}
description={t`Force Visionary to appear sold out`}
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockVisionarySoldOut', value)}
/>
{!DeveloperOptionsStore.mockVisionarySoldOut && (
<div className={styles.sliderContainer}>
<div className={styles.sliderLabel}>
<span className={styles.labelText}>
<Trans>Mock Visionary Remaining</Trans>
</span>
<p className={styles.labelDescription}>
<Trans>Set a fixed remaining count for Visionary availability.</Trans>
</p>
</div>
<Slider
defaultValue={DeveloperOptionsStore.mockVisionaryRemaining ?? 100}
factoryDefaultValue={100}
minValue={0}
maxValue={1000}
step={10}
markers={[0, 50, 100, 250, 500, 750, 1000]}
stickToMarkers={false}
onMarkerRender={(v) => `${v}`}
onValueRender={(v) => <Trans>{v} remaining</Trans>}
onValueChange={(v) => DeveloperOptionsActionCreators.updateOption('mockVisionaryRemaining', v)}
/>
<div className={styles.note}>
<Trans>Set to 0 to simulate sold out.</Trans>
</div>
</div>
)}
</SettingsTabSection>
<SettingsTabSection title={<Trans>Required Actions</Trans>}>
<Select
label={t`Mock Variant`}
value={DeveloperOptionsStore.mockRequiredActionsMode}
description={t`Choose which variant to preview`}
options={[
{value: 'email', label: t`Email`},
{value: 'phone', label: t`Phone`},
{value: 'email_or_phone', label: t`Email or Phone`},
]}
onChange={(value) =>
DeveloperOptionsActionCreators.updateOption(
'mockRequiredActionsMode',
value as DeveloperOptionsState['mockRequiredActionsMode'],
)
}
/>
<div className={styles.buttonRow}>
<Button
onClick={() =>
ModalActionCreators.pushWithKey(
modal(() => <RequiredActionModal mock={true} />),
'required-actions-mock',
)
}
disabled={!UserStore.currentUser}
>
<Trans>Open Overlay</Trans>
</Button>
</div>
{DeveloperOptionsStore.mockRequiredActionsMode === 'email_or_phone' && (
<Select
label={t`Default Tab`}
value={DeveloperOptionsStore.mockRequiredActionsSelectedTab}
description={t`Which tab is selected when the overlay opens`}
options={[
{value: 'email', label: t`Email`},
{value: 'phone', label: t`Phone`},
]}
onChange={(value) =>
DeveloperOptionsActionCreators.updateOption(
'mockRequiredActionsSelectedTab',
value as DeveloperOptionsState['mockRequiredActionsSelectedTab'],
)
}
/>
)}
{(DeveloperOptionsStore.mockRequiredActionsMode === 'phone' ||
(DeveloperOptionsStore.mockRequiredActionsMode === 'email_or_phone' &&
DeveloperOptionsStore.mockRequiredActionsSelectedTab === 'phone')) && (
<Select
label={t`Phone Step`}
value={DeveloperOptionsStore.mockRequiredActionsPhoneStep}
description={t`Pick which phone step to show`}
options={[
{value: 'phone', label: t`Enter Phone`},
{value: 'code', label: t`Enter Code`},
]}
onChange={(value) =>
DeveloperOptionsActionCreators.updateOption(
'mockRequiredActionsPhoneStep',
value as DeveloperOptionsState['mockRequiredActionsPhoneStep'],
)
}
/>
)}
<Switch
label={t`Use Reverification Text`}
value={DeveloperOptionsStore.mockRequiredActionsReverify}
description={t`Swap text to reverify variants`}
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockRequiredActionsReverify', value)}
/>
<Switch
label={t`Resend Button Loading`}
value={DeveloperOptionsStore.mockRequiredActionsResending}
description={t`Force the email resend button into a loading state`}
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockRequiredActionsResending', value)}
/>
<Select
label={t`Resend Outcome`}
value={DeveloperOptionsStore.mockRequiredActionsResendOutcome}
description={t`Toast shown when clicking resend in mock mode`}
options={[
{value: 'success', label: t`Success`},
{value: 'rate_limited', label: t`Rate Limited`},
{value: 'server_error', label: t`Server Error`},
]}
onChange={(value) =>
DeveloperOptionsActionCreators.updateOption(
'mockRequiredActionsResendOutcome',
value as DeveloperOptionsState['mockRequiredActionsResendOutcome'],
)
}
/>
</SettingsTabSection>
<SettingsTabSection title={<Trans>NSFW Gates</Trans>}>
<Select
label={t`Mock NSFW Channel Gate Reason`}
value={DeveloperOptionsStore.mockNSFWGateReason}
description={t`Mock gate reason for blocking entire NSFW channels`}
options={[
{value: 'none', label: t`None (Normal Behavior)`},
{value: 'geo_restricted', label: t`Geo Restricted`},
{value: 'age_restricted', label: t`Age Restricted`},
{value: 'consent_required', label: t`Consent Required`},
]}
onChange={(value) => {
DeveloperOptionsActionCreators.updateOption(
'mockNSFWGateReason',
value as DeveloperOptionsState['mockNSFWGateReason'],
);
}}
/>
<Select
label={t`Mock NSFW Media Gate Reason`}
value={DeveloperOptionsStore.mockNSFWMediaGateReason}
description={t`Forces all media to NSFW and shows the selected blur overlay error state`}
options={[
{value: 'none', label: t`None (Normal Behavior)`},
{value: 'geo_restricted', label: t`Geo Restricted`},
{value: 'age_restricted', label: t`Age Restricted`},
]}
onChange={(value) => {
DeveloperOptionsActionCreators.updateOption(
'mockNSFWMediaGateReason',
value as DeveloperOptionsState['mockNSFWMediaGateReason'],
);
}}
/>
<Switch
label={t`Mock Geo Block Overlay`}
value={DeveloperOptionsStore.mockGeoBlocked}
description={t`Show the geo-blocked overlay (dismissible)`}
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockGeoBlocked', value)}
/>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Gift Inventory</Trans>}>
<Switch
label={t`Mock Gift Inventory`}
value={DeveloperOptionsStore.mockGiftInventory ?? false}
description={t`Show a mock gift code in your gift inventory for testing`}
onChange={(value) => {
DeveloperOptionsActionCreators.updateOption('mockGiftInventory', value ? true : null);
if (!value) {
DeveloperOptionsActionCreators.updateOption('mockGiftDurationMonths', 12);
DeveloperOptionsActionCreators.updateOption('mockGiftRedeemed', null);
}
}}
/>
{DeveloperOptionsStore.mockGiftInventory && (
<>
<Select<string>
label={t`Gift Duration`}
value={DeveloperOptionsStore.mockGiftDurationMonths?.toString() ?? '12'}
description={t`How many months of Plutonium the mock gift provides`}
options={[
{value: '1', label: t`1 Month`},
{value: '3', label: t`3 Months`},
{value: '6', label: t`6 Months`},
{value: '12', label: t`12 Months (1 Year)`},
{value: '0', label: t`Lifetime`},
]}
onChange={(value) =>
DeveloperOptionsActionCreators.updateOption('mockGiftDurationMonths', Number.parseInt(value, 10))
}
/>
<Switch
label={t`Mark as Redeemed`}
value={DeveloperOptionsStore.mockGiftRedeemed ?? false}
description={t`Show the gift as already redeemed`}
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockGiftRedeemed', value ? true : null)}
/>
</>
)}
</SettingsTabSection>
</>
);
});

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.nagbarList {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.nagbarItem {
display: flex;
align-items: center;
justify-content: space-between;
}
.nagbarInfo {
display: flex;
flex-direction: column;
}
.nagbarLabel {
font-size: 0.875rem;
}
.nagbarStatus {
color: var(--text-tertiary);
font-size: 0.75rem;
}
.buttonGroup {
display: flex;
gap: 0.5rem;
}
.footer {
display: flex;
flex-wrap: wrap;
}
.footer > * {
flex: 1;
min-width: fit-content;
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import * as NagbarActionCreators from '~/actions/NagbarActionCreators';
import {Button} from '~/components/uikit/Button/Button';
import type {NagbarStore, NagbarToggleKey} from '~/stores/NagbarStore';
import styles from './NagbarsTab.module.css';
import {getNagbarControls, type NagbarControlDefinition} from './nagbarControls';
interface NagbarsTabContentProps {
nagbarState: NagbarStore;
}
const NagbarRow: React.FC<{
control: NagbarControlDefinition;
nagbarState: NagbarStore;
}> = observer(({control, nagbarState}) => {
const {t} = useLingui();
const getFlag = (key: NagbarToggleKey): boolean => Boolean(nagbarState[key]);
const useActualDisabled = control.useActualDisabled?.(nagbarState) ?? control.resetKeys.every((key) => !getFlag(key));
const forceShowDisabled = control.forceShowDisabled?.(nagbarState) ?? getFlag(control.forceKey);
const forceHideDisabled = control.forceHideDisabled?.(nagbarState) ?? getFlag(control.forceHideKey);
const handleUseActual = () => {
control.resetKeys.forEach((key) => NagbarActionCreators.resetNagbar(key));
NagbarActionCreators.setForceHideNagbar(control.forceHideKey, false);
};
const handleForceShow = () => {
NagbarActionCreators.dismissNagbar(control.forceKey);
NagbarActionCreators.setForceHideNagbar(control.forceHideKey, false);
};
const handleForceHide = () => {
NagbarActionCreators.setForceHideNagbar(control.forceHideKey, true);
NagbarActionCreators.resetNagbar(control.forceKey);
};
return (
<div className={styles.nagbarItem}>
<div className={styles.nagbarInfo}>
<span className={styles.nagbarLabel}>{control.label}</span>
<span className={styles.nagbarStatus}>{control.status(nagbarState)}</span>
</div>
<div className={styles.buttonGroup}>
<Button onClick={handleUseActual} disabled={useActualDisabled}>
{t`Use Actual`}
</Button>
<Button onClick={handleForceShow} disabled={forceShowDisabled}>
{t`Force Show`}
</Button>
<Button onClick={handleForceHide} disabled={forceHideDisabled}>
{t`Force Hide`}
</Button>
</div>
</div>
);
});
export const NagbarsTabContent: React.FC<NagbarsTabContentProps> = observer(({nagbarState}) => {
const {t} = useLingui();
const nagbarControls = getNagbarControls(t);
return (
<div className={styles.nagbarList}>
{nagbarControls.map((control) => (
<NagbarRow key={control.key} control={control} nagbarState={nagbarState} />
))}
<div className={styles.footer}>
<Button
onClick={() => NagbarActionCreators.resetAllNagbars()}
variant="secondary"
>{t`Reset All Nagbars`}</Button>
</div>
</div>
);
});

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.buttonGroup {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useCallback, useState} from 'react';
import * as ModalActionCreators from '~/actions/ModalActionCreators';
import {testBulkDeleteAllMessages} from '~/actions/UserActionCreators';
import {CaptchaModal} from '~/components/modals/CaptchaModal';
import {ClaimAccountModal} from '~/components/modals/ClaimAccountModal';
import {KeyboardModeIntroModal} from '~/components/modals/KeyboardModeIntroModal';
import {Button} from '~/components/uikit/Button/Button';
import type {GatewaySocket} from '~/lib/GatewaySocket';
import NewDeviceMonitoringStore from '~/stores/NewDeviceMonitoringStore';
import MediaEngineStore from '~/stores/voice/MediaEngineFacade';
import styles from './ToolsTab.module.css';
interface ToolsTabContentProps {
socket: GatewaySocket;
}
export const ToolsTabContent: React.FC<ToolsTabContentProps> = observer(({socket}) => {
const {t} = useLingui();
const [isTestingBulkDelete, setIsTestingBulkDelete] = useState(false);
const [shouldCrash, setShouldCrash] = useState(false);
const handleTestBulkDelete = useCallback(async () => {
setIsTestingBulkDelete(true);
try {
await testBulkDeleteAllMessages();
} finally {
setIsTestingBulkDelete(false);
}
}, []);
const handleOpenCaptchaModal = useCallback(() => {
ModalActionCreators.push(
ModalActionCreators.modal(() => (
<CaptchaModal
closeOnVerify={false}
onVerify={(token, captchaType) => {
console.debug('Captcha solved in Developer Options', {token, captchaType});
}}
onCancel={() => {
console.debug('Captcha cancelled in Developer Options');
}}
/>
)),
);
}, []);
const handleOpenClaimAccountModal = useCallback(() => {
ModalActionCreators.push(ModalActionCreators.modal(() => <ClaimAccountModal />));
}, []);
if (shouldCrash) {
return {} as any;
}
return (
<div className={styles.buttonGroup}>
<Button onClick={() => socket.reset()}>{t`Reset Socket`}</Button>
<Button onClick={() => socket.simulateNetworkDisconnect()}>{t`Disconnect Socket`}</Button>
<Button
onClick={() => {
void MediaEngineStore.moveToAfkChannel();
}}
>
{t`Force Move to AFK Channel`}
</Button>
<Button onClick={() => NewDeviceMonitoringStore.showTestModal()}>{t`Show New Device Modal`}</Button>
<Button onClick={handleOpenCaptchaModal}>{t`Open Captcha Modal`}</Button>
<Button
onClick={() => {
ModalActionCreators.push(ModalActionCreators.modal(() => <KeyboardModeIntroModal />));
}}
>
{t`Show Keyboard Mode Intro`}
</Button>
<Button onClick={handleOpenClaimAccountModal}>{t`Open Claim Account Modal`}</Button>
<Button onClick={() => void handleTestBulkDelete()} submitting={isTestingBulkDelete} variant="danger-primary">
{t`Test Bulk Delete (60s)`}
</Button>
<Button onClick={() => setShouldCrash(true)} variant="danger-primary">
{t`Trigger React Crash`}
</Button>
</div>
);
});

View File

@@ -0,0 +1,258 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.heading {
margin-bottom: 0;
font-weight: 600;
font-size: 1.25rem;
}
.subheading {
margin-bottom: 1rem;
font-weight: 600;
font-size: 1.125rem;
}
.description {
color: var(--text-secondary);
}
.grid {
display: grid;
gap: 0.75rem;
}
@media (min-width: 768px) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.card {
position: relative;
overflow: hidden;
border-radius: 0.5rem;
background-color: var(--background-secondary);
padding: 1rem;
transition: background-color 0.15s;
}
.card:hover {
background-color: var(--background-tertiary);
}
.cardHeader {
margin-bottom: 0.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.cardInfo {
display: flex;
flex-direction: column;
}
.fontName {
font-weight: 500;
font-size: 0.875rem;
}
.langCode {
color: var(--text-tertiary);
font-size: 0.75rem;
}
.fontFamily {
font-family: monospace;
color: var(--text-tertiary);
font-size: 0.75rem;
}
.sampleText {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.weightCard {
border-radius: 0.5rem;
background-color: var(--background-secondary);
padding: 1rem;
}
.weightHeader {
margin-bottom: 0.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.weightLabel {
font-weight: 500;
}
.weightValue {
font-family: monospace;
font-size: 0.875rem;
color: var(--text-tertiary);
}
.weightItalic {
margin-top: 0.25rem;
font-style: italic;
}
.scaleList {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.scaleItem {
display: flex;
align-items: center;
gap: 1rem;
border-radius: 0.5rem;
background-color: var(--background-secondary);
padding: 0.75rem 1rem;
}
.scaleSize {
width: 5rem;
text-align: right;
}
.scaleSizeText {
font-family: monospace;
color: var(--text-tertiary);
font-size: 0.75rem;
}
.scaleLabel {
width: 4rem;
text-align: right;
}
.scaleLabelText {
font-weight: 500;
font-size: 0.75rem;
color: var(--text-secondary);
}
.scaleSample {
flex: 1;
}
.styleGrid {
display: grid;
gap: 0.75rem;
}
@media (min-width: 768px) {
.styleGrid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.styleGrid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.styleCard {
border-radius: 0.5rem;
background-color: var(--background-secondary);
padding: 1rem;
}
.styleLabel {
margin-bottom: 0.5rem;
color: var(--text-tertiary);
font-size: 0.75rem;
}
.codeGrid {
display: grid;
gap: 1rem;
}
@media (min-width: 768px) {
.codeGrid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.codeCard {
border-radius: 0.5rem;
background-color: var(--background-secondary);
padding: 1rem;
}
.codeTitle {
margin-bottom: 0.75rem;
font-weight: 500;
}
.codeLines {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-family: monospace;
font-size: 0.875rem;
}
.multilingualCard {
border-radius: 0.5rem;
background-color: var(--background-secondary);
padding: 1.5rem;
}
.multilingualList {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.multilingualItem {
font-size: 1rem;
}
.italic {
font-style: italic;
}

View File

@@ -0,0 +1,366 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import styles from './TypographyTab.module.css';
const fontSamples = [
{
fontFamily: 'IBM Plex Sans',
name: 'IBM Plex Sans',
sample: 'The quick brown fox jumps over the lazy dog',
lang: 'en',
},
{
fontFamily: 'IBM Plex Sans JP',
name: 'IBM Plex Sans Japanese',
sample: 'これは日本語のサンプルテキストです',
lang: 'ja',
},
{
fontFamily: 'IBM Plex Sans KR',
name: 'IBM Plex Sans Korean',
sample: '이것은 한국어 샘플 텍스트입니다',
lang: 'ko',
},
{
fontFamily: 'IBM Plex Sans SC',
name: 'IBM Plex Sans Simplified Chinese',
sample: '这是简体中文的示例文本',
lang: 'zh-CN',
},
{
fontFamily: 'IBM Plex Sans TC',
name: 'IBM Plex Sans Traditional Chinese',
sample: '這是繁體中文的示例文本',
lang: 'zh-TW',
},
{
fontFamily: 'IBM Plex Sans Arabic',
name: 'IBM Plex Sans Arabic',
sample: 'هذه عينة نصية باللغة العربية',
lang: 'ar',
rtl: true,
},
{
fontFamily: 'IBM Plex Sans Hebrew',
name: 'IBM Plex Sans Hebrew',
sample: 'זוהי דוגמה לטקסט בעברית',
lang: 'he',
},
{
fontFamily: 'IBM Plex Sans Devanagari',
name: 'IBM Plex Sans Devanagari',
sample: 'यह हिंदी का नमूना पाठ है',
lang: 'hi',
},
{
fontFamily: 'IBM Plex Sans Thai',
name: 'IBM Plex Sans Thai',
sample: 'นี่คือข้อความตัวอย่างภาษาไทย',
lang: 'th',
},
{
fontFamily: 'IBM Plex Sans Thai Looped',
name: 'IBM Plex Sans Thai Looped',
sample: 'นี่คือข้อความตัวอย่างภาษาไทยแบบ Loop',
lang: 'th',
},
{
fontFamily: 'IBM Plex Mono',
name: 'IBM Plex Mono',
sample: 'const hello = "World"; function example() { return true; }',
lang: 'en',
mono: true,
},
];
const weightExamples = [
{weight: 100, label: 'Thin', text: 'Delicate and light typography'},
{weight: 200, label: 'Extra Light', text: 'Gentle and airy text display'},
{weight: 300, label: 'Light', text: 'Soft and easy reading experience'},
{weight: 400, label: 'Regular', text: 'Perfect for everyday content'},
{weight: 450, label: 'Text', text: 'Optimized for longer reading passages'},
{weight: 500, label: 'Medium', text: 'Slightly bolder emphasis'},
{weight: 600, label: 'Semi Bold', text: 'Strong visual hierarchy'},
{weight: 700, label: 'Bold', text: 'Powerful and attention-grabbing'},
];
const scaleExamples = [
{size: '12px', label: 'Caption', weight: 400},
{size: '14px', label: 'Small', weight: 400},
{size: '16px', label: 'Body', weight: 400},
{size: '18px', label: 'Large', weight: 500},
{size: '20px', label: 'Subtitle', weight: 500},
{size: '24px', label: 'Heading', weight: 600},
{size: '30px', label: 'Title', weight: 700},
{size: '36px', label: 'Display', weight: 700},
];
const contrastExamples = [
{weight: 400, size: '16px', style: 'Normal'},
{weight: 400, size: '16px', style: 'Italic'},
{weight: 600, size: '16px', style: 'Semi Bold'},
{weight: 600, size: '18px', style: 'Semi Bold Large'},
{weight: 700, size: '20px', style: 'Bold Heading'},
{weight: 700, size: '24px', style: 'Bold Title'},
];
export const TypographyTabContent: React.FC = observer(() => {
return (
<div className={styles.container}>
<div className={styles.section}>
<h2 className={styles.heading}>
<Trans>Typography Showcase</Trans>
</h2>
<p className={styles.description}>
<Trans>
Preview all available fonts, weights, and styles across different languages supported by Fluxer.
</Trans>
</p>
</div>
<div>
<h3 className={styles.subheading}>
<Trans>Language Support</Trans>
</h3>
<div className={styles.grid}>
{fontSamples.map((font) => (
<div key={font.fontFamily} className={styles.card}>
<div className={styles.cardHeader}>
<div className={styles.cardInfo}>
<span className={styles.fontName}>{font.name}</span>
<span className={styles.langCode}>{font.lang.toUpperCase()}</span>
</div>
<span className={styles.fontFamily}>{font.fontFamily}</span>
</div>
<div
className={styles.sampleText}
style={{
fontFamily: font.mono ? 'var(--font-mono)' : `"${font.fontFamily}", var(--font-sans)`,
fontSize: '16px',
textAlign: font.rtl ? 'right' : 'left',
}}
lang={font.lang}
dir={font.rtl ? 'rtl' : 'ltr'}
>
{font.sample}
</div>
</div>
))}
</div>
</div>
<div>
<h3 className={styles.subheading}>
<Trans>Font Weights</Trans>
</h3>
<div className={styles.codeGrid}>
{weightExamples.map((example) => (
<div key={example.weight} className={styles.weightCard}>
<div className={styles.cardHeader}>
<span className={styles.weightLabel}>{example.label}</span>
<span className={styles.weightValue}>{example.weight}</span>
</div>
<div
style={{
fontWeight: example.weight,
fontFamily: 'var(--font-sans)',
lineHeight: 1.4,
}}
>
{example.text}
</div>
<div
className={styles.weightItalic}
style={{
fontWeight: example.weight,
fontFamily: 'var(--font-sans)',
fontSize: '14px',
}}
>
Italic style demonstration
</div>
</div>
))}
</div>
</div>
<div>
<h3 className={styles.subheading}>
<Trans>Type Scale</Trans>
</h3>
<div className={styles.scaleList}>
{scaleExamples.map((example) => (
<div key={example.size} className={styles.scaleItem}>
<div className={styles.scaleSize}>
<span className={styles.fontFamily}>{example.size}</span>
</div>
<div className={styles.scaleLabel}>
<span className={styles.scaleLabelText}>{example.label}</span>
</div>
<div
className={styles.scaleSample}
style={{
fontSize: example.size,
fontWeight: example.weight,
fontFamily: 'var(--font-sans)',
lineHeight: 1.3,
}}
>
Typography scale demonstration
</div>
</div>
))}
</div>
</div>
<div>
<h3 className={styles.subheading}>
<Trans>Style Variations</Trans>
</h3>
<div className={styles.grid}>
{contrastExamples.map((example, index) => (
<div key={index} className={styles.weightCard}>
<div className={styles.styleLabel}>{example.style}</div>
<div
className={example.style.includes('Italic') ? styles.italic : ''}
style={{
fontSize: example.size,
fontWeight: example.weight,
fontFamily: 'var(--font-sans)',
lineHeight: 1.3,
}}
>
This text demonstrates {example.style.toLowerCase()} styling
</div>
</div>
))}
</div>
</div>
<div>
<h3 className={styles.subheading}>
<Trans>Code & Monospace</Trans>
</h3>
<div className={styles.codeGrid}>
<div className={styles.weightCard}>
<div className={styles.codeTitle}>Light Code</div>
<div
className={styles.codeLines}
style={{
fontFamily: 'var(--font-mono)',
fontWeight: 300,
}}
>
<div>const example = "light";</div>
<div>function demo() {'{'}</div>
<div> return true;</div>
<div>{'}'}</div>
</div>
</div>
<div className={styles.weightCard}>
<div className={styles.codeTitle}>Regular Code</div>
<div
className={styles.codeLines}
style={{
fontFamily: 'var(--font-mono)',
fontWeight: 400,
}}
>
<div>const example = "regular";</div>
<div>function demo() {'{'}</div>
<div> return true;</div>
<div>{'}'}</div>
</div>
</div>
<div className={styles.weightCard}>
<div className={styles.codeTitle}>Medium Code</div>
<div
className={styles.codeLines}
style={{
fontFamily: 'var(--font-mono)',
fontWeight: 500,
}}
>
<div>const example = "medium";</div>
<div>function demo() {'{'}</div>
<div> return true;</div>
<div>{'}'}</div>
</div>
</div>
<div className={styles.weightCard}>
<div className={styles.codeTitle}>Bold Code</div>
<div
className={styles.codeLines}
style={{
fontFamily: 'var(--font-mono)',
fontWeight: 600,
}}
>
<div>const example = "bold";</div>
<div>function demo() {'{'}</div>
<div> return true;</div>
<div>{'}'}</div>
</div>
</div>
</div>
</div>
<div>
<h3 className={styles.subheading}>
<Trans>Multilingual Content</Trans>
</h3>
<div className={styles.multilingualCard}>
<div className={styles.multilingualList} style={{fontFamily: 'var(--font-sans)', lineHeight: 1.6}}>
<div className={styles.multilingualItem}>
<strong>English:</strong> Welcome to Fluxer's typography showcase
</div>
<div className={styles.multilingualItem} lang="ja">
<strong>:</strong>
</div>
<div className={styles.multilingualItem} lang="ko">
<strong>:</strong> Fluxer의
</div>
<div className={styles.multilingualItem} lang="zh-CN">
<strong>:</strong> Fluxer
</div>
<div className={styles.multilingualItem} lang="zh-TW">
<strong>:</strong> Fluxer
</div>
<div className={styles.multilingualItem} lang="ar" dir="rtl">
<strong>العربية:</strong> مرحباً بك في عرض طباعة Fluxer
</div>
<div className={styles.multilingualItem} lang="he">
<strong>עברית:</strong> ברוכים הבאים לתצוגת הטיפוגרפיה של Fluxer
</div>
<div className={styles.multilingualItem} lang="hi">
<strong>ि:</strong> Fluxer
</div>
<div className={styles.multilingualItem} lang="th">
<strong>:</strong> Fluxer
</div>
</div>
</div>
</div>
</div>
);
});

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {SettingsSection} from '~/components/modals/shared/SettingsSection';
import {SettingsTabContainer, SettingsTabContent} from '~/components/modals/shared/SettingsTabLayout';
import ConnectionStore from '~/stores/ConnectionStore';
import NagbarStore from '~/stores/NagbarStore';
import UserStore from '~/stores/UserStore';
import {AccountPremiumTabContent} from './AccountPremiumTab';
import {GeneralTabContent} from './GeneralTab';
import {MockingTabContent} from './MockingTab';
import {NagbarsTabContent} from './NagbarsTab';
import {ToolsTabContent} from './ToolsTab';
import {TypographyTabContent} from './TypographyTab';
const DeveloperOptionsTab: React.FC = observer(() => {
const socket = ConnectionStore.socket;
const nagbarState = NagbarStore;
const user = UserStore.currentUser;
if (!(user && socket)) return null;
return (
<SettingsTabContainer>
<SettingsTabContent>
<SettingsSection id="general" title={<Trans>General</Trans>}>
<GeneralTabContent />
</SettingsSection>
<SettingsSection id="account_premium" title={<Trans>Account & Premium</Trans>}>
<AccountPremiumTabContent user={user} />
</SettingsSection>
<SettingsSection id="mocking" title={<Trans>Mocking</Trans>}>
<MockingTabContent />
</SettingsSection>
<SettingsSection id="nagbars" title={<Trans>Nagbars</Trans>}>
<NagbarsTabContent nagbarState={nagbarState} />
</SettingsSection>
<SettingsSection id="tools" title={<Trans>Tools</Trans>}>
<ToolsTabContent socket={socket} />
</SettingsSection>
<SettingsSection id="typography" title={<Trans>Typography</Trans>}>
<TypographyTabContent />
</SettingsSection>
</SettingsTabContent>
</SettingsTabContainer>
);
});
export default DeveloperOptionsTab;

View File

@@ -0,0 +1,188 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {MessageDescriptor} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import {Trans} from '@lingui/react/macro';
import type React from 'react';
import type {NagbarStore, NagbarToggleKey} from '~/stores/NagbarStore';
export type DismissibleNagbarKey = NagbarToggleKey;
export interface NagbarControlDefinition {
key: string;
label: React.ReactNode;
forceKey: DismissibleNagbarKey;
forceHideKey: NagbarToggleKey;
resetKeys: Array<DismissibleNagbarKey>;
status: (state: NagbarStore) => string;
useActualDisabled?: (state: NagbarStore) => boolean;
forceShowDisabled?: (state: NagbarStore) => boolean;
forceHideDisabled?: (state: NagbarStore) => boolean;
}
const FORCE_ENABLED_DESCRIPTOR = msg`Force enabled`;
const FORCE_DISABLED_DESCRIPTOR = msg`Force disabled`;
const USING_ACTUAL_ACCOUNT_STATE_DESCRIPTOR = msg`Using actual account state`;
const USING_ACTUAL_VERIFICATION_STATE_DESCRIPTOR = msg`Using actual verification state`;
const CURRENTLY_DISMISSED_DESCRIPTOR = msg`Currently dismissed`;
const CURRENTLY_SHOWING_DESCRIPTOR = msg`Currently showing`;
const USING_ACTUAL_PREMIUM_STATE_DESCRIPTOR = msg`Using actual premium state`;
const USING_ACTUAL_GIFT_INVENTORY_STATE_DESCRIPTOR = msg`Using actual gift inventory state`;
const USING_ACTUAL_STATE_DESCRIPTOR = msg`Using actual state`;
export const getNagbarControls = (t: (message: MessageDescriptor) => string): Array<NagbarControlDefinition> => [
{
key: 'forceUnclaimedAccount',
label: <Trans>Unclaimed Account Nagbar</Trans>,
forceKey: 'forceUnclaimedAccount',
forceHideKey: 'forceHideUnclaimedAccount',
resetKeys: ['forceUnclaimedAccount'],
status: (state) =>
state.forceUnclaimedAccount
? t(FORCE_ENABLED_DESCRIPTOR)
: state.forceHideUnclaimedAccount
? t(FORCE_DISABLED_DESCRIPTOR)
: t(USING_ACTUAL_ACCOUNT_STATE_DESCRIPTOR),
useActualDisabled: (state) => !state.forceUnclaimedAccount && !state.forceHideUnclaimedAccount,
forceShowDisabled: (state) => state.forceUnclaimedAccount,
forceHideDisabled: (state) => state.forceHideUnclaimedAccount,
},
{
key: 'forceEmailVerification',
label: <Trans>Email Verification Nagbar</Trans>,
forceKey: 'forceEmailVerification',
forceHideKey: 'forceHideEmailVerification',
resetKeys: ['forceEmailVerification'],
status: (state) =>
state.forceEmailVerification
? t(FORCE_ENABLED_DESCRIPTOR)
: state.forceHideEmailVerification
? t(FORCE_DISABLED_DESCRIPTOR)
: t(USING_ACTUAL_VERIFICATION_STATE_DESCRIPTOR),
useActualDisabled: (state) => !state.forceEmailVerification && !state.forceHideEmailVerification,
forceShowDisabled: (state) => state.forceEmailVerification,
forceHideDisabled: (state) => state.forceHideEmailVerification,
},
{
key: 'forceDesktopNotification',
label: <Trans>Desktop Notification Nagbar</Trans>,
forceKey: 'forceDesktopNotification',
forceHideKey: 'forceHideDesktopNotification',
resetKeys: ['forceDesktopNotification', 'desktopNotificationDismissed'],
status: (state) =>
state.forceDesktopNotification
? t(FORCE_ENABLED_DESCRIPTOR)
: state.forceHideDesktopNotification
? t(FORCE_DISABLED_DESCRIPTOR)
: state.desktopNotificationDismissed
? t(CURRENTLY_DISMISSED_DESCRIPTOR)
: t(CURRENTLY_SHOWING_DESCRIPTOR),
useActualDisabled: (state) =>
!state.forceDesktopNotification && !state.desktopNotificationDismissed && !state.forceHideDesktopNotification,
forceShowDisabled: (state) => state.forceDesktopNotification,
forceHideDisabled: (state) => state.forceHideDesktopNotification,
},
{
key: 'forcePremiumGracePeriod',
label: <Trans>Premium Grace Period Nagbar</Trans>,
forceKey: 'forcePremiumGracePeriod',
forceHideKey: 'forceHidePremiumGracePeriod',
resetKeys: ['forcePremiumGracePeriod'],
status: (state) =>
state.forcePremiumGracePeriod
? t(FORCE_ENABLED_DESCRIPTOR)
: state.forceHidePremiumGracePeriod
? t(FORCE_DISABLED_DESCRIPTOR)
: t(USING_ACTUAL_PREMIUM_STATE_DESCRIPTOR),
useActualDisabled: (state) => !state.forcePremiumGracePeriod && !state.forceHidePremiumGracePeriod,
forceShowDisabled: (state) => state.forcePremiumGracePeriod,
forceHideDisabled: (state) => state.forceHidePremiumGracePeriod,
},
{
key: 'forcePremiumExpired',
label: <Trans>Premium Expired Nagbar</Trans>,
forceKey: 'forcePremiumExpired',
forceHideKey: 'forceHidePremiumExpired',
resetKeys: ['forcePremiumExpired'],
status: (state) =>
state.forcePremiumExpired
? t(FORCE_ENABLED_DESCRIPTOR)
: state.forceHidePremiumExpired
? t(FORCE_DISABLED_DESCRIPTOR)
: t(USING_ACTUAL_PREMIUM_STATE_DESCRIPTOR),
useActualDisabled: (state) => !state.forcePremiumExpired && !state.forceHidePremiumExpired,
forceShowDisabled: (state) => state.forcePremiumExpired,
forceHideDisabled: (state) => state.forceHidePremiumExpired,
},
{
key: 'forcePremiumOnboarding',
label: <Trans>Premium Onboarding Nagbar</Trans>,
forceKey: 'forcePremiumOnboarding',
forceHideKey: 'forceHidePremiumOnboarding',
resetKeys: ['forcePremiumOnboarding', 'premiumOnboardingDismissed'],
status: (state) =>
state.forcePremiumOnboarding
? t(FORCE_ENABLED_DESCRIPTOR)
: state.forceHidePremiumOnboarding
? t(FORCE_DISABLED_DESCRIPTOR)
: state.premiumOnboardingDismissed
? t(CURRENTLY_DISMISSED_DESCRIPTOR)
: t(USING_ACTUAL_PREMIUM_STATE_DESCRIPTOR),
useActualDisabled: (state) =>
!state.forcePremiumOnboarding && !state.premiumOnboardingDismissed && !state.forceHidePremiumOnboarding,
forceShowDisabled: (state) => state.forcePremiumOnboarding,
forceHideDisabled: (state) => state.forceHidePremiumOnboarding,
},
{
key: 'forceGiftInventory',
label: <Trans>Gift Inventory Nagbar</Trans>,
forceKey: 'forceGiftInventory',
forceHideKey: 'forceHideGiftInventory',
resetKeys: ['forceGiftInventory', 'giftInventoryDismissed'],
status: (state) =>
state.forceGiftInventory
? t(FORCE_ENABLED_DESCRIPTOR)
: state.forceHideGiftInventory
? t(FORCE_DISABLED_DESCRIPTOR)
: state.giftInventoryDismissed
? t(CURRENTLY_DISMISSED_DESCRIPTOR)
: t(USING_ACTUAL_GIFT_INVENTORY_STATE_DESCRIPTOR),
useActualDisabled: (state) =>
!state.forceGiftInventory && !state.giftInventoryDismissed && !state.forceHideGiftInventory,
forceShowDisabled: (state) => state.forceGiftInventory,
forceHideDisabled: (state) => state.forceHideGiftInventory,
},
{
key: 'forceInvitesDisabled',
label: <Trans>Invites Disabled Nagbar</Trans>,
forceKey: 'forceInvitesDisabled',
forceHideKey: 'forceHideInvitesDisabled',
resetKeys: ['forceInvitesDisabled'],
status: (state) =>
state.forceInvitesDisabled
? t(FORCE_ENABLED_DESCRIPTOR)
: state.forceHideInvitesDisabled
? t(FORCE_DISABLED_DESCRIPTOR)
: t(USING_ACTUAL_STATE_DESCRIPTOR),
useActualDisabled: (state) => !state.forceInvitesDisabled && !state.forceHideInvitesDisabled,
forceShowDisabled: (state) => state.forceInvitesDisabled,
forceHideDisabled: (state) => state.forceHideInvitesDisabled,
},
];

View File

@@ -0,0 +1,204 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {MessageDescriptor} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import * as DeveloperOptionsActionCreators from '~/actions/DeveloperOptionsActionCreators';
import {UserPremiumTypes} from '~/Constants';
import type {SelectOption} from '~/components/form/Select';
interface PremiumScenarioSelectOption extends Omit<SelectOption<PremiumScenarioOption>, 'label'> {
label: MessageDescriptor;
}
export type PremiumScenarioOption =
| 'none'
| 'free_no_purchases'
| 'free_with_history'
| 'active_monthly'
| 'active_monthly_cancelled'
| 'active_yearly'
| 'active_yearly_cancelled'
| 'gift_active_no_history'
| 'gift_active_with_history'
| 'gift_expiring_soon'
| 'gift_grace_period_active'
| 'gift_expired_recent'
| 'grace_period_active'
| 'expired_recent'
| 'expired_old'
| 'visionary'
| 'reset';
export const PREMIUM_SCENARIO_OPTIONS: ReadonlyArray<PremiumScenarioSelectOption> = [
{value: 'none', label: msg`Select a scenario to apply...`},
{value: 'free_no_purchases', label: msg`Free User (No Purchases)`},
{value: 'free_with_history', label: msg`Free User (With Purchase History)`},
{value: 'active_monthly', label: msg`Active Monthly Subscriber`},
{value: 'active_monthly_cancelled', label: msg`Active Monthly Subscriber (Cancellation Scheduled)`},
{value: 'active_yearly', label: msg`Active Yearly Subscriber`},
{value: 'active_yearly_cancelled', label: msg`Active Yearly Subscriber (Cancellation Scheduled)`},
{value: 'gift_active_no_history', label: msg`Gift Active (No Purchase History)`},
{value: 'gift_active_with_history', label: msg`Gift Active (Has Purchase History)`},
{value: 'gift_expiring_soon', label: msg`Gift Expiring Soon`},
{value: 'gift_grace_period_active', label: msg`Gift Grace Period (Still Have Access)`},
{value: 'gift_expired_recent', label: msg`Gift Expired (Within 30 Days)`},
{value: 'grace_period_active', label: msg`Grace Period (Still Have Access)`},
{value: 'expired_recent', label: msg`Expired (Within 30 Days)`},
{value: 'expired_old', label: msg`Expired (Over 30 Days Ago)`},
{value: 'visionary', label: msg`Visionary (Lifetime #42)`},
{value: 'reset', label: msg`Reset to Actual Values`},
];
export const applyPremiumScenarioOption = (scenario: PremiumScenarioOption) => {
if (scenario === 'none') return;
const now = Date.now();
switch (scenario) {
case 'free_no_purchases':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'free_with_history':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'active_monthly':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 15 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 15 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'monthly');
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'active_monthly_cancelled':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 20 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 10 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'monthly');
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', true);
break;
case 'active_yearly':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 180 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 185 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'active_yearly_cancelled':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 250 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 60 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', true);
break;
case 'gift_active_no_history':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 10 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 20 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'gift_active_with_history':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 5 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 60 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'gift_expiring_soon':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 25 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'gift_grace_period_active':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 31 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'gift_expired_recent':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 35 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 5 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'grace_period_active':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 31 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'expired_recent':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 35 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 5 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'expired_old':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 65 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 35 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'visionary':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.LIFETIME);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 365 * 86400000));
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
break;
case 'reset':
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', null);
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', null);
break;
}
};

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {MessageDescriptor} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import type {DeveloperOptionsState} from '~/stores/DeveloperOptionsStore';
export interface ToggleDef {
key: keyof DeveloperOptionsState;
label: MessageDescriptor;
description?: MessageDescriptor;
}
export interface ToggleGroup {
title: MessageDescriptor;
items: Array<ToggleDef>;
}
export const getToggleGroups = (): Array<ToggleGroup> => [
{
title: msg`App State`,
items: [
{key: 'bypassSplashScreen', label: msg`Bypass Splash Screen`},
{key: 'forceUpdateReady', label: msg`Force Update Ready`},
{key: 'showMyselfTyping', label: msg`Show Myself Typing`},
{
key: 'selfHostedModeOverride',
label: msg`Self-Hosted Mode Override`,
description: msg`Enable self-hosted mode client-side (hides all premium/billing UI, grants everyone premium)`,
},
],
},
{
title: msg`UI Components`,
items: [
{key: 'forceGifPickerLoading', label: msg`Force GIF Picker Loading`},
{
key: 'forceShowVanityURLDisclaimer',
label: msg`Force Show Vanity URL Disclaimer`,
description: msg`Always show the vanity URL disclaimer warning in guild settings`,
},
],
},
{
title: msg`Networking & Performance`,
items: [
{key: 'slowMessageLoad', label: msg`Slow Message Load`},
{key: 'slowMessageSend', label: msg`Slow Message Send`},
{key: 'slowMessageEdit', label: msg`Slow Message Edit`},
{key: 'slowAttachmentUpload', label: msg`Slow Attachment Upload`},
{key: 'slowProfileLoad', label: msg`Slow Profile Load`},
{
key: 'forceProfileDataWarning',
label: msg`Force Profile Data Warning`,
description: msg`Always show the profile data warning indicator, even when the profile loads successfully`,
},
{key: 'forceFailUploads', label: msg`Force Fail Uploads`},
{key: 'forceFailMessageSends', label: msg`Force Fail Message Sends`},
],
},
{
title: msg`Features`,
items: [
{
key: 'forceUnknownMessageType',
label: msg`Force Unknown Message Type`,
description: msg`Render all your messages as unknown message type`,
},
{
key: 'forceShowVoiceConnection',
label: msg`Force Show Voice Connection`,
description: msg`Always display the voice connection status bar in mocked mode`,
},
],
},
{
title: msg`Logging & Diagnostics`,
items: [{key: 'debugLogging', label: msg`Debug Logging`}],
},
];