You've already forked AstralRinth
forked from didirus/AstralRinth
New payouts system (#1456)
* initial * finish withdraw page * Finish frontend * Fix UI a bit * tester fixes
This commit is contained in:
@@ -87,13 +87,6 @@
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Total revenue</div>
|
||||
<div class="value">
|
||||
{{ $formatMoney(payouts.all_time, true) }}
|
||||
</div>
|
||||
<span>{{ $formatMoney(payouts.last_month, true) }} in the last month</span>
|
||||
</div>
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Current balance</div>
|
||||
<div class="value">
|
||||
@@ -127,13 +120,10 @@ useHead({
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
const [{ data: projects }, { data: payouts }] = await Promise.all([
|
||||
const [{ data: projects }] = await Promise.all([
|
||||
useAsyncData(`user/${auth.value.user.id}/projects`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/projects`)
|
||||
),
|
||||
useAsyncData(`user/${auth.value.user.id}/payouts`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`)
|
||||
),
|
||||
])
|
||||
|
||||
const minWithdraw = ref(0.26)
|
||||
|
||||
@@ -1,132 +1,109 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalTransfer
|
||||
v-if="enrolled"
|
||||
ref="modal_transfer"
|
||||
:wallet="auth.user.payout_data.payout_wallet"
|
||||
:account-type="auth.user.payout_data.payout_wallet_type"
|
||||
:account="auth.user.payout_data.payout_address"
|
||||
:balance="auth.user.payout_data.balance"
|
||||
:min-withdraw="minWithdraw"
|
||||
/>
|
||||
<section class="universal-card">
|
||||
<h2>Withdraw</h2>
|
||||
<h2>Revenue</h2>
|
||||
<div v-if="auth.user.payout_data.balance >= minWithdraw">
|
||||
<p>
|
||||
You have
|
||||
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong>
|
||||
available to withdraw.
|
||||
<span v-if="!enrolled"
|
||||
>Enroll in the Creator Monetization Program to withdraw your revenue.</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<p v-else-if="auth.user.payout_data.balance > 0">
|
||||
You have made
|
||||
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong
|
||||
>, however you have not yet met the minimum of ${{ minWithdraw }} to withdraw.
|
||||
</p>
|
||||
<p v-else>
|
||||
You have made
|
||||
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong
|
||||
>, which is under the minimum of ${{ minWithdraw }} to withdraw.
|
||||
</p>
|
||||
<div v-if="!enrolled">
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Enroll in the Creator Monetization Program
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div v-if="enrolled" class="input-group">
|
||||
<button
|
||||
<div class="input-group">
|
||||
<nuxt-link
|
||||
v-if="auth.user.payout_data.balance >= minWithdraw"
|
||||
class="iconified-button brand-button"
|
||||
@click="$refs.modal_transfer.show()"
|
||||
to="/dashboard/revenue/withdraw"
|
||||
>
|
||||
<TransferIcon /> Transfer to
|
||||
{{ $formatWallet(auth.user.payout_data.payout_wallet) }}
|
||||
</button>
|
||||
<TransferIcon /> Withdraw
|
||||
</nuxt-link>
|
||||
<NuxtLink class="iconified-button" to="/dashboard/revenue/transfers">
|
||||
<HistoryIcon /> View transfer history
|
||||
</NuxtLink>
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Monetization settings
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2>About the program</h2>
|
||||
<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
|
||||
information on how the rewards system works, see our information page
|
||||
<nuxt-link to="/legal/cmp-info" class="text-link">here</nuxt-link>.
|
||||
</p>
|
||||
<h2>Processing fees</h2>
|
||||
<p>
|
||||
To avoid paying unnecessary fee deductions, you may want to wait to transfer your money out
|
||||
after it accumulates for a bit rather than transferring as soon as you reach the minimum of
|
||||
${{ minWithdraw }}.
|
||||
</p>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2>Payout methods</h2>
|
||||
<h3>PayPal</h3>
|
||||
<ul>
|
||||
<li>
|
||||
In the <strong>United States</strong>, PayPal charges a flat
|
||||
<strong>$0.25</strong>
|
||||
fee per transaction.
|
||||
</li>
|
||||
<li>
|
||||
In the rest of the world, PayPal charges a <strong>2%</strong> (up to $20) fee per
|
||||
transaction.
|
||||
</li>
|
||||
</ul>
|
||||
<template v-if="auth.user.auth_providers.includes('paypal')">
|
||||
<p>
|
||||
Your PayPal {{ auth.user.payout_data.paypal_country }} account is currently connected with
|
||||
email
|
||||
{{ auth.user.payout_data.paypal_address }}
|
||||
</p>
|
||||
<button class="btn" @click="removeAuthProvider('paypal')">
|
||||
<XIcon /> Disconnect account
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Connect your PayPal account to enable withdrawing to your PayPal balance.</p>
|
||||
<a class="btn" :href="`${getAuthUrl('paypal')}&token=${auth.token}`">
|
||||
<PayPalIcon />
|
||||
Sign in with PayPal
|
||||
</a>
|
||||
</template>
|
||||
<h3>Tremendous</h3>
|
||||
<p>
|
||||
Modrinth will deduct <strong>2%</strong> for the fee (minimum of $0.25 and maximum of $20)
|
||||
from <strong>all transfers</strong> and if the fee PayPal charges is less than the amount we
|
||||
deducted, the difference will be added back to your Modrinth balance. This happens as
|
||||
Modrinth cannot determine if a transaction will be in the United States or international or
|
||||
not until after the transaction has been made.
|
||||
</p>
|
||||
<h3>Venmo (United States only)</h3>
|
||||
<p>
|
||||
Venmo will charge a $0.25 processing fee per transaction, which will be deducted from the
|
||||
amount you choose to transfer.
|
||||
</p>
|
||||
<h2>Currency conversions</h2>
|
||||
<p>
|
||||
All revenue generated by Modrinth is in United States dollars. Any conversions to your local
|
||||
currency will happen at withdrawal and is not handled by Modrinth. Modrinth cannot guarantee
|
||||
any exchange rate, so only USD is displayed in the creator dashboard.
|
||||
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>.
|
||||
</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"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="@example"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button class="btn btn-secondary" @click="updateVenmo"><SaveIcon /> Save information</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||
import HistoryIcon from '~/assets/images/utils/history.svg'
|
||||
import ModalTransfer from '~/components/ui/ModalTransfer.vue'
|
||||
<script setup>
|
||||
import { TransferIcon, HistoryIcon, PayPalIcon, SaveIcon, XIcon } from 'omorphia'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { TransferIcon, SettingsIcon, HistoryIcon, ModalTransfer },
|
||||
async setup() {
|
||||
const auth = await useAuth()
|
||||
const auth = await useAuth()
|
||||
const minWithdraw = ref(0.1)
|
||||
|
||||
return { auth }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
minWithdraw: 0.26,
|
||||
enrolled:
|
||||
this.auth.user.payout_data.payout_wallet &&
|
||||
this.auth.user.payout_data.payout_wallet_type &&
|
||||
this.auth.user.payout_data.payout_address,
|
||||
async function updateVenmo() {
|
||||
startLoading()
|
||||
try {
|
||||
const data = {
|
||||
venmo_handle: auth.value.user.payout_data.venmo_handle ?? null,
|
||||
}
|
||||
},
|
||||
head: {
|
||||
title: 'Revenue - Modrinth',
|
||||
},
|
||||
methods: {},
|
||||
})
|
||||
|
||||
await useBaseFetch(`user/${auth.value.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
apiVersion: 3,
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
const data = useNuxtApp()
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
strong {
|
||||
|
||||
@@ -6,130 +6,133 @@
|
||||
:link-stack="[{ href: '/dashboard/revenue', label: 'Revenue' }]"
|
||||
/>
|
||||
<h2>Transfer history</h2>
|
||||
<p>
|
||||
All of your transfers from your Modrinth balance to your PayPal or Venmo accounts will be
|
||||
listed here:
|
||||
</p>
|
||||
<div class="grid-table">
|
||||
<div class="grid-table__row grid-table__header">
|
||||
<div class="desktop">Date</div>
|
||||
<div class="desktop">Status</div>
|
||||
<div class="desktop">Amount</div>
|
||||
<div class="mobile">Transaction</div>
|
||||
<p>All of your withdrawals from your Modrinth balance will be listed here:</p>
|
||||
<div v-for="payout in sortedPayouts" :key="payout.id" class="universal-card recessed payout">
|
||||
<div class="platform">
|
||||
<PayPalIcon v-if="payout.method === 'paypal'" />
|
||||
<TremendousIcon v-else-if="payout.method === 'tremendous'" />
|
||||
<VenmoIcon v-else-if="payout.method === 'venmo'" />
|
||||
<UnknownIcon v-else />
|
||||
</div>
|
||||
<div
|
||||
v-for="(payout, index) in payouts.payouts.filter((x) => x.status === 'success')"
|
||||
:key="`payout-${index}`"
|
||||
class="grid-table__row"
|
||||
>
|
||||
<div>{{ $dayjs(payout.created).format('MMMM D, YYYY [at] h:mm A') }}</div>
|
||||
<div><Badge :type="payout.status" /></div>
|
||||
<div class="amount">{{ $formatMoney(payout.amount) }}</div>
|
||||
<div class="payout-info">
|
||||
<div>
|
||||
<strong>
|
||||
{{ $dayjs(payout.created).format('MMMM D, YYYY [at] h:mm A') }}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="amount">{{ $formatMoney(payout.amount) }}</span>
|
||||
<template v-if="payout.fee">⋅ Fee {{ $formatMoney(payout.fee) }}</template>
|
||||
</div>
|
||||
<div class="payout-status">
|
||||
<span>
|
||||
<Badge v-if="payout.status === 'success'" color="green" type="Success" />
|
||||
<Badge v-else-if="payout.status === 'cancelling'" color="yellow" type="Cancelling" />
|
||||
<Badge v-else-if="payout.status === 'cancelled'" color="red" type="Cancelled" />
|
||||
<Badge v-else-if="payout.status === 'failed'" color="red" type="Failed" />
|
||||
<Badge v-else-if="payout.status === 'in-transit'" color="yellow" type="In transit" />
|
||||
<Badge v-else :type="payout.status" />
|
||||
</span>
|
||||
<template v-if="payout.method">
|
||||
<span>⋅</span>
|
||||
<span>{{ $formatWallet(payout.method) }} ({{ payout.method_address }})</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
v-if="payout.status === 'in-transit'"
|
||||
class="iconified-button raised-button"
|
||||
@click="cancelPayout(payout.id)"
|
||||
>
|
||||
<XIcon /> Cancel payment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
import { Badge, Breadcrumbs, XIcon, PayPalIcon, UnknownIcon } from 'omorphia'
|
||||
import dayjs from 'dayjs'
|
||||
import TremendousIcon from '~/assets/images/external/tremendous.svg'
|
||||
import VenmoIcon from '~/assets/images/external/venmo-small.svg'
|
||||
|
||||
useHead({
|
||||
title: 'Transfer history - Modrinth',
|
||||
})
|
||||
|
||||
const data = await useNuxtApp()
|
||||
const auth = await useAuth()
|
||||
|
||||
const { data: payouts } = await useAsyncData(`user/${auth.value.user.id}/payouts`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`)
|
||||
const { data: payouts, refresh } = await useAsyncData(`payout`, () =>
|
||||
useBaseFetch(`payout`, {
|
||||
apiVersion: 3,
|
||||
})
|
||||
)
|
||||
|
||||
const sortedPayouts = computed(() =>
|
||||
payouts.value.sort((a, b) => dayjs(b.created) - dayjs(a.created))
|
||||
)
|
||||
|
||||
async function cancelPayout(id) {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch(`payout/${id}`, {
|
||||
method: 'DELETE',
|
||||
apiVersion: 3,
|
||||
})
|
||||
await refresh()
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.grid-table {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-card-md);
|
||||
.payout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.grid-table__header {
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__row {
|
||||
display: contents;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-card-sm);
|
||||
|
||||
// Left edge of table
|
||||
&:first-child,
|
||||
&.mobile {
|
||||
padding-left: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
// Right edge of table
|
||||
&:last-child {
|
||||
padding-right: var(--spacing-card-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2n + 1) > div {
|
||||
background-color: var(--color-table-alternate-row);
|
||||
}
|
||||
|
||||
> div {
|
||||
padding-top: var(--spacing-card-bg);
|
||||
padding-bottom: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
&.grid-table__header > div {
|
||||
background-color: var(--color-bg);
|
||||
font-weight: bold;
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
.platform {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.grid-table__row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem;
|
||||
background-color: var(--color-raised-bg);
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
border-radius: 20rem;
|
||||
|
||||
> div {
|
||||
padding: var(--spacing-card-xs) var(--spacing-card-bg);
|
||||
|
||||
&:first-child,
|
||||
&.mobile {
|
||||
padding-top: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
&:last-child,
|
||||
&.mobile {
|
||||
padding-bottom: var(--spacing-card-bg);
|
||||
}
|
||||
}
|
||||
svg {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__header {
|
||||
.mobile {
|
||||
display: flex;
|
||||
}
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.payout-status {
|
||||
display: flex;
|
||||
gap: 0.5ch;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: var(--color-heading);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.input-group {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
442
pages/dashboard/revenue/withdraw.vue
Normal file
442
pages/dashboard/revenue/withdraw.vue
Normal file
@@ -0,0 +1,442 @@
|
||||
<template>
|
||||
<section class="universal-card">
|
||||
<Breadcrumbs
|
||||
current-title="Withdraw"
|
||||
:link-stack="[{ href: '/dashboard/revenue', label: 'Revenue' }]"
|
||||
/>
|
||||
|
||||
<h2>Withdraw</h2>
|
||||
|
||||
<h3>Country</h3>
|
||||
<Multiselect
|
||||
id="country-multiselect"
|
||||
v-model="country"
|
||||
class="country-multiselect"
|
||||
placeholder="Select country..."
|
||||
track-by="id"
|
||||
label="name"
|
||||
:options="countries"
|
||||
:searchable="true"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
/>
|
||||
|
||||
<h3>Withdraw method</h3>
|
||||
|
||||
<div class="iconified-input">
|
||||
<label class="hidden" for="search">Search</label>
|
||||
<SearchIcon aria-hidden="true" />
|
||||
<input
|
||||
id="search"
|
||||
v-model="search"
|
||||
name="search"
|
||||
placeholder="Search options..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="withdraw-options-scroll">
|
||||
<div class="withdraw-options">
|
||||
<button
|
||||
v-for="method in payoutMethods.filter((x) =>
|
||||
x.name.toLowerCase().includes(search.toLowerCase())
|
||||
)"
|
||||
:key="method.id"
|
||||
class="withdraw-option button-base"
|
||||
:class="{ selected: selectedMethodId === method.id }"
|
||||
@click="() => (selectedMethodId = method.id)"
|
||||
>
|
||||
<div class="preview" :class="{ 'show-bg': !method.image_url || method.name === 'ACH' }">
|
||||
<img
|
||||
v-if="method.image_url && method.name !== 'ACH'"
|
||||
class="preview-img"
|
||||
:src="method.image_url"
|
||||
:alt="method.name"
|
||||
/>
|
||||
<div v-else class="placeholder">
|
||||
<template v-if="method.type === 'venmo'">
|
||||
<VenmoIcon class="enlarge" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PayPalIcon v-if="method.type === 'paypal'" />
|
||||
<span>{{ method.name }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label">
|
||||
<RadioButtonChecked v-if="selectedMethodId === method.id" class="radio" />
|
||||
<RadioButtonIcon v-else class="radio" />
|
||||
<span>{{ method.name }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Amount</h3>
|
||||
<p>
|
||||
You are initiating a transfer of your revenue from Modrinth's Creator Monetization Program.
|
||||
How much of your
|
||||
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong> balance would you like to
|
||||
transfer to {{ selectedMethod.name }}?
|
||||
</p>
|
||||
<div class="confirmation-input">
|
||||
<template v-if="selectedMethod.interval.fixed">
|
||||
<Chips
|
||||
v-model="amount"
|
||||
:items="selectedMethod.interval.fixed.values"
|
||||
:format-label="(val) => '$' + val"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
id="confirmation"
|
||||
v-model="amount"
|
||||
type="text"
|
||||
pattern="^\d*(\.\d{0,2})?$"
|
||||
autocomplete="off"
|
||||
placeholder="Amount to transfer..."
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="confirm-text">
|
||||
<template v-if="knownErrors.length === 0 && amount">
|
||||
<Checkbox v-if="fees > 0" v-model="agreedFees" description="Consent to fee">
|
||||
I acknowledge that an estimated
|
||||
{{ $formatMoney(fees) }} will be deducted from the amount I receive to cover
|
||||
{{ $formatWallet(selectedMethod.type) }} processing fees.
|
||||
</Checkbox>
|
||||
<Checkbox v-model="agreedTransfer" description="Confirm transfer">
|
||||
<template v-if="selectedMethod.type === 'tremendous'">
|
||||
I confirm that I am initiating a transfer and I will receive further instructions on how
|
||||
to redeem this payment via email to: {{ withdrawAccount }}
|
||||
</template>
|
||||
<template v-else>
|
||||
I confirm that I am initiating a transfer to the following
|
||||
{{ $formatWallet(selectedMethod.type) }} account: {{ withdrawAccount }}
|
||||
</template>
|
||||
</Checkbox>
|
||||
<Checkbox v-model="agreedTerms" class="rewards-checkbox">
|
||||
I agree to the
|
||||
<nuxt-link to="/legal/cmp" class="text-link">Rewards Program Terms</nuxt-link>
|
||||
</Checkbox>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-for="(error, index) in knownErrors" :key="index" class="invalid">
|
||||
{{ error }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<nuxt-link to="/dashboard/revenue" class="iconified-button">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</nuxt-link>
|
||||
<button
|
||||
:disabled="
|
||||
knownErrors.length > 0 ||
|
||||
!amount ||
|
||||
!agreedTransfer ||
|
||||
!agreedTerms ||
|
||||
(fees > 0 && !agreedFees)
|
||||
"
|
||||
class="iconified-button brand-button"
|
||||
@click="withdraw"
|
||||
>
|
||||
<TransferIcon />
|
||||
Withdraw
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import {
|
||||
PayPalIcon,
|
||||
SearchIcon,
|
||||
RadioButtonIcon,
|
||||
RadioButtonChecked,
|
||||
Chips,
|
||||
XIcon,
|
||||
TransferIcon,
|
||||
Checkbox,
|
||||
Breadcrumbs,
|
||||
} from 'omorphia'
|
||||
import { all } from 'iso-3166-1'
|
||||
import VenmoIcon from '~/assets/images/external/venmo.svg'
|
||||
|
||||
const auth = await useAuth()
|
||||
const data = useNuxtApp()
|
||||
|
||||
const countries = computed(() =>
|
||||
all().map((x) => ({
|
||||
id: x.alpha2,
|
||||
name: x.alpha2 === 'TW' ? 'Taiwan' : x.country,
|
||||
}))
|
||||
)
|
||||
const search = ref('')
|
||||
|
||||
const amount = ref('')
|
||||
const country = ref(
|
||||
countries.value.find((x) => x.id === (auth.value.user.payout_data.paypal_region ?? 'US'))
|
||||
)
|
||||
|
||||
const { data: payoutMethods, refresh: refreshPayoutMethods } = await useAsyncData(
|
||||
`payout/methods?country=${country.value.id}`,
|
||||
() => useBaseFetch(`payout/methods?country=${country.value.id}`, { apiVersion: 3 })
|
||||
)
|
||||
|
||||
const selectedMethodId = ref(payoutMethods.value[0].id)
|
||||
const selectedMethod = computed(() =>
|
||||
payoutMethods.value.find((x) => x.id === selectedMethodId.value)
|
||||
)
|
||||
|
||||
const parsedAmount = computed(() => {
|
||||
const regex = /^\$?(\d*(\.\d{2})?)$/gm
|
||||
const matches = regex.exec(amount.value)
|
||||
console.log(matches)
|
||||
return matches && matches[1] ? parseFloat(matches[1]) : 0.0
|
||||
})
|
||||
const fees = computed(() => {
|
||||
return Math.min(
|
||||
Math.max(
|
||||
selectedMethod.value.fee.min,
|
||||
selectedMethod.value.fee.percentage * parsedAmount.value
|
||||
),
|
||||
selectedMethod.value.fee.max ?? Number.MAX_VALUE
|
||||
)
|
||||
})
|
||||
const withdrawAccount = computed(() => {
|
||||
if (selectedMethod.value.type === 'paypal') {
|
||||
return auth.value.user.payout_data.paypal_address
|
||||
} else if (selectedMethod.value.type === 'venmo') {
|
||||
return auth.value.user.payout_data.venmo_handle
|
||||
} else {
|
||||
return auth.value.user.email
|
||||
}
|
||||
})
|
||||
const knownErrors = computed(() => {
|
||||
const errors = []
|
||||
if (selectedMethod.value.type === 'paypal' && !auth.value.user.payout_data.paypal_address) {
|
||||
errors.push('Please link your PayPal account in the dashboard to proceed.')
|
||||
}
|
||||
if (selectedMethod.value.type === 'venmo' && !auth.value.user.payout_data.venmo_handle) {
|
||||
errors.push('Please set your Venmo handle in the dashboard to proceed.')
|
||||
}
|
||||
if (selectedMethod.value.type === 'tremendous') {
|
||||
if (!auth.value.user.email) {
|
||||
errors.push('Please set your email address in your account settings to proceed.')
|
||||
}
|
||||
if (!auth.value.user.email_verified) {
|
||||
errors.push('Please verify your email address to proceed.')
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedAmount.value && amount.value.length > 0) {
|
||||
errors.push(`${amount.value} is not a valid amount`)
|
||||
} else if (parsedAmount.value > auth.value.user.payout_data.balance) {
|
||||
errors.push(
|
||||
`The amount must be no more than ${data.$formatMoney(auth.value.user.payout_data.balance)}`
|
||||
)
|
||||
} else if (parsedAmount.value <= fees.value) {
|
||||
errors.push(`The amount must be at least ${data.$formatMoney(fees.value + 0.01)}`)
|
||||
}
|
||||
|
||||
return errors
|
||||
})
|
||||
|
||||
const agreedTransfer = ref(false)
|
||||
const agreedFees = ref(false)
|
||||
const agreedTerms = ref(false)
|
||||
|
||||
watch(country, async () => {
|
||||
await refreshPayoutMethods()
|
||||
console.log(payoutMethods.value)
|
||||
if (payoutMethods.value && payoutMethods.value[0]) {
|
||||
selectedMethodId.value = payoutMethods.value[0].id
|
||||
}
|
||||
})
|
||||
|
||||
async function withdraw() {
|
||||
startLoading()
|
||||
try {
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch(`payout`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
amount: parsedAmount.value,
|
||||
method: selectedMethod.value.type,
|
||||
method_id: selectedMethod.value.id,
|
||||
},
|
||||
apiVersion: 3,
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
await navigateTo('/dashboard/revenue')
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'Withdrawal complete',
|
||||
text:
|
||||
selectedMethod.value.type === 'tremendous'
|
||||
? 'An email has been sent to your account with further instructions on how to redeem your payout!'
|
||||
: `Payment has been sent to your ${data.$formatWallet(
|
||||
selectedMethod.value.type
|
||||
)} account!`,
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.withdraw-options-scroll {
|
||||
max-height: 460px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: var(--gap-md);
|
||||
border: 3px solid var(--color-bg);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--color-bg);
|
||||
border: 3px solid var(--color-bg);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 3px solid var(--color-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.withdraw-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: var(--gap-lg);
|
||||
padding-right: 0.5rem;
|
||||
|
||||
@media screen and (min-width: 300px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.withdraw-option {
|
||||
width: 100%;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-divider);
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-text);
|
||||
|
||||
&.selected {
|
||||
color: var(--color-contrast);
|
||||
|
||||
.label svg {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
aspect-ratio: 30 / 19;
|
||||
|
||||
&.show-bg {
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-user-drag: none;
|
||||
-khtml-user-drag: none;
|
||||
-moz-user-drag: none;
|
||||
-o-user-drag: none;
|
||||
user-drag: none;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap-xs);
|
||||
|
||||
svg {
|
||||
width: 2rem;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: 2rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.enlarge {
|
||||
width: auto;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--gap-md) var(--gap-lg);
|
||||
|
||||
svg {
|
||||
min-height: 1rem;
|
||||
min-width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: var(--color-special-red);
|
||||
}
|
||||
|
||||
.confirm-text {
|
||||
margin: var(--spacing-card-md) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.iconified-input {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.country-multiselect,
|
||||
.iconified-input {
|
||||
max-width: 16rem;
|
||||
}
|
||||
|
||||
.rewards-checkbox {
|
||||
a {
|
||||
margin-left: 0.5ch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -21,9 +21,6 @@
|
||||
<NavStackItem link="/settings/sessions" :label="formatMessage(messages.sessionsTitle)">
|
||||
<ShieldIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/settings/monetization" label="Monetization">
|
||||
<CurrencyIcon />
|
||||
</NavStackItem>
|
||||
</template>
|
||||
<template v-if="auth.user">
|
||||
<h3>Developer Settings</h3>
|
||||
@@ -49,7 +46,6 @@ import NavStackItem from '~/components/ui/NavStackItem.vue'
|
||||
|
||||
import PaintbrushIcon from '~/assets/images/utils/paintbrush.svg'
|
||||
import UserIcon from '~/assets/images/utils/user.svg'
|
||||
import CurrencyIcon from '~/assets/images/utils/currency.svg'
|
||||
import ShieldIcon from '~/assets/images/utils/shield.svg'
|
||||
import KeyIcon from '~/assets/images/utils/key.svg'
|
||||
import LanguagesIcon from '~/assets/images/utils/languages.svg'
|
||||
|
||||
@@ -596,27 +596,6 @@ const authProviders = [
|
||||
},
|
||||
]
|
||||
|
||||
async function removeAuthProvider(provider) {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
startLoading()
|
||||
try {
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<section v-if="enrolled" class="universal-card">
|
||||
<h2>Revenue and metrics</h2>
|
||||
<p>View your revenue and metrics in the creator dashboard:</p>
|
||||
<NuxtLink class="iconified-button" to="/dashboard/revenue">
|
||||
<ChartIcon /> Visit creator dashboard
|
||||
</NuxtLink>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2 class="title">Enrollment</h2>
|
||||
<template v-if="!enrolled && !auth.user.email">
|
||||
<p v-if="!enrolled">
|
||||
You are not currently enrolled in Modrinth's Creator Monetization Program. In order to
|
||||
enroll, you must first add a valid email address to your account.
|
||||
</p>
|
||||
<NuxtLink class="iconified-button" to="/settings/account">
|
||||
<SettingsIcon /> Visit account settings
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else-if="editing || !enrolled">
|
||||
<p v-if="!enrolled">
|
||||
You are not currently enrolled in Modrinth's Creator Monetization Program. Setup a method
|
||||
of receiving payments below to enable monetization.
|
||||
</p>
|
||||
<div class="enroll universal-body">
|
||||
<Chips
|
||||
v-model="selectedWallet"
|
||||
:starting-value="selectedWallet"
|
||||
:items="wallets"
|
||||
:format-label="$formatWallet"
|
||||
@update:model-value="onChangeWallet()"
|
||||
/>
|
||||
|
||||
<p>
|
||||
Enter the information for the
|
||||
{{ $formatWallet(selectedWallet) }} account you would like to receive your revenue from
|
||||
the Creator Monetization Program:
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<Multiselect
|
||||
v-model="accountType"
|
||||
:options="getAccountTypes()"
|
||||
:custom-label="(value) => formatAccountType(value)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
|
||||
<label class="hidden" for="account-input"
|
||||
>{{ $formatWallet(selectedWallet) }}
|
||||
{{ formatAccountType(accountType).toLowerCase() }} input field</label
|
||||
>
|
||||
<input
|
||||
id="account-input"
|
||||
v-model="account"
|
||||
class="account-input"
|
||||
:placeholder="`Enter your ${$formatWallet(selectedWallet)} ${formatAccountType(
|
||||
accountType
|
||||
).toLowerCase()}...`"
|
||||
:type="accountType === 'email' ? 'email' : ''"
|
||||
/>
|
||||
<span v-if="accountType === 'phone'"> Format: +18888888888 or +1-888-888-8888 </span>
|
||||
</div>
|
||||
<div class="rewards-checkbox">
|
||||
<Checkbox v-model="agreed">
|
||||
I agree to the
|
||||
<nuxt-link to="/legal/cmp" class="goto-link">Rewards Program Terms</nuxt-link>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
:disabled="!agreed"
|
||||
class="iconified-button brand-button"
|
||||
@click="updatePayoutData(false)"
|
||||
>
|
||||
<SaveIcon /> Save information
|
||||
</button>
|
||||
<button
|
||||
v-if="enrolled"
|
||||
class="iconified-button danger-button"
|
||||
@click="updatePayoutData(true)"
|
||||
>
|
||||
<TrashIcon /> Remove enrollment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>
|
||||
You are currently enrolled in the Creator Monetization Program with a
|
||||
{{ $formatWallet(selectedWallet) }} account.
|
||||
</p>
|
||||
<button class="iconified-button brand-button" @click="editing = true">
|
||||
<EditIcon /> Edit information
|
||||
</button>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Checkbox } from 'omorphia'
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import Chips from '~/components/ui/Chips.vue'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg'
|
||||
import ChartIcon from '~/assets/images/utils/chart.svg'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Multiselect,
|
||||
Chips,
|
||||
SaveIcon,
|
||||
TrashIcon,
|
||||
EditIcon,
|
||||
ChartIcon,
|
||||
SettingsIcon,
|
||||
Checkbox,
|
||||
},
|
||||
async setup() {
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
const auth = await useAuth()
|
||||
return { auth }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
agreed: false,
|
||||
editing: false,
|
||||
enrolled:
|
||||
this.auth.user.payout_data.payout_wallet &&
|
||||
this.auth.user.payout_data.payout_wallet_type &&
|
||||
this.auth.user.payout_data.payout_address,
|
||||
wallets: ['paypal', 'venmo'],
|
||||
selectedWallet: this.auth.user.payout_data.payout_wallet ?? 'paypal',
|
||||
accountType: this.auth.user.payout_data.payout_wallet_type ?? this.getAccountTypes()[0],
|
||||
account: this.auth.user.payout_data.payout_address ?? '',
|
||||
}
|
||||
},
|
||||
head: {
|
||||
title: 'Monetization settings - Modrinth',
|
||||
},
|
||||
methods: {
|
||||
getAccountTypes() {
|
||||
const types = []
|
||||
if (this.selectedWallet === 'venmo') {
|
||||
types.push('user_handle')
|
||||
}
|
||||
types.push('email')
|
||||
types.push('phone')
|
||||
return types
|
||||
},
|
||||
formatAccountType(value) {
|
||||
switch (value) {
|
||||
case 'email':
|
||||
return 'Email address'
|
||||
case 'phone':
|
||||
return 'Phone number'
|
||||
case 'user_handle':
|
||||
return 'Username'
|
||||
default:
|
||||
return value.charAt(0).toUpperCase() + value.slice(1)
|
||||
}
|
||||
},
|
||||
onChangeWallet() {
|
||||
this.account = ''
|
||||
|
||||
// Set default account type for each wallet
|
||||
if (this.selectedWallet === 'paypal') {
|
||||
this.accountType = 'email'
|
||||
} else if (this.selectedWallet === 'venmo') {
|
||||
this.accountType = 'user_handle'
|
||||
}
|
||||
},
|
||||
async updatePayoutData(unenroll) {
|
||||
startLoading()
|
||||
if (unenroll) {
|
||||
this.selectedWallet = 'paypal'
|
||||
this.accountType = this.getAccountTypes()[0]
|
||||
this.account = ''
|
||||
}
|
||||
try {
|
||||
const data = {
|
||||
payout_data: unenroll
|
||||
? null
|
||||
: {
|
||||
payout_wallet: this.selectedWallet,
|
||||
payout_wallet_type: this.accountType,
|
||||
payout_address: this.account,
|
||||
},
|
||||
}
|
||||
|
||||
await useBaseFetch(`user/${this.auth.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
await useAuth(this.auth.token)
|
||||
|
||||
this.editing = false
|
||||
this.enrolled = !unenroll
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.multiselect {
|
||||
max-width: 15rem;
|
||||
}
|
||||
|
||||
.account-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rewards-checkbox {
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
a {
|
||||
margin-left: 0.5ch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -129,7 +129,7 @@ async function revokeSession(id) {
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
<style lang="scss" socped>
|
||||
.session {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user