You've already forked AstralRinth
* 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>
458 lines
12 KiB
Vue
458 lines
12 KiB
Vue
<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>
|