mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
feat(premium): let self-hosted instances sell premium and gifts (#3025)
This commit is contained in:
1 parent
192cec689a
commit
3736d94d73
159 files changed
+15802
-9067
No files matched your search
@@ -92,3 +92,17 @@
|
||||
.comparisonTableContainer {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.redeemSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.redeemDescription {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.375rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {SelfServeRefundSection} from '@app/features/app/components/dialogs/compo
|
||||
import {SubscriptionCard} from '@app/features/app/components/dialogs/components/plutonium/SubscriptionCard';
|
||||
import {PREMIUM_PRODUCT_FULL_NAME, PREMIUM_PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import GeoIP from '@app/features/app/state/GeoIP';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import Guilds from '@app/features/guild/state/Guilds';
|
||||
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
||||
import * as PremiumCommands from '@app/features/premium/commands/PremiumCommands';
|
||||
@@ -29,6 +30,13 @@ import {
|
||||
FREE_VS_PREMIUM_DESCRIPTOR,
|
||||
VERIFY_EMAIL_TO_PURCHASE_PREMIUM_DESCRIPTOR,
|
||||
} from '@app/features/premium/utils/PremiumMessageDescriptors';
|
||||
import {
|
||||
areGiftPurchasesAvailable,
|
||||
arePremiumPurchasesAvailable,
|
||||
canServiceStripeSubscriptions,
|
||||
shouldShowPremiumFeatures,
|
||||
} from '@app/features/premium/utils/PremiumUtils';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import MobileLayout from '@app/features/ui/state/MobileLayout';
|
||||
@@ -97,6 +105,13 @@ export const PlutoniumContent = observer(({defaultGiftMode = false}: PlutoniumCo
|
||||
const isClaimed = currentUser?.isClaimed() ?? false;
|
||||
const isEmailVerified = currentUser?.verified === true;
|
||||
const purchaseDisabled = !isClaimed || !isEmailVerified;
|
||||
const userPurchaseDisabled = premiumState?.effective.premium_purchase_disabled === true;
|
||||
const purchasesAvailable = arePremiumPurchasesAvailable(userPurchaseDisabled);
|
||||
const giftPurchasesAvailable = areGiftPurchasesAvailable(priceIds, userPurchaseDisabled);
|
||||
const hasBillingRelationship = subscriptionStatus.hasEverPurchased || premiumState?.billing.subscription != null;
|
||||
const billingUnavailable =
|
||||
!shouldShowPremiumFeatures() ||
|
||||
(!arePremiumPurchasesAvailable() && !(hasBillingRelationship && canServiceStripeSubscriptions()));
|
||||
const purchaseDisabledTooltip = !isClaimed
|
||||
? i18n._(CLAIM_ACCOUNT_TO_PURCHASE_PREMIUM_DESCRIPTOR, {premiumProductFullName: PREMIUM_PRODUCT_FULL_NAME})
|
||||
: i18n._(VERIFY_EMAIL_TO_PURCHASE_PREMIUM_DESCRIPTOR, {premiumProductFullName: PREMIUM_PRODUCT_FULL_NAME});
|
||||
@@ -162,16 +177,18 @@ export const PlutoniumContent = observer(({defaultGiftMode = false}: PlutoniumCo
|
||||
return (
|
||||
<div className={styles.giftModeContainer} data-flx="app.plutonium-content.gift-mode-container">
|
||||
<PlutoniumUpsellBanner data-flx="app.plutonium-content.plutonium-upsell-banner" />
|
||||
<GiftSection
|
||||
giftSectionRef={giftSectionRef}
|
||||
giftMonthlyPrice={giftMonthlyPrice}
|
||||
giftYearlyPrice={giftYearlyPrice}
|
||||
loadingCheckout={loadingCheckout}
|
||||
handleSelectPlan={handleSelectPlanGuarded}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
data-flx="app.plutonium-content.gift-section"
|
||||
/>
|
||||
{giftPurchasesAvailable && (
|
||||
<GiftSection
|
||||
giftSectionRef={giftSectionRef}
|
||||
giftMonthlyPrice={giftMonthlyPrice}
|
||||
giftYearlyPrice={giftYearlyPrice}
|
||||
loadingCheckout={loadingCheckout}
|
||||
handleSelectPlan={handleSelectPlanGuarded}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
data-flx="app.plutonium-content.gift-section"
|
||||
/>
|
||||
)}
|
||||
<div ref={perksSectionRef} data-flx="app.plutonium-content.div">
|
||||
<section className={styles.perksSection} data-flx="app.plutonium-content.perks-section">
|
||||
<SectionHeader
|
||||
@@ -246,14 +263,17 @@ export const PlutoniumContent = observer(({defaultGiftMode = false}: PlutoniumCo
|
||||
handleCommunityButtonClick={handleCommunityButtonClick}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
billingUnavailable={billingUnavailable}
|
||||
data-flx="app.plutonium-content.subscription-card"
|
||||
/>
|
||||
<div className={styles.disclaimerContainer} data-flx="app.plutonium-content.disclaimer-container">
|
||||
<PurchaseDisclaimer align="center" isPremium data-flx="app.plutonium-content.purchase-disclaimer" />
|
||||
</div>
|
||||
{purchasesAvailable && (
|
||||
<div className={styles.disclaimerContainer} data-flx="app.plutonium-content.disclaimer-container">
|
||||
<PurchaseDisclaimer align="center" isPremium data-flx="app.plutonium-content.purchase-disclaimer" />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
{subscriptionStatus.hasEverPurchased && (
|
||||
{subscriptionStatus.hasEverPurchased && !billingUnavailable && (
|
||||
<>
|
||||
<PurchaseHistorySection
|
||||
premiumState={premiumState}
|
||||
@@ -261,16 +281,37 @@ export const PlutoniumContent = observer(({defaultGiftMode = false}: PlutoniumCo
|
||||
handleOpenCustomerPortal={handleOpenCustomerPortal}
|
||||
data-flx="app.plutonium-content.purchase-history-section"
|
||||
/>
|
||||
<SelfServeRefundSection
|
||||
eligibility={premiumState?.billing.refund_eligibility ?? null}
|
||||
refreshPremiumState={() => PremiumCommands.refreshPremiumState(countryCode ?? undefined)}
|
||||
data-flx="app.plutonium-content.self-serve-refund-section"
|
||||
/>
|
||||
{!RuntimeConfig.isSelfHosted() && (
|
||||
<SelfServeRefundSection
|
||||
eligibility={premiumState?.billing.refund_eligibility ?? null}
|
||||
refreshPremiumState={() => PremiumCommands.refreshPremiumState(countryCode ?? undefined)}
|
||||
data-flx="app.plutonium-content.self-serve-refund-section"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!subscriptionStatus.shouldShowPremiumCard ? (
|
||||
{!purchasesAvailable ? (
|
||||
!subscriptionStatus.shouldShowPremiumCard && (
|
||||
<section className={styles.redeemSection} data-flx="app.plutonium-content.redeem-section">
|
||||
<p className={styles.redeemDescription} data-flx="app.plutonium-content.redeem-description">
|
||||
<Trans comment="Premium page text shown on an instance where the premium tier cannot be bought, only redeemed. {PREMIUM_PRODUCT_NAME} is the premium tier name.">
|
||||
Have a gift code? Redeem it to unlock {PREMIUM_PRODUCT_NAME}.
|
||||
</Trans>
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
small
|
||||
onClick={navigateToRedeemGift}
|
||||
data-flx="app.plutonium-content.redeem-section.button.navigate-to-redeem-gift"
|
||||
>
|
||||
<Trans comment="Billing button for entering a premium gift code.">Redeem gift code</Trans>
|
||||
</Button>
|
||||
</section>
|
||||
)
|
||||
) : !subscriptionStatus.shouldShowPremiumCard ? (
|
||||
<PricingSection
|
||||
isGiftMode={isGiftMode}
|
||||
isGiftMode={isGiftMode && giftPurchasesAvailable}
|
||||
giftPurchasesAvailable={giftPurchasesAvailable}
|
||||
setIsGiftMode={setIsGiftMode}
|
||||
monthlyPrice={monthlyPrice}
|
||||
yearlyPrice={yearlyPrice}
|
||||
@@ -283,16 +324,18 @@ export const PlutoniumContent = observer(({defaultGiftMode = false}: PlutoniumCo
|
||||
data-flx="app.plutonium-content.pricing-section"
|
||||
/>
|
||||
) : (
|
||||
<GiftSection
|
||||
giftSectionRef={giftSectionRef}
|
||||
giftMonthlyPrice={giftMonthlyPrice}
|
||||
giftYearlyPrice={giftYearlyPrice}
|
||||
loadingCheckout={loadingCheckout}
|
||||
handleSelectPlan={handleSelectPlanGuarded}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
data-flx="app.plutonium-content.gift-section--2"
|
||||
/>
|
||||
giftPurchasesAvailable && (
|
||||
<GiftSection
|
||||
giftSectionRef={giftSectionRef}
|
||||
giftMonthlyPrice={giftMonthlyPrice}
|
||||
giftYearlyPrice={giftYearlyPrice}
|
||||
loadingCheckout={loadingCheckout}
|
||||
handleSelectPlan={handleSelectPlanGuarded}
|
||||
purchaseDisabled={purchaseDisabled}
|
||||
purchaseDisabledTooltip={purchaseDisabledTooltip}
|
||||
data-flx="app.plutonium-content.gift-section--2"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div ref={perksSectionRef} data-flx="app.plutonium-content.div--2">
|
||||
<section className={styles.perksSection} data-flx="app.plutonium-content.perks-section--2">
|
||||
@@ -308,9 +351,9 @@ export const PlutoniumContent = observer(({defaultGiftMode = false}: PlutoniumCo
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{!subscriptionStatus.isPremium && (
|
||||
{!subscriptionStatus.isPremium && purchasesAvailable && (
|
||||
<BottomCTASection
|
||||
isGiftMode={isGiftMode}
|
||||
isGiftMode={isGiftMode && giftPurchasesAvailable}
|
||||
monthlyPrice={monthlyPrice}
|
||||
yearlyPrice={yearlyPrice}
|
||||
giftMonthlyPrice={giftMonthlyPrice}
|
||||
|
||||
@@ -3,28 +3,68 @@
|
||||
import {Routes} from '@app/app/Routes';
|
||||
import styles from '@app/features/app/components/dialogs/components/PurchaseDisclaimer.module.css';
|
||||
import {ExternalLink} from '@app/features/app/components/shared/ExternalLink';
|
||||
import {PAYMENT_PROVIDER_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
|
||||
export const PurchaseDisclaimer = observer(
|
||||
({isPremium = false, align = 'center'}: {isPremium?: boolean; align?: 'left' | 'center'}) => {
|
||||
const terms = (
|
||||
<ExternalLink href={Routes.terms()} data-flx="app.purchase-disclaimer.external-link">
|
||||
const selfHosted = RuntimeConfig.isSelfHosted();
|
||||
const termsUrl = selfHosted ? RuntimeConfig.termsUrl : Routes.terms();
|
||||
const privacyUrl = selfHosted ? RuntimeConfig.privacyUrl : Routes.privacy();
|
||||
const terms = termsUrl ? (
|
||||
<ExternalLink href={termsUrl} data-flx="app.purchase-disclaimer.external-link">
|
||||
<Trans>Terms of service</Trans>
|
||||
</ExternalLink>
|
||||
);
|
||||
const privacy = (
|
||||
<ExternalLink href={Routes.privacy()} data-flx="app.purchase-disclaimer.external-link--2">
|
||||
) : null;
|
||||
const privacy = privacyUrl ? (
|
||||
<ExternalLink href={privacyUrl} data-flx="app.purchase-disclaimer.external-link--2">
|
||||
<Trans>Privacy policy</Trans>
|
||||
</ExternalLink>
|
||||
);
|
||||
return (
|
||||
<p
|
||||
className={clsx(styles.disclaimer, align === 'center' ? styles.center : styles.left)}
|
||||
data-flx="app.purchase-disclaimer.disclaimer"
|
||||
>
|
||||
{isPremium ? (
|
||||
) : null;
|
||||
const termsOnly = termsUrl ? (
|
||||
isPremium ? (
|
||||
<Trans comment="Purchase disclaimer when the instance links only its terms of service. The linked text is the terms of service document.">
|
||||
By purchasing, you agreed to our{' '}
|
||||
<ExternalLink href={termsUrl} data-flx="app.purchase-disclaimer.external-link--terms-only">
|
||||
Terms of service
|
||||
</ExternalLink>
|
||||
.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans comment="Purchase disclaimer when the instance links only its terms of service. The linked text is the terms of service document.">
|
||||
By purchasing, you agree to our{' '}
|
||||
<ExternalLink href={termsUrl} data-flx="app.purchase-disclaimer.external-link--terms-only">
|
||||
Terms of service
|
||||
</ExternalLink>
|
||||
.
|
||||
</Trans>
|
||||
)
|
||||
) : null;
|
||||
const privacyOnly = privacyUrl ? (
|
||||
isPremium ? (
|
||||
<Trans comment="Purchase disclaimer when the instance links only its privacy policy. The linked text is the privacy policy document.">
|
||||
By purchasing, you agreed to our{' '}
|
||||
<ExternalLink href={privacyUrl} data-flx="app.purchase-disclaimer.external-link--privacy-only">
|
||||
Privacy policy
|
||||
</ExternalLink>
|
||||
.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans comment="Purchase disclaimer when the instance links only its privacy policy. The linked text is the privacy policy document.">
|
||||
By purchasing, you agree to our{' '}
|
||||
<ExternalLink href={privacyUrl} data-flx="app.purchase-disclaimer.external-link--privacy-only">
|
||||
Privacy policy
|
||||
</ExternalLink>
|
||||
.
|
||||
</Trans>
|
||||
)
|
||||
) : null;
|
||||
const agreement =
|
||||
terms && privacy ? (
|
||||
isPremium ? (
|
||||
<Trans>
|
||||
By purchasing, you agreed to our {terms} and {privacy}.
|
||||
</Trans>
|
||||
@@ -32,13 +72,29 @@ export const PurchaseDisclaimer = observer(
|
||||
<Trans>
|
||||
By purchasing, you agree to our {terms} and {privacy}.
|
||||
</Trans>
|
||||
)}{' '}
|
||||
<Trans>
|
||||
Self-serve refunds available within 3 days of payment, once every 30 days. Refunding a subscription cancels
|
||||
it. EU/EEA buyers waive the 14-day right of withdrawal at checkout to access content immediately. Use the
|
||||
in-app refund button instead of a chargeback. Chargebacks can permanently restrict your account. Stripe
|
||||
handles payment securely. We never see your full card number.
|
||||
</Trans>
|
||||
)
|
||||
) : (
|
||||
(termsOnly ?? privacyOnly)
|
||||
);
|
||||
return (
|
||||
<p
|
||||
className={clsx(styles.disclaimer, align === 'center' ? styles.center : styles.left)}
|
||||
data-flx="app.purchase-disclaimer.disclaimer"
|
||||
>
|
||||
{agreement}
|
||||
{agreement ? ' ' : null}
|
||||
{selfHosted ? (
|
||||
<Trans comment="Purchase disclaimer on a self-hosted instance. {PAYMENT_PROVIDER_NAME} is the payment processor name.">
|
||||
{PAYMENT_PROVIDER_NAME} handles payment securely.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Self-serve refunds available within 3 days of payment, once every 30 days. Refunding a subscription cancels
|
||||
it. EU/EEA buyers waive the 14-day right of withdrawal at checkout to access content immediately. Use the
|
||||
in-app refund button instead of a chargeback. Chargebacks can permanently restrict your account. Stripe
|
||||
handles payment securely. We never see your full card number.
|
||||
</Trans>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
|
||||
+23
-19
@@ -70,6 +70,7 @@ const MESSAGE_1_MONTH_GIFT_DESCRIPTOR = msg({
|
||||
|
||||
interface PricingSectionProps {
|
||||
isGiftMode: boolean;
|
||||
giftPurchasesAvailable?: boolean;
|
||||
setIsGiftMode: (value: boolean) => void;
|
||||
monthlyPrice: string;
|
||||
yearlyPrice: string;
|
||||
@@ -84,6 +85,7 @@ interface PricingSectionProps {
|
||||
export const PricingSection: React.FC<PricingSectionProps> = observer(
|
||||
({
|
||||
isGiftMode,
|
||||
giftPurchasesAvailable = true,
|
||||
setIsGiftMode,
|
||||
monthlyPrice,
|
||||
yearlyPrice,
|
||||
@@ -100,25 +102,27 @@ export const PricingSection: React.FC<PricingSectionProps> = observer(
|
||||
i18n._(CLAIM_ACCOUNT_TO_PURCHASE_PREMIUM_DESCRIPTOR, {premiumProductFullName: PREMIUM_PRODUCT_FULL_NAME});
|
||||
return (
|
||||
<section className={styles.section} data-flx="app.plutonium.pricing-section.section">
|
||||
<div
|
||||
className={styles.toggleContainer}
|
||||
role="group"
|
||||
aria-label={i18n._(PURCHASE_MODE_DESCRIPTOR)}
|
||||
data-flx="app.plutonium.pricing-section.toggle-container"
|
||||
>
|
||||
<ToggleButton
|
||||
active={!isGiftMode}
|
||||
onClick={() => setIsGiftMode(false)}
|
||||
label={i18n._(FOR_ME_DESCRIPTOR)}
|
||||
data-flx="app.plutonium.pricing-section.toggle-button.set-is-gift-mode"
|
||||
/>
|
||||
<ToggleButton
|
||||
active={isGiftMode}
|
||||
onClick={() => setIsGiftMode(true)}
|
||||
label={i18n._(AS_A_GIFT_DESCRIPTOR)}
|
||||
data-flx="app.plutonium.pricing-section.toggle-button.set-is-gift-mode--2"
|
||||
/>
|
||||
</div>
|
||||
{giftPurchasesAvailable && (
|
||||
<div
|
||||
className={styles.toggleContainer}
|
||||
role="group"
|
||||
aria-label={i18n._(PURCHASE_MODE_DESCRIPTOR)}
|
||||
data-flx="app.plutonium.pricing-section.toggle-container"
|
||||
>
|
||||
<ToggleButton
|
||||
active={!isGiftMode}
|
||||
onClick={() => setIsGiftMode(false)}
|
||||
label={i18n._(FOR_ME_DESCRIPTOR)}
|
||||
data-flx="app.plutonium.pricing-section.toggle-button.set-is-gift-mode"
|
||||
/>
|
||||
<ToggleButton
|
||||
active={isGiftMode}
|
||||
onClick={() => setIsGiftMode(true)}
|
||||
label={i18n._(AS_A_GIFT_DESCRIPTOR)}
|
||||
data-flx="app.plutonium.pricing-section.toggle-button.set-is-gift-mode--2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={gridStyles.gridWrapper} data-flx="app.plutonium.pricing-section.div">
|
||||
<div className={gridStyles.gridTwoColumns} data-flx="app.plutonium.pricing-section.div--2">
|
||||
{!isGiftMode ? (
|
||||
|
||||
+3
-1
@@ -116,6 +116,7 @@ interface SubscriptionCardProps {
|
||||
handleCommunityButtonClick: () => void;
|
||||
purchaseDisabled?: boolean;
|
||||
purchaseDisabledTooltip?: React.ReactNode;
|
||||
billingUnavailable?: boolean;
|
||||
}
|
||||
|
||||
function getStatusBadgeClass(args: {
|
||||
@@ -180,6 +181,7 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = observer(
|
||||
handleCommunityButtonClick,
|
||||
purchaseDisabled = false,
|
||||
purchaseDisabledTooltip,
|
||||
billingUnavailable = false,
|
||||
}) => {
|
||||
const {i18n} = useLingui();
|
||||
const {loadingSwitchToListPrice, handleSwitchToListPrice} = useSubscriptionActions();
|
||||
@@ -704,7 +706,7 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = observer(
|
||||
})()}
|
||||
</div>
|
||||
<div className={styles.actions} data-flx="app.plutonium.subscription-card.actions">
|
||||
{isGiftSubscription ? (
|
||||
{isGiftSubscription || billingUnavailable ? (
|
||||
wrapIfDisabled(
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
+20
-7
@@ -11,6 +11,7 @@ import {
|
||||
SUPPORT_EMAIL,
|
||||
UPI_PAYMENT_METHOD,
|
||||
} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {CANCEL_DESCRIPTOR, CLOSE_DESCRIPTOR, OKAY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {HttpError} from '@app/features/platform/types/EndpointError';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
@@ -140,6 +141,10 @@ const PURCHASES_DISABLED_BODY_DESCRIPTOR = msg({
|
||||
message: 'Purchases are disabled for this account. Contact {supportEmail} if this looks wrong.',
|
||||
comment: 'Modal body shown when purchases are disabled. Provides the support email for appeals.',
|
||||
});
|
||||
const PURCHASES_DISABLED_SELF_HOSTED_BODY_DESCRIPTOR = msg({
|
||||
message: 'Purchases are disabled for this account.',
|
||||
comment: 'Modal body shown on a self-hosted instance when purchases are disabled for the account.',
|
||||
});
|
||||
const CHECKOUT_BLOCKED_TITLE_DESCRIPTOR = msg({
|
||||
message: 'Checkout unavailable',
|
||||
comment: 'Modal title for the generic "checkout blocked" state when no more specific reason is known.',
|
||||
@@ -148,6 +153,10 @@ const CHECKOUT_BLOCKED_BODY_DESCRIPTOR = msg({
|
||||
message: 'Checkout is blocked for this account. Contact {supportEmail} if you need help.',
|
||||
comment: 'Modal body for the generic "checkout blocked" state. Provides the support email.',
|
||||
});
|
||||
const CHECKOUT_BLOCKED_SELF_HOSTED_BODY_DESCRIPTOR = msg({
|
||||
message: 'Checkout is blocked for this account.',
|
||||
comment: 'Modal body for the generic "checkout blocked" state on a self-hosted instance.',
|
||||
});
|
||||
const CHECKOUT_START_FAILED_TITLE_DESCRIPTOR = msg({
|
||||
message: "Couldn't start checkout",
|
||||
comment: 'Title of the generic fallback error modal shown when creating a checkout session fails unexpectedly.',
|
||||
@@ -234,7 +243,7 @@ function alternativePaymentMethodForCurrency(
|
||||
isGift: boolean,
|
||||
plan: Plan,
|
||||
): CheckoutPaymentMethod | null {
|
||||
if (isGift || (plan !== 'monthly' && plan !== 'yearly')) {
|
||||
if (isGift || (plan !== 'monthly' && plan !== 'yearly') || RuntimeConfig.isSelfHosted()) {
|
||||
return null;
|
||||
}
|
||||
if (currency === 'BRL') return 'pix';
|
||||
@@ -404,9 +413,11 @@ export const useCheckoutActions = (
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={i18n._(PURCHASES_DISABLED_TITLE_DESCRIPTOR)}
|
||||
description={i18n._(PURCHASES_DISABLED_BODY_DESCRIPTOR, {
|
||||
supportEmail: SUPPORT_EMAIL,
|
||||
})}
|
||||
description={
|
||||
RuntimeConfig.isSelfHosted()
|
||||
? i18n._(PURCHASES_DISABLED_SELF_HOSTED_BODY_DESCRIPTOR)
|
||||
: i18n._(PURCHASES_DISABLED_BODY_DESCRIPTOR, {supportEmail: SUPPORT_EMAIL})
|
||||
}
|
||||
secondaryText={i18n._(CLOSE_DESCRIPTOR)}
|
||||
data-flx="app.plutonium.use-checkout-actions.handle-checkout-error.confirm-modal--3"
|
||||
/>
|
||||
@@ -418,9 +429,11 @@ export const useCheckoutActions = (
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={i18n._(CHECKOUT_BLOCKED_TITLE_DESCRIPTOR)}
|
||||
description={i18n._(CHECKOUT_BLOCKED_BODY_DESCRIPTOR, {
|
||||
supportEmail: SUPPORT_EMAIL,
|
||||
})}
|
||||
description={
|
||||
RuntimeConfig.isSelfHosted()
|
||||
? i18n._(CHECKOUT_BLOCKED_SELF_HOSTED_BODY_DESCRIPTOR)
|
||||
: i18n._(CHECKOUT_BLOCKED_BODY_DESCRIPTOR, {supportEmail: SUPPORT_EMAIL})
|
||||
}
|
||||
secondaryText={i18n._(CLOSE_DESCRIPTOR)}
|
||||
data-flx="app.plutonium.use-checkout-actions.handle-checkout-error.confirm-modal--4"
|
||||
/>
|
||||
|
||||
@@ -21,6 +21,7 @@ import * as NotificationUtils from '@app/features/notification/utils/Notificatio
|
||||
import NativePermission from '@app/features/permissions/system/state/NativePermission';
|
||||
import {resolvePriceAnnouncementCampaign} from '@app/features/premium/config/PriceAnnouncementCampaign';
|
||||
import PremiumState from '@app/features/premium/state/PremiumState';
|
||||
import {canServiceStripeSubscriptions, shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
||||
import StreamerMode from '@app/features/streamer_mode/state/StreamerMode';
|
||||
import Nagbar from '@app/features/ui/state/Nagbar';
|
||||
import {hasUnavailableElectronNativeContext, isDesktop} from '@app/features/ui/utils/NativeUtils';
|
||||
@@ -81,6 +82,10 @@ export const useNagbarConditions = (): NagbarConditions => {
|
||||
const premiumWillCancel = user?.premiumWillCancel ?? false;
|
||||
const isMockPremium = premiumOverrideType != null && premiumOverrideType > 0;
|
||||
const isSelfHosted = RuntimeConfig.isSelfHosted();
|
||||
const showPremium = shouldShowPremiumFeatures();
|
||||
const canServiceSubscription =
|
||||
!isSelfHosted ||
|
||||
(canServiceStripeSubscriptions() && (user?.premiumBillingCycle != null || user?.hasEverPurchased === true));
|
||||
const [startupVoiceSessionRestoreSnapshotKey, setStartupVoiceSessionRestoreSnapshotKey] = useState<
|
||||
string | null | undefined
|
||||
>(undefined);
|
||||
@@ -109,7 +114,7 @@ export const useNagbarConditions = (): NagbarConditions => {
|
||||
return true;
|
||||
})();
|
||||
const canShowPremiumGracePeriod = (() => {
|
||||
if (isSelfHosted) return false;
|
||||
if (!showPremium || !canServiceSubscription) return false;
|
||||
if (nagbarState.forceHidePremiumGracePeriod) return false;
|
||||
if (nagbarState.forcePremiumGracePeriod) return true;
|
||||
if (!user?.premiumUntil || user.premiumType === 2 || premiumWillCancel) return false;
|
||||
@@ -123,7 +128,7 @@ export const useNagbarConditions = (): NagbarConditions => {
|
||||
return isInGracePeriod && !nagbarState.premiumGracePeriodDismissed;
|
||||
})();
|
||||
const canShowPremiumExpired = (() => {
|
||||
if (isSelfHosted) return false;
|
||||
if (!showPremium || !canServiceSubscription) return false;
|
||||
if (nagbarState.forceHidePremiumExpired) return false;
|
||||
if (nagbarState.forcePremiumExpired) return true;
|
||||
if (!user?.premiumUntil || user.premiumType === 2 || premiumWillCancel) return false;
|
||||
@@ -140,13 +145,13 @@ export const useNagbarConditions = (): NagbarConditions => {
|
||||
return showExpiredState && !nagbarState.premiumExpiredDismissed;
|
||||
})();
|
||||
const canShowGiftInventory = (() => {
|
||||
if (isSelfHosted) return false;
|
||||
if (!showPremium) return false;
|
||||
if (nagbarState.forceHideGiftInventory) return false;
|
||||
if (nagbarState.forceGiftInventory) return true;
|
||||
return Boolean(user?.hasUnreadGiftInventory && !nagbarState.giftInventoryDismissed);
|
||||
})();
|
||||
const canShowPremiumOnboarding = (() => {
|
||||
if (isSelfHosted) return false;
|
||||
if (!showPremium) return false;
|
||||
if (nagbarState.forceHidePremiumOnboarding) return false;
|
||||
if (nagbarState.forcePremiumOnboarding) return true;
|
||||
if (isMockPremium) return false;
|
||||
|
||||
@@ -339,7 +339,8 @@ const PREMIUM_MIRROR_NAME_DESCRIPTOR = msg({
|
||||
comment: 'Premium model option that mirrors free and premium tiers.',
|
||||
});
|
||||
const PREMIUM_MIRROR_DESC_DESCRIPTOR = msg({
|
||||
message: 'Keep free and premium tiers. You can customize the tiers later.',
|
||||
message:
|
||||
'Keep free and premium tiers. You can customize the tiers, sell premium through Stripe, or hand out gift codes later from the admin panel.',
|
||||
comment: 'Description for the mirror premium model.',
|
||||
});
|
||||
const PREMIUM_EVERYONE_NAME_DESCRIPTOR = msg({
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import {SkeletonLine} from '@app/features/app/components/skeleton/SkeletonLine';
|
||||
import {createSkeletonRandomFromKey} from '@app/features/app/components/skeleton/SkeletonSeed';
|
||||
import {SkeletonEmphasis, SkeletonRadius} from '@app/features/app/components/skeleton/SkeletonStyle';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
||||
import MobileLayout from '@app/features/ui/state/MobileLayout';
|
||||
import {flxElementClassName} from '@app/lib/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
@@ -66,7 +66,7 @@ function createDMActionSkeletonSpecs(
|
||||
if (layout?.personalNotesVisible ?? true) {
|
||||
specs.push({key: 'personal-notes', labelWidth: PERSONAL_NOTES_ACTION_LABEL_WIDTH});
|
||||
}
|
||||
if (layout?.premiumVisible ?? (!RuntimeConfig.isSelfHosted() && !isMobile)) {
|
||||
if (layout?.premiumVisible ?? (shouldShowPremiumFeatures() && !isMobile)) {
|
||||
specs.push({key: 'premium', labelWidth: PREMIUM_ACTION_LABEL_WIDTH});
|
||||
}
|
||||
return Object.freeze(specs);
|
||||
|
||||
@@ -8,6 +8,14 @@ function getBootstrapProductName(): string {
|
||||
return productName || 'Fluxer';
|
||||
}
|
||||
|
||||
function getBootstrapPremiumProductName(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'Plutonium';
|
||||
}
|
||||
const premiumProductName = window.__FLUXER_BOOTSTRAP__?.instance.app_public?.branding?.premium_product_name?.trim();
|
||||
return premiumProductName || 'Plutonium';
|
||||
}
|
||||
|
||||
export const PRODUCT_NAME = getBootstrapProductName();
|
||||
export const PREMIUM_PRODUCT_NAME = 'Plutonium';
|
||||
export const PREMIUM_PRODUCT_NAME = getBootstrapPremiumProductName();
|
||||
export const PREMIUM_PRODUCT_FULL_NAME = `${PRODUCT_NAME} ${PREMIUM_PRODUCT_NAME}`;
|
||||
@@ -100,6 +100,8 @@ export function runtimeConfigSnapshotsAreSameInstance(
|
||||
const DEFAULT_INSTANCE_FEATURES: InstanceFeatures = {
|
||||
voice_enabled: false,
|
||||
stripe_enabled: false,
|
||||
premium_enabled: false,
|
||||
stripe_serviceable: false,
|
||||
self_hosted: false,
|
||||
presigned_attachment_uploads: false,
|
||||
emails_enabled: false,
|
||||
@@ -147,6 +149,8 @@ export const DEFAULT_APP_PUBLIC_CONFIG: InstanceAppPublic = {
|
||||
theme_color: null,
|
||||
status_page_url: null,
|
||||
status_page_incident_history_url: null,
|
||||
premium_product_name: 'Plutonium',
|
||||
premium_info_url: null,
|
||||
},
|
||||
setup: {
|
||||
configured: false,
|
||||
@@ -398,6 +402,9 @@ class RuntimeConfig {
|
||||
this.features = {
|
||||
...this.features,
|
||||
self_hosted: config.self_hosted,
|
||||
premium_enabled: !config.self_hosted || config.policy.premium_mode === 'mirror',
|
||||
stripe_enabled: config.billing.billing_active,
|
||||
stripe_serviceable: config.billing.stripe_serviceable,
|
||||
};
|
||||
this.registration = normalizeInstanceRegistration(config.registration);
|
||||
this.community = normalizeInstanceCommunity({
|
||||
@@ -495,6 +502,28 @@ class RuntimeConfig {
|
||||
return DeveloperOptions.selfHostedModeOverride || this.features.self_hosted;
|
||||
}
|
||||
|
||||
get premiumEnabled(): boolean {
|
||||
return this.features.premium_enabled;
|
||||
}
|
||||
|
||||
get stripeEnabled(): boolean {
|
||||
return this.features.stripe_enabled;
|
||||
}
|
||||
|
||||
get stripeServiceable(): boolean {
|
||||
return this.features.stripe_serviceable;
|
||||
}
|
||||
|
||||
get premiumProductName(): string {
|
||||
return (
|
||||
this.appPublic.branding.premium_product_name?.trim() || DEFAULT_APP_PUBLIC_CONFIG.branding.premium_product_name
|
||||
);
|
||||
}
|
||||
|
||||
get premiumInfoUrl(): string | null {
|
||||
return this.appPublic.branding.premium_info_url ?? null;
|
||||
}
|
||||
|
||||
get emailsEnabled(): boolean {
|
||||
return this.features.emails_enabled;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user