polish: qa changes for non-usd cards (#4926)

* polish: qa changes for non-usd cards

* fix: always show worth

* fix: padding
This commit is contained in:
Calum H.
2025-12-18 21:29:32 +00:00
committed by GitHub
parent a64c4201bb
commit 2d5568ecec
3 changed files with 188 additions and 79 deletions

View File

@@ -8,10 +8,21 @@
leave-to-class="opacity-0 max-h-0" leave-to-class="opacity-0 max-h-0"
> >
<div v-if="amount > 0" class="flex flex-col gap-2.5 rounded-[20px] bg-surface-2 p-4"> <div v-if="amount > 0" class="flex flex-col gap-2.5 rounded-[20px] bg-surface-2 p-4">
<div class="flex items-center justify-between"> <template v-if="isGiftCard && shouldShowExchangeRate">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownAmount) }}</span> <div class="flex items-center justify-between">
<span class="font-semibold text-contrast">{{ formatMoney(amount || 0) }}</span> <span class="text-primary">{{ formatMessage(messages.feeBreakdownGiftCardValue) }}</span>
</div> <span class="font-semibold text-contrast"
>{{ formatMoney(amount || 0) }} ({{ formattedLocalCurrency }})</span
>
</div>
</template>
<template v-else>
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownAmount) }}</span>
<span class="font-semibold text-contrast">{{ formatMoney(amount || 0) }}</span>
</div>
</template>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownFee) }}</span> <span class="text-primary">{{ formatMessage(messages.feeBreakdownFee) }}</span>
<span class="h-4 font-semibold text-contrast"> <span class="h-4 font-semibold text-contrast">
@@ -21,6 +32,7 @@
<template v-else>-{{ formatMoney(fee || 0) }}</template> <template v-else>-{{ formatMoney(fee || 0) }}</template>
</span> </span>
</div> </div>
<div class="h-px bg-surface-5" /> <div class="h-px bg-surface-5" />
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownNetAmount) }}</span> <span class="text-primary">{{ formatMessage(messages.feeBreakdownNetAmount) }}</span>
@@ -31,7 +43,7 @@
</template> </template>
</span> </span>
</div> </div>
<template v-if="shouldShowExchangeRate"> <template v-if="shouldShowExchangeRate && !isGiftCard">
<div class="flex items-center justify-between text-sm"> <div class="flex items-center justify-between text-sm">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownExchangeRate) }}</span> <span class="text-primary">{{ formatMessage(messages.feeBreakdownExchangeRate) }}</span>
<span class="text-secondary" <span class="text-secondary"
@@ -56,10 +68,12 @@ const props = withDefaults(
feeLoading: boolean feeLoading: boolean
exchangeRate?: number | null exchangeRate?: number | null
localCurrency?: string localCurrency?: string
isGiftCard?: boolean
}>(), }>(),
{ {
exchangeRate: null, exchangeRate: null,
localCurrency: undefined, localCurrency: undefined,
isGiftCard: false,
}, },
) )
@@ -115,5 +129,13 @@ const messages = defineMessages({
id: 'dashboard.creator-withdraw-modal.fee-breakdown-exchange-rate', id: 'dashboard.creator-withdraw-modal.fee-breakdown-exchange-rate',
defaultMessage: 'FX rate', defaultMessage: 'FX rate',
}, },
feeBreakdownGiftCardValue: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-gift-card-value',
defaultMessage: 'Gift card value',
},
feeBreakdownUsdEquivalent: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-usd-equivalent',
defaultMessage: 'USD equivalent',
},
}) })
</script> </script>

View File

@@ -161,24 +161,32 @@
<div class="flex flex-col gap-2.5"> <div class="flex flex-col gap-2.5">
<label> <label>
<span class="text-md font-semibold text-contrast" <span class="text-md font-semibold text-contrast">
>{{ formatMessage(formFieldLabels.amount) }} <template v-if="useDenominationSuggestions">
<template v-if="useDenominationSuggestions"> ({{ selectedMethodCurrencyCode }})</template> {{ formatMessage(messages.searchAmountLabel) }} ({{ selectedMethodCurrencyCode }})
<span class="text-red">*</span></span </template>
> <template v-else>
{{ formatMessage(formFieldLabels.amount) }}
</template>
<span class="text-red">*</span>
</span>
</label> </label>
<div v-if="showGiftCardSelector && useFixedDenominations" class="flex flex-col gap-2.5"> <div v-if="showGiftCardSelector && useFixedDenominations" class="flex flex-col gap-2.5">
<template v-if="useDenominationSuggestions"> <template v-if="useDenominationSuggestions">
<input <div class="iconified-input w-full">
v-model.number="denominationSearchInput" <SearchIcon aria-hidden="true" />
type="number" <input
step="0.01" v-model.number="denominationSearchInput"
:min="0" type="number"
:placeholder="formatMessage(messages.enterDenominationPlaceholder)" step="0.01"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5" :min="0"
@input="hasTouchedSuggestions = true" :disabled="effectiveMinAmount > roundedMaxAmount"
/> :placeholder="formatMessage(messages.enterDenominationPlaceholder)"
class="!bg-surface-4"
@input="hasTouchedSuggestions = true"
/>
</div>
<Transition <Transition
enter-active-class="transition-opacity duration-200 ease-out" enter-active-class="transition-opacity duration-200 ease-out"
enter-from-class="opacity-0" enter-from-class="opacity-0"
@@ -187,8 +195,24 @@
leave-from-class="opacity-100" leave-from-class="opacity-100"
leave-to-class="opacity-0" leave-to-class="opacity-0"
> >
<span v-if="!denominationSearchInput" class="text-sm text-secondary"> <span
{{ formatMessage(messages.enterAmountHint) }} v-if="
selectedMethodCurrencyCode &&
selectedMethodCurrencyCode !== 'USD' &&
selectedMethodExchangeRate
"
class="text-sm text-secondary"
>
{{
formatMessage(messages.balanceWorthHint, {
usdBalance: formatMoney(roundedMaxAmount),
localBalance: formatAmountForDisplay(
roundedMaxAmount,
selectedMethodCurrencyCode,
selectedMethodExchangeRate,
),
})
}}
</span> </span>
</Transition> </Transition>
</template> </template>
@@ -202,24 +226,60 @@
leave-to-class="opacity-0 max-h-0" leave-to-class="opacity-0 max-h-0"
> >
<div <div
v-if="!useDenominationSuggestions || displayedSuggestions.length > 0" v-if="
class="overflow-hidden" !useDenominationSuggestions ||
:class="[useDenominationSuggestions ? 'p-[2px]' : '']" (denominationSearchInput && displayedSuggestions.length > 0)
"
class="overflow-hidden pt-0"
> >
<Chips <span
v-model="selectedDenomination" v-if="useDenominationSuggestions"
:items="useDenominationSuggestions ? displayedSuggestions : denominationOptions" class="mb-1 block text-sm font-medium text-secondary"
:format-label=" >
(amt: number) => {{ formatMessage(messages.availableDenominationsLabel) }}
formatAmountForDisplay( </span>
amt, <div class="p-[2px]">
<Chips
v-model="selectedDenomination"
:items="useDenominationSuggestions ? displayedSuggestions : denominationOptions"
:format-label="
(amt: number) =>
formatAmountForDisplay(
amt,
selectedMethodCurrencyCode,
selectedMethodExchangeRate,
)
"
:never-empty="false"
:capitalize="false"
/>
</div>
<span
v-if="useDenominationSuggestions && hasTouchedSuggestions && !hasSelectedDenomination"
class="mt-2.5 block text-sm text-orange"
>
{{ formatMessage(messages.selectDenominationRequired) }}
</span>
<span
v-if="
!useDenominationSuggestions &&
selectedMethodCurrencyCode &&
selectedMethodCurrencyCode !== 'USD' &&
selectedMethodExchangeRate
"
class="mt-2 block text-sm text-secondary"
>
{{
formatMessage(messages.balanceWorthHint, {
usdBalance: formatMoney(roundedMaxAmount),
localBalance: formatAmountForDisplay(
roundedMaxAmount,
selectedMethodCurrencyCode, selectedMethodCurrencyCode,
selectedMethodExchangeRate, selectedMethodExchangeRate,
) ),
" })
:never-empty="false" }}
:capitalize="false" </span>
/>
</div> </div>
</Transition> </Transition>
@@ -243,27 +303,6 @@
</span> </span>
</Transition> </Transition>
<Transition
enter-active-class="transition-opacity duration-200 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<span
v-if="
useDenominationSuggestions &&
hasTouchedSuggestions &&
displayedSuggestions.length > 0 &&
!hasSelectedDenomination
"
class="text-sm text-orange"
>
{{ formatMessage(messages.selectDenominationRequired) }}
</span>
</Transition>
<span <span
v-if="!useDenominationSuggestions && denominationOptions.length === 0" v-if="!useDenominationSuggestions && denominationOptions.length === 0"
class="text-error text-sm" class="text-error text-sm"
@@ -284,12 +323,15 @@
</div> </div>
<WithdrawFeeBreakdown <WithdrawFeeBreakdown
v-if="allRequiredFieldsFilled" v-if="allRequiredFieldsFilled && formData.amount && formData.amount > 0"
:amount="formData.amount || 0" :amount="formData.amount || 0"
:fee="calculatedFee" :fee="calculatedFee"
:fee-loading="feeLoading" :fee-loading="feeLoading"
:exchange-rate="giftCardExchangeRate" :exchange-rate="showGiftCardSelector ? selectedMethodExchangeRate : giftCardExchangeRate"
:local-currency="giftCardCurrencyCode" :local-currency="
showGiftCardSelector ? (selectedMethodCurrencyCode ?? undefined) : giftCardCurrencyCode
"
:is-gift-card="showGiftCardSelector"
/> />
<Checkbox v-model="agreedTerms"> <Checkbox v-model="agreedTerms">
@@ -308,6 +350,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SearchIcon } from '@modrinth/assets'
import { import {
Admonition, Admonition,
Checkbox, Checkbox,
@@ -607,23 +650,23 @@ const useDenominationSuggestions = computed(() => {
if (!useFixedDenominations.value) return false if (!useFixedDenominations.value) return false
const interval = selectedMethodDetails.value?.interval const interval = selectedMethodDetails.value?.interval
if (!interval?.fixed?.values) return false if (!interval?.fixed?.values) return false
return interval.fixed.values.length > 35 return interval.fixed.values.length > 10
}) })
const denominationSuggestions = computed(() => { const denominationSuggestions = computed(() => {
const input = denominationSearchInput.value
if (!input || input <= 0) return []
const allDenominations = denominationOptions.value const allDenominations = denominationOptions.value
if (allDenominations.length === 0) return [] if (allDenominations.length === 0) return []
// convert local currency input to USD for filtering (denominations are stored in USD) const input = denominationSearchInput.value
const exchangeRate = selectedMethodExchangeRate.value
const inputInUsd = exchangeRate ? input / exchangeRate : input
const rangeSize = inputInUsd * 0.2 // When no search input, use the user's balance as the target
let lowerBound = inputInUsd - rangeSize / 2 const exchangeRate = selectedMethodExchangeRate.value
let upperBound = inputInUsd + rangeSize / 2 const targetInUsd =
input && input > 0 ? (exchangeRate ? input / exchangeRate : input) : roundedMaxAmount.value
const rangeSize = targetInUsd * 0.2
let lowerBound = targetInUsd - rangeSize / 2
let upperBound = targetInUsd + rangeSize / 2
const minAvailable = allDenominations[0] const minAvailable = allDenominations[0]
const maxAvailable = allDenominations[allDenominations.length - 1] const maxAvailable = allDenominations[allDenominations.length - 1]
@@ -650,14 +693,15 @@ const displayedSuggestions = computed(() => {
if (all.length <= maxDisplayedSuggestions) return all if (all.length <= maxDisplayedSuggestions) return all
const input = denominationSearchInput.value const input = denominationSearchInput.value
if (!input) return all.slice(0, maxDisplayedSuggestions)
const exchangeRate = selectedMethodExchangeRate.value const exchangeRate = selectedMethodExchangeRate.value
const inputInUsd = exchangeRate ? input / exchangeRate : input
// select values closest to input, then sort ascending for display // Use balance as target when no search input
const targetInUsd =
input && input > 0 ? (exchangeRate ? input / exchangeRate : input) : roundedMaxAmount.value
// select values closest to target, then sort ascending for display
const closest = [...all] const closest = [...all]
.sort((a, b) => Math.abs(a - inputInUsd) - Math.abs(b - inputInUsd)) .sort((a, b) => Math.abs(a - targetInUsd) - Math.abs(b - targetInUsd))
.slice(0, maxDisplayedSuggestions) .slice(0, maxDisplayedSuggestions)
return closest.sort((a, b) => a - b) return closest.sort((a, b) => a - b)
@@ -896,13 +940,29 @@ watch(
watch(selectedGiftCardId, (newId, oldId) => { watch(selectedGiftCardId, (newId, oldId) => {
if (oldId && newId !== oldId) { if (oldId && newId !== oldId) {
// Reset denomination search when gift card changes // Reset state when gift card changes
denominationSearchInput.value = undefined
hasTouchedSuggestions.value = false hasTouchedSuggestions.value = false
formData.value.amount = undefined formData.value.amount = undefined
// denominationSearchInput will be prefilled by the watch below
denominationSearchInput.value = undefined
} }
}) })
// Prefill denomination search with balance in local currency when suggestions mode is enabled
watch(
[useDenominationSuggestions, selectedMethodExchangeRate],
([showSuggestions, exchangeRate]) => {
if (showSuggestions && denominationSearchInput.value === undefined) {
const balanceInLocal = exchangeRate
? roundedMaxAmount.value * exchangeRate
: roundedMaxAmount.value
denominationSearchInput.value = Math.floor(balanceInLocal * 100) / 100
hasTouchedSuggestions.value = true
}
},
{ immediate: true },
)
async function switchToDirectPaypal() { async function switchToDirectPaypal() {
withdrawData.value.selection.country = { withdrawData.value.selection.country = {
id: 'US', id: 'US',
@@ -985,7 +1045,19 @@ const messages = defineMessages({
}, },
enterAmountHint: { enterAmountHint: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.enter-amount-hint', id: 'dashboard.creator-withdraw-modal.tremendous-details.enter-amount-hint',
defaultMessage: 'Enter an amount to see available gift card denominations', defaultMessage: 'Find gift cards near this value.',
},
balanceWorthHint: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.balance-worth-hint',
defaultMessage: 'Your balance of {usdBalance} is currently worth {localBalance}.',
},
searchAmountLabel: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.search-amount-label',
defaultMessage: 'Search amount',
},
availableDenominationsLabel: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.available-denominations-label',
defaultMessage: 'Available denominations',
}, },
selectDenominationHint: { selectDenominationHint: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.select-denomination-hint', id: 'dashboard.creator-withdraw-modal.tremendous-details.select-denomination-hint',

View File

@@ -668,9 +668,15 @@
"dashboard.creator-withdraw-modal.fee-breakdown-fee": { "dashboard.creator-withdraw-modal.fee-breakdown-fee": {
"message": "Fee" "message": "Fee"
}, },
"dashboard.creator-withdraw-modal.fee-breakdown-gift-card-value": {
"message": "Gift card value"
},
"dashboard.creator-withdraw-modal.fee-breakdown-net-amount": { "dashboard.creator-withdraw-modal.fee-breakdown-net-amount": {
"message": "Net amount" "message": "Net amount"
}, },
"dashboard.creator-withdraw-modal.fee-breakdown-usd-equivalent": {
"message": "USD equivalent"
},
"dashboard.creator-withdraw-modal.kyc.business-entity": { "dashboard.creator-withdraw-modal.kyc.business-entity": {
"message": "Business entity" "message": "Business entity"
}, },
@@ -815,8 +821,14 @@
"dashboard.creator-withdraw-modal.tax-form-required.header": { "dashboard.creator-withdraw-modal.tax-form-required.header": {
"message": "Tax form required" "message": "Tax form required"
}, },
"dashboard.creator-withdraw-modal.tremendous-details.available-denominations-label": {
"message": "Available denominations"
},
"dashboard.creator-withdraw-modal.tremendous-details.balance-worth-hint": {
"message": "Your balance of {usdBalance} is currently worth {localBalance}."
},
"dashboard.creator-withdraw-modal.tremendous-details.enter-amount-hint": { "dashboard.creator-withdraw-modal.tremendous-details.enter-amount-hint": {
"message": "Enter an amount to see available gift card denominations" "message": "Find gift cards near this value."
}, },
"dashboard.creator-withdraw-modal.tremendous-details.enter-denomination-placeholder": { "dashboard.creator-withdraw-modal.tremendous-details.enter-denomination-placeholder": {
"message": "Enter amount" "message": "Enter amount"
@@ -833,6 +845,9 @@
"dashboard.creator-withdraw-modal.tremendous-details.reward-plural": { "dashboard.creator-withdraw-modal.tremendous-details.reward-plural": {
"message": "Rewards" "message": "Rewards"
}, },
"dashboard.creator-withdraw-modal.tremendous-details.search-amount-label": {
"message": "Search amount"
},
"dashboard.creator-withdraw-modal.tremendous-details.select-denomination-hint": { "dashboard.creator-withdraw-modal.tremendous-details.select-denomination-hint": {
"message": "Select a denomination:" "message": "Select a denomination:"
}, },