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"
>
<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">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownAmount) }}</span>
<span class="font-semibold text-contrast">{{ formatMoney(amount || 0) }}</span>
</div>
<template v-if="isGiftCard && shouldShowExchangeRate">
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownGiftCardValue) }}</span>
<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">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownFee) }}</span>
<span class="h-4 font-semibold text-contrast">
@@ -21,6 +32,7 @@
<template v-else>-{{ formatMoney(fee || 0) }}</template>
</span>
</div>
<div class="h-px bg-surface-5" />
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownNetAmount) }}</span>
@@ -31,7 +43,7 @@
</template>
</span>
</div>
<template v-if="shouldShowExchangeRate">
<template v-if="shouldShowExchangeRate && !isGiftCard">
<div class="flex items-center justify-between text-sm">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownExchangeRate) }}</span>
<span class="text-secondary"
@@ -56,10 +68,12 @@ const props = withDefaults(
feeLoading: boolean
exchangeRate?: number | null
localCurrency?: string
isGiftCard?: boolean
}>(),
{
exchangeRate: null,
localCurrency: undefined,
isGiftCard: false,
},
)
@@ -115,5 +129,13 @@ const messages = defineMessages({
id: 'dashboard.creator-withdraw-modal.fee-breakdown-exchange-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>

View File

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

View File

@@ -668,9 +668,15 @@
"dashboard.creator-withdraw-modal.fee-breakdown-fee": {
"message": "Fee"
},
"dashboard.creator-withdraw-modal.fee-breakdown-gift-card-value": {
"message": "Gift card value"
},
"dashboard.creator-withdraw-modal.fee-breakdown-net-amount": {
"message": "Net amount"
},
"dashboard.creator-withdraw-modal.fee-breakdown-usd-equivalent": {
"message": "USD equivalent"
},
"dashboard.creator-withdraw-modal.kyc.business-entity": {
"message": "Business entity"
},
@@ -815,8 +821,14 @@
"dashboard.creator-withdraw-modal.tax-form-required.header": {
"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": {
"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": {
"message": "Enter amount"
@@ -833,6 +845,9 @@
"dashboard.creator-withdraw-modal.tremendous-details.reward-plural": {
"message": "Rewards"
},
"dashboard.creator-withdraw-modal.tremendous-details.search-amount-label": {
"message": "Search amount"
},
"dashboard.creator-withdraw-modal.tremendous-details.select-denomination-hint": {
"message": "Select a denomination:"
},