Skip to content

Commit

Permalink
Merge pull request #88 from Shopify/feature-recent-carts
Browse files Browse the repository at this point in the history
[FEATURE] create a `fetchRecentCart` method
  • Loading branch information
minasmart committed Apr 20, 2016
2 parents 8a65ede + 5bdc399 commit f085e51
Show file tree
Hide file tree
Showing 7 changed files with 537 additions and 4 deletions.
39 changes: 39 additions & 0 deletions src/models/reference-model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import BaseModel from './base-model';
import { GUID_KEY } from '../metal/set-guid-for';

const ReferenceModel = BaseModel.extend({

/**
* Class for reference model
* @class ReferenceModel
* @constructor
*/
constructor(attrs) {
if (Object.keys(attrs).indexOf('referenceId') < 0) {
throw new Error('Missing key referenceId of reference. References to null are not allowed');
}

this.super(...arguments);
},

/**
* get the ID for current reference (not what it refers to, but its own unique identifier)
* @property id
* @type {String}
*/
get id() {
return this.attrs[GUID_KEY];
},

get referenceId() {
return this.attrs.referenceId;
},
set referenceId(value) {
this.attrs.referenceId = value;

return value;
}

});

export default ReferenceModel;
27 changes: 27 additions & 0 deletions src/serializers/reference-serializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import CoreObject from '../metal/core-object';
import assign from '../metal/assign';
import ReferenceModel from '../models/reference-model';

const ReferenceSerializer = CoreObject.extend({
constructor(config) {
this.config = config;
},

modelForType(/* type */) {
return ReferenceModel;
},

deserializeSingle(type, singlePayload = {}, metaAttrs = {}) {
const Model = this.modelForType(type);

return new Model(singlePayload, metaAttrs);
},

serialize(type, model) {
const attrs = assign({}, model.attrs);

return attrs;
}
});

export default ReferenceSerializer;
46 changes: 43 additions & 3 deletions src/shop-client.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import ListingsSerializer from './serializers/listings-serializer';
import ListingsAdapter from './adapters/listings-adapter';
import CartSerializer from './serializers/cart-serializer';
import ReferenceSerializer from './serializers/reference-serializer';
import LocalStorageAdapter from './adapters/local-storage-adapter';
import CoreObject from './metal/core-object';
import assign from './metal/assign';
import { GUID_KEY } from './metal/set-guid-for';

/**
* @module shopify-buy
Expand Down Expand Up @@ -47,13 +49,15 @@ const ShopClient = CoreObject.extend({
this.serializers = {
products: ListingsSerializer,
collections: ListingsSerializer,
carts: CartSerializer
carts: CartSerializer,
references: ReferenceSerializer
};

this.adapters = {
products: ListingsAdapter,
collections: ListingsAdapter,
carts: LocalStorageAdapter
carts: LocalStorageAdapter,
references: LocalStorageAdapter
};
},

Expand Down Expand Up @@ -401,7 +405,43 @@ const ShopClient = CoreObject.extend({
* @param {String|Number} [query.limit=50] the number of collections to retrieve per page
* @return {Promise|Array} The collection models.
*/
fetchQueryCollections: fetchFactory('query', 'collections')
fetchQueryCollections: fetchFactory('query', 'collections'),


/**
* This method looks up a reference in localStorage to the most recent cart.
* If one is not found, creates one. If the cart the reference points to
* doesn't exist, create one and store the new reference.
*
* ```javascript
* client.fetchRecentCart().then(cart => {
* // do stuff with the cart
* });
* ```
*
* @method fetchRecentCart
* @public
* @return {Promise|CartModel} The cart.
*/
fetchRecentCart() {
return this.fetch('references', `${this.config.myShopifyDomain}.recent-cart`).then(reference => {
const cartId = reference.referenceId;

return this.fetchCart(cartId);
}).catch(() => {
return this.createCart().then(cart => {
const refAttrs = {
referenceId: cart.id
};

refAttrs[GUID_KEY] = `${this.config.myShopifyDomain}.recent-cart`;

this.create('references', refAttrs);

return cart;
});
});
}
});

export default ShopClient;
135 changes: 135 additions & 0 deletions tests/integration/shop-client-recent-cart-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { module, test } from 'qunit';
import ShopClient from 'shopify-buy/shop-client';
import Config from 'shopify-buy/config';
import { GUID_KEY } from 'shopify-buy/metal/set-guid-for';
import CartModel from 'shopify-buy/models/cart-model';

const configAttrs = {
myShopifyDomain: 'buckets-o-stuff',
apiKey: 'abc123',
appId: 6
};


const config = new Config(configAttrs);

let shopClient;
let fakeLocalStorage;

const { getItem, setItem, removeItem } = localStorage;

module('Integration | ShopClient#fetchRecentCart', {
setup() {
shopClient = new ShopClient(config);
fakeLocalStorage = {};

localStorage.getItem = function (key) {
return JSON.stringify(fakeLocalStorage[key]);
};
localStorage.setItem = function (key, value) {
fakeLocalStorage[key] = JSON.parse(value);
};
localStorage.removeItem = function (key) {
delete fakeLocalStorage[key];
};
},
teardown() {
shopClient = null;
localStorage.getItem = getItem;
localStorage.setItem = setItem;
localStorage.removeItem = removeItem;
}
});


test('it resolves with an exisitng cart when a reference and corresponding cart exist', function (assert) {
assert.expect(1);

const done = assert.async();

const cartReferenceKey = `references.${config.myShopifyDomain}.recent-cart`;
const cartId = 'carts.shopify-buy.123';

const cartAttrs = {
cart: {
line_items: [{ variantId: 123 }]
}
};

fakeLocalStorage[cartReferenceKey] = {};
fakeLocalStorage[cartReferenceKey].referenceId = cartId.replace('carts.', '');
fakeLocalStorage[cartReferenceKey][GUID_KEY] = cartReferenceKey;
fakeLocalStorage[cartId] = cartAttrs;

shopClient.fetchRecentCart().then(cart => {
assert.deepEqual(cart.attrs, cartAttrs.cart);
done();
}).catch(() => {
assert.ok(false, 'promise should not reject');
done();
});
});

test('it resolves with a new cart when a no reference exists, persisting both the cart and reference', function (assert) {
assert.expect(6);

const done = assert.async();

assert.equal(Object.keys(fakeLocalStorage).length, 0);

shopClient.fetchRecentCart().then(cart => {
assert.equal(Object.keys(fakeLocalStorage).length, 2);

assert.ok(cart);
assert.ok(CartModel.prototype.isPrototypeOf(cart));

assert.equal(Object.keys(fakeLocalStorage).filter(key => {
return key.match(/^references\./);
}).length, 1);

assert.equal(Object.keys(fakeLocalStorage).filter(key => {
return key.match(/^carts\./);
}).length, 1);

done();
}).catch(() => {
assert.ok(false, 'promise should not reject');
done();
});
});

test('it recovers from broken state when a reference exists to a non-existent cart', function (assert) {
assert.expect(6);

const done = assert.async();

const cartReferenceKey = `references.${config.myShopifyDomain}.recent-cart`;
const cartId = 'carts.shopify-buy.123';


fakeLocalStorage[cartReferenceKey] = {};
fakeLocalStorage[cartReferenceKey].referenceId = cartId.replace('carts.', '');
fakeLocalStorage[cartReferenceKey][GUID_KEY] = cartReferenceKey;

assert.equal(Object.keys(fakeLocalStorage).length, 1);

shopClient.fetchRecentCart().then(cart => {
assert.equal(Object.keys(fakeLocalStorage).length, 2);

assert.ok(cart);
assert.ok(CartModel.prototype.isPrototypeOf(cart));

assert.equal(Object.keys(fakeLocalStorage).filter(key => {
return key.match(/^references\./);
}).length, 1);

assert.equal(Object.keys(fakeLocalStorage).filter(key => {
return key.match(/^carts\./);
}).length, 1);

done();
}).catch(() => {
assert.ok(false, 'promise should not reject');
done();
});
});
Loading

0 comments on commit f085e51

Please sign in to comment.