[skip ci] feat: prepare for public release
This commit is contained in:
@@ -24,6 +24,7 @@ import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {ChannelTypes} from '~/Constants';
|
||||
import * as Modal from '~/components/modals/Modal';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import ConnectionStore from '~/stores/gateway/ConnectionStore';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import {isMobileExperienceEnabled} from '~/utils/mobileExperience';
|
||||
import {
|
||||
@@ -41,6 +42,7 @@ import type {ChannelSettingsTabType} from './utils/channelSettingsConstants';
|
||||
export const ChannelSettingsModal: React.FC<ChannelSettingsModalProps> = observer(({channelId, initialMobileTab}) => {
|
||||
const {t} = useLingui();
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
const guildId = channel?.guildId;
|
||||
const [selectedTab, setSelectedTab] = React.useState<ChannelSettingsTabType>('overview');
|
||||
|
||||
const availableTabs = React.useMemo(() => {
|
||||
@@ -59,6 +61,12 @@ export const ChannelSettingsModal: React.FC<ChannelSettingsModalProps> = observe
|
||||
const mobileNav = useMobileNavigation<ChannelSettingsTabType>(initialTab);
|
||||
const {enabled: isMobile} = MobileLayoutStore;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (guildId) {
|
||||
ConnectionStore.syncGuildIfNeeded(guildId, 'channel-settings-modal');
|
||||
}
|
||||
}, [guildId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!channel) {
|
||||
ModalActionCreators.pop();
|
||||
|
||||
@@ -22,6 +22,7 @@ import {observer} from 'mobx-react-lite';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import * as UserActionCreators from '~/actions/UserActionCreators';
|
||||
import {Form} from '~/components/form/Form';
|
||||
@@ -31,6 +32,7 @@ import confirmStyles from '~/components/modals/ConfirmModal.module.css';
|
||||
import * as Modal from '~/components/modals/Modal';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {useFormSubmit} from '~/hooks/useFormSubmit';
|
||||
import ModalStore from '~/stores/ModalStore';
|
||||
|
||||
interface FormInputs {
|
||||
email: string;
|
||||
@@ -230,3 +232,20 @@ export const ClaimAccountModal = observer(() => {
|
||||
</Modal.Root>
|
||||
);
|
||||
});
|
||||
|
||||
const CLAIM_ACCOUNT_MODAL_KEY = 'claim-account-modal';
|
||||
let hasShownClaimAccountModalThisSession = false;
|
||||
|
||||
export const openClaimAccountModal = ({force = false}: {force?: boolean} = {}): void => {
|
||||
if (ModalStore.hasModal(CLAIM_ACCOUNT_MODAL_KEY)) {
|
||||
return;
|
||||
}
|
||||
if (!force && hasShownClaimAccountModalThisSession) {
|
||||
return;
|
||||
}
|
||||
hasShownClaimAccountModalThisSession = true;
|
||||
ModalActionCreators.pushWithKey(
|
||||
modal(() => <ClaimAccountModal />),
|
||||
CLAIM_ACCOUNT_MODAL_KEY,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,12 +23,14 @@ import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as GiftActionCreators from '~/actions/GiftActionCreators';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import * as Modal from '~/components/modals/Modal';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {Spinner} from '~/components/uikit/Spinner';
|
||||
import i18n from '~/i18n';
|
||||
import {UserRecord} from '~/records/UserRecord';
|
||||
import GiftStore from '~/stores/GiftStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import {getGiftDurationText} from '~/utils/giftUtils';
|
||||
import styles from './GiftAcceptModal.module.css';
|
||||
|
||||
@@ -41,6 +43,7 @@ export const GiftAcceptModal = observer(function GiftAcceptModal({code}: GiftAcc
|
||||
const giftState = GiftStore.gifts.get(code) ?? null;
|
||||
const gift = giftState?.data ?? null;
|
||||
const [isRedeeming, setIsRedeeming] = React.useState(false);
|
||||
const isUnclaimed = !(UserStore.currentUser?.isClaimed() ?? false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!giftState) {
|
||||
@@ -64,6 +67,10 @@ export const GiftAcceptModal = observer(function GiftAcceptModal({code}: GiftAcc
|
||||
};
|
||||
|
||||
const handleRedeem = async () => {
|
||||
if (isUnclaimed) {
|
||||
openClaimAccountModal({force: true});
|
||||
return;
|
||||
}
|
||||
setIsRedeeming(true);
|
||||
try {
|
||||
await GiftActionCreators.redeem(i18n, code);
|
||||
@@ -130,6 +137,42 @@ export const GiftAcceptModal = observer(function GiftAcceptModal({code}: GiftAcc
|
||||
|
||||
const renderGift = () => {
|
||||
const durationText = getGiftDurationText(i18n, gift!);
|
||||
if (isUnclaimed) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardGrid}>
|
||||
<div className={`${styles.iconCircle} ${styles.iconCircleInactive}`}>
|
||||
<GiftIcon className={styles.icon} weight="fill" />
|
||||
</div>
|
||||
<div className={styles.cardContent}>
|
||||
<h3 className={`${styles.title} ${styles.titlePrimary}`}>{durationText}</h3>
|
||||
{creator && (
|
||||
<span className={styles.subtitle}>{t`From ${creator.username}#${creator.discriminator}`}</span>
|
||||
)}
|
||||
<span className={styles.helpText}>
|
||||
<Trans>Claim your account to redeem this gift.</Trans>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<Button variant="secondary" onClick={handleDismiss}>
|
||||
<Trans>Maybe later</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
openClaimAccountModal({force: true});
|
||||
handleDismiss();
|
||||
}}
|
||||
>
|
||||
<Trans>Claim Account</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={styles.card}>
|
||||
|
||||
@@ -25,6 +25,7 @@ import * as UnsavedChangesActionCreators from '~/actions/UnsavedChangesActionCre
|
||||
import * as Modal from '~/components/modals/Modal';
|
||||
import GuildSettingsModalStore from '~/stores/GuildSettingsModalStore';
|
||||
import GuildStore from '~/stores/GuildStore';
|
||||
import ConnectionStore from '~/stores/gateway/ConnectionStore';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import PermissionStore from '~/stores/PermissionStore';
|
||||
import UnsavedChangesStore from '~/stores/UnsavedChangesStore';
|
||||
@@ -79,6 +80,10 @@ export const GuildSettingsModal: React.FC<GuildSettingsModalProps> = observer(
|
||||
|
||||
const unsavedChangesStore = UnsavedChangesStore;
|
||||
|
||||
React.useEffect(() => {
|
||||
ConnectionStore.syncGuildIfNeeded(guildId, 'guild-settings-modal');
|
||||
}, [guildId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!guild) {
|
||||
ModalActionCreators.pop();
|
||||
|
||||
@@ -230,6 +230,17 @@ export const InviteModal = observer(({channelId}: {channelId: string}) => {
|
||||
<span className={styles.channelName}>{channel.name}</span>
|
||||
</Trans>
|
||||
</p>
|
||||
{invitesDisabled && (
|
||||
<div className={styles.warningContainer}>
|
||||
<WarningCircleIcon className={styles.warningIcon} weight="fill" />
|
||||
<p className={styles.warningText}>
|
||||
<Trans>
|
||||
Invites are currently disabled in this community by an admin. While this invite can be created, it
|
||||
cannot be accepted until invites are re-enabled.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={selectorStyles.headerSearch}>
|
||||
<Input
|
||||
value={searchQuery}
|
||||
@@ -248,33 +259,19 @@ export const InviteModal = observer(({channelId}: {channelId: string}) => {
|
||||
<Spinner />
|
||||
</div>
|
||||
) : !showAdvanced ? (
|
||||
<>
|
||||
<RecipientList
|
||||
recipients={recipients}
|
||||
sendingTo={sendingTo}
|
||||
sentTo={sentInvites}
|
||||
onSend={handleSendInvite}
|
||||
defaultButtonLabel={t`Invite`}
|
||||
sentButtonLabel={t`Sent`}
|
||||
buttonClassName={styles.inviteButton}
|
||||
scrollerKey="invite-modal-friend-list-scroller"
|
||||
searchQuery={searchQuery}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
showSearchInput={false}
|
||||
/>
|
||||
|
||||
{invitesDisabled && (
|
||||
<div className={styles.warningContainer}>
|
||||
<WarningCircleIcon className={styles.warningIcon} weight="fill" />
|
||||
<p className={styles.warningText}>
|
||||
<Trans>
|
||||
Invites are currently disabled in this community by an admin. While this invite can be created, it
|
||||
cannot be accepted until invites are re-enabled.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<RecipientList
|
||||
recipients={recipients}
|
||||
sendingTo={sendingTo}
|
||||
sentTo={sentInvites}
|
||||
onSend={handleSendInvite}
|
||||
defaultButtonLabel={t`Invite`}
|
||||
sentButtonLabel={t`Sent`}
|
||||
buttonClassName={styles.inviteButton}
|
||||
scrollerKey="invite-modal-friend-list-scroller"
|
||||
searchQuery={searchQuery}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
showSearchInput={false}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.advancedView}>
|
||||
<Select
|
||||
|
||||
@@ -217,6 +217,7 @@ const UserProfileMobileSheetContent: React.FC<UserProfileMobileSheetContentProps
|
||||
const isCurrentUser = user.id === AuthenticationStore.currentUserId;
|
||||
const relationship = RelationshipStore.getRelationship(user.id);
|
||||
const relationshipType = relationship?.type;
|
||||
const currentUserUnclaimed = !(UserStore.currentUser?.isClaimed() ?? true);
|
||||
|
||||
const guildMember = GuildMemberStore.getMember(profile?.guildId ?? guildId ?? '', user.id);
|
||||
const memberRoles = profile?.guildId && guildMember ? guildMember.getSortedRoles() : [];
|
||||
@@ -389,11 +390,14 @@ const UserProfileMobileSheetContent: React.FC<UserProfileMobileSheetContentProps
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button type="button" onClick={handleSendFriendRequest} className={styles.actionButton}>
|
||||
<UserPlusIcon className={styles.icon} />
|
||||
</button>
|
||||
);
|
||||
if (relationshipType === undefined && !currentUserUnclaimed) {
|
||||
return (
|
||||
<button type="button" onClick={handleSendFriendRequest} className={styles.actionButton}>
|
||||
<UserPlusIcon className={styles.icon} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1164,6 +1164,7 @@ export const UserProfileModal: UserProfileModalComponent = observer(
|
||||
};
|
||||
|
||||
const renderActionButtons = () => {
|
||||
const currentUserUnclaimed = !(UserStore.currentUser?.isClaimed() ?? true);
|
||||
if (isCurrentUser && disableEditProfile) {
|
||||
return (
|
||||
<div className={userProfileModalStyles.actionButtons}>
|
||||
@@ -1284,8 +1285,11 @@ export const UserProfileModal: UserProfileModalComponent = observer(
|
||||
);
|
||||
}
|
||||
if (relationshipType === undefined && !isUserBot) {
|
||||
const tooltipText = currentUserUnclaimed
|
||||
? t`Claim your account to send friend requests.`
|
||||
: t`Send Friend Request`;
|
||||
return (
|
||||
<Tooltip text={t`Send Friend Request`} maxWidth="xl">
|
||||
<Tooltip text={tooltipText} maxWidth="xl">
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -1293,6 +1297,7 @@ export const UserProfileModal: UserProfileModalComponent = observer(
|
||||
square={true}
|
||||
icon={<UserPlusIcon className={userProfileModalStyles.buttonIcon} />}
|
||||
onClick={handleSendFriendRequest}
|
||||
disabled={currentUserUnclaimed}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -32,11 +32,9 @@ import {ConfirmModal} from '~/components/modals/ConfirmModal';
|
||||
import {ClientInfo} from '~/components/modals/components/ClientInfo';
|
||||
import {LogoutModal} from '~/components/modals/components/LogoutModal';
|
||||
import styles from '~/components/modals/components/MobileSettingsView.module.css';
|
||||
import {ScrollSpyProvider, useScrollSpyContext} from '~/components/modals/hooks/ScrollSpyContext';
|
||||
import type {MobileNavigationState} from '~/components/modals/hooks/useMobileNavigation';
|
||||
import {useSettingsContentKey} from '~/components/modals/hooks/useSettingsContentKey';
|
||||
import {
|
||||
MobileSectionNav,
|
||||
MobileSettingsDangerItem,
|
||||
MobileHeader as SharedMobileHeader,
|
||||
} from '~/components/modals/shared/MobileSettingsComponents';
|
||||
@@ -44,16 +42,13 @@ import userSettingsStyles from '~/components/modals/UserSettingsModal.module.css
|
||||
import {getSettingsTabComponent} from '~/components/modals/utils/desktopSettingsTabs';
|
||||
import {
|
||||
getCategoryLabel,
|
||||
getSectionIdsForTab,
|
||||
getSectionsForTab,
|
||||
type SettingsTab,
|
||||
tabHasSections,
|
||||
type UserSettingsTabType,
|
||||
} from '~/components/modals/utils/settingsConstants';
|
||||
import {filterSettingsTabsForDeveloperMode} from '~/components/modals/utils/settingsTabFilters';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {MentionBadgeAnimated} from '~/components/uikit/MentionBadge';
|
||||
import {Scroller, type ScrollerHandle} from '~/components/uikit/Scroller';
|
||||
import {Scroller} from '~/components/uikit/Scroller';
|
||||
import {Spinner} from '~/components/uikit/Spinner';
|
||||
import {usePressable} from '~/hooks/usePressable';
|
||||
import {usePushSubscriptions} from '~/hooks/usePushSubscriptions';
|
||||
@@ -397,26 +392,7 @@ const headerFadeVariants = {
|
||||
exit: {opacity: 0},
|
||||
};
|
||||
|
||||
interface MobileSectionNavWrapperProps {
|
||||
tabType: UserSettingsTabType;
|
||||
}
|
||||
|
||||
const MobileSectionNavWrapper: React.FC<MobileSectionNavWrapperProps> = observer(({tabType}) => {
|
||||
const {t} = useLingui();
|
||||
const scrollSpyContext = useScrollSpyContext();
|
||||
const sections = getSectionsForTab(tabType, t);
|
||||
|
||||
if (!scrollSpyContext || sections.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {activeSectionId, scrollToSection} = scrollSpyContext;
|
||||
|
||||
return <MobileSectionNav sections={sections} activeSectionId={activeSectionId} onSectionClick={scrollToSection} />;
|
||||
});
|
||||
|
||||
interface MobileContentWithScrollSpyProps {
|
||||
tabType: UserSettingsTabType;
|
||||
scrollKey: string;
|
||||
initialGuildId?: string;
|
||||
initialSubtab?: string;
|
||||
@@ -424,21 +400,9 @@ interface MobileContentWithScrollSpyProps {
|
||||
}
|
||||
|
||||
const MobileContentWithScrollSpy: React.FC<MobileContentWithScrollSpyProps> = observer(
|
||||
({tabType, scrollKey, initialGuildId, initialSubtab, currentTabComponent}) => {
|
||||
const scrollerRef = React.useRef<ScrollerHandle | null>(null);
|
||||
const scrollContainerRef = React.useRef<HTMLElement | null>(null);
|
||||
const sectionIds = React.useMemo(() => getSectionIdsForTab(tabType), [tabType]);
|
||||
const hasSections = tabHasSections(tabType);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (scrollerRef.current) {
|
||||
scrollContainerRef.current = scrollerRef.current.getScrollerNode();
|
||||
}
|
||||
});
|
||||
|
||||
const content = (
|
||||
<Scroller ref={scrollerRef} className={styles.scrollerFlex} key={scrollKey} data-settings-scroll-container>
|
||||
{hasSections && <MobileSectionNavWrapper tabType={tabType} />}
|
||||
({scrollKey, initialGuildId, initialSubtab, currentTabComponent}) => {
|
||||
return (
|
||||
<Scroller className={styles.scrollerFlex} key={scrollKey} data-settings-scroll-container>
|
||||
<div className={styles.contentContainer}>
|
||||
{currentTabComponent &&
|
||||
React.createElement(currentTabComponent, {
|
||||
@@ -448,16 +412,6 @@ const MobileContentWithScrollSpy: React.FC<MobileContentWithScrollSpyProps> = ob
|
||||
</div>
|
||||
</Scroller>
|
||||
);
|
||||
|
||||
if (hasSections) {
|
||||
return (
|
||||
<ScrollSpyProvider sectionIds={sectionIds} containerRef={scrollContainerRef}>
|
||||
{content}
|
||||
</ScrollSpyProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -582,7 +536,6 @@ export const MobileSettingsView: React.FC<MobileSettingsViewProps> = observer(
|
||||
style={{willChange: 'transform'}}
|
||||
>
|
||||
<MobileContentWithScrollSpy
|
||||
tabType={currentTab.type}
|
||||
scrollKey={scrollKey}
|
||||
initialGuildId={initialGuildId}
|
||||
initialSubtab={initialSubtab}
|
||||
|
||||
@@ -94,6 +94,17 @@ export const PlutoniumContent: React.FC<{defaultGiftMode?: boolean}> = observer(
|
||||
mobileLayoutState.enabled,
|
||||
);
|
||||
|
||||
const isClaimed = currentUser?.isClaimed() ?? false;
|
||||
const purchaseDisabled = !isClaimed;
|
||||
const purchaseDisabledTooltip = <Trans>Claim your account to purchase Fluxer Plutonium.</Trans>;
|
||||
const handleSelectPlanGuarded = React.useCallback(
|
||||
(plan: 'monthly' | 'yearly' | 'visionary' | 'gift1Month' | 'gift1Year' | 'giftVisionary') => {
|
||||
if (purchaseDisabled) return;
|
||||
handleSelectPlan(plan);
|
||||
},
|
||||
[handleSelectPlan, purchaseDisabled],
|
||||
);
|
||||
|
||||
const monthlyPrice = React.useMemo(() => getFormattedPrice(PricingTier.Monthly, countryCode), [countryCode]);
|
||||
const yearlyPrice = React.useMemo(() => getFormattedPrice(PricingTier.Yearly, countryCode), [countryCode]);
|
||||
const visionaryPrice = React.useMemo(() => getFormattedPrice(PricingTier.Visionary, countryCode), [countryCode]);
|
||||
@@ -221,12 +232,14 @@ export const PlutoniumContent: React.FC<{defaultGiftMode?: boolean}> = observer(
|
||||
scrollToPerks={scrollToPerks}
|
||||
handlePerksKeyDown={handlePerksKeyDown}
|
||||
navigateToRedeemGift={navigateToRedeemGift}
|
||||
handleSelectPlan={handleSelectPlan}
|
||||
handleSelectPlan={handleSelectPlanGuarded}
|
||||
handleOpenCustomerPortal={handleOpenCustomerPortal}
|
||||
handleReactivateSubscription={handleReactivateSubscription}
|
||||
handleCancelSubscription={handleCancelSubscription}
|
||||
handleCommunityButtonPointerDown={handleCommunityButtonPointerDown}
|
||||
handleCommunityButtonClick={handleCommunityButtonClick}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
/>
|
||||
<div className={styles.disclaimerContainer}>
|
||||
<PurchaseDisclaimer align="center" isPremium />
|
||||
@@ -245,7 +258,9 @@ export const PlutoniumContent: React.FC<{defaultGiftMode?: boolean}> = observer(
|
||||
loadingCheckout={loadingCheckout}
|
||||
loadingSlots={loadingSlots}
|
||||
isVisionarySoldOut={isVisionarySoldOut}
|
||||
handleSelectPlan={handleSelectPlan}
|
||||
handleSelectPlan={handleSelectPlanGuarded}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
/>
|
||||
) : (
|
||||
<GiftSection
|
||||
@@ -257,7 +272,9 @@ export const PlutoniumContent: React.FC<{defaultGiftMode?: boolean}> = observer(
|
||||
loadingCheckout={loadingCheckout}
|
||||
loadingSlots={loadingSlots}
|
||||
isVisionarySoldOut={isVisionarySoldOut}
|
||||
handleSelectPlan={handleSelectPlan}
|
||||
handleSelectPlan={handleSelectPlanGuarded}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -280,7 +297,9 @@ export const PlutoniumContent: React.FC<{defaultGiftMode?: boolean}> = observer(
|
||||
isGiftSubscription={subscriptionStatus.isGiftSubscription}
|
||||
loadingCheckout={loadingCheckout}
|
||||
loadingSlots={loadingSlots}
|
||||
handleSelectPlan={handleSelectPlan}
|
||||
handleSelectPlan={handleSelectPlanGuarded}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -302,7 +321,9 @@ export const PlutoniumContent: React.FC<{defaultGiftMode?: boolean}> = observer(
|
||||
loadingCheckout={loadingCheckout}
|
||||
loadingSlots={loadingSlots}
|
||||
isVisionarySoldOut={isVisionarySoldOut}
|
||||
handleSelectPlan={handleSelectPlan}
|
||||
handleSelectPlan={handleSelectPlanGuarded}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {ClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {WarningAlert} from '~/components/uikit/WarningAlert/WarningAlert';
|
||||
|
||||
@@ -30,7 +28,7 @@ export const UnclaimedAccountAlert = observer(() => {
|
||||
<WarningAlert
|
||||
title={<Trans>Unclaimed Account</Trans>}
|
||||
actions={
|
||||
<Button small={true} onClick={() => ModalActionCreators.push(modal(() => <ClaimAccountModal />))}>
|
||||
<Button small={true} onClick={() => openClaimAccountModal({force: true})}>
|
||||
<Trans>Claim Account</Trans>
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {PurchaseDisclaimer} from '../PurchaseDisclaimer';
|
||||
import styles from './BottomCTASection.module.css';
|
||||
import {PurchaseDisabledWrapper} from './PurchaseDisabledWrapper';
|
||||
|
||||
interface BottomCTASectionProps {
|
||||
isGiftMode: boolean;
|
||||
@@ -33,6 +34,8 @@ interface BottomCTASectionProps {
|
||||
loadingSlots: boolean;
|
||||
isVisionarySoldOut: boolean;
|
||||
handleSelectPlan: (plan: 'monthly' | 'yearly' | 'visionary' | 'gift1Month' | 'gift1Year' | 'giftVisionary') => void;
|
||||
purchaseDisabled?: boolean;
|
||||
purchaseDisabledTooltip?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BottomCTASection: React.FC<BottomCTASectionProps> = observer(
|
||||
@@ -45,7 +48,12 @@ export const BottomCTASection: React.FC<BottomCTASectionProps> = observer(
|
||||
loadingSlots,
|
||||
isVisionarySoldOut,
|
||||
handleSelectPlan,
|
||||
purchaseDisabled = false,
|
||||
purchaseDisabledTooltip,
|
||||
}) => {
|
||||
const {t} = useLingui();
|
||||
const tooltipText: React.ReactNode = purchaseDisabledTooltip ?? t`Claim your account to purchase Fluxer Plutonium.`;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h2 className={styles.title}>
|
||||
@@ -54,63 +62,79 @@ export const BottomCTASection: React.FC<BottomCTASectionProps> = observer(
|
||||
<div className={styles.buttonContainer}>
|
||||
{!isGiftMode ? (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleSelectPlan('monthly')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.button}
|
||||
>
|
||||
<Trans>Monthly {monthlyPrice}</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('yearly')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.button}
|
||||
>
|
||||
<Trans>Yearly {yearlyPrice}</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('visionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
disabled={isVisionarySoldOut}
|
||||
className={styles.button}
|
||||
>
|
||||
{isVisionarySoldOut ? <Trans>Visionary Sold Out</Trans> : <Trans>Visionary {visionaryPrice}</Trans>}
|
||||
</Button>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleSelectPlan('monthly')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.button}
|
||||
disabled={purchaseDisabled}
|
||||
>
|
||||
<Trans>Monthly {monthlyPrice}</Trans>
|
||||
</Button>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('yearly')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.button}
|
||||
disabled={purchaseDisabled}
|
||||
>
|
||||
<Trans>Yearly {yearlyPrice}</Trans>
|
||||
</Button>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled || isVisionarySoldOut} tooltipText={tooltipText}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('visionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled || isVisionarySoldOut}
|
||||
className={styles.button}
|
||||
>
|
||||
{isVisionarySoldOut ? <Trans>Visionary Sold Out</Trans> : <Trans>Visionary {visionaryPrice}</Trans>}
|
||||
</Button>
|
||||
</PurchaseDisabledWrapper>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleSelectPlan('gift1Year')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.button}
|
||||
>
|
||||
<Trans>1 Year {yearlyPrice}</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('gift1Month')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.button}
|
||||
>
|
||||
<Trans>1 Month {monthlyPrice}</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('giftVisionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
disabled={isVisionarySoldOut}
|
||||
className={styles.button}
|
||||
>
|
||||
{isVisionarySoldOut ? (
|
||||
<Trans>Visionary Gift Sold Out</Trans>
|
||||
) : (
|
||||
<Trans>Visionary {visionaryPrice}</Trans>
|
||||
)}
|
||||
</Button>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleSelectPlan('gift1Year')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.button}
|
||||
disabled={purchaseDisabled}
|
||||
>
|
||||
<Trans>1 Year {yearlyPrice}</Trans>
|
||||
</Button>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('gift1Month')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.button}
|
||||
disabled={purchaseDisabled}
|
||||
>
|
||||
<Trans>1 Month {monthlyPrice}</Trans>
|
||||
</Button>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled || isVisionarySoldOut} tooltipText={tooltipText}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('giftVisionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled || isVisionarySoldOut}
|
||||
className={styles.button}
|
||||
>
|
||||
{isVisionarySoldOut ? (
|
||||
<Trans>Visionary Gift Sold Out</Trans>
|
||||
) : (
|
||||
<Trans>Visionary {visionaryPrice}</Trans>
|
||||
)}
|
||||
</Button>
|
||||
</PurchaseDisabledWrapper>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {PricingCard} from '../PricingCard';
|
||||
import gridStyles from '../PricingGrid.module.css';
|
||||
import {PurchaseDisclaimer} from '../PurchaseDisclaimer';
|
||||
import styles from './GiftSection.module.css';
|
||||
import {PurchaseDisabledWrapper} from './PurchaseDisabledWrapper';
|
||||
import {SectionHeader} from './SectionHeader';
|
||||
|
||||
interface GiftSectionProps {
|
||||
@@ -38,6 +39,8 @@ interface GiftSectionProps {
|
||||
loadingSlots: boolean;
|
||||
isVisionarySoldOut: boolean;
|
||||
handleSelectPlan: (plan: 'gift1Month' | 'gift1Year' | 'giftVisionary') => void;
|
||||
purchaseDisabled?: boolean;
|
||||
purchaseDisabledTooltip?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const GiftSection: React.FC<GiftSectionProps> = observer(
|
||||
@@ -51,8 +54,11 @@ export const GiftSection: React.FC<GiftSectionProps> = observer(
|
||||
loadingSlots,
|
||||
isVisionarySoldOut,
|
||||
handleSelectPlan,
|
||||
purchaseDisabled = false,
|
||||
purchaseDisabledTooltip,
|
||||
}) => {
|
||||
const {t} = useLingui();
|
||||
const tooltipText: React.ReactNode = purchaseDisabledTooltip ?? t`Claim your account to purchase Fluxer Plutonium.`;
|
||||
|
||||
return (
|
||||
<div ref={giftSectionRef}>
|
||||
@@ -65,35 +71,43 @@ export const GiftSection: React.FC<GiftSectionProps> = observer(
|
||||
/>
|
||||
<div className={gridStyles.gridWrapper}>
|
||||
<div className={gridStyles.gridThreeColumns}>
|
||||
<PricingCard
|
||||
title={t`1 Year Gift`}
|
||||
price={yearlyPrice}
|
||||
period={t`one-time purchase`}
|
||||
badge={t`Save 17%`}
|
||||
onSelect={() => handleSelectPlan('gift1Year')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
/>
|
||||
<PricingCard
|
||||
title={t`1 Month Gift`}
|
||||
price={monthlyPrice}
|
||||
period={t`one-time purchase`}
|
||||
isPopular
|
||||
onSelect={() => handleSelectPlan('gift1Month')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
/>
|
||||
<PricingCard
|
||||
title={t`Visionary Gift`}
|
||||
price={visionaryPrice}
|
||||
period={t`one-time, lifetime`}
|
||||
remainingSlots={loadingSlots ? undefined : visionarySlots?.remaining}
|
||||
onSelect={() => handleSelectPlan('giftVisionary')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={isVisionarySoldOut}
|
||||
soldOut={isVisionarySoldOut}
|
||||
/>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<PricingCard
|
||||
title={t`1 Year Gift`}
|
||||
price={yearlyPrice}
|
||||
period={t`one-time purchase`}
|
||||
badge={t`Save 17%`}
|
||||
onSelect={() => handleSelectPlan('gift1Year')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled}
|
||||
/>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<PricingCard
|
||||
title={t`1 Month Gift`}
|
||||
price={monthlyPrice}
|
||||
period={t`one-time purchase`}
|
||||
isPopular
|
||||
onSelect={() => handleSelectPlan('gift1Month')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled}
|
||||
/>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled || isVisionarySoldOut} tooltipText={tooltipText}>
|
||||
<PricingCard
|
||||
title={t`Visionary Gift`}
|
||||
price={visionaryPrice}
|
||||
period={t`one-time, lifetime`}
|
||||
remainingSlots={loadingSlots ? undefined : visionarySlots?.remaining}
|
||||
onSelect={() => handleSelectPlan('giftVisionary')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled || isVisionarySoldOut}
|
||||
soldOut={isVisionarySoldOut}
|
||||
/>
|
||||
</PurchaseDisabledWrapper>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.footerContainer}>
|
||||
|
||||
@@ -27,6 +27,7 @@ import gridStyles from '../PricingGrid.module.css';
|
||||
import {PurchaseDisclaimer} from '../PurchaseDisclaimer';
|
||||
import {ToggleButton} from '../ToggleButton';
|
||||
import styles from './PricingSection.module.css';
|
||||
import {PurchaseDisabledWrapper} from './PurchaseDisabledWrapper';
|
||||
|
||||
interface PricingSectionProps {
|
||||
isGiftMode: boolean;
|
||||
@@ -39,6 +40,8 @@ interface PricingSectionProps {
|
||||
loadingSlots: boolean;
|
||||
isVisionarySoldOut: boolean;
|
||||
handleSelectPlan: (plan: 'monthly' | 'yearly' | 'visionary' | 'gift1Month' | 'gift1Year' | 'giftVisionary') => void;
|
||||
purchaseDisabled?: boolean;
|
||||
purchaseDisabledTooltip?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const PricingSection: React.FC<PricingSectionProps> = observer(
|
||||
@@ -53,8 +56,11 @@ export const PricingSection: React.FC<PricingSectionProps> = observer(
|
||||
loadingSlots,
|
||||
isVisionarySoldOut,
|
||||
handleSelectPlan,
|
||||
purchaseDisabled = false,
|
||||
purchaseDisabledTooltip,
|
||||
}) => {
|
||||
const {t} = useLingui();
|
||||
const tooltipText: React.ReactNode = purchaseDisabledTooltip ?? t`Claim your account to purchase Fluxer Plutonium.`;
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
@@ -67,65 +73,81 @@ export const PricingSection: React.FC<PricingSectionProps> = observer(
|
||||
<div className={gridStyles.gridThreeColumns}>
|
||||
{!isGiftMode ? (
|
||||
<>
|
||||
<PricingCard
|
||||
title={t`Monthly`}
|
||||
price={monthlyPrice}
|
||||
period={t`per month`}
|
||||
onSelect={() => handleSelectPlan('monthly')}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
/>
|
||||
<PricingCard
|
||||
title={t`Yearly`}
|
||||
price={yearlyPrice}
|
||||
period={t`per year`}
|
||||
badge={t`Save 17%`}
|
||||
isPopular
|
||||
onSelect={() => handleSelectPlan('yearly')}
|
||||
buttonText={t`Upgrade Now`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
/>
|
||||
<PricingCard
|
||||
title={t`Visionary`}
|
||||
price={visionaryPrice}
|
||||
period={t`one-time, lifetime`}
|
||||
remainingSlots={loadingSlots ? undefined : visionarySlots?.remaining}
|
||||
onSelect={() => handleSelectPlan('visionary')}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={isVisionarySoldOut}
|
||||
soldOut={isVisionarySoldOut}
|
||||
/>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<PricingCard
|
||||
title={t`Monthly`}
|
||||
price={monthlyPrice}
|
||||
period={t`per month`}
|
||||
onSelect={() => handleSelectPlan('monthly')}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled}
|
||||
/>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<PricingCard
|
||||
title={t`Yearly`}
|
||||
price={yearlyPrice}
|
||||
period={t`per year`}
|
||||
badge={t`Save 17%`}
|
||||
isPopular
|
||||
onSelect={() => handleSelectPlan('yearly')}
|
||||
buttonText={t`Upgrade Now`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled}
|
||||
/>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled || isVisionarySoldOut} tooltipText={tooltipText}>
|
||||
<PricingCard
|
||||
title={t`Visionary`}
|
||||
price={visionaryPrice}
|
||||
period={t`one-time, lifetime`}
|
||||
remainingSlots={loadingSlots ? undefined : visionarySlots?.remaining}
|
||||
onSelect={() => handleSelectPlan('visionary')}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled || isVisionarySoldOut}
|
||||
soldOut={isVisionarySoldOut}
|
||||
/>
|
||||
</PurchaseDisabledWrapper>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PricingCard
|
||||
title={t`1 Year Gift`}
|
||||
price={yearlyPrice}
|
||||
period={t`one-time purchase`}
|
||||
badge={t`Save 17%`}
|
||||
onSelect={() => handleSelectPlan('gift1Year')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
/>
|
||||
<PricingCard
|
||||
title={t`1 Month Gift`}
|
||||
price={monthlyPrice}
|
||||
period={t`one-time purchase`}
|
||||
isPopular
|
||||
onSelect={() => handleSelectPlan('gift1Month')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
/>
|
||||
<PricingCard
|
||||
title={t`Visionary Gift`}
|
||||
price={visionaryPrice}
|
||||
period={t`one-time, lifetime`}
|
||||
remainingSlots={loadingSlots ? undefined : visionarySlots?.remaining}
|
||||
onSelect={() => handleSelectPlan('giftVisionary')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={isVisionarySoldOut}
|
||||
soldOut={isVisionarySoldOut}
|
||||
/>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<PricingCard
|
||||
title={t`1 Year Gift`}
|
||||
price={yearlyPrice}
|
||||
period={t`one-time purchase`}
|
||||
badge={t`Save 17%`}
|
||||
onSelect={() => handleSelectPlan('gift1Year')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled}
|
||||
/>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled} tooltipText={tooltipText}>
|
||||
<PricingCard
|
||||
title={t`1 Month Gift`}
|
||||
price={monthlyPrice}
|
||||
period={t`one-time purchase`}
|
||||
isPopular
|
||||
onSelect={() => handleSelectPlan('gift1Month')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled}
|
||||
/>
|
||||
</PurchaseDisabledWrapper>
|
||||
<PurchaseDisabledWrapper disabled={purchaseDisabled || isVisionarySoldOut} tooltipText={tooltipText}>
|
||||
<PricingCard
|
||||
title={t`Visionary Gift`}
|
||||
price={visionaryPrice}
|
||||
period={t`one-time, lifetime`}
|
||||
remainingSlots={loadingSlots ? undefined : visionarySlots?.remaining}
|
||||
onSelect={() => handleSelectPlan('giftVisionary')}
|
||||
buttonText={t`Buy Gift`}
|
||||
isLoading={loadingCheckout || loadingSlots}
|
||||
disabled={purchaseDisabled || isVisionarySoldOut}
|
||||
soldOut={isVisionarySoldOut}
|
||||
/>
|
||||
</PurchaseDisabledWrapper>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
|
||||
|
||||
interface PurchaseDisabledWrapperProps {
|
||||
disabled: boolean;
|
||||
tooltipText: React.ReactNode;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export const PurchaseDisabledWrapper: React.FC<PurchaseDisabledWrapperProps> = ({disabled, tooltipText, children}) => {
|
||||
if (!disabled) return children;
|
||||
|
||||
const tooltipContent = typeof tooltipText === 'function' ? (tooltipText as () => React.ReactNode) : () => tooltipText;
|
||||
|
||||
return (
|
||||
<Tooltip text={tooltipContent}>
|
||||
<div>{children}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -17,12 +17,13 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {DotsThreeIcon} from '@phosphor-icons/react';
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
|
||||
import type {UserRecord} from '~/records/UserRecord';
|
||||
import {PerksButton} from '../PerksButton';
|
||||
import type {GracePeriodInfo} from './hooks/useSubscriptionStatus';
|
||||
@@ -61,6 +62,8 @@ interface SubscriptionCardProps {
|
||||
handleCancelSubscription: () => void;
|
||||
handleCommunityButtonPointerDown: (event: React.PointerEvent) => void;
|
||||
handleCommunityButtonClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
purchaseDisabled?: boolean;
|
||||
purchaseDisabledTooltip?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SubscriptionCard: React.FC<SubscriptionCardProps> = observer(
|
||||
@@ -97,9 +100,25 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = observer(
|
||||
handleCancelSubscription,
|
||||
handleCommunityButtonPointerDown,
|
||||
handleCommunityButtonClick,
|
||||
purchaseDisabled = false,
|
||||
purchaseDisabledTooltip,
|
||||
}) => {
|
||||
const {t} = useLingui();
|
||||
const {isInGracePeriod, isExpired: isFullyExpired, graceEndDate} = gracePeriodInfo;
|
||||
const isPremium = currentUser.isPremium();
|
||||
const tooltipText: string | (() => React.ReactNode) =
|
||||
purchaseDisabledTooltip != null
|
||||
? () => purchaseDisabledTooltip
|
||||
: t`Claim your account to purchase or redeem Fluxer Plutonium.`;
|
||||
|
||||
const wrapIfDisabled = (element: React.ReactElement, key: string, disabled: boolean) =>
|
||||
disabled ? (
|
||||
<Tooltip key={key} text={tooltipText}>
|
||||
<div>{element}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
element
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.card, subscriptionCardColorClass)}>
|
||||
@@ -251,44 +270,63 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = observer(
|
||||
<div className={styles.actions}>
|
||||
{isGiftSubscription ? (
|
||||
<>
|
||||
<Button variant="inverted" onClick={navigateToRedeemGift} small className={styles.actionButton}>
|
||||
<Trans>Redeem Gift Code</Trans>
|
||||
</Button>
|
||||
{!isVisionary && !isVisionarySoldOut && (
|
||||
{wrapIfDisabled(
|
||||
<Button
|
||||
variant="inverted"
|
||||
onClick={() => handleSelectPlan('visionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
onClick={navigateToRedeemGift}
|
||||
small
|
||||
className={styles.actionButton}
|
||||
disabled={purchaseDisabled}
|
||||
>
|
||||
<Trans>Upgrade to Visionary</Trans>
|
||||
</Button>
|
||||
<Trans>Redeem Gift Code</Trans>
|
||||
</Button>,
|
||||
'redeem-gift',
|
||||
purchaseDisabled,
|
||||
)}
|
||||
{!isVisionary &&
|
||||
!isVisionarySoldOut &&
|
||||
wrapIfDisabled(
|
||||
<Button
|
||||
variant="inverted"
|
||||
onClick={() => handleSelectPlan('visionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
small
|
||||
className={styles.actionButton}
|
||||
disabled={purchaseDisabled}
|
||||
>
|
||||
<Trans>Upgrade to Visionary</Trans>
|
||||
</Button>,
|
||||
'upgrade-gift-visionary',
|
||||
purchaseDisabled,
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{hasEverPurchased && (
|
||||
<Button
|
||||
variant="inverted"
|
||||
onClick={shouldUseReactivateQuickAction ? handleReactivateSubscription : handleOpenCustomerPortal}
|
||||
submitting={shouldUseReactivateQuickAction ? loadingReactivate : loadingPortal}
|
||||
small
|
||||
className={styles.actionButton}
|
||||
>
|
||||
{isFullyExpired ? (
|
||||
<Trans>Resubscribe</Trans>
|
||||
) : isInGracePeriod ? (
|
||||
<Trans>Resubscribe</Trans>
|
||||
) : premiumWillCancel ? (
|
||||
<Trans>Reactivate</Trans>
|
||||
) : isVisionary ? (
|
||||
<Trans>Open Customer Portal</Trans>
|
||||
) : (
|
||||
<Trans>Manage Subscription</Trans>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{hasEverPurchased &&
|
||||
wrapIfDisabled(
|
||||
<Button
|
||||
variant="inverted"
|
||||
onClick={shouldUseReactivateQuickAction ? handleReactivateSubscription : handleOpenCustomerPortal}
|
||||
submitting={shouldUseReactivateQuickAction ? loadingReactivate : loadingPortal}
|
||||
small
|
||||
className={styles.actionButton}
|
||||
disabled={purchaseDisabled && shouldUseReactivateQuickAction}
|
||||
>
|
||||
{isFullyExpired ? (
|
||||
<Trans>Resubscribe</Trans>
|
||||
) : isInGracePeriod ? (
|
||||
<Trans>Resubscribe</Trans>
|
||||
) : premiumWillCancel ? (
|
||||
<Trans>Reactivate</Trans>
|
||||
) : isVisionary ? (
|
||||
<Trans>Open Customer Portal</Trans>
|
||||
) : (
|
||||
<Trans>Manage Subscription</Trans>
|
||||
)}
|
||||
</Button>,
|
||||
'manage-reactivate',
|
||||
purchaseDisabled && shouldUseReactivateQuickAction,
|
||||
)}
|
||||
|
||||
{isVisionary && (
|
||||
<Button
|
||||
@@ -305,17 +343,22 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = observer(
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isVisionary && !isVisionarySoldOut && (
|
||||
<Button
|
||||
variant="inverted"
|
||||
onClick={() => handleSelectPlan('visionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
small
|
||||
className={styles.actionButton}
|
||||
>
|
||||
<Trans>Upgrade to Visionary</Trans>
|
||||
</Button>
|
||||
)}
|
||||
{!isVisionary &&
|
||||
!isVisionarySoldOut &&
|
||||
wrapIfDisabled(
|
||||
<Button
|
||||
variant="inverted"
|
||||
onClick={() => handleSelectPlan('visionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
small
|
||||
className={styles.actionButton}
|
||||
disabled={purchaseDisabled}
|
||||
>
|
||||
<Trans>Upgrade to Visionary</Trans>
|
||||
</Button>,
|
||||
'upgrade-visionary',
|
||||
purchaseDisabled,
|
||||
)}
|
||||
|
||||
{shouldUseCancelQuickAction && (
|
||||
<Button
|
||||
|
||||
@@ -23,6 +23,7 @@ import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import type {VisionarySlots} from '~/actions/PremiumActionCreators';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
|
||||
import {VisionaryBenefit} from '../VisionaryBenefit';
|
||||
import {SectionHeader} from './SectionHeader';
|
||||
import styles from './VisionarySection.module.css';
|
||||
@@ -36,6 +37,8 @@ interface VisionarySectionProps {
|
||||
loadingCheckout: boolean;
|
||||
loadingSlots: boolean;
|
||||
handleSelectPlan: (plan: 'visionary') => void;
|
||||
purchaseDisabled?: boolean;
|
||||
purchaseDisabledTooltip?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const VisionarySection: React.FC<VisionarySectionProps> = observer(
|
||||
@@ -48,9 +51,15 @@ export const VisionarySection: React.FC<VisionarySectionProps> = observer(
|
||||
loadingCheckout,
|
||||
loadingSlots,
|
||||
handleSelectPlan,
|
||||
purchaseDisabled = false,
|
||||
purchaseDisabledTooltip,
|
||||
}) => {
|
||||
const {t} = useLingui();
|
||||
const currentAccessLabel = isGiftSubscription ? t`gift time` : t`subscription`;
|
||||
const tooltipText: string | (() => React.ReactNode) =
|
||||
purchaseDisabledTooltip != null
|
||||
? () => purchaseDisabledTooltip
|
||||
: t`Claim your account to purchase Fluxer Plutonium.`;
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
@@ -99,15 +108,32 @@ export const VisionarySection: React.FC<VisionarySectionProps> = observer(
|
||||
|
||||
{!isVisionary && visionarySlots && visionarySlots.remaining > 0 && (
|
||||
<div className={styles.ctaContainer}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('visionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.ctaButton}
|
||||
>
|
||||
<CrownIcon className={styles.ctaIcon} weight="fill" />
|
||||
<Trans>Upgrade to Visionary — {formatter.format(visionarySlots.remaining)} Left</Trans>
|
||||
</Button>
|
||||
{purchaseDisabled ? (
|
||||
<Tooltip text={tooltipText}>
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('visionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.ctaButton}
|
||||
disabled
|
||||
>
|
||||
<CrownIcon className={styles.ctaIcon} weight="fill" />
|
||||
<Trans>Upgrade to Visionary — {formatter.format(visionarySlots.remaining)} Left</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSelectPlan('visionary')}
|
||||
submitting={loadingCheckout || loadingSlots}
|
||||
className={styles.ctaButton}
|
||||
>
|
||||
<CrownIcon className={styles.ctaIcon} weight="fill" />
|
||||
<Trans>Upgrade to Visionary — {formatter.format(visionarySlots.remaining)} Left</Trans>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isPremium && (
|
||||
<p className={styles.disclaimer}>
|
||||
|
||||
@@ -54,6 +54,7 @@ import GuildStore from '~/stores/GuildStore';
|
||||
import PermissionStore from '~/stores/PermissionStore';
|
||||
import RelationshipStore from '~/stores/RelationshipStore';
|
||||
import UserProfileMobileStore from '~/stores/UserProfileMobileStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import * as CallUtils from '~/utils/CallUtils';
|
||||
import * as NicknameUtils from '~/utils/NicknameUtils';
|
||||
import * as PermissionUtils from '~/utils/PermissionUtils';
|
||||
@@ -74,6 +75,7 @@ export const GuildMemberActionsSheet: FC<GuildMemberActionsSheetProps> = observe
|
||||
const currentUserId = AuthenticationStore.currentUserId;
|
||||
const isCurrentUser = user.id === currentUserId;
|
||||
const isBot = user.bot;
|
||||
const currentUserUnclaimed = !(UserStore.currentUser?.isClaimed() ?? true);
|
||||
|
||||
const relationship = RelationshipStore.getRelationship(user.id);
|
||||
const relationshipType = relationship?.type;
|
||||
@@ -262,7 +264,8 @@ export const GuildMemberActionsSheet: FC<GuildMemberActionsSheetProps> = observe
|
||||
});
|
||||
} else if (
|
||||
relationshipType !== RelationshipTypes.OUTGOING_REQUEST &&
|
||||
relationshipType !== RelationshipTypes.BLOCKED
|
||||
relationshipType !== RelationshipTypes.BLOCKED &&
|
||||
!currentUserUnclaimed
|
||||
) {
|
||||
relationshipItems.push({
|
||||
icon: <UserPlusIcon className={styles.icon} />,
|
||||
|
||||
@@ -36,13 +36,16 @@ interface StatusSlateProps {
|
||||
description: React.ReactNode;
|
||||
actions?: Array<StatusAction>;
|
||||
fullHeight?: boolean;
|
||||
iconClassName?: string;
|
||||
iconStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const StatusSlate: React.FC<StatusSlateProps> = observer(
|
||||
({Icon, title, description, actions = [], fullHeight = false}) => {
|
||||
({Icon, title, description, actions = [], fullHeight = false, iconClassName, iconStyle}) => {
|
||||
const iconClass = [styles.icon, iconClassName].filter(Boolean).join(' ');
|
||||
return (
|
||||
<div className={`${styles.container} ${fullHeight ? styles.fullHeight : ''}`}>
|
||||
<Icon className={styles.icon} aria-hidden />
|
||||
<Icon className={iconClass} style={iconStyle} aria-hidden />
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
<p className={styles.description}>{description}</p>
|
||||
{actions.length > 0 && (
|
||||
|
||||
@@ -100,3 +100,7 @@
|
||||
border-top: 1px solid var(--background-header-secondary);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.claimButton {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {ClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {EmailChangeModal} from '~/components/modals/EmailChangeModal';
|
||||
import {PasswordChangeModal} from '~/components/modals/PasswordChangeModal';
|
||||
import {SettingsTabSection} from '~/components/modals/shared/SettingsTabLayout';
|
||||
@@ -94,7 +94,7 @@ export const AccountTabContent: React.FC<AccountTabProps> = observer(
|
||||
<Trans>No email address set</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<Button small={true} onClick={() => ModalActionCreators.push(modal(() => <ClaimAccountModal />))}>
|
||||
<Button small={true} className={styles.claimButton} fitContent onClick={() => openClaimAccountModal()}>
|
||||
<Trans>Add Email</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -134,7 +134,7 @@ export const AccountTabContent: React.FC<AccountTabProps> = observer(
|
||||
<Trans>No password set</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<Button small={true} onClick={() => ModalActionCreators.push(modal(() => <ClaimAccountModal />))}>
|
||||
<Button small={true} className={styles.claimButton} fitContent onClick={() => openClaimAccountModal()}>
|
||||
<Trans>Set Password</Trans>
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -96,3 +96,7 @@
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.claimButton {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as UserActionCreators from '~/actions/UserActionCreators';
|
||||
import {BackupCodesViewModal} from '~/components/modals/BackupCodesViewModal';
|
||||
import {ClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {ConfirmModal} from '~/components/modals/ConfirmModal';
|
||||
import {MfaTotpDisableModal} from '~/components/modals/MfaTotpDisableModal';
|
||||
import {MfaTotpEnableModal} from '~/components/modals/MfaTotpEnableModal';
|
||||
@@ -202,7 +202,7 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
<Trans>Claim your account to access security features like two-factor authentication and passkeys.</Trans>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => ModalActionCreators.push(modal(() => <ClaimAccountModal />))}>
|
||||
<Button className={styles.claimButton} fitContent onClick={() => openClaimAccountModal()}>
|
||||
<Trans>Claim Account</Trans>
|
||||
</Button>
|
||||
</SettingsTabSection>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {BookOpenIcon, WarningCircleIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
@@ -38,12 +38,16 @@ import styles from '~/components/modals/tabs/ApplicationsTab/ApplicationsTab.mod
|
||||
import ApplicationsTabStore from '~/components/modals/tabs/ApplicationsTab/ApplicationsTabStore';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {Spinner} from '~/components/uikit/Spinner';
|
||||
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
|
||||
import type {DeveloperApplication} from '~/records/DeveloperApplicationRecord';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
|
||||
const ApplicationsTab: React.FC = observer(() => {
|
||||
const {t} = useLingui();
|
||||
const {checkUnsavedChanges} = useUnsavedChangesFlash('applications');
|
||||
const {setContentKey} = useSettingsContentKey();
|
||||
const store = ApplicationsTabStore;
|
||||
const isUnclaimed = !(UserStore.currentUser?.isClaimed() ?? false);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
setContentKey(store.contentKey);
|
||||
@@ -138,9 +142,19 @@ const ApplicationsTab: React.FC = observer(() => {
|
||||
description={<Trans>Create and manage applications and bots for your account.</Trans>}
|
||||
>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button variant="primary" fitContainer={false} fitContent onClick={openCreateModal}>
|
||||
<Trans>Create Application</Trans>
|
||||
</Button>
|
||||
{isUnclaimed ? (
|
||||
<Tooltip text={t`Claim your account to create applications.`}>
|
||||
<div>
|
||||
<Button variant="primary" fitContainer={false} fitContent onClick={openCreateModal} disabled>
|
||||
<Trans>Create Application</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button variant="primary" fitContainer={false} fitContent onClick={openCreateModal}>
|
||||
<Trans>Create Application</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<a className={styles.documentationLink} href="https://fluxer.dev" target="_blank" rel="noreferrer">
|
||||
<BookOpenIcon weight="fill" size={18} className={styles.documentationIcon} />
|
||||
<Trans>Read the Documentation (fluxer.dev)</Trans>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as BetaCodeActionCreators from '~/actions/BetaCodeActionCreators';
|
||||
import * as TextCopyActionCreators from '~/actions/TextCopyActionCreators';
|
||||
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {
|
||||
SettingsTabContainer,
|
||||
SettingsTabContent,
|
||||
@@ -39,6 +40,7 @@ import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
|
||||
import type {BetaCodeRecord} from '~/records/BetaCodeRecord';
|
||||
import BetaCodeStore from '~/stores/BetaCodeStore';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import * as DateUtils from '~/utils/DateUtils';
|
||||
import styles from './BetaCodesTab.module.css';
|
||||
|
||||
@@ -247,10 +249,12 @@ const BetaCodesTab: React.FC = observer(() => {
|
||||
const fetchStatus = BetaCodeStore.fetchStatus;
|
||||
const allowance = BetaCodeStore.allowance;
|
||||
const nextResetAt = BetaCodeStore.nextResetAt;
|
||||
const isClaimed = UserStore.currentUser?.isClaimed() ?? false;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isClaimed) return;
|
||||
BetaCodeActionCreators.fetch();
|
||||
}, []);
|
||||
}, [isClaimed]);
|
||||
|
||||
const sortedBetaCodes = React.useMemo(() => {
|
||||
return [...betaCodes].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
@@ -280,6 +284,27 @@ const BetaCodesTab: React.FC = observer(() => {
|
||||
return i18n._(msg`${allowance} codes remaining this week`);
|
||||
}, [allowance, nextResetAt, i18n]);
|
||||
|
||||
if (!isClaimed) {
|
||||
return (
|
||||
<SettingsTabContainer>
|
||||
<SettingsTabContent>
|
||||
<StatusSlate
|
||||
Icon={TicketIcon}
|
||||
title={<Trans>Claim your account</Trans>}
|
||||
description={<Trans>Claim your account to generate beta codes.</Trans>}
|
||||
actions={[
|
||||
{
|
||||
text: <Trans>Claim Account</Trans>,
|
||||
onClick: () => openClaimAccountModal({force: true}),
|
||||
variant: 'primary',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SettingsTabContent>
|
||||
</SettingsTabContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (fetchStatus === 'pending' || fetchStatus === 'idle') {
|
||||
return (
|
||||
<div className={styles.spinnerContainer}>
|
||||
|
||||
@@ -24,7 +24,7 @@ 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 {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {KeyboardModeIntroModal} from '~/components/modals/KeyboardModeIntroModal';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import type {GatewaySocket} from '~/lib/GatewaySocket';
|
||||
@@ -67,7 +67,7 @@ export const ToolsTabContent: React.FC<ToolsTabContentProps> = observer(({socket
|
||||
}, []);
|
||||
|
||||
const handleOpenClaimAccountModal = useCallback(() => {
|
||||
ModalActionCreators.push(ModalActionCreators.modal(() => <ClaimAccountModal />));
|
||||
openClaimAccountModal({force: true});
|
||||
}, []);
|
||||
|
||||
if (shouldCrash) {
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--background-header-secondary);
|
||||
@@ -117,6 +118,8 @@
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -129,15 +132,39 @@
|
||||
}
|
||||
|
||||
.authSessionLocation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary-muted);
|
||||
min-width: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.locationText {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.locationSeparator {
|
||||
background-color: var(--background-modifier-accent);
|
||||
width: 0.25rem;
|
||||
height: 0.25rem;
|
||||
border-radius: 9999px;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
margin: 0 0.15rem;
|
||||
}
|
||||
|
||||
.lastUsed {
|
||||
font-size: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.authSessionActions {
|
||||
@@ -228,17 +255,11 @@
|
||||
}
|
||||
|
||||
.devicesGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.devicesGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.logoutSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -115,10 +115,10 @@ const AuthSession: React.FC<AuthSessionProps> = observer(
|
||||
</span>
|
||||
|
||||
<div className={styles.authSessionLocation}>
|
||||
{authSession.clientLocation}
|
||||
<span className={styles.locationText}>{authSession.clientLocation}</span>
|
||||
{!isCurrent && (
|
||||
<>
|
||||
<StatusDot />
|
||||
<span aria-hidden className={styles.locationSeparator} />
|
||||
<span className={styles.lastUsed}>
|
||||
{DateUtils.getShortRelativeDateString(authSession.approxLastUsedAt ?? new Date(0))}
|
||||
</span>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {CaretDownIcon, CheckIcon, CopyIcon, GiftIcon, NetworkSlashIcon} from '@phosphor-icons/react';
|
||||
import {CaretDownIcon, CheckIcon, CopyIcon, GiftIcon, NetworkSlashIcon, WarningCircleIcon} from '@phosphor-icons/react';
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
@@ -31,6 +31,7 @@ import * as UserActionCreators from '~/actions/UserActionCreators';
|
||||
import {UserPremiumTypes} from '~/Constants';
|
||||
import {Form} from '~/components/form/Form';
|
||||
import {Input} from '~/components/form/Input';
|
||||
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {StatusSlate} from '~/components/modals/shared/StatusSlate';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {Spinner} from '~/components/uikit/Spinner';
|
||||
@@ -180,6 +181,7 @@ const GiftInventoryTab: React.FC = observer(() => {
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState(false);
|
||||
const [expandedGiftId, setExpandedGiftId] = React.useState<string | null>(null);
|
||||
const isUnclaimed = !(UserStore.currentUser?.isClaimed() ?? false);
|
||||
|
||||
const giftCodeForm = useForm<GiftCodeFormInputs>({defaultValues: {code: ''}});
|
||||
|
||||
@@ -201,6 +203,10 @@ const GiftInventoryTab: React.FC = observer(() => {
|
||||
});
|
||||
|
||||
const fetchGifts = React.useCallback(async () => {
|
||||
if (isUnclaimed) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError(false);
|
||||
const userGifts = await GiftActionCreators.fetchUserGifts();
|
||||
@@ -211,7 +217,7 @@ const GiftInventoryTab: React.FC = observer(() => {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [isUnclaimed]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchGifts();
|
||||
@@ -225,6 +231,23 @@ const GiftInventoryTab: React.FC = observer(() => {
|
||||
fetchGifts();
|
||||
};
|
||||
|
||||
if (isUnclaimed) {
|
||||
return (
|
||||
<StatusSlate
|
||||
Icon={WarningCircleIcon}
|
||||
title={<Trans>Claim your account</Trans>}
|
||||
description={<Trans>Claim your account to redeem or manage Plutonium gift codes.</Trans>}
|
||||
actions={[
|
||||
{
|
||||
text: <Trans>Claim Account</Trans>,
|
||||
onClick: () => openClaimAccountModal({force: true}),
|
||||
variant: 'primary',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user