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

Add wallet balance transfer Page #6878

Merged
merged 33 commits into from
Jan 13, 2022

Conversation

parasharrajat
Copy link
Member

Please review @marcaaron

Details

Fixed Issues

$ #3922

Tests | QA Steps

  1. Open Payments page.
  2. Click on the wallet transfer.
  3. You should be taken to the Transfer balance page.
  4. If your wallet balance is greater than the fee and which account? has an account then you should be able to click the transfer button and it should be enabled.
  5. Observe if Which account? is a debit card then the Instant method is selected.
  6. Observe if Which account? is a bank account then the 1-3 business days method is selected.
  7. Paypal should not be shown as Which account?

Tested On

  • Web
  • Mobile Web
  • Desktop
  • iOS
  • Android

Screenshots

Web | Mobile Web | Desktop

image

iOS

Android

@parasharrajat parasharrajat requested a review from a team as a code owner December 22, 2021 16:41
@MelvinBot MelvinBot requested review from stitesExpensify and removed request for a team December 22, 2021 16:41
Comment on lines 132 to 156
function saveWalletTransferAmountAndAccount(currentBalance, selectedAccountID) {
Onyx.set(ONYXKEYS.WALLET_TRANSFER, {
transferAmount: currentBalance - CONST.WALLET.TRANSFER_BALANCE_FEE,
selectedAccountID,
filterPaymentMethodType: null,
loading: false,
completed: false,
});
}

/**
* Update selected accountID and other data for wallet transfer
* @param {Object} data
*/
function updateWalletTransferData(data) {
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, data);
}

/**
* Cancel the wallet transfer
*/
function cancelWalletTransfer() {
Onyx.set(ONYXKEYS.WALLET_TRANSFER, null);
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I am not sure what would be the best way to handle these but I need these to share data across screens.

Copy link
Contributor

Choose a reason for hiding this comment

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

There's perhaps no other way to share this data. But the logic can be improved.

cancelWalletTransfer sounds like either:

  1. We called the API to start a transfer and want to make a new request to cancel it
  2. We are in the middle of a flow and have not yet called the API to start the transfer and want to cancel it

But then if we look at where this method is called we see it happens when we "confirm" a modal that says {amount} will hit your account shortly!.

If we are just sharing data about a pending transfer then maybe it's best to call this resetWalletTransferData().

But looking into it more - it seems like the primary purpose is to dismiss the confirm modal. So, why not do this instead...

function dismissWalletConfirmModal() {
    Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {shouldShowConfirmModal: false});
}

If we need to "clear" the rest of the data like balance, accountID, etc why are we doing that when the modal button is pressed? Shouldn't this always happen when we have a successful transfer here:

return API.TransferWalletBalance(parameters)
.then((response) => {
if (response.jsonCode !== 200) {
throw new Error(response.message);
}
Onyx.merge(ONYXKEYS.USER_WALLET, {balance: 0});
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {completed: true, loading: false});
Navigation.navigate(ROUTES.SETTINGS_PAYMENTS);

And not just when a user "confirms"?

<ConfirmModal
title={this.props.translate('paymentsPage.allSet')}
onConfirm={PaymentMethods.cancelWalletTransfer}
isVisible={this.props.walletTransfer.completed}
prompt={this.props.translate('paymentsPage.transferConfirmText', {

Copy link
Member Author

Choose a reason for hiding this comment

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

It was just the shorter way to do two things at once. reset the data and hide the confirm modal.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that instinct, but my feedback would be that the shorter way is not always the easiest to understand. It's usually better to write code that leaves no doubt about what is happening vs. code where we must inspect the details of every method to figure out what it does.

Comment on lines 91 to 109
getSelectedAccount() {
const paymentMethods = PaymentUtils.getPaymentMethods(
this.props.bankAccountList,
this.props.cardList,
);

const defaultAccount = _.find(
paymentMethods,
method => method.id === lodashGet(this.props, 'userWallet.walletLinkedAccountID', ''),
);
const selectedAccount = this.props.walletTransfer.selectedAccountID
? _.find(
paymentMethods,
method => method.id === this.props.walletTransfer.selectedAccountID,
)
: defaultAccount;

return selectedAccount;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see any selected account by default. userWallet.walletLinkedAccountID does not have any value.

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 we have to call the SetWalletLinkedAccount command to set one. @stitesExpensify would maybe know for sure.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah we have to set the account manually for now. We should start defaulting them in the future though..

@parasharrajat
Copy link
Member Author

Note: I can't test the wallet transfer, I would need your help to do that. Thanks.

Comment on lines 132 to 156
function saveWalletTransferAmountAndAccount(currentBalance, selectedAccountID) {
Onyx.set(ONYXKEYS.WALLET_TRANSFER, {
transferAmount: currentBalance - CONST.WALLET.TRANSFER_BALANCE_FEE,
selectedAccountID,
filterPaymentMethodType: null,
loading: false,
completed: false,
});
}

/**
* Update selected accountID and other data for wallet transfer
* @param {Object} data
*/
function updateWalletTransferData(data) {
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, data);
}

/**
* Cancel the wallet transfer
*/
function cancelWalletTransfer() {
Onyx.set(ONYXKEYS.WALLET_TRANSFER, null);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

There's perhaps no other way to share this data. But the logic can be improved.

cancelWalletTransfer sounds like either:

  1. We called the API to start a transfer and want to make a new request to cancel it
  2. We are in the middle of a flow and have not yet called the API to start the transfer and want to cancel it

But then if we look at where this method is called we see it happens when we "confirm" a modal that says {amount} will hit your account shortly!.

If we are just sharing data about a pending transfer then maybe it's best to call this resetWalletTransferData().

But looking into it more - it seems like the primary purpose is to dismiss the confirm modal. So, why not do this instead...

function dismissWalletConfirmModal() {
    Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {shouldShowConfirmModal: false});
}

If we need to "clear" the rest of the data like balance, accountID, etc why are we doing that when the modal button is pressed? Shouldn't this always happen when we have a successful transfer here:

return API.TransferWalletBalance(parameters)
.then((response) => {
if (response.jsonCode !== 200) {
throw new Error(response.message);
}
Onyx.merge(ONYXKEYS.USER_WALLET, {balance: 0});
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {completed: true, loading: false});
Navigation.navigate(ROUTES.SETTINGS_PAYMENTS);

And not just when a user "confirms"?

<ConfirmModal
title={this.props.translate('paymentsPage.allSet')}
onConfirm={PaymentMethods.cancelWalletTransfer}
isVisible={this.props.walletTransfer.completed}
prompt={this.props.translate('paymentsPage.transferConfirmText', {

PaymentMethods.saveWalletTransferAmountAndAccount(
this.props.userWallet.currentBalance,
selectedAccount ? selectedAccount.id : '',
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to do this in the constructor()? If the currentBalance changes the user will transfer whatever we first had when the page opened and not the new balance.

Copy link
Member Author

@parasharrajat parasharrajat Dec 22, 2021

Choose a reason for hiding this comment

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

Actually, the balance is transferred via the backend. API just takes the account where the user wants to transfer. But there is no other way on the app where wallet values will update automatically. The wallet is only refreshed when we open some pages and the First-time app loads.

But I agree I can subscribe to wallet wherever I need the transfer amount.

Copy link
Member Author

@parasharrajat parasharrajat Dec 29, 2021

Choose a reason for hiding this comment

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

I wanted to remove the temporary data that is added for wallet transfer. we can't clear the data(set that to null) before Confirm modal is shown as the same key ONYXKEYS.WALLET_TRANSFER.shouldShowConfirmModal would be used to show the modal.

After the modal is shown and user close it. We can remove all the temporary data from Onyx by setting it to null. And by setting it to null, we close the modal as well.

@marcaaron

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I don't understand the explanation. Can you try asking this in the form of a question or point to some examples?

Copy link
Member Author

Choose a reason for hiding this comment

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

So we use an Onyx key to store the temporary data used for wallet transfer. And the same key is used to determine the isVisible state of ConfirmModal.

  1. After the transfer is done, we don't need the temporary data and thus we have to set the Onyx key to null to clear it.
  2. Confirm Modal is shown to user after API call is made and we need shouldShowConfirmModal to open this.
  3. If we set the Onyx data to null after API call we won't be able to determine the state for ConfirmModal.

we need a trigger point to clear the Onyx data after wallet transfer i.e. a place where we can set the Key to null. I think the best place would be after user has confirmed the Transfer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, not sure if I totally understand still. But can we solve this problem by using a new key for the modal visibility flag to separate it from the wallet data? That way we can reset or clear the data however we want and have separate controls to show/hide the modal?

Copy link
Contributor

Choose a reason for hiding this comment

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

we have to set the Onyx key to null to clear it

Unsure why we need to set this null - what happens if we don't do this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nothing. It will just remove those keys from storage but I think storage is never a problem for us. So I am correcting it now.

@parasharrajat
Copy link
Member Author

Updating the PR asap

* @param {String} selectedAccountID
*/
function saveWalletTransferAccount(selectedAccountID) {
Onyx.set(ONYXKEYS.WALLET_TRANSFER, {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to use a merge() here instead of a set()?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. It does not hurt. Sorry, forgot the conflict issues between set and merge.

}

/**
* Remove all the data related to wallet transfer and close the ConfirmModal
Copy link
Contributor

Choose a reason for hiding this comment

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

Also think we should not be doing these two things at once. Closing a modal is a very explicit action. Resetting the wallet transfer data is another specific action. Is it easy to tell that calling clearWalletTransfer() has a side effect where a modal closes?

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree. Now, I am not worried about the storage so I am refactoring it.

Comment on lines 132 to 156
function saveWalletTransferAmountAndAccount(currentBalance, selectedAccountID) {
Onyx.set(ONYXKEYS.WALLET_TRANSFER, {
transferAmount: currentBalance - CONST.WALLET.TRANSFER_BALANCE_FEE,
selectedAccountID,
filterPaymentMethodType: null,
loading: false,
completed: false,
});
}

/**
* Update selected accountID and other data for wallet transfer
* @param {Object} data
*/
function updateWalletTransferData(data) {
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, data);
}

/**
* Cancel the wallet transfer
*/
function cancelWalletTransfer() {
Onyx.set(ONYXKEYS.WALLET_TRANSFER, null);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that instinct, but my feedback would be that the shorter way is not always the easiest to understand. It's usually better to write code that leaves no doubt about what is happening vs. code where we must inspect the details of every method to figure out what it does.

Comment on lines 161 to 163
function saveWalletTransferTransferredAmount(transferAmount) {
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {transferAmount});
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I had to store this transferAmount as after user click transfer amount, balance in the userWallet will be set to 0 and then in the ConfirmModal we will not have any way to show the transferred amount.

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 saveWalletTransferAmount is fine descriptiveness-wise, and not as long 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

👀

😆

@parasharrajat
Copy link
Member Author

PR Updated.

I have not added logic to set the Default Account for two reasons:

  1. I don't know where to set it.
  2. I don't know How to set it 😉

@parasharrajat
Copy link
Member Author

parasharrajat commented Jan 11, 2022

PR updated @marcaaron @stitesExpensify

Copy link
Contributor

@stitesExpensify stitesExpensify left a comment

Choose a reason for hiding this comment

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

Looking good, we're getting close!

function hasExpensifyPaymentMethod(cardList = [], bankAccountList = []) {
return _.some(cardList, card => card) || _.some(bankAccountList, (bankAccountJSON) => {
const bankAccount = new BankAccount(bankAccountJSON);
return bankAccount.isDefaultCredit();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we only checking for default credit here? Isn't it any bank account that is verified / open?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

I might not understand what "default credit" means. Is it not a personal / deposit account?

I thought VBA is not a valid payment method, but maybe wrong about that too. I asked this once here.

Comment on lines 161 to 163
function saveWalletTransferTransferredAmount(transferAmount) {
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {transferAmount});
}
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 saveWalletTransferAmount is fine descriptiveness-wise, and not as long 😄

const canTransfer = transferAmount > CONST.WALLET.TRANSFER_BALANCE_FEE;
const calculatedFee = PaymentUtils.calculateWalletTransferBalanceFee(this.props.userWallet.currentBalance, selectedPaymentType);
const transferAmount = this.props.userWallet.currentBalance - calculatedFee;
const canTransfer = transferAmount > calculatedFee;
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this line either be this.props.userWallet.currentBalance > calculatedFee or transferAmount > 0?

As it is, you couldn't transfer $0.50 because 50-25 = 25 and 25 !> 25

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it is supposed to be transferAmount > 0. Thanks for checking in.

@parasharrajat
Copy link
Member Author

Ready @marcaaron @stitesExpensify

* Call the API to transfer wallet balance.
* @param {Object} paymentMethod
* @param {String|Number} paymentMethod.methodID
* @param {'bankAccount'|'debitCard'} paymentMethod.type
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB, but these docs are inconsistent with how we do things. If there are multiple types we use * and the last one should be String. I will try to propose a style guide change so it doesn't need to be mentioned.

*/
function transferWalletBalance(paymentMethod) {
const parameters = {
[paymentMethod.type === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? 'bankAccountID' : 'fundID']: paymentMethod.methodID,
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use the constants for 'bankAccountID' and 'fundID' or add them if they don't exist.

Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {shouldShowConfirmModal: true, loading: false});
Navigation.navigate(ROUTES.SETTINGS_PAYMENTS);
}).catch((error) => {
Log.alert(`[Payments] Failed to transfer wallet balance: ${error.message}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure about this alert and think we should remove it for now. We can see in the server logs why a request returned a jsonCode other than 200. @stitesExpensify do you agree?


/**
* Close the ConfirmModal of Wallet balance transfer
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove please.

Future reference: avoid docs that don't add any additional information - https://github.com/Expensify/App/blob/main/STYLE.md#jsdocs

@@ -165,6 +165,10 @@ export default {
marginBottom: -4,
},

mrn3: {
marginRight: -12,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove it please.

@parasharrajat
Copy link
Member Author

All comments addressed. Ready for final review.

Copy link
Contributor

@marcaaron marcaaron left a comment

Choose a reason for hiding this comment

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

Thanks great job!

Copy link
Contributor

@stitesExpensify stitesExpensify left a comment

Choose a reason for hiding this comment

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

Almost there!

@parasharrajat
Copy link
Member Author

@stitesExpensify Awaiting final review from you and merge. Hope it answers your question #6878 (comment)

Copy link
Contributor

@stitesExpensify stitesExpensify left a comment

Choose a reason for hiding this comment

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

Looks great!

@stitesExpensify stitesExpensify merged commit 01757b2 into Expensify:main Jan 13, 2022
@OSBotify
Copy link
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify
Copy link
Contributor

🚀 Deployed to staging by @stitesExpensify in version: 1.1.29-6 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 failure ❌
🕸 web 🕸 success ✅

@OSBotify
Copy link
Contributor

🚀 Deployed to production by @roryabraham in version: 1.1.30-3 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 success ✅
🕸 web 🕸 success ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants