From f88676198d4dbeaf8d6ba126a988e966e208b1c0 Mon Sep 17 00:00:00 2001 From: bluefuton Date: Thu, 18 Oct 2018 18:55:22 +1300 Subject: [PATCH 1/4] Add selector for cancelable site purchases --- .../site-settings/delete-site/index.jsx | 15 +- .../has-cancelable-site-purchases.js | 33 ++++ .../test/has-cancelable-site-purchases.js | 147 ++++++++++++++++++ 3 files changed, 186 insertions(+), 9 deletions(-) create mode 100644 client/state/selectors/has-cancelable-site-purchases.js create mode 100644 client/state/selectors/test/has-cancelable-site-purchases.js diff --git a/client/my-sites/site-settings/delete-site/index.jsx b/client/my-sites/site-settings/delete-site/index.jsx index cc65498fa8bb6b..e73ec642c96775 100644 --- a/client/my-sites/site-settings/delete-site/index.jsx +++ b/client/my-sites/site-settings/delete-site/index.jsx @@ -7,7 +7,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import page from 'page'; -import { some } from 'lodash'; import Gridicon from 'gridicons'; import { localize } from 'i18n-calypso'; @@ -23,7 +22,7 @@ import ActionPanelFooter from 'components/action-panel/footer'; import Button from 'components/button'; import DeleteSiteWarningDialog from 'my-sites/site-settings/delete-site-warning-dialog'; import Dialog from 'components/dialog'; -import { getSitePurchases, hasLoadedSitePurchasesFromServer } from 'state/purchases/selectors'; +import { hasLoadedSitePurchasesFromServer } from 'state/purchases/selectors'; import { getSelectedSiteId, getSelectedSiteSlug } from 'state/ui/selectors'; import { getSite, getSiteDomain } from 'state/sites/selectors'; import Notice from 'components/notice'; @@ -32,6 +31,7 @@ import { deleteSite } from 'state/sites/actions'; import { setSelectedSiteId } from 'state/ui/actions'; import isSiteAutomatedTransfer from 'state/selectors/is-site-automated-transfer'; import FormLabel from 'components/forms/form-label'; +import hasCancelableSitePurchases from 'state/selectors/has-cancelable-site-purchases'; class DeleteSite extends Component { static propTypes = { @@ -40,7 +40,6 @@ class DeleteSite extends Component { siteDomain: PropTypes.string, siteExists: PropTypes.bool, siteId: PropTypes.number, - sitePurchases: PropTypes.array, siteSlug: PropTypes.string, translate: PropTypes.func.isRequired, }; @@ -79,9 +78,7 @@ class DeleteSite extends Component { return; } - const hasActiveSubscriptions = some( this.props.sitePurchases, 'active' ); - - if ( hasActiveSubscriptions ) { + if ( this.props.hasCancelablePurchases ) { this.setState( { showWarningDialog: true } ); } else { this.setState( { showConfirmDialog: true } ); @@ -101,10 +98,10 @@ class DeleteSite extends Component { page( '/settings/general/' + siteSlug ); }; - componentWillReceiveProps( nextProps ) { + componentDidUpdate( prevProps ) { const { siteId, siteExists } = this.props; - if ( siteId && siteExists && ! nextProps.siteExists ) { + if ( siteId && prevProps.siteExists && ! siteExists ) { this.props.setSelectedSiteId( null ); page.redirect( '/stats' ); } @@ -379,9 +376,9 @@ export default connect( isAtomic: isSiteAutomatedTransfer( state, siteId ), siteDomain, siteId, - sitePurchases: getSitePurchases( state, siteId ), siteSlug, siteExists: !! getSite( state, siteId ), + hasCancelablePurchases: hasCancelableSitePurchases( state, siteId ), }; }, { diff --git a/client/state/selectors/has-cancelable-site-purchases.js b/client/state/selectors/has-cancelable-site-purchases.js new file mode 100644 index 00000000000000..668e8e9006bf9e --- /dev/null +++ b/client/state/selectors/has-cancelable-site-purchases.js @@ -0,0 +1,33 @@ +/** @format */ + +/** + * Internal dependencies + */ +import { getSitePurchases } from 'state/purchases/selectors'; + +/** + * Does the site have any current purchases that can be canceled (i.e. purchases other than premium themes)? + * + * Note: there is an is_cancelable flag on the purchase object, but it returns true for premium themes. + * + * @param {Object} state global state + * @param {Number} siteId the site ID + * @return {Boolean} if the site currently has any purchases that can be canceled. + */ +export const hasCancelableSitePurchases = ( state, siteId ) => { + if ( ! state.purchases.hasLoadedSitePurchasesFromServer ) { + return false; + } + + const purchases = getSitePurchases( state, siteId ).filter( purchase => { + if ( purchase.isRefundable ) { + return true; + } + + return purchase.productSlug !== 'premium_theme'; + } ); + + return purchases && purchases.length > 0; +}; + +export default hasCancelableSitePurchases; diff --git a/client/state/selectors/test/has-cancelable-site-purchases.js b/client/state/selectors/test/has-cancelable-site-purchases.js new file mode 100644 index 00000000000000..f9a9016f9377ee --- /dev/null +++ b/client/state/selectors/test/has-cancelable-site-purchases.js @@ -0,0 +1,147 @@ +/** @format */ + +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import hasCancelableSitePurchases from 'state/selectors/has-cancelable-site-purchases'; + +describe( 'hasCancelableSitePurchases', () => { + const targetUserId = 123; + const targetSiteId = 1337; + const examplePurchases = [ + { + ID: 1, + product_name: 'domain registration', + product_slug: 'domain_registration', + blog_id: targetSiteId, + user_id: targetUserId, + }, + { + ID: 2, + product_name: 'premium plan', + blog_id: targetSiteId, + user_id: targetUserId, + product_slug: 'premium_plan', + }, + { + ID: 3, + product_name: 'premium theme', + product_slug: 'premium_theme', + blog_id: targetSiteId, + user_id: targetUserId, + }, + ]; + + test( 'should return false because there are no purchases', () => { + const state = deepFreeze( { + purchases: { + data: [], + error: null, + isFetchingSitePurchases: false, + isFetchingUserPurchases: false, + hasLoadedSitePurchasesFromServer: true, + hasLoadedUserPurchasesFromServer: false, + }, + } ); + + expect( hasCancelableSitePurchases( state, targetSiteId ) ).toBe( false ); + } ); + + test( 'should return true because there are purchases from the target site', () => { + const state = deepFreeze( { + purchases: { + data: examplePurchases, + error: null, + isFetchingSitePurchases: false, + isFetchingUserPurchases: false, + hasLoadedSitePurchasesFromServer: true, + hasLoadedUserPurchasesFromServer: false, + }, + } ); + + expect( hasCancelableSitePurchases( state, targetSiteId ) ).toBe( true ); + } ); + + test( 'should return false because there are no purchases for this site', () => { + const state = deepFreeze( { + purchases: { + data: examplePurchases, + error: null, + isFetchingSitePurchases: false, + isFetchingUserPurchases: false, + hasLoadedSitePurchasesFromServer: true, + hasLoadedUserPurchasesFromServer: false, + }, + } ); + + expect( hasCancelableSitePurchases( state, 65535 ) ).toBe( false ); + } ); + + test( 'should return false because the data is not ready', () => { + const state = deepFreeze( { + purchases: { + data: examplePurchases, + error: null, + isFetchingSitePurchases: false, + isFetchingUserPurchases: false, + hasLoadedSitePurchasesFromServer: false, + hasLoadedUserPurchasesFromServer: false, + }, + } ); + + expect( hasCancelableSitePurchases( state, targetSiteId ) ).toBe( false ); + } ); + + test( 'should return false because the only purchase is a non-refundable theme', () => { + const state = deepFreeze( { + purchases: { + data: [ + { + ID: 3, + product_name: 'premium theme', + product_slug: 'premium_theme', + blog_id: targetSiteId, + user_id: targetUserId, + is_refundable: false, + }, + ], + error: null, + isFetchingSitePurchases: false, + isFetchingUserPurchases: false, + hasLoadedSitePurchasesFromServer: true, + hasLoadedUserPurchasesFromServer: true, + }, + } ); + + expect( hasCancelableSitePurchases( state, targetSiteId ) ).toBe( false ); + } ); + + test( 'should return true because one of the purchases is a refundable theme', () => { + const state = deepFreeze( { + purchases: { + data: [ + { + ID: 3, + product_name: 'premium theme', + product_slug: 'premium_theme', + blog_id: targetSiteId, + user_id: targetUserId, + is_refundable: true, + }, + ], + error: null, + isFetchingSitePurchases: false, + isFetchingUserPurchases: false, + hasLoadedSitePurchasesFromServer: true, + hasLoadedUserPurchasesFromServer: true, + }, + } ); + + expect( hasCancelableSitePurchases( state, targetSiteId ) ).toBe( true ); + } ); +} ); From a78df6f69ea23330c6df768e790015176e1f1b46 Mon Sep 17 00:00:00 2001 From: bluefuton Date: Fri, 19 Oct 2018 14:32:29 +1300 Subject: [PATCH 2/4] Add active purchase check to selector --- .../has-cancelable-site-purchases.js | 4 +++ .../test/has-cancelable-site-purchases.js | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/client/state/selectors/has-cancelable-site-purchases.js b/client/state/selectors/has-cancelable-site-purchases.js index 668e8e9006bf9e..939ffea56d84bb 100644 --- a/client/state/selectors/has-cancelable-site-purchases.js +++ b/client/state/selectors/has-cancelable-site-purchases.js @@ -20,6 +20,10 @@ export const hasCancelableSitePurchases = ( state, siteId ) => { } const purchases = getSitePurchases( state, siteId ).filter( purchase => { + if ( ! purchase.active ) { + return false; + } + if ( purchase.isRefundable ) { return true; } diff --git a/client/state/selectors/test/has-cancelable-site-purchases.js b/client/state/selectors/test/has-cancelable-site-purchases.js index f9a9016f9377ee..ef5bb510f1a629 100644 --- a/client/state/selectors/test/has-cancelable-site-purchases.js +++ b/client/state/selectors/test/has-cancelable-site-purchases.js @@ -20,6 +20,7 @@ describe( 'hasCancelableSitePurchases', () => { product_slug: 'domain_registration', blog_id: targetSiteId, user_id: targetUserId, + active: true, }, { ID: 2, @@ -27,6 +28,7 @@ describe( 'hasCancelableSitePurchases', () => { blog_id: targetSiteId, user_id: targetUserId, product_slug: 'premium_plan', + active: true, }, { ID: 3, @@ -34,6 +36,7 @@ describe( 'hasCancelableSitePurchases', () => { product_slug: 'premium_theme', blog_id: targetSiteId, user_id: targetUserId, + active: true, }, ]; @@ -108,6 +111,7 @@ describe( 'hasCancelableSitePurchases', () => { blog_id: targetSiteId, user_id: targetUserId, is_refundable: false, + active: true, }, ], error: null, @@ -132,6 +136,7 @@ describe( 'hasCancelableSitePurchases', () => { blog_id: targetSiteId, user_id: targetUserId, is_refundable: true, + active: true, }, ], error: null, @@ -144,4 +149,29 @@ describe( 'hasCancelableSitePurchases', () => { expect( hasCancelableSitePurchases( state, targetSiteId ) ).toBe( true ); } ); + + test( 'should return false if the only purchase is inactive', () => { + const state = deepFreeze( { + purchases: { + data: [ + { + ID: 3, + product_name: 'premium_plan', + product_slug: 'premium_plan', + blog_id: targetSiteId, + user_id: targetUserId, + is_refundable: true, + active: false, + }, + ], + error: null, + isFetchingSitePurchases: false, + isFetchingUserPurchases: false, + hasLoadedSitePurchasesFromServer: true, + hasLoadedUserPurchasesFromServer: true, + }, + } ); + + expect( hasCancelableSitePurchases( state, targetSiteId ) ).toBe( false ); + } ); } ); From 929affa7fd1f0b5dac9336745b3c7f2f1b5e7d5c Mon Sep 17 00:00:00 2001 From: bluefuton Date: Fri, 19 Oct 2018 14:34:36 +1300 Subject: [PATCH 3/4] Check cancelable purchases before displaying warning dialog in site settings --- .../site-settings/site-tools/index.jsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/client/my-sites/site-settings/site-tools/index.jsx b/client/my-sites/site-settings/site-tools/index.jsx index b364542ce1a51b..8378f2d98b4eb5 100644 --- a/client/my-sites/site-settings/site-tools/index.jsx +++ b/client/my-sites/site-settings/site-tools/index.jsx @@ -3,10 +3,9 @@ /** * External dependencies */ - import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { find, some } from 'lodash'; +import { find } from 'lodash'; /** * Internal dependencies @@ -23,12 +22,9 @@ import { isJetpackSite, getSiteAdminUrl } from 'state/sites/selectors'; import isSiteAutomatedTransfer from 'state/selectors/is-site-automated-transfer'; import isVipSite from 'state/selectors/is-vip-site'; import getRewindState from 'state/selectors/get-rewind-state'; -import { - getSitePurchases, - hasLoadedSitePurchasesFromServer, - getPurchasesError, -} from 'state/purchases/selectors'; +import { hasLoadedSitePurchasesFromServer, getPurchasesError } from 'state/purchases/selectors'; import notices from 'notices'; +import hasCancelableSitePurchases from 'state/selectors/has-cancelable-site-purchases'; const trackDeleteSiteOption = option => { tracks.recordEvent( 'calypso_settings_delete_site_options', { @@ -42,9 +38,9 @@ class SiteTools extends Component { showStartOverDialog: false, }; - componentWillReceiveProps( nextProps ) { - if ( nextProps.purchasesError ) { - notices.error( nextProps.purchasesError ); + componentDidUpdate( prevProps ) { + if ( ! prevProps.purchasesError && this.props.purchasesError ) { + notices.error( this.props.purchasesError ); } } @@ -173,11 +169,12 @@ class SiteTools extends Component { checkForSubscriptions = event => { trackDeleteSiteOption( 'delete-site' ); - if ( this.props.isAtomic || ! some( this.props.sitePurchases, 'active' ) ) { + if ( this.props.isAtomic || ! this.props.hasCancelablePurchases ) { return true; } event.preventDefault(); + this.setState( { showDialog: true } ); }; @@ -206,7 +203,6 @@ export default connect( state => { return { isAtomic, siteSlug, - sitePurchases: getSitePurchases( state, siteId ), purchasesError: getPurchasesError( state ), importUrl, exportUrl, @@ -219,5 +215,6 @@ export default connect( state => { showDeleteSite: ( ! isJetpack || isAtomic ) && ! isVip && sitePurchasesLoaded, showManageConnection: isJetpack && ! isAtomic, siteId, + hasCancelablePurchases: hasCancelableSitePurchases( state, siteId ), }; } )( localize( SiteTools ) ); From 9d6d222da173bf9084d894df9a1e20ba45a1d538 Mon Sep 17 00:00:00 2001 From: bluefuton Date: Fri, 19 Oct 2018 14:46:09 +1300 Subject: [PATCH 4/4] Update copy to include premium themes --- client/my-sites/site-settings/delete-site/index.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/my-sites/site-settings/delete-site/index.jsx b/client/my-sites/site-settings/delete-site/index.jsx index e73ec642c96775..e1e49a62c66bd4 100644 --- a/client/my-sites/site-settings/delete-site/index.jsx +++ b/client/my-sites/site-settings/delete-site/index.jsx @@ -260,6 +260,9 @@ class DeleteSite extends Component {
  • { translate( 'Purchased Upgrades' ) }
  • +
  • + { translate( 'Premium Themes' ) } +
  • { ! isAtomic && ( @@ -267,7 +270,7 @@ class DeleteSite extends Component {

    { translate( 'Deletion {{strong}}can not{{/strong}} be undone, ' + - 'and will remove all content, contributors, domains, and upgrades from this site.', + 'and will remove all content, contributors, domains, themes and upgrades from this site.', { components: { strong: , @@ -296,7 +299,7 @@ class DeleteSite extends Component {

    { translate( "To delete this site, you'll need to contact our support team. Deletion can not be undone, " + - 'and will remove all content, contributors, domains, and upgrades from this site.' + 'and will remove all content, contributors, domains, themes and upgrades from this site.' ) }