diff --git a/app/components/payments/bank-account.js b/app/components/payments/bank-account.js index 606b5d7c3..3c19424f1 100644 --- a/app/components/payments/bank-account.js +++ b/app/components/payments/bank-account.js @@ -1,9 +1,21 @@ import Ember from 'ember'; const { - Component + Component, + computed, + get } = Ember; export default Component.extend({ - classNames: ['bank-account'] + classNameBindings: ['statusClass'], + classNames: ['bank-account', 'account-setup__section'], + + accountNumber: '000123456789', + routingNumber: '110000000', + + status: computed.alias('account.bankAccountStatus'), + + statusClass: computed('status', function() { + return `account-setup__section--${get(this, 'status')}`; + }) }); diff --git a/app/controllers/project/settings/donations/payments.js b/app/controllers/project/settings/donations/payments.js index 1c2aa71bd..ee8fc1fae 100644 --- a/app/controllers/project/settings/donations/payments.js +++ b/app/controllers/project/settings/donations/payments.js @@ -1,17 +1,26 @@ import Ember from 'ember'; +import FriendlyError from 'code-corps-ember/utils/friendly-error'; + const { computed: { alias }, Controller, get, inject: { service }, - merge + merge, + RSVP, + set } = Ember; +const ACCOUNT_TOKEN_CREATION_ERROR = 'There was a problem in using your bank account information. Please check your input and try again.'; +const ACCOUNT_ADDING_ERROR = 'There was a problem in attaching the provided bank account information to your Stripe account.'; + export default Controller.extend({ currentUser: service(), store: service(), + stripe: service(), user: alias('currentUser.user'), + stripeConnectAccount: alias('project.organization.stripeConnectAccount'), actions: { onRecipientInformationSubmitted(organization, email, recipientInformation) { @@ -20,8 +29,63 @@ export default Controller.extend({ .save(); }, - onBankAccountInformationSubmitted(/* bankAccountInformation */) { - // TODO: Handle receiving bank account information + onBankAccountInformationSubmitted({ accountNumber, routingNumber }) { + set(this, 'isBusy', true); + + let promises = { + tokenData: this._createAccountToken(accountNumber, routingNumber), + stripeConnectAccount: get(this, 'stripeConnectAccount') + }; + + RSVP.hash(promises) + .then(({ tokenData, stripeConnectAccount }) => this._addBankAccount(tokenData, stripeConnectAccount)) + .catch((response) => this._handleError(response)) + .finally(() => set(this, 'isBusy', false)); } + }, + + // adding bank account information + + _createAccountToken(accountNumber, routingNumber) { + let stripe = get(this, 'stripe'); + let params = this._bankAccountTokenParams(accountNumber, routingNumber); + + return stripe.bankAccount.createToken(params) + .then((stripeResponse) => RSVP.resolve(stripeResponse)) + .catch((reason) => this._handleBankAccountTokenError(reason)); + }, + + _handleBankAccountTokenError() { + let friendlyError = new FriendlyError(ACCOUNT_TOKEN_CREATION_ERROR); + return RSVP.reject(friendlyError); + }, + + _addBankAccount(tokenData, stripeConnectAccount) { + set(stripeConnectAccount, 'externalAccount', tokenData.id); + + return stripeConnectAccount.save() + .then((stripeConnectAccount) => RSVP.resolve(stripeConnectAccount)) + .catch((reason) => this._handleAddBankAccountError(reason)); + }, + + _handleAddBankAccountError() { + let friendlyError = new FriendlyError(ACCOUNT_ADDING_ERROR); + return RSVP.reject(friendlyError); + }, + + _bankAccountTokenParams(accountNumber, routingNumber) { + return { + account_number: accountNumber, + routing_number: routingNumber, + object: 'bank_account', + country: 'US', + currency: 'USD' + }; + }, + + // setting error property + + _handleError(error) { + this.set('error', error); } }); diff --git a/app/styles/_colors.scss b/app/styles/_colors.scss index 8afa7167b..5ebae6b24 100644 --- a/app/styles/_colors.scss +++ b/app/styles/_colors.scss @@ -71,10 +71,11 @@ $taskTypeTextOnBackgroundColors: ( $light-red-background: #FFE5E1; $light-blue-background: #EDF9FF; +$light-green-background: #EFFFEE; $idea-background: #FFF9DE; $issue-background: $light-red-background; -$progress-background: #EFFFEE; +$progress-background: $light-green-background; $task-background: $light-blue-background; $taskTypeBackgrounds: ( diff --git a/app/styles/_icons.scss b/app/styles/_icons.scss index 95107757e..4413b2b28 100644 --- a/app/styles/_icons.scss +++ b/app/styles/_icons.scss @@ -15,7 +15,7 @@ $spritePath: '/assets/images/icons/sprite'; $spriteURL: $spritePath + '.png'; $spritex2URL: $spritePath + '@2x.png'; $sprite-bgiSizeW: 230px; -$sprite-bgiSizeH: 304px; +$sprite-bgiSizeH: 324px; $logo: 230px 50px $spriteURL 0px 0px $spritex2URL; $logo-small: 161px 35px $spriteURL 0px -50px $spritex2URL; $location: 16px 16px $spriteURL 0px -85px $spritex2URL; @@ -59,6 +59,7 @@ $calendar: 20px 20px $spriteURL -42px -268px $spritex2URL; $task-small: 16px 16px $spriteURL 0px -288px $spritex2URL; $issue-small: 16px 16px $spriteURL -16px -288px $spritex2URL; $idea-small: 16px 16px $spriteURL -32px -288px $spritex2URL; +$tick-green-large: 20px 20px $spriteURL 0px -304px $spritex2URL; .box-icon { @include sprite($box); diff --git a/app/styles/app.scss b/app/styles/app.scss index c0e790526..1a377fdab 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -50,6 +50,8 @@ @import "components/navigation-menu"; @import "components/organization-header"; @import "components/organization-members"; +@import "components/payments/account-setup"; +@import "components/payments/bank-account"; @import "components/pager-control"; @import "components/task-card"; @import "components/task-comment-list"; diff --git a/app/styles/components/payments/account-setup.scss b/app/styles/components/payments/account-setup.scss new file mode 100644 index 000000000..1ce0db356 --- /dev/null +++ b/app/styles/components/payments/account-setup.scss @@ -0,0 +1,55 @@ +// +// ACCOUNT SETUP SECTIONS +// +.account-setup__section { + align-items: center; + background-color: #FCFCFC; + border: 1px solid #DDD; + border-radius: 4px; + display: flex; + min-height: 4em; + padding: 1.5em; + + aside { + @include span-columns(3); + align-self: baseline; + } + + h1 { + font-size: 1.1em; + font-weight: normal; + margin: 0; + } + + label { + display: block; + font-weight: 600; + margin-bottom: .3em; + } + + section { + @include span-columns(9); + } +} + +// +// STATES +// +.account-setup__section--required { + @include outer-container; +} + +.account-setup__section--verified { + background-color: $light-green-background; + border-color: $green; + + aside h1:before { + content: ""; + display: block; + float: left; + height: 20px; + margin: 0 10px 0 0; + width: 24px; + @include sprite($tick-green-large); + } +} diff --git a/app/styles/components/payments/bank-account.scss b/app/styles/components/payments/bank-account.scss new file mode 100644 index 000000000..e19e6c30e --- /dev/null +++ b/app/styles/components/payments/bank-account.scss @@ -0,0 +1,10 @@ +// +// ACCOUNT INFORMATION +// +.bank-account__account { + @extend .input-group; +} + +.bank-account__routing { + @extend .input-group; +} diff --git a/app/templates/components/payments/account-setup.hbs b/app/templates/components/payments/account-setup.hbs index ca83e60e3..438c82af5 100644 --- a/app/templates/components/payments/account-setup.hbs +++ b/app/templates/components/payments/account-setup.hbs @@ -1,18 +1,9 @@

Create a Stripe account for {{organizationName}}

-{{! - TODO: - Account setup should decide if it should render the "form" - for each section, or a "done" component for that particular data. - - Possibly, even, the payments controller should decide and then - pass in flags to the account setup, but that might be overkill. -}} {{payments/contact-info email=email}} {{payments/funds-recipient fundsRecipient=fundsRecipient recipientInformationSubmitted=(action onRecipientInformationSubmitted email)}} -{{payments/bank-account - bankAccountInformationSubmitted=(action onBankAccountInformationSubmitted)}} +{{payments/bank-account account=account isBusy=isBusy submit=(action onBankAccountInformationSubmitted)}} diff --git a/app/templates/components/payments/bank-account.hbs b/app/templates/components/payments/bank-account.hbs index 8a57174ff..9816599e6 100644 --- a/app/templates/components/payments/bank-account.hbs +++ b/app/templates/components/payments/bank-account.hbs @@ -1,12 +1,30 @@ -
- - {{input type="text" name="routing-number" value=routingNumber}} -
-
- - {{input type="text" name="account-number" value=accountNumber}} -
+ - \ No newline at end of file +{{#if (eq status 'required')}} +
+
+ + {{input disabled=isBusy type="text" name="routing-number" value=routingNumber}} +
+
+ + {{input disabled=isBusy type="text" name="account-number" value=accountNumber}} +
+ +
+{{/if}} + +{{#if (eq status 'verified')}} +
+
+ +

{{account.bankAccountRoutingNumber}}

+
+
+ +

{{account.bankAccountLast4}}

+
+
+{{/if}} diff --git a/app/templates/components/project-settings-menu.hbs b/app/templates/components/project-settings-menu.hbs index ea89a999d..d7e2ec01d 100644 --- a/app/templates/components/project-settings-menu.hbs +++ b/app/templates/components/project-settings-menu.hbs @@ -5,10 +5,10 @@ {{link-to "Contributors" "project.settings.contributors" project.slug}}
  • - {{link-to "Donations" "project.settings.donations.goals" project.slug}} + {{link-to "Donation goals" "project.settings.donations.goals" project.slug}}
  • - {{link-to "Payments" "project.settings.donations.payments" project.slug}} + {{link-to "Payment settings" "project.settings.donations.payments" project.slug}}
  • {{link-to "Basic settings" "project.settings.profile" project.slug}} diff --git a/app/templates/project/settings/donations/payments.hbs b/app/templates/project/settings/donations/payments.hbs index 60e73e85b..f5b23fc1f 100644 --- a/app/templates/project/settings/donations/payments.hbs +++ b/app/templates/project/settings/donations/payments.hbs @@ -1,6 +1,10 @@ {{payments/account-setup - email=user.email + account=project.organization.stripeConnectAccount + isBusy=isBusy onBankAccountInformationSubmitted=(action 'onBankAccountInformationSubmitted') onRecipientInformationSubmitted=(action 'onRecipientInformationSubmitted' project.organization) organizationName=project.organization.name }} +{{#if error}} + {{error-formatter error=error}} +{{/if}} \ No newline at end of file diff --git a/public/assets/images/icons/sprite.png b/public/assets/images/icons/sprite.png index 925ab848b..f7f9eb408 100644 Binary files a/public/assets/images/icons/sprite.png and b/public/assets/images/icons/sprite.png differ diff --git a/public/assets/images/icons/sprite@2x.png b/public/assets/images/icons/sprite@2x.png index 837c6d091..1c8f429ff 100644 Binary files a/public/assets/images/icons/sprite@2x.png and b/public/assets/images/icons/sprite@2x.png differ diff --git a/tests/integration/components/payments/bank-account-test.js b/tests/integration/components/payments/bank-account-test.js index fe870aa16..5e868b30a 100644 --- a/tests/integration/components/payments/bank-account-test.js +++ b/tests/integration/components/payments/bank-account-test.js @@ -9,11 +9,8 @@ const { K } = Ember; let page = PageObject.create(bankAccountComponent); -function setHandler(context, bankAccountInformationSubmittedHandler = K) { - context.set( - 'bankAccountInformationSubmittedHandler', - bankAccountInformationSubmittedHandler - ); +function setHandler(context, submitHandler = K) { + context.set('submitHandler', submitHandler); } moduleForComponent('payments/bank-account', 'Integration | Component | payments/bank account', { @@ -27,11 +24,71 @@ moduleForComponent('payments/bank-account', 'Integration | Component | payments/ } }); -test('it renders', function(assert) { - page.render(hbs`{{payments/bank-account bankAccountInformationSubmitted=bankAccountInformationSubmittedHandler}}`); - assert.equal(this.$('.bank-account').length, 1, 'Component renders'); +test('it renders correctly for "pending" status', function(assert) { + assert.expect(1); + + let account = { bankAccountStatus: 'pending_requirement' }; + this.set('account', account); + + page.render(hbs`{{payments/bank-account account=account submit=submitHandler}}`); + assert.ok(page.rendersPending, 'Component is rendered in pending status.'); +}); + +test('it renders correctly for "required" status', function(assert) { + assert.expect(4); + + let account = { bankAccountStatus: 'required' }; + this.set('account', account); + + page.render(hbs`{{payments/bank-account account=account submit=submitHandler}}`); + assert.ok(page.rendersRequired, 'Component is rendered in required mode.'); + assert.ok(page.rendersAccountNumberField, 'Component renders the account number field.'); + assert.ok(page.rendersRoutingNumberField, 'Component renders the routing number field.'); + assert.ok(page.rendersSubmitButton, 'Component renders the submit button.'); +}); + +test('it renders correctly for "verified" status', function(assert) { + assert.expect(3); + + let account = { bankAccountLast4: 4321, bankAccountRoutingNumber: 123456, bankAccountStatus: 'verified' }; + this.set('account', account); + + page.render(hbs`{{payments/bank-account account=account submit=submitHandler}}`); + assert.ok(page.rendersVerified, 'Component is rendered in verified mode.'); + assert.equal(page.accountLast4Text, 4321, 'Component renders last 4 digits of account number.'); + assert.equal(page.routingNumberText, 123456, 'Component renders routing number.'); }); -// TODO: Write tests, remove 'it renders' test +test('it sends properties with submit action', function(assert) { + assert.expect(1); -// test('it sends out bank account parameters on submit') + let account = { bankAccountStatus: 'required' }; + this.set('account', account); + + let input = { + routingNumber: '123456', + accountNumber: '654321' + }; + + setHandler(this, (output) => { + assert.deepEqual(output, input, 'Correct parameters were sent out with action.'); + }); + + page.render(hbs`{{payments/bank-account account=account submit=submitHandler}}`) + .accountNumber(input.accountNumber) + .routingNumber(input.routingNumber) + .clickSubmit(); +}); + +test('it disables controls when busy', function(assert) { + assert.expect(3); + + let account = { bankAccountStatus: 'required' }; + this.set('account', account); + + page.render(hbs`{{payments/bank-account account=account isBusy=true submit=submitHandler}}`); + + assert.ok(page.accountNumberFieldIsDisabled, 'Account number field is disabled when busy.'); + assert.ok(page.routingNumberFieldIsDisabled, 'Routing number field is disabled when buys.'); + assert.ok(page.submitButtonIsDisabled, 'Submit button is disabled when busy.'); +}); diff --git a/tests/integration/components/project-settings-menu-test.js b/tests/integration/components/project-settings-menu-test.js index 6852f9637..eae38bb9f 100644 --- a/tests/integration/components/project-settings-menu-test.js +++ b/tests/integration/components/project-settings-menu-test.js @@ -26,8 +26,8 @@ test('when authenticated and can manage organization, it renders properly', func assert.equal(this.$('.project-settings-menu').length, 1, 'The component itself renders'); assert.equal(this.$('.project-settings-menu li').length, 4, 'The list items render'); assert.equal(this.$('li a:contains("Contributors")').length, 1, 'The contributors link renders'); - assert.equal(this.$('li a:contains("Donations")').length, 1, 'The donations link renders'); - assert.equal(this.$('li a:contains("Payments")').length, 1, 'The payments link renders'); + assert.equal(this.$('li a:contains("Donation goals")').length, 1, 'The donation goals link renders'); + assert.equal(this.$('li a:contains("Payment settings")').length, 1, 'The payment settings link renders'); assert.equal(this.$('li a:contains("Basic settings")').length, 1, 'The basic settings link renders'); }); diff --git a/tests/pages/components/payments/bank-account.js b/tests/pages/components/payments/bank-account.js index 8794276b2..ecf0a56b4 100644 --- a/tests/pages/components/payments/bank-account.js +++ b/tests/pages/components/payments/bank-account.js @@ -1,3 +1,25 @@ +import { clickable, fillable, is, isVisible, hasClass, text } from 'ember-cli-page-object'; + export default { - scope: '.bank-account' + scope: '.bank-account', + + rendersPending: hasClass('account-setup__section--pending_requirement'), + rendersRequired: hasClass('account-setup__section--required'), + rendersVerified: hasClass('account-setup__section--verified'), + + rendersAccountNumberField: isVisible('[name=account-number]'), + rendersRoutingNumberField: isVisible('[name=routing-number]'), + rendersSubmitButton:isVisible('button'), + + accountLast4Text: text('.bank-account__account p'), + routingNumberText: text('.bank-account__routing p'), + + accountNumber: fillable('[name=account-number]'), + routingNumber: fillable('[name=routing-number]'), + + clickSubmit: clickable('button'), + + accountNumberFieldIsDisabled: is(':disabled', '[name=account-number]'), + routingNumberFieldIsDisabled: is(':disabled', '[name=routing-number]'), + submitButtonIsDisabled: is(':disabled', 'button') };