Skip to content
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

Merged
merged 40 commits into from Oct 24, 2018

Conversation

Rcureton
Copy link
Contributor

@Rcureton Rcureton commented Oct 15, 2018

What

  • User's that have selected and chosen currency can now view projects in their preferred currency.
  • If the user hasn't chosen a currency then the default US$ will be shown.
  • Created the UserCurrency class which displays the proper currency symbol also the project's pledge and goal amounts based on the fxRate.

See

oct-15-2018 10-33-59

@Rcureton Rcureton changed the title [Enhancement] - [Enhancement] - Currency Preferences on Project Pages Oct 15, 2018
@Rcureton Rcureton requested a review from eoji October 15, 2018 14:34
@Rcureton
Copy link
Contributor Author

Copy link
Contributor

@eoji eoji left a 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.
Copy link
Contributor

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 🕵️‍♀️

app/src/main/java/com/kickstarter/libs/UserCurrency.kt Outdated Show resolved Hide resolved
* in $US as a default.
*/
private fun userCurrencyOptions(value: Float, project: Project, symbol: String): KSCurrency.CurrencyOptions {
val fxRate = project.fx_rate()
Copy link
Contributor

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

Copy link
Contributor Author

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 = "$ "
Copy link
Contributor

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();
Copy link
Contributor

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. */
/**
Copy link
Contributor

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()
Copy link
Contributor

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
Copy link
Contributor

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() {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@eoji eoji left a 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 = "$ "
Copy link
Contributor

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?

Copy link

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. */
Copy link
Contributor

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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is causing the funded amount to be blank when a user is logged out or doesn't have service.

Copy link
Contributor

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() {
Copy link
Contributor

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() {

Copy link
Contributor

@eoji eoji left a 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("")
Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

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();
Copy link
Contributor

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 {
Copy link
Contributor

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()))
Copy link
Contributor

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?

Copy link
Contributor Author

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 $.

Copy link
Contributor

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?

Copy link
Contributor Author

@Rcureton Rcureton Oct 18, 2018

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.

Copy link
Contributor

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? 👀

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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 🧐

Copy link
Contributor

@eoji eoji left a 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 in KSCurrency that do what we want. We shouldn't be changing any of the exising KSCurrency methods. Something like KSCurrency.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 to getSymbolForCurrency.
  • 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 the if/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.

Copy link
Contributor

@eoji eoji left a 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.

@Rcureton
Copy link
Contributor Author

The story says that the rewards should be converted to the user's preferred currency.

@eoji
Copy link
Contributor

eoji commented Oct 22, 2018

@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

@nnekab
Copy link

nnekab commented Oct 22, 2018

@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

@eoji
Copy link
Contributor

eoji commented Oct 22, 2018

Will update on Trello 👍

@Rcureton
Copy link
Contributor Author

Rcureton commented Oct 23, 2018

I refactored the methods to show the conversion when the project currency and user currency differ.

Copy link
Contributor

@eoji eoji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had some comments about documentation, tests, some redundancy and the blocking thing I found is fxRate can be null. After those things are wrapped up, we're G2G

image

app/src/main/java/com/kickstarter/libs/KSCurrency.java Outdated Show resolved Hide resolved
return CurrencyOptions.builder()
.country(project.country())
.currencyCode("")
.currencySymbol(config.countryCode().equals("XX") ? "US$" : getSymbolForCurrency(symbol))
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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()));
Copy link
Contributor

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.

Copy link
Contributor

@eoji eoji left a 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:

  1. 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 use formatWithUserPreference(final float initialValue, final @NonNull Project project, final @NonNull RoundingMode roundingMode, final String symbol) in our tests.

  2. That method is also missing documentation for roundingMode.


return CurrencyOptions.builder()
.country(project.country())
.currencyCode("")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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) {
Copy link
Contributor

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)

Copy link
Contributor

@eoji eoji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@Rcureton Rcureton merged commit 673cf10 into master Oct 24, 2018
@Rcureton Rcureton deleted the rc/project-currency branch October 24, 2018 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants