diff --git a/client/state/action-types.js b/client/state/action-types.js index 2c40731e2fe4b..1064675e44b1e 100644 --- a/client/state/action-types.js +++ b/client/state/action-types.js @@ -285,6 +285,12 @@ export const SITE_VOUCHERS_REQUEST = 'SITE_VOUCHERS_REQUEST'; export const SITE_VOUCHERS_REQUEST_FAILURE = 'SITE_VOUCHERS_REQUEST_FAILURE'; export const SITE_VOUCHERS_REQUEST_SUCCESS = 'SITE_VOUCHERS_REQUEST_SUCCESS'; export const SUPPORT_USER_ACTIVATE = 'SUPPORT_USER_ACTIVATE'; +export const STORED_CARDS_DELETE = 'STORED_CARDS_DELETE'; +export const STORED_CARDS_DELETE_COMPLETED = 'STORED_CARDS_DELETE_COMPLETED'; +export const STORED_CARDS_DELETE_FAILED = 'STORED_CARDS_DELETE_FAILED'; +export const STORED_CARDS_FETCH = 'STORED_CARDS_FETCH'; +export const STORED_CARDS_FETCH_COMPLETED = 'STORED_CARDS_FETCH_COMPLETED'; +export const STORED_CARDS_FETCH_FAILED = 'STORED_CARDS_FETCH_FAILED'; export const SUPPORT_USER_ERROR = 'SUPPORT_USER_ERROR'; export const SUPPORT_USER_TOGGLE_DIALOG = 'SUPPORT_USER_TOGGLE_DIALOG'; export const SUPPORT_USER_TOKEN_FETCH = 'SUPPORT_USER_TOKEN_FETCH'; diff --git a/client/state/stored-cards/reducer.js b/client/state/stored-cards/reducer.js new file mode 100644 index 0000000000000..74ab5531517bd --- /dev/null +++ b/client/state/stored-cards/reducer.js @@ -0,0 +1,95 @@ +/** + * External dependencies + */ +import { combineReducers } from 'redux'; + +/** + * Internal dependencies + */ +import { + SERIALIZE, + DESERIALIZE, + STORED_CARDS_FETCH, + STORED_CARDS_FETCH_COMPLETED, + STORED_CARDS_FETCH_FAILED, + STORED_CARDS_DELETE, + STORED_CARDS_DELETE_COMPLETED, + STORED_CARDS_DELETE_FAILED +} from 'state/action-types'; + +/** + * `Reducer` function which handles request/response actions + * concerning stored cards data updates + * + * @param {Array} state Current state + * @param {Object} action storeCard action + * @return {Array} Updated state + */ +export const items = ( state = [], action ) => { + switch ( action.type ) { + case STORED_CARDS_FETCH_COMPLETED: + return action.list; + case STORED_CARDS_DELETE_COMPLETED: + return state.filter( item => item.stored_details_id !== action.card.stored_details_id ); + // return initial state when serializing/deserializing + case SERIALIZE: + case DESERIALIZE: + return []; + } + + return state; +}; + +/** + * `Reducer` function which handles request/response actions + * concerning stored cards fetching + * + * @param {Object} state - current state + * @param {Object} action - storedCard action + * @return {Object} updated state + */ +export const isFetching = ( state = false, action ) => { + switch ( action.type ) { + case STORED_CARDS_FETCH: + return true; + case STORED_CARDS_FETCH_COMPLETED: + case STORED_CARDS_FETCH_FAILED: + return false; + // return initial state when serializing/deserializing + case SERIALIZE: + case DESERIALIZE: + return false; + } + + return state; +}; + +/** + * `Reducer` function which handles request/response actions + * concerning stored card deletion + * + * @param {Object} state - current state + * @param {Object} action - storedCard action + * @return {Object} updated state + */ +export const isDeleting = ( state = false, action ) => { + switch ( action.type ) { + case STORED_CARDS_DELETE: + return true; + case STORED_CARDS_DELETE_FAILED: + case STORED_CARDS_DELETE_COMPLETED: + return false; + // return initial state when serializing/deserializing + case SERIALIZE: + case DESERIALIZE: + return false; + } + + return state; +}; + +export default combineReducers( { + items, + isFetching, + isDeleting +} ); diff --git a/client/state/stored-cards/selectors.js b/client/state/stored-cards/selectors.js new file mode 100644 index 0000000000000..17de7a1620d04 --- /dev/null +++ b/client/state/stored-cards/selectors.js @@ -0,0 +1,17 @@ +/** + * Return user's stored cards from state object + * + * @param {Object} state - current state object + * @return {Array} Stored Cards + */ +export const getCards = state => state.storedCards.items; + +/** + * Returns a Stored Card + * @param {Object} state global state + * @param {Number} cardId the card id + * @return {Object} the matching card if there is one + */ +export const getByCardId = ( state, cardId ) => ( + getCards( state ).filter( card => card.stored_details_id === cardId ).shift() +); diff --git a/client/state/stored-cards/test/fixture.js b/client/state/stored-cards/test/fixture.js new file mode 100644 index 0000000000000..b4ed58a65d927 --- /dev/null +++ b/client/state/stored-cards/test/fixture.js @@ -0,0 +1,32 @@ +export const STORED_CARDS_FROM_API = [ + { + user_id: 12345678, + stored_details_id: 1234567, + expiry: '2016-01-31', + card: 1234, + card_type: 'visa', + mp_ref: '8qkGjuMJJbRhyrwq8qkGjuMJJbRhyrwq', + payment_partner: 'moneypress', + name: 'John Doe', + email: 'john@example.com', + remember: 1, + meta: [], + added: '2015-10-22 11:14:05', + last_used: '2015-10-22 11:14:05' + }, + { + user_id: 12345678, + stored_details_id: 12345, + expiry: '2016-11-30', + card: 2596, + card_type: 'amex', + mp_ref: 'Cb9S1bxEZDhl20cfCb9S1bxEZDhl20cf', + payment_partner: 'moneypress', + name: 'Jane Doe', + email: 'jane@example.com', + remember: 1, + meta: [], + added: '2015-02-06 20:28:11', + last_used: '2015-10-22 11:10:10' + } +]; diff --git a/client/state/stored-cards/test/reducer.js b/client/state/stored-cards/test/reducer.js new file mode 100644 index 0000000000000..2170e92155514 --- /dev/null +++ b/client/state/stored-cards/test/reducer.js @@ -0,0 +1,112 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { + STORED_CARDS_FETCH, + STORED_CARDS_FETCH_COMPLETED, + STORED_CARDS_FETCH_FAILED, + STORED_CARDS_DELETE, + STORED_CARDS_DELETE_COMPLETED, + STORED_CARDS_DELETE_FAILED +} from 'state/action-types'; +import reducer from '../reducer'; +import { STORED_CARDS_FROM_API } from './fixture'; + +describe( 'items', () => { + it( 'should return an object with the initial state', () => { + expect( reducer( undefined, { type: 'UNRELATED' } ) ).to.be.eql( { + items: [], + isFetching: false, + isDeleting: false + } ); + } ); + + it( 'should return an object with an empty list and fetching enabled when fetching is triggered', () => { + expect( reducer( undefined, { type: STORED_CARDS_FETCH } ) ).to.be.eql( { + items: [], + isFetching: true, + isDeleting: false + } ); + } ); + + it( 'should return an object with the list of stored cards when fetching completed', () => { + const state = reducer( undefined, { + type: STORED_CARDS_FETCH_COMPLETED, + list: STORED_CARDS_FROM_API + } ); + + expect( state ).to.be.eql( { + items: STORED_CARDS_FROM_API, + isFetching: false, + isDeleting: false + } ); + } ); + + it( 'should return an object with an empty list of stored cards when fetching failed', () => { + const state = reducer( undefined, { + type: STORED_CARDS_FETCH_FAILED + } ); + + expect( state ).to.be.eql( { + items: [], + isFetching: false, + isDeleting: false + } ); + } ); + + it( 'should keep the current state and enable isDeleting when requesting a stored card deletion', () => { + const state = reducer( deepFreeze( { + items: STORED_CARDS_FROM_API, + isFetching: false, + isDeleting: false + } ), { + type: STORED_CARDS_DELETE, + card: STORED_CARDS_FROM_API[ 0 ] + } ); + + expect( state ).to.be.eql( { + items: STORED_CARDS_FROM_API, + isFetching: false, + isDeleting: true + } ); + } ); + + it( 'should remove a stored card from the list if the stored card deletion request succeeded', () => { + const state = reducer( deepFreeze( { + items: STORED_CARDS_FROM_API, + isFetching: false, + isDeleting: true + } ), { + type: STORED_CARDS_DELETE_COMPLETED, + card: STORED_CARDS_FROM_API[ 0 ] + } ); + + expect( state ).to.be.eql( { + items: [ STORED_CARDS_FROM_API[ 1 ] ], + isFetching: false, + isDeleting: false + } ); + } ); + + it( 'should not change the list of items if the stored card deletion request failed', () => { + const state = reducer( deepFreeze( { + items: STORED_CARDS_FROM_API, + isFetching: false, + isDeleting: true + } ), { + type: STORED_CARDS_DELETE_FAILED + } ); + + expect( state ).to.be.eql( { + items: STORED_CARDS_FROM_API, + isFetching: false, + isDeleting: false + } ); + } ); +} ); diff --git a/client/state/stored-cards/test/selectors.js b/client/state/stored-cards/test/selectors.js new file mode 100644 index 0000000000000..159d0e219ac65 --- /dev/null +++ b/client/state/stored-cards/test/selectors.js @@ -0,0 +1,37 @@ +// External dependencies +import deepFreeze from 'deep-freeze'; +import { expect } from 'chai'; + +// Internal dependencies +import { getCards, getByCardId } from '../selectors'; +import { STORED_CARDS_FROM_API } from './fixture'; + +describe( 'selectors', () => { + describe( 'getCards', () => { + it( 'should return a purchase by its ID, preserving the top-level flags', () => { + const state = deepFreeze( { + storedCards: { + isFetching: false, + isDeleting: false, + items: STORED_CARDS_FROM_API + } + } ); + + expect( getCards( state ) ).to.be.eql( STORED_CARDS_FROM_API ); + } ); + } ); + + describe( 'getByCardId', () => { + it( 'should return a purchase by its ID, preserving the top-level flags', () => { + const state = deepFreeze( { + storedCards: { + isFetching: false, + isDeleting: false, + items: STORED_CARDS_FROM_API + } + } ); + + expect( getByCardId( state, 12345 ) ).to.be.eql( STORED_CARDS_FROM_API[ 1 ] ); + } ); + } ); +} );