Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): Improve revenue information #3250

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

148 changes: 107 additions & 41 deletions apps/frontend/src/pages/dashboard/revenue/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,68 @@
<div>
<section class="universal-card">
<h2 class="text-2xl">Revenue</h2>
<div class="grid-display">
<div class="grid-display__item">
<div class="label">Current Balance</div>
<div class="value">
{{ $formatMoney(userBalance.available) }}
</div>
</div>
<div class="grid-display__item">
<div class="label">Total Pending <nuxt-link v-tooltip="`Click to read about pending revenue.`" class="text-link align-middle" to="/legal/cmp-info#pending"><UnknownIcon /></nuxt-link></div>

Check failure on line 13 in apps/frontend/src/pages/dashboard/revenue/index.vue

View workflow job for this annotation

GitHub Actions / Build, Test, and Lint

Replace `Total·Pending·<nuxt-link·v-tooltip="`Click·to·read·about·pending·revenue.`"·class="text-link·align-middle"·to="/legal/cmp-info#pending"><UnknownIcon·/></nuxt-link>` with `⏎············Total·Pending⏎············<nuxt-link⏎··············v-tooltip="`Click·to·read·about·pending·revenue.`"⏎··············class="align-middle·text-link"⏎··············to="/legal/cmp-info#pending"⏎··············><UnknownIcon⏎············/></nuxt-link>⏎··········`
<div class="value">
{{ $formatMoney(userBalance.pending) }}
</div>
</div>
<div class="grid-display__item">
<div class="label">Pending In Transit <nuxt-link v-tooltip="`Click to read about pending revenue.`" class="text-link align-middle" to="/legal/cmp-info#pending"><UnknownIcon /></nuxt-link>

Check failure on line 19 in apps/frontend/src/pages/dashboard/revenue/index.vue

View workflow job for this annotation

GitHub Actions / Build, Test, and Lint

Replace `··<div·class="label">Pending·In·Transit·<nuxt-link·v-tooltip="`Click·to·read·about·pending·revenue.`"·class="text-link·align-middle"·to="/legal/cmp-info#pending"><UnknownIcon` with `<div·class="label">⏎············Pending·In·Transit⏎············<nuxt-link⏎··············v-tooltip="`Click·to·read·about·pending·revenue.`"⏎··············class="align-middle·text-link"⏎··············to="/legal/cmp-info#pending"⏎··············><UnknownIcon⏎···········`
</div>
<div class="value">

Check failure on line 21 in apps/frontend/src/pages/dashboard/revenue/index.vue

View workflow job for this annotation

GitHub Actions / Build, Test, and Lint

Replace `⏎············≈{{·$formatMoney(totalTwoMonthsAgo)·}}⏎··········` with `≈{{·$formatMoney(totalTwoMonthsAgo)·}}`
≈{{ $formatMoney(totalTwoMonthsAgo) }}
</div>
<span>
<span
>
accessible on {{ formatDate(deadlineEnding)}}
</span
></span
>
</div>
</div>

<div v-if="userBalance.available >= minWithdraw">
<p>
You have
<strong>{{ $formatMoney(userBalance.available) }}</strong>
available to withdraw. <strong>{{ $formatMoney(userBalance.pending) }}</strong> of your
balance is <nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
You have funds available to withdraw.
</p>
</div>
<p v-else>
You have made
<strong>{{ $formatMoney(userBalance.available) }}</strong
>, which is under the minimum of ${{ minWithdraw }} to withdraw.
<strong>{{ $formatMoney(userBalance.pending) }}</strong> of your balance is
<nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
Your current balance is under the minimum of <strong>${{ minWithdraw }}</strong> to withdraw.
</p>
<div class="input-group mt-4">
<nuxt-link
v-if="userBalance.available >= minWithdraw"
class="iconified-button brand-button"
to="/dashboard/revenue/withdraw"
>
<span :class="{ 'disabled-cursor-wrapper': userBalance.available < minWithdraw }">
<nuxt-link
:aria-disabled="userBalance.available < minWithdraw ? 'true' : 'false'"
:class="{ 'disabled-link': userBalance.available < minWithdraw }"
:disabled="userBalance.available < minWithdraw ? 'true' : 'false'"
:tabindex="userBalance.available < minWithdraw ? -1 : undefined"
class="iconified-button brand-button"
to="/dashboard/revenue/withdraw"
>
<TransferIcon /> Withdraw
</nuxt-link>
</span>
<NuxtLink class="iconified-button" to="/dashboard/revenue/transfers">
<HistoryIcon /> View transfer history
<HistoryIcon />
View transfer history
</NuxtLink>
</div>
<p>
By uploading projects to Modrinth and withdrawing money from your account, you agree to the
<nuxt-link to="/legal/cmp" class="text-link">Rewards Program Terms</nuxt-link>. For more
<nuxt-link class="text-link" to="/legal/cmp">Rewards Program Terms</nuxt-link>
. For more
information on how the rewards system works, see our information page
<nuxt-link to="/legal/cmp-info" class="text-link">here</nuxt-link>.
<nuxt-link class="text-link" to="/legal/cmp-info">here</nuxt-link>
.
</p>
</section>
<section class="universal-card">
Expand All @@ -46,12 +76,13 @@
{{ auth.user.payout_data.paypal_address }}
</p>
<button class="btn mt-4" @click="removeAuthProvider('paypal')">
<XIcon /> Disconnect account
<XIcon />
Disconnect account
</button>
</template>
<template v-else>
<p>Connect your PayPal account to enable withdrawing to your PayPal balance.</p>
<a class="btn mt-4" :href="`${getAuthUrl('paypal')}&token=${auth.token}`">
<a :href="`${getAuthUrl('paypal')}&token=${auth.token}`" class="btn mt-4">
<PayPalIcon />
Sign in with PayPal
</a>
Expand All @@ -60,62 +91,97 @@
<p>
Tremendous payments are sent to your Modrinth email. To change/set your Modrinth email,
visit
<nuxt-link to="/settings/account" class="text-link">here</nuxt-link>.
<nuxt-link class="text-link" to="/settings/account">here</nuxt-link>
.
</p>
<h3>Venmo</h3>
<p>Enter your Venmo username below to enable withdrawing to your Venmo balance.</p>
<label class="hidden" for="venmo">Venmo address</label>
<input
id="venmo"
v-model="auth.user.payout_data.venmo_handle"
autocomplete="off"
class="mt-4"
type="search"
name="search"
placeholder="@example"
autocomplete="off"
type="search"
/>
<button class="btn btn-secondary" @click="updateVenmo"><SaveIcon /> Save information</button>
<button class="btn btn-secondary" @click="updateVenmo">
<SaveIcon />
Save information
</button>
</section>
</div>
</template>
<script setup>
import { TransferIcon, HistoryIcon, PayPalIcon, SaveIcon, XIcon } from "@modrinth/assets";
import { HistoryIcon, PayPalIcon, SaveIcon, TransferIcon, XIcon, UnknownIcon } from '@modrinth/assets'
import { formatDate } from "@modrinth/utils"
import dayjs from 'dayjs'

const auth = await useAuth();
const minWithdraw = ref(0.01);
const auth = await useAuth()
const minWithdraw = ref(0.01)

const { data: userBalance } = await useAsyncData(`payout/balance`, () =>
useBaseFetch(`payout/balance`, { apiVersion: 3 }),
);
useBaseFetch(`payout/balance`, { apiVersion: 3 })
)

async function updateVenmo() {
startLoading();
startLoading()
try {
const data = {
venmo_handle: auth.value.user.payout_data.venmo_handle ?? null,
};
venmo_handle: auth.value.user.payout_data.venmo_handle ?? null
}

await useBaseFetch(`user/${auth.value.user.id}`, {
method: "PATCH",
method: 'PATCH',
body: data,
apiVersion: 3,
});
await useAuth(auth.value.token);
apiVersion: 3
})
await useAuth(auth.value.token)
} catch (err) {
const data = useNuxtApp();
const data = useNuxtApp()
data.$notify({
group: "main",
title: "An error occurred",
group: 'main',
title: 'An error occurred',
text: err.data.description,
type: "error",
});
type: 'error'
})
}
stopLoading();
stopLoading()
}

let targetDate = dayjs().subtract(2, 'month').startOf('month')
if (targetDate.endOf('month').add(60, 'days').isBefore(dayjs().startOf('day'))) {
targetDate = dayjs().subtract(1, 'month').startOf('month');
}

const { data: pendingInTransit } = await useAsyncData('analytics/revenue', () =>
useBaseFetch('analytics/revenue', {
apiVersion: 3,
query: {
start_date: targetDate.toISOString(),
end_date: targetDate.endOf('month').toISOString(),
resolution_minutes: 1140
}
})
)

const deadlineEnding = targetDate.endOf('month').add(60, 'days')
const totalTwoMonthsAgo = Object.values(pendingInTransit.value || {}).reduce((acc, project) => {
return acc + Object.values(project || {}).reduce((acc, value) => acc + parseFloat(value), 0)
}, 0)
</script>
<style lang="scss" scoped>
strong {
color: var(--color-text-dark);
font-weight: 500;
}

.disabled-cursor-wrapper {
cursor: not-allowed;
}

.disabled-link {
pointer-events: none;
}
</style>
127 changes: 86 additions & 41 deletions apps/frontend/src/pages/legal/cmp-info.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,42 +82,46 @@
<p>
Modrinth receives ad revenue from our ad providers on a NET 60 day basis. Due to this, not all
revenue is immediately available to withdraw. We pay creators as soon as we receive the money
from our ad providers, which is 60 days after the last day of each month. This table outlines
some example dates of how NET 60 payments are made:
from our ad providers, which is 60 days after the last day of each month.
</p>

<p id="pending-in-transit-explanation">
<strong>Pending In Transit:</strong> This is an estimate of the total amount of pending
revenue that
you can expect to move into your current balance after the next NET 60 day period has passed. The next NET 60 day period ends on {{ formatDate(deadlineEnding) }}.
</p>

<p>
To understand when revenue becomes available, you can use this calculator to estimate when
revenue earned on a specific date will be available for withdrawal.
</p>

<table>
<thead>
<tr>
<th>Date</th>
<th>Payment available date</th>
</tr>
</thead>
<tbody>
<tr>
<td>January 1st</td>
<td>March 31st</td>
</tr>
<tr>
<td>January 15th</td>
<td>March 31st</td>
</tr>
<tr>
<td>March 3rd</td>
<td>May 30th</td>
</tr>
<tr>
<td>June 30th</td>
<td>August 29th</td>
</tr>
<tr>
<td>July 14th</td>
<td>September 29th</td>
</tr>
<tr>
<td>October 12th</td>
<td>December 30th</td>
</tr>
</tbody>
<tr>
<th>Timeline</th>
<th>Date</th>
</tr>
<tr>
<td>Revenue earned on</td>
<td>
<input id="revenue-date-picker" v-model="selectedDateValue" type="date" />
<noscript>(JavaScript must be enabled for the date picker to function, example date:
2024-07-15)
</noscript>
</td>
</tr>
<tr>
<td>End of the month</td>
<td>{{ formatDate(endOfMonthDayjs) }}</td>
</tr>
<tr>
<td>NET 60 policy applied</td>
<td>+ 60 days</td>
</tr>
<tr class="final-result">
<td>Available for withdrawal</td>
<td>{{ formatDate(withdrawalDateDayjs) }}</td>
</tr>
</table>
<h3>How do I know Modrinth is being transparent about revenue?</h3>
<p>
Expand All @@ -127,19 +131,60 @@
revenue distribution system</a
>. We also have an
<a href="https://api.modrinth.com/v3/payout/platform_revenue">API route</a> that allows users
to query exact daily revenue for the site.
to query exact daily revenue for the site - so far, Modrinth has generated
<strong>{{ formatMoney(platformRevenue) }}</strong> in revenue.
</p>
<table>
<tr>
<th>Date</th>
<th>Revenue</th>
<th>Creator Revenue (75%)</th>
<th>Modrinth's Cut (25%)</th>
</tr>
<tr v-for="item in platformRevenueData">
<td>{{ formatDate(dayjs.unix(item.time)) }}</td>
<td>{{ formatMoney(item.revenue) }}</td>
<td>{{ formatMoney(item.creator_revenue) }}</td>
<td>{{ formatMoney(item.revenue - item.creator_revenue) }}</td>
</tr>
</table>
<small>Modrinth's total revenue in the previous 5 days, for the entire dataset, use the
aforementioned <a href="https://api.modrinth.com/v3/payout/platform_revenue">API
route</a>.</small>
</div>
</template>

<script setup>
<script lang="ts" setup>
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
import { formatDate, formatMoney } from '@modrinth/utils'

const description =
"Information about the Rewards Program of Modrinth, an open source modding platform focused on Minecraft.";
'Information about the Rewards Program of Modrinth, an open source modding platform focused on Minecraft.'

useSeoMeta({
title: "Rewards Program Information - Modrinth",
title: 'Rewards Program Information - Modrinth',
description,
ogTitle: "Rewards Program Information",
ogDescription: description,
});
ogTitle: 'Rewards Program Information',
ogDescription: description
})

const selectedDateValue = ref(dayjs().format('YYYY-MM-DD'))
const selectedDateDayjs = computed(() => dayjs(selectedDateValue.value))
const endOfMonthDayjs = computed(() => selectedDateDayjs.value.endOf('month'))
const withdrawalDateDayjs = computed(() => endOfMonthDayjs.value.add(60, 'days'))

let deadlineEnding = dayjs().subtract(2, 'month').endOf('month').add(60, 'days')
if (deadlineEnding.isBefore(dayjs().startOf('day'))) {
deadlineEnding = dayjs().subtract(1, 'month').endOf('month').add(60, 'days')
}

const { data: transparencyInformation } = await useAsyncData('payout/platform_revenue', () =>
useBaseFetch('payout/platform_revenue', {
apiVersion: 3
})
)

const platformRevenue = transparencyInformation.value.all_time
const platformRevenueData = transparencyInformation.value.data.slice(0, 5)
</script>
2 changes: 2 additions & 0 deletions apps/frontend/src/plugins/dayjs.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import dayjs from "dayjs";
import quarterOfYear from "dayjs/plugin/quarterOfYear";
import advanced from "dayjs/plugin/advancedFormat";

dayjs.extend(quarterOfYear);
dayjs.extend(advanced);

export default defineNuxtPlugin(() => {
return {
Expand Down
Loading
Loading