Skip to content

Commit

Permalink
MBL-1992: Currency format for PaymentIncrement (#2203)
Browse files Browse the repository at this point in the history
  • Loading branch information
Arkariang authored Jan 16, 2025
1 parent 9c2a703 commit 1d59f2c
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 55 deletions.
37 changes: 0 additions & 37 deletions app/src/main/java/com/kickstarter/libs/utils/ProjectViewUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -148,41 +148,4 @@ object ProjectViewUtils {

return styledCurrency.trim()
}

/**
* Returns a CharSequence representing a value in a project's currency based on a user's locale.
* The precision is shrunken and centered if the number is not whole.
* The currency symbol is shrunken and centered.
* Special case: US people looking at US currency just get the currency symbol.
*/
fun styleCurrency(
value: Double,
projectCurrency: String?,
projectCurrentCurrency: String?,
ksCurrency: KSCurrency,
): CharSequence {
val spannedDigits = styleCurrency(value)

val formattedCurrency = ksCurrency.format(initialValue = value, projectCurrency = projectCurrency, projectCurrentCurrency = projectCurrentCurrency, roundingMode = RoundingMode.HALF_UP)

val country = projectCurrency?.let { Country.findByCurrencyCode(it) }
?: return SpannableStringBuilder()

val currencySymbolToDisplay = ksCurrency.getCurrencySymbol(country, true)

val spannedCurrencySymbol = SpannableString(currencySymbolToDisplay)

val startOfSymbol = 0
val endOfSymbol = currencySymbolToDisplay.length
spannedCurrencySymbol.setSpan(RelativeSizeSpan(.6f), startOfSymbol, endOfSymbol, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
spannedCurrencySymbol.setSpan(CenterSpan(), startOfSymbol, endOfSymbol, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

val styledCurrency = if (formattedCurrency.startsWith(currencySymbolToDisplay.trimAllWhitespace())) {
TextUtils.concat(spannedCurrencySymbol, spannedDigits)
} else {
TextUtils.concat(spannedDigits, spannedCurrencySymbol)
}

return styledCurrency.trim()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ fun String.isMP3Url(): Boolean {
return matcher.find()
}

/**
* Using regex find the first occurrency of a Currency symbol
* Sample string: "USD $1234", "EUR €1234"
* @return: index of the first currency symbol found or null if no currency symbol is found
*/
fun String.findCurrencySymbolIndex(): Int? {
val currencyRegex = Regex("[\$\u20AC\u00A3\u00A5\u20B4\u00A2\u20A9\u20B9\u20AA\u20B5\u060B\u0E3F\u20A4\u09F3\u20AD\u20BD\uFDFC\u0024\u00A2\u00A3\u00A4\u00A5\u09F2\u09F3\u09FB\u0AF1\u0BF9\u0E3F\u17DB\u20A0\u20A1\u20A2\u20A3\u20A4\u20A5\u20A6\u20A7\u20A8\u20A9\u20AA\u20AB\u20AC\u20AD\u20AE\u20AF\u20B0\u20B1\u20B2\u20B3\u20B4\u20B5\u20B6\u20B7\u20B8\u20B9\u20BA\u20BB\u20BC\u20BD\u20BE\u20BF\uA838\uFDFC\uFE69\uFF04\uFFE0\uFFE1\uFFE5\uFFE6]")
val matchResult = currencyRegex.find(this)
return matchResult?.range?.first
}

/**
* Returns a boolean that reflects if the string is empty or the length is zero when white space
* characters are trimmed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.kickstarter.R
import com.kickstarter.libs.KSCurrency
import com.kickstarter.libs.models.Country
import com.kickstarter.libs.utils.DateTimeUtils
import com.kickstarter.libs.utils.ProjectViewUtils
import com.kickstarter.libs.utils.extensions.parseToDouble
import com.kickstarter.libs.utils.extensions.findCurrencySymbolIndex
import com.kickstarter.libs.utils.extensions.trimAllWhitespace
import com.kickstarter.mock.MockCurrentConfigV2
import com.kickstarter.mock.factories.ConfigFactory
import com.kickstarter.mock.factories.PaymentIncrementFactory
import com.kickstarter.models.PaymentIncrement
import com.kickstarter.type.PaymentIncrementState
Expand Down Expand Up @@ -65,6 +73,16 @@ fun PreviewCollapsedPaymentScheduleWhite() {
}
}

private fun getMockKSCurrencyForUS(): KSCurrency {
val config = ConfigFactory.configForUSUser()

val currentConfig = MockCurrentConfigV2()
currentConfig.config(config)
val mockCurrency = KSCurrency(currentConfig)

return mockCurrency
}

// Expanded State Preview
@Preview(showBackground = true, name = "Expanded State", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
Expand All @@ -73,7 +91,8 @@ fun PreviewExpandedPaymentScheduleDark() {
PaymentSchedule(
isExpanded = true,
onExpandChange = {},
paymentIncrements = PaymentIncrementFactory.samplePaymentIncrements()
paymentIncrements = PaymentIncrementFactory.samplePaymentIncrements(),
ksCurrency = getMockKSCurrencyForUS()
)
}
}
Expand All @@ -86,7 +105,8 @@ fun PreviewExpandedPaymentSchedule() {
PaymentSchedule(
isExpanded = true,
onExpandChange = {},
paymentIncrements = PaymentIncrementFactory.samplePaymentIncrements()
paymentIncrements = PaymentIncrementFactory.samplePaymentIncrements(),
ksCurrency = getMockKSCurrencyForUS()
)
}
}
Expand All @@ -99,7 +119,8 @@ fun InteractivePaymentSchedulePreview() {
PaymentSchedule(
isExpanded = isExpanded,
onExpandChange = { isExpanded = it },
paymentIncrements = PaymentIncrementFactory.samplePaymentIncrements()
paymentIncrements = PaymentIncrementFactory.samplePaymentIncrements(),
ksCurrency = getMockKSCurrencyForUS()
)
}
}
Expand Down Expand Up @@ -148,8 +169,7 @@ fun PaymentSchedule(
paymentIncrements.forEach { paymentIncrement ->
PaymentRow(
paymentIncrement,
ksCurrency = ksCurrency,
projectCurrentCurrency = projectCurrentCurrency
ksCurrency = ksCurrency
)
}
Spacer(modifier = Modifier.height(dimensions.paddingSmall))
Expand All @@ -165,17 +185,8 @@ fun PaymentSchedule(
@Composable
fun PaymentRow(
paymentIncrement: PaymentIncrement,
ksCurrency: KSCurrency?,
projectCurrentCurrency: String?
ksCurrency: KSCurrency?
) {
val formattedAmount = ksCurrency?.let {
ProjectViewUtils.styleCurrency(
value = paymentIncrement.amount().amountAsFloat.parseToDouble(),
ksCurrency = it,
projectCurrency = paymentIncrement.amount().currencyCode,
projectCurrentCurrency = projectCurrentCurrency
)
}.toString()
Row(
modifier = Modifier
.fillMaxWidth()
Expand All @@ -196,13 +207,55 @@ fun PaymentRow(
}
Text(
modifier = Modifier.testTag(PaymentScheduleTestTags.AMOUNT_TEXT.name),
text = formattedAmount,
text = paymentIncrementStyledCurrency(paymentIncrement, ksCurrency),
style = typography.title3,
color = colors.textPrimary
)
}
}

@Composable
private fun paymentIncrementStyledCurrency(
paymentIncrement: PaymentIncrement,
ksCurrency: KSCurrency?
): AnnotatedString {
val country = Country.findByCurrencyCode(paymentIncrement.amount().currencyCode ?: "")
val currencySymbol = country?.let { ksCurrency?.getCurrencySymbol(it, false) } ?: ""

val currencyToFormat = "${currencySymbol.trimAllWhitespace()} ${paymentIncrement.amount().amountAsFloat}"
val annotatedString = currencyToFormat.let {
return@let buildAnnotatedString {
val currencySymbolIndex = it.findCurrencySymbolIndex()
val dotIndex = it.indexOf('.')

if (currencySymbolIndex != null && dotIndex != -1) {
// Append "USD $" with smaller size and top alignment
withStyle(
style = SpanStyle(
fontSize = typography.title3.fontSize * 0.6f, // Relative to typography style
baselineShift = BaselineShift(0.25f) // Align on top
)
) {
append(it.substring(0, currencySymbolIndex + 1))
}
append(it.substring(currencySymbolIndex + 1, dotIndex))
// Append ".75" with smaller size and top alignment
withStyle(
style = SpanStyle(
fontSize = typography.title3.fontSize * 0.6f, // Relative to typography style
baselineShift = BaselineShift(0.25f) // Align on top
)
) {
append(it.substring(dotIndex))
}
} else {
append(it)
}
}
}
return annotatedString
}

@Composable
fun StatusBadge(state: PaymentIncrementState, stateReason: PaymentIncrementStateReason) {
when (state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,20 @@ class StringExtKtTest : KSRobolectricTestCase() {
assertNull(null.toInteger())
}

@Test
fun findCurrencySymbolIndex_whenCurrencySymbolIsMultiple_returnsFirstIndex() {
val text = "USD $123.45 €"
val index = text.findCurrencySymbolIndex()
assertEquals(index, 4)
}

@Test
fun findCurrencySymbolIndex_whenNoCurrencySymbolIsPresent_returnsNull() {
val text = "123.45"
val index = text.findCurrencySymbolIndex()
assertNull(index)
}

companion object {
private const val VALID_EMAIL = "[email protected]"
private const val VALID_GIF_URL = "https://i.kickstarter.com/assets/035/272/960/eae68383730822ffe949f3825600a80a_original.gif?origin=ugc-qa&q=92&sig=C1dWB6NvmlwKGw4lty6s4FGU6Dn3rzNv%2F3p%2B4bhSpzk%3D"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.test.platform.app.InstrumentationRegistry
import com.kickstarter.KSRobolectricTestCase
import com.kickstarter.R
import com.kickstarter.libs.KSCurrency
import com.kickstarter.mock.MockCurrentConfigV2
import com.kickstarter.mock.factories.ConfigFactory
import com.kickstarter.mock.factories.PaymentIncrementFactory
import com.kickstarter.models.PaymentIncrement
import com.kickstarter.models.PaymentIncrementAmount
import com.kickstarter.type.PaymentIncrementState
Expand Down Expand Up @@ -416,4 +420,29 @@ class PaymentScheduleTest : KSRobolectricTestCase() {
badgeText.assertCountEquals(samplePaymentIncrementsWithCollectedState.size)
badgeText.assertAll(hasText(context.getString(R.string.fpo_cancelled)))
}

@Test
fun testPaymentScheduleAmountsText() {
composeTestRule.setContent {
KSTheme {
val config = ConfigFactory.configForUSUser()

val currentConfig = MockCurrentConfigV2()
currentConfig.config(config)
val mockCurrency = KSCurrency(currentConfig)
PaymentSchedule(
isExpanded = true,
onExpandChange = {},
paymentIncrements = PaymentIncrementFactory.samplePaymentIncrements(),
ksCurrency = mockCurrency
)
}
}

composeTestRule.waitForIdle()
amountText.assertCountEquals(5)

// Assert Currency text
amountText.assertAll(hasText("US$ 99.75", ignoreCase = true))
}
}

0 comments on commit 1d59f2c

Please sign in to comment.