diff --git a/client/lib/cart/store/cart-synchronizer.js b/client/lib/cart/store/cart-synchronizer.js index 09b2624a4a4266..9b7dc8d50c95a5 100644 --- a/client/lib/cart/store/cart-synchronizer.js +++ b/client/lib/cart/store/cart-synchronizer.js @@ -56,12 +56,12 @@ function preprocessCartForServer( cart ) { return newCart; } -function CartSynchronizer( siteID, wpcom ) { +function CartSynchronizer( cartKey, wpcom ) { if ( ! ( this instanceof CartSynchronizer ) ) { - return new CartSynchronizer( siteID, wpcom ); + return new CartSynchronizer( cartKey, wpcom ); } - this._siteID = siteID; + this._cartKey = cartKey; this._wpcom = wpcom; this._latestValue = null; this._hasLoadedFromServer = false; @@ -151,7 +151,7 @@ CartSynchronizer.prototype._processQueuedChanges = function() { }; CartSynchronizer.prototype._postToServer = function( callback ) { - this._wpcom.cart( this._siteID, 'POST', preprocessCartForServer( this._latestValue ), function( error, newValue ) { + this._wpcom.cart( this._cartKey, 'POST', preprocessCartForServer( this._latestValue ), function( error, newValue ) { if ( error ) { callback( error ); return; @@ -170,7 +170,7 @@ CartSynchronizer.prototype.fetch = function() { }; CartSynchronizer.prototype._getFromServer = function( callback ) { - this._wpcom.cart( this._siteID, 'GET', function( error, newValue ) { + this._wpcom.cart( this._cartKey, 'GET', function( error, newValue ) { if ( error ) { callback( error ); return; diff --git a/client/lib/cart/store/index.js b/client/lib/cart/store/index.js index d86e5a8fbc542d..6c8ca8e010c9f3 100644 --- a/client/lib/cart/store/index.js +++ b/client/lib/cart/store/index.js @@ -22,7 +22,7 @@ var UpgradesActionTypes = require( 'lib/upgrades/constants' ).action, applyCoupon = cartValues.applyCoupon, cartItems = cartValues.cartItems; -var _selectedSiteID = null, +var _cartKey = null, _synchronizer = null, _poller = null; @@ -50,13 +50,14 @@ function hasPendingServerUpdates() { function setSelectedSite() { var selectedSite = sites.getSelectedSite(); - if ( ! selectedSite ) { - _selectedSiteID = null; + if ( _cartKey === selectedSite.ID ) { return; } - if ( _selectedSiteID === selectedSite.ID ) { - return; + if ( ! selectedSite ) { + _cartKey = 'no-site'; + } else { + _cartKey = selectedSite.ID; } if ( _synchronizer && _poller ) { @@ -64,9 +65,7 @@ function setSelectedSite() { _synchronizer.off( 'change', emitChange ); } - _selectedSiteID = selectedSite.ID; - - _synchronizer = cartSynchronizer( selectedSite.ID, wpcom ); + _synchronizer = cartSynchronizer( _cartKey, wpcom ); _synchronizer.on( 'change', emitChange ); _poller = PollerPool.add( CartStore, _synchronizer._poll.bind( _synchronizer ) ); diff --git a/client/lib/cart/store/test/cart-synchronizer.js b/client/lib/cart/store/test/cart-synchronizer.js index 7d7efe3841dad9..c2866ef5ba5fb8 100644 --- a/client/lib/cart/store/test/cart-synchronizer.js +++ b/client/lib/cart/store/test/cart-synchronizer.js @@ -10,7 +10,7 @@ import CartSynchronizer from '../cart-synchronizer'; import FakeWPCOM from './fake-wpcom'; import useFilesystemMocks from 'test/helpers/use-filesystem-mocks'; -var TEST_SITE_ID = 91234567890; +var TEST_CART_KEY = 91234567890; var poller = { add: function() {} @@ -31,7 +31,7 @@ describe( 'cart-synchronizer', function() { describe( '*before* the first fetch from the server', function() { it( 'should *not* allow the value to be read', function() { var wpcom = FakeWPCOM(), - synchronizer = CartSynchronizer( TEST_SITE_ID, wpcom, poller ); + synchronizer = CartSynchronizer( TEST_CART_KEY, wpcom, poller ); assert.throws( () => { synchronizer.getLatestValue(); @@ -40,8 +40,8 @@ describe( 'cart-synchronizer', function() { it( 'should enqueue local changes and POST them after fetching', function() { var wpcom = FakeWPCOM(), - synchronizer = CartSynchronizer( TEST_SITE_ID, wpcom, poller ), - serverCart = emptyCart( TEST_SITE_ID ); + synchronizer = CartSynchronizer( TEST_CART_KEY, wpcom, poller ), + serverCart = emptyCart( TEST_CART_KEY ); synchronizer.fetch(); synchronizer.update( applyCoupon( 'foo' ) ); @@ -63,8 +63,8 @@ describe( 'cart-synchronizer', function() { describe( '*after* the first fetch from the server', function() { it( 'should allow the value to be read', function() { var wpcom = FakeWPCOM(), - synchronizer = CartSynchronizer( TEST_SITE_ID, wpcom, poller ), - serverCart = emptyCart( TEST_SITE_ID ); + synchronizer = CartSynchronizer( TEST_CART_KEY, wpcom, poller ), + serverCart = emptyCart( TEST_CART_KEY ); synchronizer.fetch(); wpcom.resolveRequest( 0, serverCart ); @@ -75,8 +75,8 @@ describe( 'cart-synchronizer', function() { it( 'should make local changes visible immediately', function() { var wpcom = FakeWPCOM(), - synchronizer = CartSynchronizer( TEST_SITE_ID, wpcom, poller ), - serverCart = emptyCart( TEST_SITE_ID ); + synchronizer = CartSynchronizer( TEST_CART_KEY, wpcom, poller ), + serverCart = emptyCart( TEST_CART_KEY ); synchronizer.fetch(); wpcom.resolveRequest( 0, serverCart ); diff --git a/client/lib/signup/cart.js b/client/lib/signup/cart.js index 0108a12cf1c719..94cb5a470b6218 100644 --- a/client/lib/signup/cart.js +++ b/client/lib/signup/cart.js @@ -12,8 +12,8 @@ var wpcom = require( 'lib/wp' ), cartItems = cartValues.cartItems; module.exports = { - addToCart: function( siteSlug, newCartItems, callback ) { - wpcom.undocumented().cart( siteSlug, function( error, data ) { + addToCart: function( cartKey, newCartItems, callback ) { + wpcom.undocumented().cart( cartKey, function( error, data ) { if ( error ) { return callback( error ); } @@ -33,7 +33,7 @@ module.exports = { newCart = cartValues.fillInAllCartItemAttributes( addFunction( newCart ), productsList.get() ); } ); - wpcom.undocumented().cart( siteSlug, 'POST', newCart, function( postError ) { + wpcom.undocumented().cart( cartKey, 'POST', newCart, function( postError ) { callback( postError ); } ); } ); diff --git a/client/lib/signup/step-actions.js b/client/lib/signup/step-actions.js index c6ca0f08f74ea3..67f2d2b9653227 100644 --- a/client/lib/signup/step-actions.js +++ b/client/lib/signup/step-actions.js @@ -28,6 +28,25 @@ import { import { getSiteTitle } from 'state/signup/steps/site-title/selectors'; import { getSurveyVertical, getSurveySiteType } from 'state/signup/steps/survey/selectors'; +function createCart( callback, dependencies, data ) { + const { designType } = dependencies; + const { domainItem, themeItem } = data; + + if ( designType === 'domain' ) { + const cartKey = 'no-site'; + const providedDependencies = { + siteId: null, + siteSlug: cartKey, + domainItem, + themeItem + }; + + SignupCart.addToCart( cartKey, [ domainItem ], error => callback( error, providedDependencies ) ); + } else { + createSiteWithCart( callback, dependencies, data ); + } +} + function createSiteWithCart( callback, dependencies, { cartItem, domainItem, @@ -49,8 +68,6 @@ function createSiteWithCart( callback, dependencies, { // query. See `getThemeSlug` in `DomainsStep`. theme: dependencies.themeSlugWithRepo || themeSlugWithRepo, vertical: surveyVertical || undefined, - // the API wants the `is_domain_only` flag provided as a number - is_domain_only: dependencies.designType === 'domain' ? 1 : 0 }, validate: false, find_available_url: isPurchasingItem @@ -222,7 +239,9 @@ function getUsernameSuggestion( username, reduxState ) { } module.exports = { - createSiteWithCart: createSiteWithCart, + createCart, + + createSiteWithCart, createSiteWithCartAndStartFreeTrial( callback, dependencies, data ) { createSiteWithCart( ( error, providedDependencies ) => { @@ -302,6 +321,8 @@ module.exports = { } ); }, + fetchSitesAndUser: fetchSitesAndUser, + setThemeOnSite: setThemeOnSite, getUsernameSuggestion: getUsernameSuggestion diff --git a/client/lib/wpcom-undocumented/lib/undocumented.js b/client/lib/wpcom-undocumented/lib/undocumented.js index afd894379c2edc..6f92e69c0f5926 100644 --- a/client/lib/wpcom-undocumented/lib/undocumented.js +++ b/client/lib/wpcom-undocumented/lib/undocumented.js @@ -643,14 +643,14 @@ Undocumented.prototype.getSitePlans = function( siteDomain, fn ) { /** * GET/POST cart * - * @param {string} [siteDomain] The site's slug + * @param {string} [cartKey] The cart's key * @param {string} [method] The request method * @param {object} [data] The REQUEST data * @param {Function} fn The callback function * @api public */ -Undocumented.prototype.cart = function( siteDomain, method, data, fn ) { - debug( '/sites/:site_id:/shopping-cart query' ); +Undocumented.prototype.cart = function( cartKey, method, data, fn ) { + debug( '/me/shopping-cart/:cart-key query' ); if ( arguments.length === 2 ) { fn = method; method = 'GET'; @@ -661,7 +661,7 @@ Undocumented.prototype.cart = function( siteDomain, method, data, fn ) { data = {}; } return this._sendRequestWithLocale( { - path: '/sites/' + siteDomain + '/shopping-cart', + path: '/me/shopping-cart/' + cartKey, method: method, body: data }, fn ); diff --git a/client/my-sites/upgrades/cart/cart-plan-ad.jsx b/client/my-sites/upgrades/cart/cart-plan-ad.jsx index 39b3bca21224ae..1c11706aac7726 100644 --- a/client/my-sites/upgrades/cart/cart-plan-ad.jsx +++ b/client/my-sites/upgrades/cart/cart-plan-ad.jsx @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; import { localize } from 'i18n-calypso'; import page from 'page'; -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; /** * Internal dependencies @@ -31,6 +31,7 @@ class CartPlanAd extends Component { cart.hasLoadedFromServer && ! cartItems.hasDomainCredit( cart ) && cartItems.getDomainRegistrations( cart ).length === 1 && + selectedSite && selectedSite.plan && ! isPlan( selectedSite.plan ); } @@ -54,6 +55,15 @@ class CartPlanAd extends Component { } } +CartPlanAd.propTypes = { + cart: PropTypes.object.isRequired, + isDomainOnlySite: PropTypes.bool, + selectedSite: PropTypes.oneOfType( [ + PropTypes.bool, + PropTypes.object + ] ) +}; + export default connect( ( state ) => { const selectedSiteId = getSelectedSiteId( state ); diff --git a/client/my-sites/upgrades/cart/secondary-cart.jsx b/client/my-sites/upgrades/cart/secondary-cart.jsx index 66c198c7a04fd1..9d377627e88927 100644 --- a/client/my-sites/upgrades/cart/secondary-cart.jsx +++ b/client/my-sites/upgrades/cart/secondary-cart.jsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import React from 'react'; +import React, { PropTypes } from 'react'; import { localize } from 'i18n-calypso'; /** @@ -17,6 +17,14 @@ import observe from 'lib/mixins/data-observe'; import CartBodyLoadingPlaceholder from 'my-sites/upgrades/cart/cart-body/loading-placeholder'; const SecondaryCart = React.createClass( { + propTypes: { + cart: PropTypes.object.isRequired, + selectedSite: PropTypes.oneOfType( [ + PropTypes.bool, + PropTypes.object + ] ) + }, + mixins: [ CartMessagesMixin, observe( 'sites' ) ], render() { diff --git a/client/my-sites/upgrades/checkout/checkout.jsx b/client/my-sites/upgrades/checkout/checkout.jsx index b27912e0363878..b323406de37f3f 100644 --- a/client/my-sites/upgrades/checkout/checkout.jsx +++ b/client/my-sites/upgrades/checkout/checkout.jsx @@ -43,10 +43,8 @@ import { getSelectedSiteId, getSelectedSiteSlug, } from 'state/ui/selectors'; -import { - getSiteOption -} from 'state/sites/selectors'; import { domainManagementList } from 'my-sites/upgrades/paths'; +import { fetchSitesAndUser } from 'lib/signup/step-actions'; const Checkout = React.createClass( { mixins: [ observe( 'sites', 'productsList' ) ], @@ -178,23 +176,63 @@ const Checkout = React.createClass( { }, getCheckoutCompleteRedirectPath: function() { + let renewalItem; + const { + cart, + selectedSiteSlug, + transaction: { + step: { + data: receipt + } + } + } = this.props; + + if ( cartItems.hasRenewalItem( cart ) ) { + renewalItem = cartItems.getRenewalItems( cart )[ 0 ]; + + return purchasePaths.managePurchase( renewalItem.extra.purchaseDomain, renewalItem.extra.purchaseId ); + } else if ( cartItems.hasFreeTrial( cart ) ) { + return selectedSiteSlug + ? `/plans/${ selectedSiteSlug }/thank-you` + : '/checkout/thank-you/plans'; + } else if ( cart.create_new_blog && cartItems.hasDomainRegistration( cart ) && ! cartItems.hasPlan( cart ) ) { + const domainName = cartItems.getDomainRegistrations( cart )[ 0 ].meta; + return domainManagementList( domainName ); + } + + if ( ! selectedSiteSlug ) { + return '/checkout/thank-you/features'; + } + + // The `:receiptId` string is filled in by our callback page after the PayPal checkout + const receiptId = receipt ? receipt.receipt_id : ':receiptId'; + + return this.props.selectedFeature && isValidFeatureKey( this.props.selectedFeature ) + ? `/checkout/thank-you/features/${ this.props.selectedFeature }/${ selectedSiteSlug }/${ receiptId }` + : `/checkout/thank-you/${ selectedSiteSlug }/${ receiptId }`; + }, + + handleCheckoutCompleteRedirect: function() { let product, purchasedProducts, - renewalItem, - receiptId = ':receiptId'; + renewalItem; const { cart, - isDomainOnly, - selectedSite, selectedSiteId, - selectedSiteSlug + transaction: { + step: { + data: receipt + } + } } = this.props; - const receipt = this.props.transaction.step.data; + const redirectPath = this.getCheckoutCompleteRedirectPath(); this.props.clearPurchases(); if ( cartItems.hasRenewalItem( cart ) ) { + // checkouts for renewals redirect back to `/purchases` with a notice + renewalItem = cartItems.getRenewalItems( cart )[ 0 ]; // group all purchases into an array purchasedProducts = reduce( receipt && receipt.purchases || {}, function( result, value ) { @@ -234,21 +272,25 @@ const Checkout = React.createClass( { { persistent: true } ); } - - return purchasePaths.managePurchase( renewalItem.extra.purchaseDomain, renewalItem.extra.purchaseId ); } else if ( cartItems.hasFreeTrial( cart ) ) { this.props.clearSitePlans( selectedSiteId ); + } - return selectedSiteSlug - ? `/plans/${ selectedSiteSlug }/thank-you` - : '/checkout/thank-you/plans'; - } else if ( isDomainOnly && cartItems.hasDomainRegistration( cart ) && ! cartItems.hasPlan( cart ) ) { - // TODO: Use purchased domain name once it is possible to set it as a primary domain when site is created. - return domainManagementList( selectedSite.slug ); + if ( cart.create_new_blog ) { + notices.info( + this.translate( 'Almost doneā€¦' ) + ); + + const domainName = cartItems.getDomainRegistrations( cart )[ 0 ].meta; + + fetchSitesAndUser( domainName, () => { + page( redirectPath ); + } ); + return; } if ( receipt && receipt.receipt_id ) { - receiptId = receipt.receipt_id; + const receiptId = receipt.receipt_id; this.props.fetchReceiptCompleted( receiptId, { receiptId: receiptId, @@ -257,13 +299,7 @@ const Checkout = React.createClass( { } ); } - if ( ! selectedSiteSlug ) { - return '/checkout/thank-you/features'; - } - - return this.props.selectedFeature && isValidFeatureKey( this.props.selectedFeature ) - ? `/checkout/thank-you/features/${ this.props.selectedFeature }/${ selectedSiteSlug }/${ receiptId }` - : `/checkout/thank-you/${ selectedSiteSlug }/${ receiptId }`; + page( redirectPath ); }, content: function() { @@ -290,7 +326,9 @@ const Checkout = React.createClass( { cards={ this.props.cards } products={ this.props.productsList.get() } selectedSite={ selectedSite } - redirectTo={ this.getCheckoutCompleteRedirectPath } /> + redirectTo={ this.getCheckoutCompleteRedirectPath } + handleCheckoutCompleteRedirect={ this.handleCheckoutCompleteRedirect } + /> ); }, @@ -331,7 +369,6 @@ module.exports = connect( return { cards: getStoredCards( state ), - isDomainOnly: getSiteOption( state, selectedSiteId, 'is_domain_only' ), selectedSite: getSelectedSite( state ), selectedSiteId, selectedSiteSlug: getSelectedSiteSlug( state ), diff --git a/client/my-sites/upgrades/checkout/pay-button.jsx b/client/my-sites/upgrades/checkout/pay-button.jsx index 7014974f9f61b8..28dc1385c05001 100644 --- a/client/my-sites/upgrades/checkout/pay-button.jsx +++ b/client/my-sites/upgrades/checkout/pay-button.jsx @@ -43,7 +43,7 @@ var PayButton = React.createClass( { if ( this.props.transactionStep.error || ! this.props.transactionStep.data.success ) { state = this.beforeSubmit(); } else { - state = this.completed(); + state = this.completing(); } break; @@ -126,13 +126,6 @@ var PayButton = React.createClass( { }; }, - completed: function() { - return { - disabled: true, - text: this.translate( 'Purchase complete', { context: 'Loading state on /checkout' } ) - }; - }, - render: function() { var buttonState = this.buttonState(); diff --git a/client/my-sites/upgrades/checkout/secure-payment-form.jsx b/client/my-sites/upgrades/checkout/secure-payment-form.jsx index e70d6af3d861d7..9127dc18545324 100644 --- a/client/my-sites/upgrades/checkout/secure-payment-form.jsx +++ b/client/my-sites/upgrades/checkout/secure-payment-form.jsx @@ -40,8 +40,9 @@ const SecurePaymentForm = React.createClass( { mixins: [ TransactionStepsMixin ], propTypes: { + handleCheckoutCompleteRedirect: React.PropTypes.func.isRequired, products: React.PropTypes.object.isRequired, - redirectTo: React.PropTypes.func.isRequired + redirectTo: React.PropTypes.func.isRequired, }, getInitialState() { diff --git a/client/my-sites/upgrades/checkout/transaction-steps-mixin.jsx b/client/my-sites/upgrades/checkout/transaction-steps-mixin.jsx index dd384884612410..35c4f82724fe0b 100644 --- a/client/my-sites/upgrades/checkout/transaction-steps-mixin.jsx +++ b/client/my-sites/upgrades/checkout/transaction-steps-mixin.jsx @@ -5,8 +5,7 @@ var React = require( 'react' ), // eslint-disable-line no-unused-vars debug = require( 'debug' )( 'calypso:my-sites:upgrades:checkout:transaction-steps-mixin' ), pick = require( 'lodash/pick' ), defer = require( 'lodash/defer' ), - isEqual = require( 'lodash/isEqual' ), - page = require( 'page' ); + isEqual = require( 'lodash/isEqual' ); /** * Internal dependencies @@ -140,7 +139,7 @@ var TransactionStepsMixin = { defer( () => { // The Thank You page throws a rendering error if this is not in a defer. - page( this.props.redirectTo() ); + this.props.handleCheckoutCompleteRedirect(); } ); } }; diff --git a/client/my-sites/upgrades/controller.jsx b/client/my-sites/upgrades/controller.jsx index 8bf21096039c77..4390521ab0374d 100644 --- a/client/my-sites/upgrades/controller.jsx +++ b/client/my-sites/upgrades/controller.jsx @@ -214,6 +214,40 @@ module.exports = { ); }, + sitelessCheckout: function( context ) { + const Checkout = require( './checkout' ), + CheckoutData = require( 'components/data/checkout' ), + CartData = require( 'components/data/cart' ), + SecondaryCart = require( './cart/secondary-cart' ); + + analytics.pageView.record( '/checkout/no-site', 'Checkout' ); + + // FIXME: Auto-converted from the Flux setTitle action. Please use instead. + context.store.dispatch( setTitle( i18n.translate( 'Checkout' ) ) ); + + renderWithReduxStore( + ( + + + + ), + document.getElementById( 'primary' ), + context.store + ); + + renderWithReduxStore( + ( + + + + ), + document.getElementById( 'secondary' ), + context.store + ); + }, + checkoutThankYou: function( context ) { const CheckoutThankYouComponent = require( './checkout-thank-you' ), basePath = route.sectionify( context.path ), diff --git a/client/my-sites/upgrades/index.js b/client/my-sites/upgrades/index.js index 6a651b5dbbc54d..577d6dd51f2ea0 100644 --- a/client/my-sites/upgrades/index.js +++ b/client/my-sites/upgrades/index.js @@ -251,6 +251,11 @@ module.exports = function() { upgradesController.checkoutThankYou ); + page( + '/checkout/no-site', + upgradesController.sitelessCheckout + ); + page( '/checkout/:domain/:product?', controller.siteSelection, diff --git a/client/signup/config/steps.js b/client/signup/config/steps.js index 2bf7347be5c154..66abf793519cd5 100644 --- a/client/signup/config/steps.js +++ b/client/signup/config/steps.js @@ -107,7 +107,7 @@ module.exports = { 'domain-only': { stepName: 'domain-only', - apiRequestFunction: stepActions.createSiteWithCart, + apiRequestFunction: stepActions.createCart, props: { isDomainOnly: true },