feat: add monetization toggle for projects (#5961)

* Add monetization toggle for projects

* add flag for monetization toggle

* remove feature flag toggle
This commit is contained in:
Prospector
2026-05-03 11:56:08 -07:00
committed by GitHub
parent 7dbbbe590f
commit a4c8154438

View File

@@ -271,6 +271,60 @@
</div>
</div>
</div>
<div v-if="!isServerProject" class="mt-4 flex flex-col gap-2">
<div class="grid grid-cols-[1fr_auto] items-center gap-6">
<label for="project-monetization-toggle">
<span class="mb-1 block text-lg font-semibold text-contrast">Monetization</span>
<span class="block">
When enabled, this project can earn revenue through Modrinth's
<nuxt-link to="/legal/cmp-info" target="_blank" class="text-link"
>Rewards Program</nuxt-link
>. If you don't want to (or can't for legal reasons) earn revenue from this project,
you can turn it off here.
</span>
</label>
<Toggle
id="project-monetization-toggle"
v-model="monetizationEnabled"
:disabled="monetizationToggleDisabled"
/>
</div>
<div v-if="isForceDemonetized" class="mt-2 flex flex-wrap items-center gap-2 text-orange">
<TriangleAlertIcon aria-hidden="true" />
<span>
Your project is not eligible for monetization. If you think this is a mistake, please
<a
class="text-orange underline hover:brightness-110"
href="https://support.modrinth.com"
target="_blank"
rel="noopener noreferrer"
>
contact support</a
>.
</span>
</div>
<div v-if="isStaff" class="mt-2">
<ButtonStyled color="orange">
<button
v-if="!isForceDemonetized"
:disabled="loadingModeratorMonetization"
@click="updateMonetizationStatus('force-demonetized')"
>
<ScaleIcon aria-hidden="true" />
Disable monetization
</button>
<button
v-else
:disabled="loadingModeratorMonetization"
@click="updateMonetizationStatus('monetized')"
>
<ScaleIcon aria-hidden="true" />
Allow monetization
</button>
</ButtonStyled>
</div>
</div>
</section>
<section class="universal-card">
@@ -306,6 +360,7 @@ import {
CheckIcon,
ImageIcon,
IssuesIcon,
ScaleIcon,
TrashIcon,
TriangleAlertIcon,
UploadIcon,
@@ -322,14 +377,18 @@ import {
injectNotificationManager,
injectProjectPageContext,
StyledInput,
Toggle,
UnsavedChangesPopup,
usePageLeaveSafety,
} from '@modrinth/ui'
import { fileIsValid, formatProjectStatus, formatProjectType } from '@modrinth/utils'
import FileInput from '~/components/ui/FileInput.vue'
import { useAuth } from '~/composables/auth.js'
import { useFeatureFlags } from '~/composables/featureFlags.ts'
const auth = await useAuth()
const { addNotification } = injectNotificationManager()
const {
projectV2: project,
@@ -360,6 +419,22 @@ const visibility = ref(
: project.value.requested_status,
)
const monetizationEnabled = ref(project.value.monetization_status === 'monetized')
const loadingModeratorMonetization = ref(false)
watch(
() => project.value.monetization_status,
() => {
monetizationEnabled.value = project.value.monetization_status === 'monetized'
},
)
const isStaff = computed(
() => !!auth.value.user && tags.value.staffRoles.includes(auth.value.user.role),
)
const isForceDemonetized = computed(() => project.value.monetization_status === 'force-demonetized')
// Server project specific refs
const MC_SERVER_BANNER_NAME = '__mc_server_banner__'
const isServerProject = computed(() => projectV3.value?.minecraft_server != null)
@@ -374,6 +449,8 @@ const hasPermission = computed(() => {
return ((currentMember.value?.permissions ?? 0) & EDIT_DETAILS) === EDIT_DETAILS
})
const monetizationToggleDisabled = computed(() => !hasPermission.value || isForceDemonetized.value)
const hasDeletePermission = computed(() => {
const DELETE_PROJECT = 1 << 7
return ((currentMember.value?.permissions ?? 0) & DELETE_PROJECT) === DELETE_PROJECT
@@ -446,6 +523,13 @@ const basePatchData = computed(() => {
data.requested_status = visibility.value
}
if (project.value.monetization_status !== 'force-demonetized') {
const wasMonetized = project.value.monetization_status === 'monetized'
if (monetizationEnabled.value !== wasMonetized) {
data.monetization_status = monetizationEnabled.value ? 'monetized' : 'demonetized'
}
}
return data
})
@@ -464,6 +548,7 @@ const original = computed(() => ({
deletedIcon: false,
bannerFile: null,
deletedBanner: false,
monetizationEnabled: project.value.monetization_status === 'monetized',
}))
const modified = computed(() => ({
@@ -477,6 +562,7 @@ const modified = computed(() => ({
deletedIcon: deletedIcon.value,
bannerFile: bannerFile.value,
deletedBanner: deletedBanner.value,
monetizationEnabled: monetizationEnabled.value,
}))
const hasChanges = computed(() =>
@@ -500,6 +586,16 @@ function resetChanges() {
bannerFile.value = null
bannerPreview.value = null
deletedBanner.value = false
monetizationEnabled.value = project.value.monetization_status === 'monetized'
}
async function updateMonetizationStatus(status) {
loadingModeratorMonetization.value = true
try {
await patchProject({ monetization_status: status })
} finally {
loadingModeratorMonetization.value = false
}
}
const hasModifiedVisibility = () => {