diff --git a/extensions/blocks/donations/amount.js b/extensions/blocks/donations/amount.js
new file mode 100644
index 0000000000000..307829891b164
--- /dev/null
+++ b/extensions/blocks/donations/amount.js
@@ -0,0 +1,150 @@
+/**
+ * External dependencies
+ */
+import formatCurrency, { CURRENCIES } from '@automattic/format-currency';
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { RichText } from '@wordpress/block-editor';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { minimumTransactionAmountForCurrency } from '../../shared/currencies';
+
+const Amount = ( {
+ className = '',
+ currency = null,
+ defaultValue = null,
+ editable = false,
+ label = '',
+ onChange = null,
+ value = '',
+} ) => {
+ const [ editedValue, setEditedValue ] = useState(
+ formatCurrency( value, currency, { symbol: '' } )
+ );
+ const [ isFocused, setIsFocused ] = useState( false );
+ const [ isInvalid, setIsInvalid ] = useState( false );
+ const richTextRef = useRef( null );
+
+ const parseAmount = useCallback(
+ amount => {
+ if ( ! amount ) {
+ return null;
+ }
+
+ if ( typeof amount === 'number' ) {
+ return amount;
+ }
+
+ amount = parseFloat(
+ amount
+ // Remove any thousand grouping separator.
+ .replace( new RegExp( '\\' + CURRENCIES[ currency ].grouping, 'g' ), '' )
+ // Replace the localized decimal separator with a dot (the standard decimal separator in float numbers).
+ .replace( new RegExp( '\\' + CURRENCIES[ currency ].decimal, 'g' ), '.' )
+ );
+
+ if ( isNaN( amount ) ) {
+ return null;
+ }
+
+ return amount;
+ },
+ [ currency ]
+ );
+
+ const setAmount = useCallback(
+ amount => {
+ setEditedValue( amount );
+
+ if ( ! onChange ) {
+ return;
+ }
+
+ const parsedAmount = parseAmount( amount, currency );
+ if ( parsedAmount && parsedAmount >= minimumTransactionAmountForCurrency( currency ) ) {
+ onChange( parsedAmount );
+ setIsInvalid( false );
+ } else if ( amount ) {
+ setIsInvalid( true );
+ }
+ },
+ [ currency, parseAmount, onChange ]
+ );
+
+ const setFocus = () => {
+ if ( ! richTextRef.current ) {
+ return;
+ }
+
+ richTextRef.current.focus();
+ setIsFocused( true );
+ };
+
+ // Tracks when user clicks out the input. Cannot be done with an `onBlur` prop because `RichText` does not support it.
+ useEffect( () => {
+ if ( ! richTextRef.current ) {
+ return;
+ }
+
+ richTextRef.current.addEventListener( 'blur', () => setIsFocused( false ) );
+ }, [ richTextRef ] );
+
+ // Sets a default value if empty when user clicks out the input.
+ useEffect( () => {
+ if ( isFocused || editedValue ) {
+ return;
+ }
+
+ setAmount( formatCurrency( defaultValue, currency, { symbol: '' } ) );
+ }, [ currency, defaultValue, editedValue, isFocused, setAmount ] );
+
+ // Syncs the edited value with the actual value whenever the latter changes (e.g. new default amount after a currency change).
+ useEffect( () => {
+ if ( isFocused || isInvalid ) {
+ return;
+ }
+ setEditedValue( formatCurrency( value, currency, { symbol: '' } ) );
+ }, [ currency, isFocused, isInvalid, setAmount, value ] );
+
+ return (
+
+
+ { CURRENCIES[ currency ].symbol }
+ { editable ? (
+ setAmount( amount ) }
+ placeholder={ formatCurrency( defaultValue, currency, { symbol: '' } ) }
+ ref={ richTextRef }
+ value={ editedValue }
+ withoutInteractiveFormatting
+ />
+ ) : (
+
+ { formatCurrency( value ? value : defaultValue, currency, { symbol: '' } ) }
+
+ ) }
+
+
+ );
+};
+
+export default Amount;
diff --git a/extensions/blocks/donations/attributes.js b/extensions/blocks/donations/attributes.js
index 5446d6a8ce5cf..2beef97025102 100644
--- a/extensions/blocks/donations/attributes.js
+++ b/extensions/blocks/donations/attributes.js
@@ -8,6 +8,13 @@ export default {
type: 'string',
default: 'USD',
},
+ amounts: {
+ type: 'array',
+ items: {
+ type: 'number',
+ },
+ default: [ 5, 15, 100 ],
+ },
oneTimePlanId: {
type: 'number',
default: null,
@@ -38,7 +45,7 @@ export default {
},
chooseAmountText: {
type: 'string',
- default: __( 'Choose an amount (USD)', 'jetpack' ),
+ default: __( 'Choose an amount', 'jetpack' ),
},
customAmountText: {
type: 'string',
diff --git a/extensions/blocks/donations/controls.js b/extensions/blocks/donations/controls.js
index 46e2c1607cd0a..e60c0c38c5a5d 100644
--- a/extensions/blocks/donations/controls.js
+++ b/extensions/blocks/donations/controls.js
@@ -1,40 +1,107 @@
/**
* WordPress dependencies
*/
-import { ExternalLink, PanelBody, ToggleControl } from '@wordpress/components';
+import { BlockControls, InspectorControls } from '@wordpress/block-editor';
+import {
+ Button,
+ Dashicon,
+ Dropdown,
+ ExternalLink,
+ MenuGroup,
+ MenuItem,
+ PanelBody,
+ ToggleControl,
+ ToolbarGroup,
+} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { InspectorControls } from '@wordpress/block-editor';
+import { DOWN } from '@wordpress/keycodes';
+
+/**
+ * Internal dependencies
+ */
+import { SUPPORTED_CURRENCIES } from '../../shared/currencies';
+import { CURRENCIES } from '@automattic/format-currency';
const Controls = props => {
const { attributes, setAttributes, products, siteSlug } = props;
- const { monthlyPlanId, annuallyPlanId, showCustomAmount } = attributes;
+ const { currency, monthlyPlanId, annuallyPlanId, showCustomAmount } = attributes;
+
return (
-
-
-
- setAttributes( { monthlyPlanId: value ? products[ '1 month' ] : null } )
- }
- label={ __( 'Show monthly donations', 'jetpack' ) }
- />
-
- setAttributes( { annuallyPlanId: value ? products[ '1 year' ] : null } )
- }
- label={ __( 'Show annual donations', 'jetpack' ) }
- />
- setAttributes( { showCustomAmount: value } ) }
- label={ __( 'Show custom amount option', 'jetpack' ) }
- />
-
- { __( 'View donation earnings', 'jetpack' ) }
-
-
-
+ <>
+
+
+ {
+ const openOnArrowDown = event => {
+ if ( ! isOpen && event.keyCode === DOWN ) {
+ event.preventDefault();
+ event.stopPropagation();
+ onToggle();
+ }
+ };
+
+ return (
+
+
+
+
+
+ setAttributes( { monthlyPlanId: value ? products[ '1 month' ] : null } )
+ }
+ label={ __( 'Show monthly donations', 'jetpack' ) }
+ />
+
+ setAttributes( { annuallyPlanId: value ? products[ '1 year' ] : null } )
+ }
+ label={ __( 'Show annual donations', 'jetpack' ) }
+ />
+ setAttributes( { showCustomAmount: value } ) }
+ label={ __( 'Show custom amount option', 'jetpack' ) }
+ />
+
+ { __( 'View donation earnings', 'jetpack' ) }
+
+
+
+ >
);
};
diff --git a/extensions/blocks/donations/edit.js b/extensions/blocks/donations/edit.js
index c9fd8fb452100..c840d56419d0f 100644
--- a/extensions/blocks/donations/edit.js
+++ b/extensions/blocks/donations/edit.js
@@ -100,7 +100,6 @@ const Edit = props => {
return (
{
- const { attributes, interval, setAttributes } = props;
+ const { attributes, setAttributes } = props;
+ const { activeTab } = useContext( Context );
const getAttribute = attributeName => {
if ( attributeName in attributesPerInterval ) {
- return attributes[ attributesPerInterval[ attributeName ][ interval ] ];
+ return attributes[ attributesPerInterval[ attributeName ][ activeTab ] ];
}
return attributes[ attributeName ];
};
@@ -37,85 +44,115 @@ const Tab = props => {
const setAttribute = ( attributeName, value ) => {
if ( attributeName in attributesPerInterval ) {
return setAttributes( {
- [ attributesPerInterval[ attributeName ][ interval ] ]: value,
+ [ attributesPerInterval[ attributeName ][ activeTab ] ]: value,
} );
}
return setAttributes( { [ attributeName ]: value } );
};
- const currency = getAttribute( 'currency' );
+ const amounts = getAttribute( 'amounts' );
+ const currency = getAttribute( 'currency' );
+ const showCustomAmount = getAttribute( 'showCustomAmount' );
const minAmount = minimumTransactionAmountForCurrency( currency );
- // TODO: This generates good amounts for USD, but let's revisit once we support more currencies.
- const tiers = [
- minAmount * 10, // USD 5
- minAmount * 30, // USD 15
- minAmount * 200, // USD 100
- ];
- const customAmountPlaceholder = minAmount * 100; // USD 50
+
+ const [ defaultAmounts, setDefaultAmounts ] = useState( [
+ minAmount * 10, // 1st tier (USD 5)
+ minAmount * 30, // 2nd tier (USD 15)
+ minAmount * 200, // 3rd tier (USD 100)
+ ] );
+ const [ defaultCustomAmount, setDefaultCustomAmount ] = useState( minAmount * 100 );
+ const [ previousCurrency, setPreviousCurrency ] = useState( currency );
+
+ // Updates the amounts whenever the currency changes.
+ useEffect( () => {
+ if ( previousCurrency === currency ) {
+ return;
+ }
+ setPreviousCurrency( currency );
+
+ const newDefaultAmounts = [
+ minAmount * 10, // 1st tier (USD 5)
+ minAmount * 30, // 2nd tier (USD 15)
+ minAmount * 200, // 3rd tier (USD 100)
+ ];
+ setDefaultAmounts( newDefaultAmounts );
+ setAttributes( { amounts: newDefaultAmounts } );
+ setDefaultCustomAmount( minAmount * 100 ); // USD 50
+ }, [ currency, minAmount, previousCurrency, setAttributes ] );
+
+ const setAmount = ( amount, tier ) => {
+ const newAmounts = [ ...amounts ];
+ newAmounts[ tier ] = amount;
+ setAttributes( { amounts: newAmounts } );
+ };
+
+ if ( ! amounts ) {
+ return null;
+ }
return (
-
- { ( { activeTab } ) => (
-
-
setAttribute( 'heading', value ) }
- inlineToolbar
- />
- setAttribute( 'chooseAmountText', value ) }
- inlineToolbar
+ <>
+ setAttribute( 'heading', value ) }
+ />
+ setAttribute( 'chooseAmountText', value ) }
+ />
+
+ { amounts.map( ( amount, index ) => (
+
setAmount( newAmount, index ) }
+ value={ amount }
/>
-
- { tiers.map( amount => (
-
-
{ formatCurrency( amount, currency ) }
-
- ) ) }
-
- { getAttribute( 'showCustomAmount' ) && (
- <>
- setAttribute( 'customAmountText', value ) }
- inlineToolbar
- />
-
-
- { CURRENCIES[ currency ].symbol }
-
- { formatCurrency( customAmountPlaceholder, currency, { symbol: '' } ) }
-
-
-
- >
- ) }
- ——
+ ) ) }
+
+ { showCustomAmount && (
+ <>
setAttribute( 'extraText', value ) }
- inlineToolbar
+ value={ getAttribute( 'customAmountText' ) }
+ onChange={ value => setAttribute( 'customAmountText', value ) }
/>
- setAttribute( 'buttonText', value ) }
- inlineToolbar
+
-
+ >
) }
-
+ ——
+ setAttribute( 'extraText', value ) }
+ />
+
+ setAttribute( 'buttonText', value ) }
+ />
+
+ >
);
};
diff --git a/extensions/blocks/donations/tabs.js b/extensions/blocks/donations/tabs.js
index 0eb8a01bf37e7..9408323e6d5a6 100644
--- a/extensions/blocks/donations/tabs.js
+++ b/extensions/blocks/donations/tabs.js
@@ -7,7 +7,7 @@ import classNames from 'classnames';
* WordPress dependencies
*/
import { Button } from '@wordpress/components';
-import { useEffect, useState } from '@wordpress/element';
+import { useCallback, useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
@@ -23,7 +23,7 @@ const Tabs = props => {
const { oneTimePlanId, monthlyPlanId, annuallyPlanId } = attributes;
const [ activeTab, setActiveTab ] = useState( 'one-time' );
- const isTabActive = tab => activeTab === tab;
+ const isTabActive = useCallback( tab => activeTab === tab, [ activeTab ] );
const tabs = {
'one-time': { title: __( 'One-Time', 'jetpack' ) },
@@ -35,14 +35,16 @@ const Tabs = props => {
useEffect( () => {
// Since there is no setting for disabling the one-time option, we can assume that the block has been just
// inserted if the attribute `oneTimePlanId` is not set.
- if ( ! oneTimePlanId ) {
- setAttributes( {
- oneTimePlanId: products[ 'one-time' ],
- monthlyPlanId: products[ '1 month' ],
- annuallyPlanId: products[ '1 year' ],
- } );
+ if ( oneTimePlanId ) {
+ return;
}
- }, [ oneTimePlanId ] );
+
+ setAttributes( {
+ oneTimePlanId: products[ 'one-time' ],
+ monthlyPlanId: products[ '1 month' ],
+ annuallyPlanId: products[ '1 year' ],
+ } );
+ }, [ oneTimePlanId, products, setAttributes ] );
// Sets the plans when Stripe has been connected (we use fake plans while Stripe is not connected so user can still try the block).
useEffect( () => {
@@ -53,7 +55,7 @@ const Tabs = props => {
...( annuallyPlanId && { annuallyPlanId: products[ '1 year' ] } ),
} );
}
- }, [ oneTimePlanId, monthlyPlanId, annuallyPlanId ] );
+ }, [ oneTimePlanId, monthlyPlanId, annuallyPlanId, setAttributes, products ] );
// Activates the one-time tab if the interval of the current active tab is disabled.
useEffect( () => {
@@ -64,7 +66,7 @@ const Tabs = props => {
if ( ! annuallyPlanId && isTabActive( '1 year' ) ) {
setActiveTab( 'one-time' );
}
- }, [ monthlyPlanId, annuallyPlanId ] );
+ }, [ monthlyPlanId, annuallyPlanId, setActiveTab, isTabActive ] );
return (
@@ -80,6 +82,7 @@ const Tabs = props => {
'is-active': isTabActive( interval ),
} ) }
onClick={ () => setActiveTab( interval ) }
+ key={ `jetpack-donations-tab-${ interval } ` }
>
{ title }
@@ -88,9 +91,7 @@ const Tabs = props => {
) }
- { Object.keys( tabs ).map( interval => (
-
- ) ) }
+