Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 104 additions & 10 deletions apps/frontend/src/components/ui/dashboard/RevenueTransaction.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
<template>
<div class="flex flex-row gap-2 md:gap-3">
<div
class="flex h-10 min-h-10 w-10 min-w-10 justify-center rounded-full border-[1px] border-solid border-button-bg bg-bg-raised !p-0 shadow-md md:h-12 md:min-h-12 md:w-12 md:min-w-12"
class="flex h-10 min-h-10 w-10 min-w-10 items-center justify-center rounded-full border-[1px] border-solid border-button-bg bg-bg-raised !p-0 shadow-md md:h-12 md:min-h-12 md:w-12 md:min-w-12"
>
<ArrowDownIcon v-if="isIncome" class="my-auto size-6 text-secondary md:size-8" />
<ArrowUpIcon v-else class="my-auto size-6 text-secondary md:size-8" />
<img
v-if="methodIconUrl"
:src="methodIconUrl"
alt=""
class="size-6 rounded-full object-cover md:size-8"
/>
<component
:is="methodIconComponent"
v-else-if="methodIconComponent"
class="size-6 md:size-8"
/>
<ArrowDownIcon v-else-if="isIncome" class="size-6 text-secondary md:size-8" />
<ArrowUpIcon v-else class="size-6 text-secondary md:size-8" />
</div>
<div class="flex w-full flex-row justify-between">
<div class="flex flex-col">
<span class="text-base font-semibold text-contrast md:text-lg">{{
transaction.type === 'payout_available'
? formatPayoutSource(transaction.payout_source)
: formatMethodName(transaction.method_type || transaction.method)
: formatMethodName(transaction.method_type || transaction.method, transaction.method_id)
}}</span>
<span class="text-xs text-secondary md:text-sm">
<template v-if="transaction.type === 'withdrawal'">
Expand All @@ -33,8 +44,8 @@
<div class="my-auto flex flex-row items-center gap-2">
<span
class="text-base font-semibold md:text-lg"
:class="transaction.type === 'payout_available' ? 'text-green' : 'text-contrast'"
>{{ formatMoney(transaction.amount) }}</span
:class="isIncome ? 'text-green' : 'text-contrast'"
>{{ isIncome ? '' : '-' }}{{ formatMoney(transaction.amount) }}</span
>
<template v-if="transaction.type === 'withdrawal' && transaction.status === 'in-transit'">
<Tooltip theme="dismissable-prompt" :triggers="['hover', 'focus']" no-auto-focus>
Expand All @@ -55,12 +66,27 @@
</template>

<script setup lang="ts">
import { ArrowDownIcon, ArrowUpIcon, XIcon } from '@modrinth/assets'
import { BulletDivider, ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import {
ArrowDownIcon,
ArrowUpIcon,
LandmarkIcon,
PayPalColorIcon,
VenmoColorIcon,
XIcon,
} from '@modrinth/assets'
import {
BulletDivider,
ButtonStyled,
getCurrencyIcon,
injectNotificationManager,
} from '@modrinth/ui'
import { capitalizeString, formatMoney } from '@modrinth/utils'
import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue'

import { useGeneratedState } from '~/composables/generated'
import { findRail } from '~/utils/muralpay-rails'

type PayoutStatus = 'in-transit' | 'cancelling' | 'cancelled' | 'success' | 'failed'
type PayoutMethodType = 'paypal' | 'venmo' | 'tremendous' | 'muralpay'
type PayoutSource = 'creator_rewards' | 'affilites'
Expand Down Expand Up @@ -96,25 +122,93 @@ const emit = defineEmits<{
}>()

const { addNotification } = injectNotificationManager()
const generatedState = useGeneratedState()

const isIncome = computed(() => props.transaction.type === 'payout_available')

const methodIconUrl = computed(() => {
if (props.transaction.type !== 'withdrawal') return null
const method = props.transaction.method_type || props.transaction.method
const methodId = props.transaction.method_id

if (method === 'tremendous' && methodId) {
const methodInfo = generatedState.value.tremendousIdMap?.[methodId]
if (methodInfo?.name?.toLowerCase()?.includes('paypal')) return null
return methodInfo?.image_url ?? null
}

return null
})

const methodIconComponent = computed(() => {
if (props.transaction.type !== 'withdrawal') return null
const method = props.transaction.method_type || props.transaction.method
switch (method) {
case 'paypal':
return PayPalColorIcon
case 'tremendous': {
const methodId = props.transaction.method_id
if (methodId) {
const info = generatedState.value.tremendousIdMap?.[methodId]
if (info?.name?.toLowerCase()?.includes('paypal')) {
return PayPalColorIcon
}
}
return null
}
case 'venmo':
return VenmoColorIcon
case 'muralpay': {
const methodId = props.transaction.method_id
if (methodId) {
const rail = findRail(methodId)
if (rail) {
if (rail.type === 'crypto') {
const currencyIcon = getCurrencyIcon(rail.currency)
if (currencyIcon) return currencyIcon
}
if (rail.type === 'fiat') {
const currencyIcon = getCurrencyIcon(rail.currency)
if (currencyIcon) return currencyIcon
return LandmarkIcon
}
}
}
return null
}
default:
return null
}
})

function formatTransactionStatus(status: string): string {
if (status === 'in-transit') return 'In Transit'
return capitalizeString(status)
}

function formatMethodName(method: string | undefined): string {
const { formatMessage } = useVIntl()

function formatMethodName(method: string | undefined, method_id: string | undefined): string {
if (!method) return 'Unknown'
switch (method) {
case 'paypal':
return 'PayPal'
case 'venmo':
return 'Venmo'
case 'tremendous':
if (method_id) {
const info = generatedState.value.tremendousIdMap?.[method_id]
if (info) return `${info.name}`
}
return 'Tremendous'
case 'muralpay':
return 'Muralpay'
if (method_id) {
const rail = findRail(method_id)
if (rail) {
return formatMessage(rail.name)
}
}
return 'Mural Pay (Unknown)'
default:
return capitalizeString(method)
}
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/composables/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export const useGeneratedState = () =>
muralBankDetails: generatedState.muralBankDetails as
| Record<string, { bankNames: string[] }>
| undefined,
tremendousIdMap: generatedState.tremendousIdMap as
| Record<string, { name: string; image_url: string | null }>
| undefined,
countries: (generatedState.countries ?? []) as ISO3166.Country[],
subdivisions: (generatedState.subdivisions ?? {}) as Record<string, ISO3166.Subdivision[]>,

Expand Down
19 changes: 18 additions & 1 deletion apps/frontend/src/pages/dashboard/revenue/transfers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ import { formatMoney } from '@modrinth/utils'
import dayjs from 'dayjs'

import RevenueTransaction from '~/components/ui/dashboard/RevenueTransaction.vue'
import { useGeneratedState } from '~/composables/generated'
import { findRail } from '~/utils/muralpay-rails'

const { formatMessage } = useVIntl()
const generatedState = useGeneratedState()

useHead({
title: 'Transaction history - Modrinth',
Expand Down Expand Up @@ -216,10 +219,24 @@ function transactionsToCSV() {
methodOrSource = 'Venmo'
break
case 'tremendous':
if (txn.method_id) {
const info = generatedState.value.tremendousIdMap?.[txn.method_id]
if (info) {
methodOrSource = `Tremendous (${info.name})`
break
}
}
methodOrSource = 'Tremendous'
break
case 'muralpay':
methodOrSource = 'Muralpay'
if (txn.method_id) {
const rail = findRail(txn.method_id)
if (rail) {
methodOrSource = `${rail.name.defaultMessage}`
break
}
}
methodOrSource = 'Mural Pay (Unknown)'
break
default:
methodOrSource = method.charAt(0).toUpperCase() + method.slice(1)
Expand Down
8 changes: 8 additions & 0 deletions apps/frontend/src/utils/muralpay-rails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,7 @@ export const MURALPAY_RAILS: Record<string, RailConfig> = {
currency: 'USDC',
type: 'crypto',
fee: '≈ 1%',
railCode: 'blockchain-usdc-polygon',
blockchain: 'POLYGON',
warningMessage: defineMessage({
id: 'muralpay.warning.wallet-address',
Expand Down Expand Up @@ -888,6 +889,7 @@ export const MURALPAY_RAILS: Record<string, RailConfig> = {
currency: 'USDC',
type: 'crypto',
fee: '≈ 1%',
railCode: 'blockchain-usdc-base',
blockchain: 'BASE',
warningMessage: defineMessage({
id: 'muralpay.warning.wallet-address',
Expand Down Expand Up @@ -924,6 +926,7 @@ export const MURALPAY_RAILS: Record<string, RailConfig> = {
currency: 'USDC',
type: 'crypto',
fee: '≈ 1%',
railCode: 'blockchain-usdc-ethereum',
blockchain: 'ETHEREUM',
warningMessage: defineMessage({
id: 'muralpay.warning.wallet-address',
Expand Down Expand Up @@ -957,6 +960,7 @@ export const MURALPAY_RAILS: Record<string, RailConfig> = {
currency: 'USDC',
type: 'crypto',
fee: '≈ 1%',
railCode: 'blockchain-usdc-celo',
blockchain: 'CELO',
warningMessage: defineMessage({
id: 'muralpay.warning.wallet-address',
Expand Down Expand Up @@ -996,3 +1000,7 @@ export function getRailsByType(type: 'fiat' | 'crypto'): RailConfig[] {
export function getRailConfig(railId: string): RailConfig | undefined {
return MURALPAY_RAILS[railId]
}

export function findRail(railCode: string): RailConfig | undefined {
return Object.values(MURALPAY_RAILS).find((rail) => rail.railCode === railCode)
}
17 changes: 17 additions & 0 deletions packages/api-client/src/modules/labrinth/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class LabrinthStateModule extends AbstractModule {
products,
muralBankDetails,
iso3166Data,
payoutMethods,
] = await Promise.all([
// Tag endpoints
this.client
Expand Down Expand Up @@ -114,8 +115,23 @@ export class LabrinthStateModule extends AbstractModule {
this.client.iso3166.data
.build()
.catch((err) => handleError(err, { countries: [], subdivisions: {} })),

// Payout methods for tremendous ID mapping
this.client
.request<Labrinth.State.PayoutMethodInfo[]>('/payout/methods', {
api: 'labrinth',
version: 3,
method: 'GET',
})
.catch((err) => handleError(err, [])),
])

const tremendousIdMap = Object.fromEntries(
(payoutMethods as Labrinth.State.PayoutMethodInfo[])
.filter((m) => m.type === 'tremendous')
.map((m) => [m.id, { name: m.name, image_url: m.image_logo_url }]),
)

return {
categories,
loaders,
Expand All @@ -127,6 +143,7 @@ export class LabrinthStateModule extends AbstractModule {
homePageNotifs,
products,
muralBankDetails: muralBankDetails?.bankDetails,
tremendousIdMap,
countries: iso3166Data.countries,
subdivisions: iso3166Data.subdivisions,
errors,
Expand Down
14 changes: 14 additions & 0 deletions packages/api-client/src/modules/labrinth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,13 @@ export namespace Labrinth {
}

export namespace State {
export interface PayoutMethodInfo {
id: string
type: string
name: string
image_logo_url: string | null
}

export interface GeneratedState {
categories: Tags.v2.Category[]
loaders: Tags.v2.Loader[]
Expand All @@ -711,6 +718,13 @@ export namespace Labrinth {
bankNames: string[]
}
>
tremendousIdMap?: Record<
string,
{
name: string
image_url: string | null
}
>

homePageProjects?: Projects.v2.Project[]
homePageSearch?: Search.v2.SearchResults
Expand Down