You've already forked AstralRinth
forked from didirus/AstralRinth
feat: creator revenue page overhaul (#4204)
* feat: start on tax compliance * feat: avarala1099 composable * fix: shouldShow should be managed on the page itself * refactor: move show logic to revenue page * feat: security practices rather than info * feat: withdraw page lock * fix: empty modal bug & lint issues * feat: hide behind feature flag * Use standard admonition components, make casing consistent * modal title * lint * feat: withdrawal check * feat: tax cap on withdrawals warning * feat: start on revenue page overhaul * feat: segment generation for bar * feat: tooltips and links * fix: tooltip border * feat: finish initial layout, start on withdraw modal * feat: start on withdrawal limit stage * feat: shade support for primary colors * feat: start on withdraw details stage * fix: convert swatches to hex * feat: payout method/region dropdown temporarily using multiselect * feat: fix modal open issues and use teleport dropdowns * feat: hide transactions section if there are no transactions * refactor: NavStack surfaces * feat: new dropdown component * feat: remove teleport dropdown modal in favour of new combobox component * fix: lint * refactor: dashboard sidebar layout * feat: cleanup * fix: niche bugs * fix: ComboBox styling * feat: first part of qa * feat: animate flash rather than tooltip * fix: lint * feat: qa border gradient * fix: seg hover flashes * feat: i18n * feat: i18n and final QA * fix: lint * feat: QA * fix: lint * fix: merge conflicts * fix: intl * fix: blue hover * fix: transfers page * feat: surface variables & gradients * feat: text vars * fix: lint * fix: intl * feat: stages * fix: lint * feat: region selection * feat: method selection btns * fix: flex col on transactions * feat: hook up method selection to ctx * feat: muralpay kyc stage info * wip: muralpay integration * Basic Mural Pay API bindings * Fix clippy * use dotenvy in muralpay example * Refactor payout creation code * wip: muralpay payout requests * Mural Pay payouts work * Fix clippy * feat: progress * fix: broken tax form stage logic * polish: tax form stage and method selection stage layout * add mural pay fees API * Work on payout fee API * Fees API for more payment methods * Fix CI * polish: muralpay qa * refactor: clean up combobox component * polish: change from critical -> warning admonition in MuralpayDetailsStage * Temporarily disable Venmo and PayPal methods from frontend * polish: clean up transaction component & page * polish: navbar qa, text color-contrast in chips type buttonstyled, mb on rev/index.vue page * fix: incorrectly using available balance as tax form withdraw limit after tax forms submitted * wip: counterparties * Start on counterparties and payment methods API * polish: combobox component * polish: fix broken scroll logic using a composable & web:fix * fix: lint * polish: various QA fixes * feat: hook up with backend (wip) * feat: draft muralpay rails dynamic logic * polish: modify rails to support backend changes * Mural Pay multiple methods when fetching * Don't send supported_countries to frontend * Mural Pay multiple methods when fetching * Don't send supported_countries to frontend * feat: fees & methods endpoint hookup * chore: remove duplicates fix * polish: qa changes + figma match * Add countries to muralpay fiat methods * Compile fix * Add exchange rate info to fees endpoint * Add fees to premium Tremendous options * polish: i18n and better document type dropdown -> id input labels * feat: tremendous * fix: lint & i18n * feat: reintroduce tin mismatch logic to index.vue * polish: qa * fix: i18n * feat: remove teleport dropdown menu - combobox should be used * fix: lint * fix: jsdoc * feat: checkbox for reward program terms * Add delivery email field to Tremendous payouts * Add Tremendous product category to payout methods * Add bank details API to muralpay * Fix CI * Fix CI * polish: qa changes * feat: i18n pass * feat: deduplicate methods endpoint & fix i18n issues * chore: deduplicate i18n strings into common-messages.ts * fix: lint * fix: i18n * feat: estimates * polish: more QA * Remove prepaid visa, compute fees properly for Tremendous methods * Add more details to Tremendous errors * feat: withdraw endpoint impl & internals refactor * Add more details to Tremendous errors * feat: completion stage * Add fees to Mural * feat: transactions page match figma * fix: i18n * polish: QA changes * polish: qa * Payout history route and bank details * polish: autofill and requirements checks * fix: i18n + lint * fix: fiat rail fees * polish: move scroll fade stuff into NewModal rather than just CreatorWithdrawModal * feat: simplify action btn logic & tax form error * fix: tax -> Tax form * Re-add legacy PayPal/Venmo options for US * feat: mobile responsiveness fixes for modal * fix: responsiveness issues * feat: navstack responsiveness * fix: responsiveness * move the mural bank details route * fix: generated state cleanup & bank details input * fix: lint & i18n * Add utoipa support to payout endpoints * address some PR comments * polish: qa * add CORS to new utoipa routes * feat: legacy paypal/venmo stage * polish: reset amount on back qa * revert: navstack mr changes * polish: loading indicator on method selection stage * fix: paypal modal doesnt reopen after auth * fix: lint & i18n * fix: paypal flow * polish: qa changes * fix: gitignore * polish: qa fixes * fix: payouts_available in payouts.rs * fix: bug when limit is zero * polish: qa changes * fix: qa stuff & muralpay sub-division fix * Immediately approve mural payouts * Add currency support to Tremendous payouts * Currency forex * add forex to tremendous fee request * polish: qa & currency support for paypal tremendous * polish: fx qa * feat: demo mode flag * fix: i18n & padding issues * polish: qa changes * fix: ml * Add Mural balance to bank balance info * polish: show warning for paypal international USD withdrawals + more currencies * Add more Tremendous currencies support * fix: colors on balance bars * fix: empty states * fix: pl-8 mobile issue * fix: hide see all * Transaction payouts available use the correct date * Address my own review comment * Address PR comments * Change Mural withdrawal limit to 3k * fix: empty state + paypal warning * maybe fix tremendous gift cards * Change how Mural minimum withdrawals are calculated * Tweak min/max withdrawal values * fix: segment brightness * fix: min & max for muralpay & legacy paypal * Fix some icon issues * more issues * fix user menu * fix: remove + network --------- Signed-off-by: Calum H. <contact@cal.engineer> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> Co-authored-by: aecsocket <aecsocket@tutanota.com> Co-authored-by: Alejandro González <me@alegon.dev>
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<NewModal
|
||||
ref="withdrawModal"
|
||||
:closable="currentStage !== 'completion'"
|
||||
:hide-header="currentStage === 'completion'"
|
||||
:merge-header="currentStage === 'completion'"
|
||||
:scrollable="true"
|
||||
max-content-height="72vh"
|
||||
:on-hide="onModalHide"
|
||||
>
|
||||
<template #title>
|
||||
<div v-if="shouldShowTitle" class="flex flex-wrap items-center gap-1 text-secondary">
|
||||
<template v-if="currentStage === 'tax-form'">
|
||||
<span class="text-lg font-bold text-contrast sm:text-xl">{{
|
||||
formatMessage(messages.taxFormStage)
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-else-if="currentStage === 'method-selection'">
|
||||
<span class="text-lg font-bold text-contrast sm:text-xl">{{
|
||||
formatMessage(messages.methodSelectionStage)
|
||||
}}</span>
|
||||
<ChevronRightIcon class="size-5 text-secondary" stroke-width="3" />
|
||||
<span class="text-lg text-secondary sm:text-xl">{{
|
||||
formatMessage(messages.detailsLabel)
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-else-if="isDetailsStage">
|
||||
<button
|
||||
class="active:scale-9 bg-transparent p-0 text-lg text-secondary transition-colors duration-200 hover:text-primary sm:text-xl"
|
||||
@click="goToBreadcrumbStage('method-selection')"
|
||||
>
|
||||
{{ formatMessage(messages.methodSelectionStage) }}
|
||||
</button>
|
||||
<ChevronRightIcon class="size-5 text-secondary" stroke-width="3" />
|
||||
<span class="text-lg font-bold text-contrast sm:text-xl">{{
|
||||
formatMessage(messages.detailsLabel)
|
||||
}}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full max-w-[496px] lg:min-w-[496px]">
|
||||
<TaxFormStage
|
||||
v-if="currentStage === 'tax-form'"
|
||||
:balance="balance"
|
||||
:on-show-tax-form="showTaxFormModal"
|
||||
/>
|
||||
<MethodSelectionStage
|
||||
v-else-if="currentStage === 'method-selection'"
|
||||
:on-show-tax-form="showTaxFormModal"
|
||||
@close-modal="withdrawModal?.hide()"
|
||||
/>
|
||||
<TremendousDetailsStage v-else-if="currentStage === 'tremendous-details'" />
|
||||
<MuralpayKycStage v-else-if="currentStage === 'muralpay-kyc'" />
|
||||
<MuralpayDetailsStage v-else-if="currentStage === 'muralpay-details'" />
|
||||
<LegacyPaypalDetailsStage v-else-if="currentStage === 'paypal-details'" />
|
||||
<CompletionStage v-else-if="currentStage === 'completion'" />
|
||||
<div v-else>Something went wrong</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div v-if="currentStage === 'completion'" class="mt-4 flex w-full gap-3">
|
||||
<ButtonStyled class="flex-1">
|
||||
<button class="w-full text-contrast" @click="handleClose">
|
||||
{{ formatMessage(messages.closeButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled class="flex-1">
|
||||
<button class="w-full text-contrast" @click="handleViewTransactions">
|
||||
{{ formatMessage(messages.transactionsButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else class="mt-4 flex flex-col justify-end gap-2 sm:flex-row">
|
||||
<ButtonStyled type="outlined">
|
||||
<button
|
||||
class="!border-surface-5"
|
||||
:disabled="leftButtonConfig.disabled"
|
||||
@click="leftButtonConfig.handler"
|
||||
>
|
||||
<component :is="leftButtonConfig.icon" />
|
||||
{{ leftButtonConfig.label }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled :color="rightButtonConfig.color">
|
||||
<button :disabled="rightButtonConfig.disabled" @click="rightButtonConfig.handler">
|
||||
<component
|
||||
:is="rightButtonConfig.icon"
|
||||
v-if="rightButtonConfig.iconPosition === 'before'"
|
||||
:class="rightButtonConfig.iconClass"
|
||||
/>
|
||||
{{ rightButtonConfig.label }}
|
||||
<component
|
||||
:is="rightButtonConfig.icon"
|
||||
v-if="rightButtonConfig.iconPosition === 'after'"
|
||||
:class="rightButtonConfig.iconClass"
|
||||
/>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
<CreatorTaxFormModal
|
||||
ref="taxFormModal"
|
||||
close-button-text="Continue"
|
||||
@success="onTaxFormSuccess"
|
||||
@cancelled="onTaxFormCancelled"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
ChevronRightIcon,
|
||||
FileTextIcon,
|
||||
LeftArrowIcon,
|
||||
RightArrowIcon,
|
||||
SpinnerIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import {
|
||||
createWithdrawContext,
|
||||
type PayoutMethod,
|
||||
provideWithdrawContext,
|
||||
TAX_THRESHOLD_ACTUAL,
|
||||
type WithdrawStage,
|
||||
} from '@/providers/creator-withdraw.ts'
|
||||
|
||||
import CreatorTaxFormModal from './CreatorTaxFormModal.vue'
|
||||
import CompletionStage from './withdraw-stages/CompletionStage.vue'
|
||||
import LegacyPaypalDetailsStage from './withdraw-stages/LegacyPaypalDetailsStage.vue'
|
||||
import MethodSelectionStage from './withdraw-stages/MethodSelectionStage.vue'
|
||||
import MuralpayDetailsStage from './withdraw-stages/MuralpayDetailsStage.vue'
|
||||
import MuralpayKycStage from './withdraw-stages/MuralpayKycStage.vue'
|
||||
import TaxFormStage from './withdraw-stages/TaxFormStage.vue'
|
||||
import TremendousDetailsStage from './withdraw-stages/TremendousDetailsStage.vue'
|
||||
|
||||
type FormCompletionStatus = 'unknown' | 'unrequested' | 'unsigned' | 'tin-mismatch' | 'complete'
|
||||
|
||||
interface UserBalanceResponse {
|
||||
available: number
|
||||
withdrawn_lifetime: number
|
||||
withdrawn_ytd: number
|
||||
pending: number
|
||||
dates: Record<string, number>
|
||||
requested_form_type: string | null
|
||||
form_completion_status: FormCompletionStatus | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
balance: UserBalanceResponse | null
|
||||
preloadedPaymentData?: { country: string; methods: PayoutMethod[] } | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'refresh-data' | 'hide'): void
|
||||
}>()
|
||||
|
||||
const withdrawModal = useTemplateRef<InstanceType<typeof NewModal>>('withdrawModal')
|
||||
const taxFormModal = ref<InstanceType<typeof CreatorTaxFormModal> | null>(null)
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
function show(preferred?: WithdrawStage) {
|
||||
if (preferred) {
|
||||
setStage(preferred, true)
|
||||
withdrawModal.value?.show()
|
||||
return
|
||||
}
|
||||
|
||||
const firstStage = stages.value[0]
|
||||
if (firstStage) {
|
||||
setStage(firstStage, true)
|
||||
}
|
||||
|
||||
withdrawModal.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const withdrawContext = createWithdrawContext(
|
||||
props.balance,
|
||||
props.preloadedPaymentData || undefined,
|
||||
)
|
||||
provideWithdrawContext(withdrawContext)
|
||||
|
||||
const {
|
||||
currentStage,
|
||||
previousStep,
|
||||
nextStep,
|
||||
canProceed,
|
||||
setStage,
|
||||
withdrawData,
|
||||
resetData,
|
||||
stages,
|
||||
submitWithdrawal,
|
||||
restoreStateFromStorage,
|
||||
clearSavedState,
|
||||
} = withdrawContext
|
||||
|
||||
watch(
|
||||
() => props.balance,
|
||||
(newBalance) => {
|
||||
if (newBalance) {
|
||||
withdrawContext.balance.value = newBalance
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
if (route.query.paypal_auth_return === 'true') {
|
||||
const savedState = restoreStateFromStorage()
|
||||
|
||||
if (savedState?.data) {
|
||||
withdrawData.value = { ...savedState.data }
|
||||
|
||||
nextTick(() => {
|
||||
show(savedState.stage)
|
||||
})
|
||||
|
||||
clearSavedState()
|
||||
}
|
||||
|
||||
const query = { ...route.query }
|
||||
delete query.paypal_auth_return
|
||||
router.replace({ query })
|
||||
}
|
||||
})
|
||||
|
||||
const needsTaxForm = computed(() => {
|
||||
if (!props.balance || currentStage.value !== 'tax-form') return false
|
||||
const ytd = props.balance.withdrawn_ytd ?? 0
|
||||
const available = props.balance.available ?? 0
|
||||
const status = props.balance.form_completion_status
|
||||
return status !== 'complete' && ytd + available >= 600
|
||||
})
|
||||
|
||||
const remainingLimit = computed(() => {
|
||||
if (!props.balance) return 0
|
||||
const ytd = props.balance.withdrawn_ytd ?? 0
|
||||
const raw = TAX_THRESHOLD_ACTUAL - ytd
|
||||
if (raw <= 0) return 0
|
||||
const cents = Math.floor(raw * 100)
|
||||
return cents / 100
|
||||
})
|
||||
|
||||
const leftButtonConfig = computed(() => {
|
||||
if (previousStep.value) {
|
||||
return {
|
||||
icon: LeftArrowIcon,
|
||||
label: formatMessage(commonMessages.backButton),
|
||||
handler: () => setStage(previousStep.value, true),
|
||||
disabled: isSubmitting.value,
|
||||
}
|
||||
}
|
||||
return {
|
||||
icon: XIcon,
|
||||
label: formatMessage(commonMessages.cancelButton),
|
||||
handler: () => withdrawModal.value?.hide(),
|
||||
disabled: isSubmitting.value,
|
||||
}
|
||||
})
|
||||
|
||||
const rightButtonConfig = computed(() => {
|
||||
const stage = currentStage.value
|
||||
const isTaxFormStage = stage === 'tax-form'
|
||||
const isDetailsStage =
|
||||
stage === 'muralpay-details' || stage === 'tremendous-details' || stage === 'paypal-details'
|
||||
|
||||
if (isTaxFormStage && needsTaxForm.value && remainingLimit.value > 0) {
|
||||
return {
|
||||
icon: RightArrowIcon,
|
||||
label: formatMessage(messages.continueWithLimit),
|
||||
handler: continueWithLimit,
|
||||
disabled: false,
|
||||
color: 'standard' as const,
|
||||
iconPosition: 'after' as const,
|
||||
}
|
||||
}
|
||||
|
||||
if (isTaxFormStage && needsTaxForm.value) {
|
||||
return {
|
||||
icon: FileTextIcon,
|
||||
label: formatMessage(messages.completeTaxForm),
|
||||
handler: showTaxFormModal,
|
||||
disabled: false,
|
||||
color: 'orange' as const,
|
||||
iconPosition: 'before' as const,
|
||||
}
|
||||
}
|
||||
|
||||
if (isDetailsStage) {
|
||||
return {
|
||||
icon: isSubmitting.value ? SpinnerIcon : ArrowLeftRightIcon,
|
||||
iconClass: isSubmitting.value ? 'animate-spin' : undefined,
|
||||
label: formatMessage(messages.withdrawButton),
|
||||
handler: handleWithdraw,
|
||||
disabled: !canProceed.value || isSubmitting.value,
|
||||
color: 'brand' as const,
|
||||
iconPosition: 'before' as const,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
icon: RightArrowIcon,
|
||||
label: formatMessage(commonMessages.nextButton),
|
||||
handler: () => setStage(nextStep.value),
|
||||
disabled: !canProceed.value,
|
||||
color: 'standard' as const,
|
||||
iconPosition: 'after' as const,
|
||||
}
|
||||
})
|
||||
|
||||
function continueWithLimit() {
|
||||
withdrawData.value.tax.skipped = true
|
||||
setStage(nextStep.value)
|
||||
}
|
||||
|
||||
async function handleWithdraw() {
|
||||
if (isSubmitting.value) return
|
||||
|
||||
try {
|
||||
isSubmitting.value = true
|
||||
await submitWithdrawal()
|
||||
setStage('completion')
|
||||
} catch (error) {
|
||||
console.error('Withdrawal failed:', error)
|
||||
|
||||
if ((error as any)?.data?.description?.toLower?.()?.includes('Tax form')) {
|
||||
addNotification({
|
||||
title: 'Please complete tax form',
|
||||
text: 'You must complete a tax form to submit your withdrawal request.',
|
||||
type: 'error',
|
||||
})
|
||||
} else {
|
||||
addNotification({
|
||||
title: 'Unable to withdraw',
|
||||
text: 'We were unable to submit your withdrawal request, please check your details or contact support.',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const shouldShowTitle = computed(() => {
|
||||
return currentStage.value !== 'completion'
|
||||
})
|
||||
|
||||
const isDetailsStage = computed(() => {
|
||||
const detailsStages: WithdrawStage[] = [
|
||||
'tremendous-details',
|
||||
'muralpay-kyc',
|
||||
'muralpay-details',
|
||||
'paypal-details',
|
||||
]
|
||||
const current = currentStage.value
|
||||
return current ? detailsStages.includes(current) : false
|
||||
})
|
||||
|
||||
function showTaxFormModal(e?: MouseEvent) {
|
||||
withdrawModal.value?.hide()
|
||||
taxFormModal.value?.startTaxForm(e ?? new MouseEvent('click'))
|
||||
}
|
||||
|
||||
function onTaxFormSuccess() {
|
||||
emit('refresh-data')
|
||||
nextTick(() => {
|
||||
show('method-selection')
|
||||
})
|
||||
}
|
||||
|
||||
function onTaxFormCancelled() {
|
||||
show('tax-form')
|
||||
}
|
||||
|
||||
function onModalHide() {
|
||||
resetData()
|
||||
emit('hide')
|
||||
}
|
||||
|
||||
function goToBreadcrumbStage(stage: WithdrawStage) {
|
||||
setStage(stage, true)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
withdrawModal.value?.hide()
|
||||
emit('refresh-data')
|
||||
}
|
||||
|
||||
function handleViewTransactions() {
|
||||
withdrawModal.value?.hide()
|
||||
navigateTo('/dashboard/revenue/transfers')
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
taxFormStage: {
|
||||
id: 'dashboard.creator-withdraw-modal.stage.tax-form',
|
||||
defaultMessage: 'Tax form',
|
||||
},
|
||||
methodSelectionStage: {
|
||||
id: 'dashboard.creator-withdraw-modal.stage.method-selection',
|
||||
defaultMessage: 'Method',
|
||||
},
|
||||
tremendousDetailsStage: {
|
||||
id: 'dashboard.creator-withdraw-modal.stage.tremendous-details',
|
||||
defaultMessage: 'Details',
|
||||
},
|
||||
muralpayKycStage: {
|
||||
id: 'dashboard.creator-withdraw-modal.stage.muralpay-kyc',
|
||||
defaultMessage: 'Verification',
|
||||
},
|
||||
muralpayDetailsStage: {
|
||||
id: 'dashboard.creator-withdraw-modal.stage.muralpay-details',
|
||||
defaultMessage: 'Account Details',
|
||||
},
|
||||
completionStage: {
|
||||
id: 'dashboard.creator-withdraw-modal.stage.completion',
|
||||
defaultMessage: 'Complete',
|
||||
},
|
||||
detailsLabel: {
|
||||
id: 'dashboard.creator-withdraw-modal.details-label',
|
||||
defaultMessage: 'Details',
|
||||
},
|
||||
completeTaxForm: {
|
||||
id: 'dashboard.creator-withdraw-modal.complete-tax-form',
|
||||
defaultMessage: 'Complete tax form',
|
||||
},
|
||||
continueWithLimit: {
|
||||
id: 'dashboard.creator-withdraw-modal.continue-with-limit',
|
||||
defaultMessage: 'Continue with limit',
|
||||
},
|
||||
withdrawButton: {
|
||||
id: 'dashboard.creator-withdraw-modal.withdraw-button',
|
||||
defaultMessage: 'Withdraw',
|
||||
},
|
||||
closeButton: {
|
||||
id: 'dashboard.withdraw.completion.close-button',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
transactionsButton: {
|
||||
id: 'dashboard.withdraw.completion.transactions-button',
|
||||
defaultMessage: 'Transactions',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user