New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Enhancement] - Currency Preferences on Project Pages #354
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work 💰 💰 💰
This PR is really big because a lot of stuff was reformatted but not changed. Let's revert those changes and then I had feedback about how we're getting the user's currency preference.
class UserCurrency(private val currentConfigType: CurrentConfigType) { | ||
|
||
/** | ||
* Returns a currency string appropriate to the user's locale and location relative to a project. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if this is accurate to this class. Might be a case of copy pasta 🕵️♀️
* in $US as a default. | ||
*/ | ||
private fun userCurrencyOptions(value: Float, project: Project, symbol: String): KSCurrency.CurrencyOptions { | ||
val fxRate = project.fx_rate() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if this is null? Doing !!
will still cause a crash
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It shouldn't be null we get this value with every project that we pull in.
CurrencyCode.SGD.rawValue() -> symbol = "S$ " | ||
CurrencyCode.USD.rawValue() -> symbol = "$ " | ||
|
||
else -> symbol = "$ " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be returning US$ here?
@@ -19,6 +19,7 @@ | |||
public abstract @Nullable Boolean artsCultureNewsletter(); | |||
public abstract Avatar avatar(); | |||
public abstract @Nullable Integer backedProjectsCount(); | |||
public abstract @Nullable String chosenCurrency(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to push back on this. Stay tuned for my followup in the viewmodel!
@@ -37,141 +43,243 @@ | |||
public interface ProjectHolderViewModel { | |||
|
|||
interface Inputs { | |||
/** Call to configure view holder with a project and the config country. */ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert all of these comments that got reformatted.
this.ksCurrency = environment.ksCurrency(); | ||
this.userCurrency = environment.userCurrency(); | ||
|
||
this.client.fetchCurrentUser() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the followup to my push back. We're updating the user using GraphQL, we should just query for their currencyPreference using GraphQL. The only info we want is the currency but we're pulling down the whole user object from REST. Then we won't need to update the user every time we look at a project.
@@ -350,131 +466,173 @@ public ViewModel(final @NonNull Environment environment) { | |||
public final Inputs inputs = this; | |||
public final Outputs outputs = this; | |||
|
|||
@Override public void configureWith(final @NonNull Pair<Project, String> projectAndCountry) { | |||
@Override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's revert all of this reformatting.
return UserCurrency(currentConfig) | ||
} | ||
|
||
fun testFormatCurrency_withUserInUS() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what these tests are testing. Is it testing that the user prefers a currency or they're in the a locale? Let's rename it to what it's actually testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So these tests are testing where the user is and how the currency should be displayed based on the locale.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found some bugs 🐛but great progress on this. You're almost there!
val config = this.currentConfigType.getConfig() | ||
|
||
if (config.countryCode() == "US" && chosenCurrency == CurrencyCode.USD.rawValue()) { | ||
symbol = "$ " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for US users looking at US projects, we'd just see "$1100" not "$ 1100". @nnekab what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eoji correct -- remove the space between the currency symbol and amount/number. This is huge -- i'm really excited for this feature
@@ -37,7 +43,7 @@ | |||
public interface ProjectHolderViewModel { | |||
|
|||
interface Inputs { | |||
/** Call to configure view holder with a project and the config country. */ | |||
/**Call to configure view holder with a project and the config country. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are still messed up 😭
|
||
this.apolloClient.userPrivacy() | ||
.map(currency -> Objects.requireNonNull(currency.me()).chosenCurrency()) | ||
.compose(Transformers.neverError()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think adding tests for ProjectHolderViewModel
will help you catch these edge cases!
@@ -414,7 +444,8 @@ public ViewModel(final @NonNull Environment environment) { | |||
@Override public @NonNull Observable<Boolean> projectDisclaimerTextViewIsGone() { | |||
return this.projectDisclaimerTextViewIsGone; | |||
} | |||
@Override public @NonNull Observable<Integer> projectMetadataViewGroupBackgroundDrawableInt() { | |||
@Override | |||
public @NonNull Observable<Integer> projectMetadataViewGroupBackgroundDrawableInt() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's do this all on 1 line like
@Override public @NonNull Observable<Integer> projectMetadataViewGroupBackgroundDrawableInt() {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have some more questions
return if (config.countryCode() == "XX") { | ||
KSCurrency.CurrencyOptions.builder() | ||
.country(project.country()) | ||
.currencyCode("") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is currencyCode
used for and why do you pass "" for both the if and the else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So after some deep diving, we actually we were actually never using it because it would've shown in the projects currency. Now we actually capture it and map it to the showCurrencySymbol method where we map to the display strings we want to show the user. I just removed it. Great catch!
@@ -33,9 +33,11 @@ | |||
public abstract User creator(); | |||
public abstract String currency(); // e.g.: USD | |||
public abstract String currencySymbol(); // e.g.: $ | |||
public abstract String current_currency(); // e.g.: User's Preferred currency USD |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be currentCurrency
public abstract boolean currencyTrailingCode(); | ||
public abstract @Nullable DateTime featuredAt(); | ||
public abstract @Nullable List<User> friends(); | ||
public abstract Float fx_rate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this one should be fxRate
|
||
class UserCurrencyTest: TestCase() { | ||
|
||
private fun createUserCurrency(countryCode: String): UserCurrency { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's move private methods to the bottom of the class
fun testFormatCurrency_withUserInUS() { | ||
val currency = createUserCurrency("US") | ||
Assert.assertEquals("$100", currency.format(100.0f, ProjectFactory.project(), CurrencyCode.USD.rawValue())) | ||
Assert.assertEquals("CA$100", currency.format(100.0f, ProjectFactory.caProject(), CurrencyCode.CAD.rawValue())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not understanding what these tests are testing. My understanding is if I prefer USD, if a project is in Canadian dollars, I'd see the conversion in USD.
For this project, I'd assume I would see US$100 because I prefer USD.
How does UserCurrency
take in the user's currency preference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So for these tests it tests the user's location and based upon the location and user's preferred currency it will show the proper currency symbol.
So for the test above the user is in the US
and if the user's preferred currency is $US
or they don't have a preference it still should show as $
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this test is asserting that when we call currency.format(100.0f, ProjectFactory.caProject(), CurrencyCode.CAD.rawValue()
, it will output "CA$100"
. Where does the user's location (US
) come in? Shouldn't it be US$100
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The createCurrency
method set's the config location then we use the currency
object to format the currency. So if the user's location is US
then currency.format(100.0f, ProjectFactory.caProject(), CurrencyCode.CAD.rawValue()
should output CA$ 100
it's a Canadian project with my currency preference as Canadian Dollar but I'm in the US.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does the user's location matter? 👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Location matters because the display of the currency like $
or US$
depends on the location of the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok but this test is called testFormatCurrency_withUserInUS
so i'm assuming the user is always in the US. is the test name accurate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes isn't it? It's testing the format of the currency with the user in the US 🧐
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pairing with me!
Here's a summary of the feedback I'd recommend:
- Let's get rid of
UserCurrency
and add methods inKSCurrency
that do what we want. We shouldn't be changing any of the exisingKSCurrency
methods. Something likeKSCurrency.formatWithUserPreference()
that takes in the symbol we prefer. Let's also be sure to update the Javadoc so it accurately reflects what the method is doing. - Let's rename
currencySymbol
togetSymbolForCurrency
. - Let's handle the case of if the user is in an unlaunched country in our newly named method
getSymbolForCurrency
, then we can get rid of theif/else
. - Let's add tests that handle the case of a user
-who is in the US and prefers a currency different from the project they're viewing and who prefers USD for a project not based in the US,
-who is in a launched country and 1. prefers a currency different from the project they're viewing 2. who prefers USD for a project based in the US 3. who prefers USD for a project not based in the US
-an unlaunched country and 1. prefers a currency different from the project they're viewing 2. who prefers USD for a project based in the US 3. who prefers USD for a project not based in the US
My last question:
- I don't fully understand the rounding that should happen. If we convert a project's goal or pledged amount, should we be rounding up or down?
I think that's it. Lemme know if I missed anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just tested this. I think we're almost there. I noticed you're not showing the converted amount for US projects when a user prefers a different currency. Any time there's a conversion, you need to show the amounts in the project's original currency. And the rewards need to have the "About X" conversion. Here's a US project that shows the funded amount without the conversion and the rewards don't show the conversion:
We also need to handle if the chosen currency already matches the project's currency:
The user shouldn't see the converted pledge amount and the rewards don't need to be converted.
The story says that the rewards should be converted to the user's preferred currency. |
@nnekab Wanted to confirm if we prefer a currency that's different from the project's, we should see the reward in the original currency amount and the converted amount underneath. Here's what it looks like now in the current version of the app. I'm viewing a project that's in Euros and I am defaulted to USD. The user sees the rewards in the project's currency with a conversion of the user's currency underneath |
@dannyalright @eoji this is how we handle on web -- i think its fine as shown in the image you shared. I get that it's little confusing because we convert the project goal and then show reward amount conversion underneath. but im fine with leaving similar to web |
Will update on Trello 👍 |
…ct currency and user chosen currency match
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return CurrencyOptions.builder() | ||
.country(project.country()) | ||
.currencyCode("") | ||
.currencySymbol(config.countryCode().equals("XX") ? "US$" : getSymbolForCurrency(symbol)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the getSymbolForCurrency
, looks like we're already checking if the country code is "XX"
@@ -291,8 +291,8 @@ public ViewModel(final @NonNull Environment environment) { | |||
.filter(ObjectUtils::isNotNull) | |||
.map(NumberUtils::format); | |||
|
|||
this.usdConversionTextViewIsGone = this.projectAndCountry | |||
.map(pc -> I18nUtils.isCountryUS(pc.second) && !I18nUtils.isCountryUS(pc.first.country())) | |||
this.usdConversionTextViewIsGone = project |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename this output since the name usdConversionTextViewIsGone
is no longer accurate.
this.usdConversionTextViewIsGone = shouldDisplayUsdConversion | ||
.map(BooleanUtils::negate) | ||
.distinctUntilChanged(); | ||
this.usdConversionTextViewIsGone = this.projectAndReward |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename this one also
public class KSCurrencyTest extends TestCase { | ||
public void testFormatCurrency_withUserInUS() { | ||
final KSCurrency currency = createKSCurrency("US"); | ||
assertEquals("$100", currency.format(100.0f, ProjectFactory.project())); | ||
assertEquals("$100 CAD", currency.format(100.0f, ProjectFactory.caProject())); | ||
assertEquals("£100", currency.format(100.0f, ProjectFactory.ukProject())); | ||
|
||
assertEquals("$100", currency.formatWithUserPreference(100.0f, ProjectFactory.project(), CurrencyCode.USD.rawValue())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed all of the tests use formatWithUserPreference
which rounds up. For project goals and pledged amounts, we should test rounding down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍Just 2 things left:
-
We should get rid of
formatWithUserPreference(final float initialValue, final @NonNull Project project, final String symbol)
since we only use it to test and not in the view models. We should useformatWithUserPreference(final float initialValue, final @NonNull Project project, final @NonNull RoundingMode roundingMode, final String symbol)
in our tests. -
That method is also missing documentation for
roundingMode
.
|
||
return CurrencyOptions.builder() | ||
.country(project.country()) | ||
.currencyCode("") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need this because it's defined in the CurrencyOptions Model class. If we remove it, it won't build properly but also there's many other classes that use this KSCurrency class removing it altogether would affect the other classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean do we need it in this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes because we will get this error if it's removed:
Caused by: java.lang.IllegalStateException: Missing required properties: currencyCode
I think later we can actually remove it once we scope out how currency should be handled in those other classes.
* @param project The project to use to look up currency information. | ||
* @param symbol The currency symbol that should be shown next to the pledge and goal amounts. | ||
*/ | ||
public String formatWithUserPreference(final float initialValue, final @NonNull Project project, final String symbol) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only place this method is used is in tests, so we're not actually testing what we're showing the user. I don't think we need it since we are only using
formatWithUserPreference(final float initialValue, final @NonNull Project project, final @NonNull RoundingMode roundingMode, final String symbol)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
What
US$
will be shown.UserCurrency
class which displays the proper currency symbol also the project's pledge and goal amounts based on thefxRate
.See