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 new card component #668

Merged
merged 1 commit into from
Nov 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/components/donation/card-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Ember from 'ember';

const {
Component,
computed
} = Ember;

export default Component.extend({
classNames: ['card-item'],
classNameBindings: ['isSelected:selected'],

isSelected: computed('card', 'selectedCard', function() {
return this.get('card.id') === this.get('selectedCard.id');
}),

selectedCard: null,

click() {
this.get('select')();
}
});
19 changes: 19 additions & 0 deletions app/components/donation/card-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Ember from 'ember';

const {
Component,
computed: { alias, empty }
} = Ember;

export default Component.extend({
classNames: ['card-list'],

cardNotSelected: empty('selectedCard'),
donationDisabled: alias('cardNotSelected'),

selectedCard: null,

selectCard(card) {
this.set('selectedCard', card);
}
});
84 changes: 81 additions & 3 deletions app/components/donation/credit-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,89 @@ import Ember from 'ember';

const {
Component,
computed: { not }
computed,
computed: { and, not, or },
inject: { service }
} = Ember;

export default Component.extend({
classNames: ['credit-card-form'],
canDonate: true,
cannotDonate: not('canDonate')
month: '',
months: ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'],
year: '',
years: [],

/**
@property stripe
@type Ember.Service
*/
stripe: service(),

cardInvalid: not('cardValid'),
cardValid: and('isCardNumberValid', 'isCVCValid', 'isExpiryValid'),

preventSubmit: or('isSubmitting', 'cardInvalid'),

isSubmitting: false,

date: computed('month', 'year', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thoughts on simply renaming this to dateString so it is clear we expect it to be a string and not a date object?

let month = this.get('month');
let year = this.get('year');
return `${month} ${year}`;
}),

isCardNumberValid: computed('cardNumber', function() {
let stripe = this.get('stripe');
let cardNumber = this.get('cardNumber');
return stripe.card.validateCardNumber(cardNumber);
}),

isCVCValid: computed('cvc', function() {
let stripe = this.get('stripe');
let cvc = this.get('cvc');
return stripe.card.validateCVC(cvc);
}),

isExpiryValid: computed('date', function() {
let stripe = this.get('stripe');
let date = this.get('date');
return stripe.card.validateExpiry(date);
}),

init() {
let date = new Date();
let currentMonth = `0${date.getMonth() + 1}`.slice(-2);
this.set('month', currentMonth);
let currentYear = date.getFullYear();
this.set('year', currentYear);
let years = this.generateYears(currentYear);
this.set('years', years);
this._super(...arguments);
},

generateYears(currentYear) {
let years = [];
let endYear = currentYear + 20;
while (endYear >= currentYear) {
years.push(currentYear++);
}
return years;
},

_afterSubmit() {
if (this.get('isDestroyed')) {
return;
}
this.set('isSubmitting', false);
},

actions: {
submit() {
this.set('isSubmitting', true);
let cardAttrs = this.getProperties('cvc', 'cardNumber', 'year', 'month');
let onSubmit = this.get('submit');

onSubmit(cardAttrs).finally(() => this._afterSubmit());
}
}
});
15 changes: 13 additions & 2 deletions app/components/donation/donation-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@ import Ember from 'ember';

const {
Component,
computed: { bool }
computed,
computed: {
and, gt, not
}
} = Ember;

export default Component.extend({
classNames: ['donation-container'],
donationAmount: 0,
projectTitle: null,

canDonate: bool('projectTitle')
canAddCard: computed('hasCards', 'isAddingCard', function() {
let { hasCards, isAddingCard } = this.getProperties('hasCards', 'isAddingCard');
return hasCards ? isAddingCard : true;
}),
canDonate: and('hasCards', 'isNotAddingCard'),
canShowCardList: and('hasCards', 'isNotAddingCard'),
hasCards: gt('cards.length', 0),
hasNoCards: not('hasCards'),
isNotAddingCard: not('isAddingCard')
});
9 changes: 7 additions & 2 deletions app/components/donations/create-donation.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import Ember from 'ember';

const { Component } = Ember;
const { Component, computed, isEmpty } = Ember;

export default Component.extend({
classNames: ['create-donation'],
presetAmounts: [10, 15, 25, 50],
amount: 15
amount: 10,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we add customAmount: null here as well, so it is clear it's a property set on this component?


selectedAmount: computed('amount', 'customAmount', function() {
let { amount, customAmount } = this.getProperties('amount', 'customAmount');
return isEmpty(customAmount) ? amount : customAmount;
})
});
12 changes: 7 additions & 5 deletions app/components/donations/donation-amount-button.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import Ember from 'ember';

const { Component, computed } = Ember;
const { Component, computed, isEmpty } = Ember;

export default Component.extend({
classNames: ['preset-amount'],
classNameBindings: ['presetAmount', 'selected:default:clear'],
tagName: 'button',

selected: computed('presetAmount', 'selectedAmount', function() {
let { presetAmount, selectedAmount } = this.getProperties('presetAmount', 'selectedAmount');
return parseInt(presetAmount) === parseInt(selectedAmount);
selected: computed('customAmount', 'presetAmount', 'selectedAmount', function() {
let { customAmount, presetAmount, selectedAmount } = this.getProperties('customAmount', 'presetAmount', 'selectedAmount');
let amountInPresets = parseInt(presetAmount) === parseInt(selectedAmount);
return isEmpty(customAmount) ? amountInPresets : false;
}),

click() {
this.sendAction('setCustomAmount', null);
let presetAmount = this.get('presetAmount');
this.sendAction('action', presetAmount);
this.sendAction('setAmount', presetAmount);
}
});
86 changes: 81 additions & 5 deletions app/components/error-formatter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Ember from 'ember';

const { Component, computed } = Ember;
const { Component, computed, Object, String } = Ember;

/**
`error-formatter' returns a formatted error message. Place within an 'if'
Expand Down Expand Up @@ -36,9 +36,85 @@ export default Component.extend({
@property messages
@type String
*/
messages: computed('error.errors', function() {
return (this.get('error.errors') || []).map((e) => {
return `${e.title}: ${e.detail}`;
messages: computed('error', function() {
let errorResponse = Object.create(this.get('error'));
let handler = this._findHandler(errorResponse);
if (handler) {
return handler(errorResponse);
}
}),

/**
* Determines the type of error from an error response and returns
* the correct messsage formatter function for it.
*
* @param {Object} errorResponse The error response received from the server
* @return {Function} Function which takes the error response and returns a list of messages
*/
_findHandler(errorResponse) {
if (errorResponse.get('isAdapterError')) {
return this._findHandlerForAdapterError(errorResponse);
} else if (errorResponse.get('error.type') === 'card_error') {
return this._stripeCardErrorMessages;
}
},

/**
* If the error response is determined to be an adapter error, this
* function further determines the type of adapter error and returns
* the correct message formatter for it.
* @param {Object} errorResponse The adapter error response received from the server
* @return {Function} Function which takes the response and returns a list of messages
*/
_findHandlerForAdapterError(errorResponse) {
let payloadContainsValidationErrors = errorResponse.errors.some((error) => error.status === 422);

if (payloadContainsValidationErrors) {
return this._validationErrorMessages;
} else {
return this._adapterErrorMessages;
}
},

/**
* Formats messages for a validation error response
* In most cases, we do not need this, since we do not render those elements outside
* a form. In some cases, however, we are creating a record in the background, so
* we are forced to render those errors separate from a form.
*
* @param {Object} errorResponse The payload received from the server
* @return {Array} An array of string messages
*/
_validationErrorMessages(errorResponse) {
return (errorResponse.get('errors')).map((e) => {
let attributePathElements = e.source.pointer.split('/');
let unformattedAttributeName = attributePathElements[attributePathElements.length - 1];
let attributeName = String.capitalize(unformattedAttributeName);
return `${attributeName} ${e.detail}`;
});
})
},

/**
* Formats messages for an adapter error response.
* An adapter error response contains an array of errors with a
* `title` and a `detail` property, but in most cases, that array only contains
* one error.
* @param {Object} errorResponse Response received from the server
* @return {Array} Array of message strings
*/
_adapterErrorMessages(errorResponse) {
return (errorResponse.get('errors')).map((e) => `${e.title}: ${e.detail}`);
},

/**
* Formats messages for a stripe card error response.
*
* The response contains an `error` object, for which the relevant key is
* the `message` property.
* @param {Object} errorResponse Error response received from the stripe service
* @return {Array} An array of string messages, containing single string
*/
_stripeCardErrorMessages(errorResponse) {
return [errorResponse.get('error.message')];
}
});
Loading