diff --git a/client/lib/purchases/assembler.js b/client/lib/purchases/assembler.js index 430ab39bf640f..5902b12a6f254 100644 --- a/client/lib/purchases/assembler.js +++ b/client/lib/purchases/assembler.js @@ -22,6 +22,7 @@ function createPurchasesArray( dataTransferObject ) { active: Boolean( purchase.active ), amount: Number( purchase.amount ), attachedToPurchaseId: Number( purchase.attached_to_purchase_id ), + canDisableAutoRenew: Boolean( purchase.can_disable_auto_renew ), currencyCode: purchase.currency_code, currencySymbol: purchase.currency_symbol, domain: purchase.domain, diff --git a/client/lib/purchases/index.js b/client/lib/purchases/index.js index 3701a6aaddbc3..9a98b74946849 100644 --- a/client/lib/purchases/index.js +++ b/client/lib/purchases/index.js @@ -36,6 +36,18 @@ function getPurchasesBySite( purchases ) { }, [] ); } +function getName( purchase ) { + if ( isDomainRegistration( purchase ) ) { + return purchase.meta; + } + + return purchase.productName; +} + +function getSubscriptionEndDate( purchase ) { + return purchase.expiryMoment.format( 'LL' ); +} + function hasPaymentMethod( purchase ) { return isPaidWithPaypal( purchase ) || isPaidWithCreditCard( purchase ); } @@ -44,6 +56,18 @@ function hasPrivateRegistration( purchase ) { return purchase.hasPrivateRegistration; } +function isCancelable( purchase ) { + if ( isRefundable( purchase ) ) { + return true; + } + + if ( isIncludedWithPlan( purchase ) ) { + return false; + } + + return purchase.canDisableAutoRenew; +} + function isExpired( purchase ) { return 'expired' === purchase.expiryStatus; } @@ -133,14 +157,6 @@ function purchaseType( purchase ) { return null; } -function purchaseTitle( purchase ) { - if ( isDomainRegistration( purchase ) ) { - return purchase.meta; - } - - return purchase.productName; -} - function showCreditCardExpiringWarning( purchase ) { return ! isIncludedWithPlan( purchase ) && isPaidWithCreditCard( purchase ) && @@ -154,9 +170,12 @@ function showEditPaymentDetails( purchase ) { export { creditCardExpiresBeforeSubscription, + getName, getPurchasesBySite, + getSubscriptionEndDate, hasPaymentMethod, hasPrivateRegistration, + isCancelable, isPaidWithCreditCard, isPaidWithPaypal, isExpired, @@ -169,7 +188,6 @@ export { isRenewing, paymentLogoType, purchaseType, - purchaseTitle, showCreditCardExpiringWarning, showEditPaymentDetails } diff --git a/client/lib/purchases/test/assembler-test.js b/client/lib/purchases/test/assembler-test.js index 350d67b500692..b0279fb32163b 100644 --- a/client/lib/purchases/test/assembler-test.js +++ b/client/lib/purchases/test/assembler-test.js @@ -7,7 +7,7 @@ import moment from 'moment'; /** * Internal dependencies */ -import { createPurchasesArray } from '../assembler.js'; +import { createPurchasesArray } from '../assembler'; describe( 'Purchases assembler', () => { it( 'should be a function', () => { diff --git a/client/lib/upgrades/actions/purchases.js b/client/lib/upgrades/actions/purchases.js index 0837ad8862ed1..767c07c9af560 100644 --- a/client/lib/upgrades/actions/purchases.js +++ b/client/lib/upgrades/actions/purchases.js @@ -17,6 +17,16 @@ const debug = debugFactory( 'calypso:upgrades:actions:purchases' ), const PURCHASES_FETCH_ERROR_MESSAGE = i18n.translate( 'There was an error retrieving purchases.' ); +function cancelPurchase( purchaseId, onComplete ) { + wpcom.cancelPurchase( purchaseId, ( error, data ) => { + debug( error, data ); + + const success = ! error && data.success; + + onComplete( success ); + } ); +} + function cancelPrivateRegistration( purchaseId, onComplete ) { Dispatcher.handleViewAction( { type: ActionTypes.PURCHASES_PRIVATE_REGISTRATION_CANCEL, @@ -142,6 +152,7 @@ function fetchUserPurchases() { } export { + cancelPurchase, cancelPrivateRegistration, deleteStoredCard, fetchSitePurchases, diff --git a/client/me/purchases/cancel-purchase/button.jsx b/client/me/purchases/cancel-purchase/button.jsx index c2e35428cf74a..6e50b3c316e6c 100644 --- a/client/me/purchases/cancel-purchase/button.jsx +++ b/client/me/purchases/cancel-purchase/button.jsx @@ -1,41 +1,99 @@ /** * External Dependencies */ +import page from 'page'; import React from 'react'; /** * Internal Dependencies */ -import { purchaseTitle } from 'lib/purchases'; +import Button from 'components/button'; +import { cancelPurchase } from 'lib/upgrades/actions'; +import { getName, getSubscriptionEndDate, isRefundable } from 'lib/purchases'; +import notices from 'notices'; +import paths from 'me/purchases/paths'; const CancelPurchaseButton = React.createClass( { propTypes: { purchase: React.PropTypes.object.isRequired }, - render() { - return ( - - ); + getInitialState() { + return { + disabled: false + }; }, - renderText() { - const { isRefundable } = this.props.purchase, - productName = purchaseTitle( this.props.purchase ); + goToCancelConfirmation() { + const { domain, id } = this.props.purchase; - if ( isRefundable ) { - return this.translate( 'Cancel and Refund %(productName)s', { - args: { productName } - } ); - } + page( paths.confirmCancelPurchase( domain, id ) ); + }, - return this.translate( 'Cancel %(productName)s', { - args: { productName } + cancelPurchase() { + const { purchase } = this.props, + { id } = purchase; + + this.toggleDisabled(); + + cancelPurchase( id, ( success ) => { + const purchaseName = getName( purchase ), + subscriptionEndDate = getSubscriptionEndDate( purchase ); + + if ( success ) { + notices.success( this.translate( + '%(purchaseName)s was successfully cancelled. It will be available for use until it expires on %(subscriptionEndDate)s.', + { + args: { + purchaseName, + subscriptionEndDate + } + } + ), { persistent: true } ); + page( paths.list() ); + } else { + notices.error( this.translate( + 'There was a problem canceling %(purchaseName)s. ' + + 'Please try again later or contact support.', + { + args: { purchaseName } + } + ) ); + this.toggleDisabled(); + } } ); + }, + + toggleDisabled() { + this.setState( { + disabled: ! this.state.disabled + } ); + }, + + render() { + const { purchase } = this.props, + purchaseName = getName( purchase ); + + if ( isRefundable( purchase ) ) { + return ( + + ); + } + + return ( + + ); } } ); diff --git a/client/me/purchases/cancel-purchase/index.jsx b/client/me/purchases/cancel-purchase/index.jsx index bb2d13d3c818e..b73408ce27975 100644 --- a/client/me/purchases/cancel-purchase/index.jsx +++ b/client/me/purchases/cancel-purchase/index.jsx @@ -16,7 +16,7 @@ import CompactCard from 'components/card/compact'; import HeaderCake from 'components/header-cake'; import Main from 'components/main'; import paths from '../paths'; -import { purchaseTitle } from 'lib/purchases'; +import { getName, isCancelable } from 'lib/purchases'; import purchasesMixin from '../purchases-mixin'; const CancelPurchase = React.createClass( { @@ -54,9 +54,9 @@ const CancelPurchase = React.createClass( {

- { this.translate( 'Cancel %(productName)s', { + { this.translate( 'Cancel %(purchaseName)s', { args: { - productName: purchaseTitle( purchase ) + purchaseName: getName( purchase ) } } ) }

@@ -87,8 +87,7 @@ const CancelPurchase = React.createClass( { + purchase={ purchase } /> ); @@ -102,9 +101,9 @@ const CancelPurchase = React.createClass( { const purchase = this.getPurchase(); if ( purchase ) { - const { domain, id, isCancelable } = purchase; + const { domain, id } = purchase; - if ( ! isCancelable ) { + if ( ! isCancelable( purchase ) ) { page.redirect( paths.managePurchase( domain, id ) ); } } else { diff --git a/client/me/purchases/cancel-purchase/refund-information.jsx b/client/me/purchases/cancel-purchase/refund-information.jsx index ad882063cf7a4..527565cba204c 100644 --- a/client/me/purchases/cancel-purchase/refund-information.jsx +++ b/client/me/purchases/cancel-purchase/refund-information.jsx @@ -3,16 +3,22 @@ */ import React from 'react'; +/** + * Internal Dependencies + */ +import { getSubscriptionEndDate, isRefundable } from 'lib/purchases'; + const CancelPurchaseRefundInformation = React.createClass( { propTypes: { purchase: React.PropTypes.object.isRequired }, render() { - const { expiryDate, isRefundable, priceText, refundPeriodInDays } = this.props.purchase, + const purchase = this.props.purchase, + { priceText, refundPeriodInDays } = purchase, refundsSupportLink = ; - if ( isRefundable ) { + if ( isRefundable( purchase ) ) { return (

{ this.translate( 'Yes! You are canceling this purchase within the %(refundPeriodInDays)d day refund period. ' + @@ -38,7 +44,7 @@ const CancelPurchaseRefundInformation = React.createClass( { '{{a}}Learn more.{{/a}}', { args: { - subscriptionEndDate: this.moment( expiryDate ).format( 'LL' ), + subscriptionEndDate: getSubscriptionEndDate( purchase ), refundPeriodInDays }, components: { diff --git a/client/me/purchases/confirm-cancel-purchase/index.jsx b/client/me/purchases/confirm-cancel-purchase/index.jsx index d3d83793ca91d..0f7e8e0385fd1 100644 --- a/client/me/purchases/confirm-cancel-purchase/index.jsx +++ b/client/me/purchases/confirm-cancel-purchase/index.jsx @@ -59,7 +59,8 @@ const ConfirmCancelPurchase = React.createClass( { handleSubmit( error, response ) { if ( error ) { notices.error( this.translate( - "Something went wrong and we couldn't cancel your subscription." + 'There was a problem canceling this purchase. ' + + 'Please try again later or contact support.' ) ); return; } diff --git a/client/me/purchases/confirm-cancel-purchase/load-endpoint-form.js b/client/me/purchases/confirm-cancel-purchase/load-endpoint-form.js index 258d1db7e05a3..abbf0657bb31f 100644 --- a/client/me/purchases/confirm-cancel-purchase/load-endpoint-form.js +++ b/client/me/purchases/confirm-cancel-purchase/load-endpoint-form.js @@ -63,7 +63,7 @@ function submitForm( { form, onSubmit, selectedPurchase, selectedSite } ) { button.disabled = true; - wpcom.cancelProduct( selectedPurchase.id, formData, ( error, response ) => { + wpcom.cancelAndRefundPurchase( selectedPurchase.id, formData, ( error, response ) => { if ( error ) { button.disabled = false; onSubmit( error ); diff --git a/client/me/purchases/list/item/index.jsx b/client/me/purchases/list/item/index.jsx index a3c51cbc5f382..4567d0c6a105f 100644 --- a/client/me/purchases/list/item/index.jsx +++ b/client/me/purchases/list/item/index.jsx @@ -11,13 +11,13 @@ import paths from '../../paths'; import CompactCard from 'components/card/compact'; import Flag from 'components/flag'; import { + getName, isExpired, isExpiring, isRenewing, isIncludedWithPlan, isOneTimePurchase, purchaseType, - purchaseTitle, showCreditCardExpiringWarning } from 'lib/purchases'; @@ -119,7 +119,7 @@ const PurchaseItem = React.createClass( { content = (

- { purchaseTitle( this.props.purchase ) } + { getName( this.props.purchase ) }
{ purchaseType( this.props.purchase ) }
diff --git a/client/me/purchases/manage-purchase/index.jsx b/client/me/purchases/manage-purchase/index.jsx index ea1be3f08ea4a..cc72b416c3f33 100644 --- a/client/me/purchases/manage-purchase/index.jsx +++ b/client/me/purchases/manage-purchase/index.jsx @@ -26,8 +26,10 @@ import { domainManagementEdit } from 'my-sites/upgrades/paths'; import { oldShowcaseUrl } from 'lib/themes/helpers'; import { creditCardExpiresBeforeSubscription, + getName, hasPaymentMethod, hasPrivateRegistration, + isCancelable, isExpired, isExpiring, isPaidWithCreditCard, @@ -39,7 +41,6 @@ import { isOneTimePurchase, paymentLogoType, purchaseType, - purchaseTitle, showCreditCardExpiringWarning, showEditPaymentDetails } from 'lib/purchases'; @@ -80,10 +81,10 @@ const ManagePurchase = React.createClass( { className="manage-purchase__purchase-expiring-notice" showDismiss={ false } status={ noticeStatus } - text={ this.translate( '%(purchase)s will expire and be removed from your site %(expiry)s.', + text={ this.translate( '%(purchaseName)s will expire and be removed from your site %(expiry)s.', { args: { - purchase: purchaseTitle( purchase ), + purchaseName: getName( purchase ), expiry: this.moment( purchase.expiryMoment ).fromNow() } } @@ -141,9 +142,9 @@ const ManagePurchase = React.createClass( { let text; if ( 'thank-you' === this.props.destinationType ) { - text = this.translate( '%(purchase)s has been renewed and will now auto renew in the future. {{a}}Learn more{{/a}}', { + text = this.translate( '%(purchaseName)s has been renewed and will now auto renew in the future. {{a}}Learn more{{/a}}', { args: { - purchase: purchaseTitle( purchase ) + purchaseName: getName( purchase ) }, components: { a: @@ -433,22 +434,22 @@ const ManagePurchase = React.createClass( { renderCancelPurchaseNavItem() { const purchase = this.getPurchase(), - { domain, id, isCancelable } = purchase; + { domain, id } = purchase; - if ( isExpired( purchase ) || ! isCancelable ) { + if ( isExpired( purchase ) || ! isCancelable( purchase ) ) { return null; } const translateArgs = { - args: { purchase: purchaseTitle( purchase ) } + args: { purchaseName: getName( purchase ) } }; return ( { isRefundable( purchase ) - ? this.translate( 'Cancel and Refund %(purchase)s', translateArgs ) - : this.translate( 'Cancel %(purchase)s', translateArgs ) + ? this.translate( 'Cancel and Refund %(purchaseName)s', translateArgs ) + : this.translate( 'Cancel %(purchaseName)s', translateArgs ) } ); @@ -497,7 +498,7 @@ const ManagePurchase = React.createClass( { 'is-expired': purchase && isExpired( purchase ) } ); purchaseTypeSeparator = purchaseType( purchase ) ? '|' : ''; - purchaseTitleText = purchaseTitle( purchase ); + purchaseTitleText = getName( purchase ); purchaseTypeText = purchaseType( purchase ); siteName = purchase.siteName; siteDomain = purchase.domain; diff --git a/client/me/purchases/purchases-mixin.js b/client/me/purchases/purchases-mixin.js index 5a9d6d6d89d56..569798f12f88f 100644 --- a/client/me/purchases/purchases-mixin.js +++ b/client/me/purchases/purchases-mixin.js @@ -17,12 +17,6 @@ export default { page( paths.list() ); }, - goToCancelConfirmation() { - const { domain, id } = this.getPurchase(); - - page( paths.confirmCancelPurchase( domain, id ) ); - }, - goToEditCardDetails() { const { domain, id, payment: { creditCard } } = this.getPurchase(); diff --git a/shared/lib/wpcom-undocumented/lib/undocumented.js b/shared/lib/wpcom-undocumented/lib/undocumented.js index 4028e7e374a38..ca8b3f6f54ae6 100644 --- a/shared/lib/wpcom-undocumented/lib/undocumented.js +++ b/shared/lib/wpcom-undocumented/lib/undocumented.js @@ -1706,23 +1706,31 @@ Undocumented.prototype.getHelpLinks = function( searchQuery, fn ) { }, fn ); }; -Undocumented.prototype.getCancellationPageHTML = function( cancellationId, productId, fn ) { - debug( 'upgrades/{cancellationId}/cancel_form?product_id={productId}' ); +Undocumented.prototype.cancelPurchase = function( purchaseId, fn ) { + debug( 'upgrades/{purchaseId}/disable-auto-renew' ); + + this.wpcom.req.post( { + path: `/upgrades/${purchaseId}/disable-auto-renew` + }, fn ); +}; + +Undocumented.prototype.getCancellationPageHTML = function( purchaseId, productId, fn ) { + debug( 'upgrades/{purchaseId}/cancel_form?product_id={productId}' ); this.wpcom.req.get( { - path: `/upgrades/${cancellationId}/cancel_form`, + path: `/upgrades/${purchaseId}/cancel_form`, body: { client_timezone_offset: moment().format( 'Z' ) } }, { product_id: productId }, fn ); -} +}; -Undocumented.prototype.cancelProduct = function( cancellationId, data, fn ) { - debug( 'upgrades/{cancellationId}/cancel' ); +Undocumented.prototype.cancelAndRefundPurchase = function( purchaseId, data, fn ) { + debug( 'upgrades/{purchaseId}/cancel' ); this.wpcom.req.post( { - path: `/upgrades/${cancellationId}/cancel`, + path: `/upgrades/${purchaseId}/cancel`, body: data }, fn ); -} +}; Undocumented.prototype.cancelPrivateRegistration = function( purchaseId, fn ) { debug( 'upgrades/{purchaseId}/cancel-privacy-protection' );