New payouts system (#1456)
* initial * finish withdraw page * Finish frontend * Fix UI a bit * tester fixes
This commit is contained in:
parent
1f58aebb2b
commit
2d14e5682d
3
assets/images/external/tremendous.svg
vendored
Normal file
3
assets/images/external/tremendous.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 21 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.8586 1.56939H0V6.06253H7.92626V22.4304H12.9323V6.06253H20.8586V1.56939Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 196 B |
3
assets/images/external/venmo-small.svg
vendored
Normal file
3
assets/images/external/venmo-small.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M17,3H7C4.8,3,3,4.8,3,7v10c0,2.2,1.8,4,4,4h10c2.2,0,4-1.8,4-4V7C21,4.8,19.2,3,17,3z M12.88,17.25c-3.5,0-4.13,0-4.13,0 l-1.63-10l3.63-0.5L11.62,14c0,0,2.63-3.75,1.5-6.63c0.76-0.12,3.26-0.62,3.26-0.62S19.25,8.5,12.88,17.25z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
38
assets/images/external/venmo.svg
vendored
Normal file
38
assets/images/external/venmo.svg
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="194.46875mm"
|
||||
height="35.71875mm"
|
||||
viewBox="0 0 194.46875 35.71875"
|
||||
version="1.1"
|
||||
>
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(-5.7593441,-130.66032)">
|
||||
<g
|
||||
style="fill:none"
|
||||
id="g838"
|
||||
transform="matrix(0.9244437,0,0,0.89314785,5.7593441,129.92613)">
|
||||
<path
|
||||
id="path815"
|
||||
d="m 34.5771,0.822021 c 1.4203,2.345309 2.0606,4.760989 2.0606,7.812489 0,9.73269 -8.31,22.37619 -15.0545,31.25429 H 6.17825 L 0,2.95296 13.4887,1.67258 16.7552,27.9548 c 3.0522,-4.9714 6.8186,-12.7838 6.8186,-18.11027 0,-2.91551 -0.4995,-4.90135 -1.2803,-6.53647 z"
|
||||
style="fill:#fff" />
|
||||
<path
|
||||
id="path817"
|
||||
d="m 52.0595,17.0887 c 2.4822,0 8.7312,-1.1353 8.7312,-4.6863 0,-1.7051 -1.2059,-2.55564 -2.627,-2.55564 -2.4861,0 -5.7487,2.98074 -6.1042,7.24194 z m -0.2844,7.0327 c 0,4.3359 2.4114,6.037 5.6083,6.037 3.4813,0 6.8145,-0.8506 11.1469,-3.0519 l -1.6318,11.0787 c -3.0525,1.4911 -7.8097,2.4861 -12.4272,2.4861 -11.7129,0 -15.9049,-7.102 -15.9049,-15.9805 0,-11.5074 6.8189,-23.7262 20.8772,-23.7262 7.7401,0 12.0681,4.33553 12.0681,10.3725 7e-4,9.7324 -12.4929,12.7139 -19.7366,12.7843 z"
|
||||
style="fill:#fff" />
|
||||
<path
|
||||
id="path819"
|
||||
d="m 110.439,9.34835 c 0,1.42035 -0.215,3.48055 -0.43,4.82695 l -4.047,25.5721 H 92.8275 l 3.6921,-23.4415 c 0.07,-0.6358 0.2852,-1.9158 0.2852,-2.626 0,-1.7052 -1.0655,-2.1306 -2.3465,-2.1306 -1.7015,0 -3.407,0.7805 -4.5428,1.3504 l -4.1877,26.848 H 72.5195 L 78.5537,1.46185 h 11.4318 l 0.1447,3.05588 c 2.697,-1.77549 6.2483,-3.695708 11.2868,-3.695708 6.676,-7.3e-4 9.022,3.409878 9.022,8.526328 z"
|
||||
style="fill:#fff" />
|
||||
<path
|
||||
id="path821"
|
||||
d="m 149.432,5.15577 c 3.762,-2.69641 7.314,-4.19117 12.211,-4.19117 6.744,0 9.09,3.41061 9.09,8.52707 0,1.42043 -0.215,3.48063 -0.429,4.82703 l -4.043,25.572 h -13.138 l 3.763,-23.9369 c 0.069,-0.6399 0.215,-1.4204 0.215,-1.9155 0,-1.9199 -1.066,-2.3457 -2.347,-2.3457 -1.631,0 -3.262,0.7102 -4.473,1.3504 l -4.187,26.8481 H 132.96 l 3.762,-23.937 c 0.069,-0.6398 0.211,-1.4203 0.211,-1.9154 0,-1.9199 -1.067,-2.3457 -2.343,-2.3457 -1.705,0 -3.407,0.7805 -4.543,1.3504 l -4.191,26.8481 h -13.204 l 6.034,-38.28598 h 11.292 l 0.355,3.19624 c 2.627,-1.91548 6.175,-3.835703 10.932,-3.835703 4.119,-0.001458 6.815,1.774393 8.167,4.189713 z"
|
||||
style="fill:#fff" />
|
||||
<path
|
||||
id="path823"
|
||||
d="m 196.869,16.3076 c 0,-3.1255 -0.782,-5.2564 -3.123,-5.2564 -5.183,0 -6.248,9.1621 -6.248,13.8491 0,3.5557 0.995,5.7563 3.336,5.7563 4.899,0 6.035,-9.6624 6.035,-14.349 z m -22.719,8.0269 c 0,-12.0737 6.389,-23.371121 21.088,-23.371121 11.076,0 15.125,6.536471 15.125,15.558621 0,11.9336 -6.32,24.292 -21.374,24.292 -11.147,0 -14.839,-7.317 -14.839,-16.4795 z"
|
||||
style="fill:#fff" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@ -268,7 +268,7 @@
|
||||
@media screen and (max-width: 750px) {
|
||||
&:not(&.small) {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
align-items: flex-start;
|
||||
|
||||
.stylized-toggle {
|
||||
flex-basis: 0;
|
||||
@ -339,7 +339,7 @@
|
||||
.adjacent-input,
|
||||
&.adjacent-input &:not(&.small) {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,207 +0,0 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="'Transfer to ' + $formatWallet(wallet)">
|
||||
<div class="modal-transfer">
|
||||
<span
|
||||
>You are initiating a transfer of your revenue from Modrinth's Creator Monetization Program.
|
||||
How much of your <strong>{{ $formatMoney(balance) }}</strong> balance would you like to
|
||||
transfer?</span
|
||||
>
|
||||
<div class="confirmation-input">
|
||||
<input
|
||||
id="confirmation"
|
||||
v-model="amount"
|
||||
type="text"
|
||||
pattern="^\d*(\.\d{0,2})?$"
|
||||
autocomplete="off"
|
||||
placeholder="Amount to transfer..."
|
||||
/>
|
||||
</div>
|
||||
<div class="confirm-text">
|
||||
<Checkbox
|
||||
v-if="isValidInput() && parseInput() >= minWithdraw && parseInput() <= balance"
|
||||
v-model="consentedFee"
|
||||
description="Consent to fee"
|
||||
>
|
||||
<template v-if="wallet === 'venmo'">
|
||||
I acknowledge that $0.25 will be deducted from the amount I receive to cover
|
||||
{{ $formatWallet(wallet) }} processing fees.
|
||||
</template>
|
||||
<template v-else>
|
||||
I acknowledge that an estimated
|
||||
{{ $formatMoney(calcProcessingFees()) }} will be deducted from the amount I receive to
|
||||
cover {{ $formatWallet(wallet) }} processing fees and that any excess will be returned
|
||||
to my Modrinth balance.
|
||||
</template>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
v-if="isValidInput() && parseInput() >= minWithdraw && parseInput() <= balance"
|
||||
v-model="consentedAccount"
|
||||
description="Confirm transfer"
|
||||
>
|
||||
I confirm that I an initiating a transfer to the following
|
||||
{{ $formatWallet(wallet) }} account: {{ account }}
|
||||
</Checkbox>
|
||||
<span v-else-if="validInput && parseInput() < minWithdraw" class="invalid">
|
||||
The amount must be at least {{ $formatMoney(minWithdraw) }}</span
|
||||
>
|
||||
<span v-else-if="validInput && parseInput() > balance" class="invalid">
|
||||
The amount must be no more than {{ $formatMoney(balance) }}</span
|
||||
>
|
||||
<span v-else-if="amount.length > 0" class="invalid">
|
||||
{{ amount }} is not a valid amount</span
|
||||
>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Monetization settings
|
||||
</NuxtLink>
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!consentedFee || !consentedAccount"
|
||||
@click="proceed"
|
||||
>
|
||||
<TransferIcon />
|
||||
Transfer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
CrossIcon,
|
||||
SettingsIcon,
|
||||
TransferIcon,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
wallet: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
accountType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
balance: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
minWithdraw: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
consentedFee: false,
|
||||
consentedAccount: false,
|
||||
amount: '',
|
||||
validInput: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.amount = ''
|
||||
this.consentedFee = false
|
||||
this.consentedAccount = false
|
||||
this.validInput = false
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
async proceed() {
|
||||
startLoading()
|
||||
try {
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch(`user/${auth.value.user.id}/payouts`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
amount: Number(this.amount.replace('$', '')),
|
||||
},
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
this.$refs.modal.hide()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
isValidInput() {
|
||||
const regex = /^\$?(\d*(\.\d{2})?)$/gm
|
||||
this.validInput = regex.test(this.amount) && this.amount.length > 0
|
||||
return this.validInput
|
||||
},
|
||||
parseInput() {
|
||||
const regex = /^\$?(\d*(\.\d{2})?)$/gm
|
||||
const matches = regex.exec(this.amount)
|
||||
return parseFloat(matches[1])
|
||||
},
|
||||
calcProcessingFees() {
|
||||
if (this.wallet === 'venmo') {
|
||||
return 0.25
|
||||
} else {
|
||||
return Math.max(0.25, Math.min(this.parseInput() * 0.02, 20))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-transfer {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-sm);
|
||||
|
||||
.confirmation-input {
|
||||
input {
|
||||
width: 14rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: var(--color-special-red);
|
||||
}
|
||||
|
||||
.confirm-text {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
min-height: 7rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -103,3 +103,27 @@ export const getAuthUrl = (provider, redirect = '') => {
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?url=${fullURL}&provider=${provider}`
|
||||
}
|
||||
|
||||
export const removeAuthProvider = async (provider) => {
|
||||
startLoading()
|
||||
try {
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
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()
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
"floating-vue": "^2.0.0-beta.20",
|
||||
"fuse.js": "^6.6.2",
|
||||
"highlight.js": "^11.7.0",
|
||||
"iso-3166-1": "^2.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
|
||||
@ -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;
|
||||
|
||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -25,6 +25,9 @@ dependencies:
|
||||
highlight.js:
|
||||
specifier: ^11.7.0
|
||||
version: 11.7.0
|
||||
iso-3166-1:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
@ -5612,6 +5615,10 @@ packages:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
dev: true
|
||||
|
||||
/iso-3166-1@2.1.1:
|
||||
resolution: {integrity: sha512-RZxXf8cw5Y8LyHZIwIRvKw8sWTIHh2/txBT+ehO0QroesVfnz3JNFFX4i/OC/Yuv2bDIVYrHna5PMvjtpefq5w==}
|
||||
dev: false
|
||||
|
||||
/jackspeak@2.2.1:
|
||||
resolution: {integrity: sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user