diff --git a/_inc/lib/class-jetpack-currencies.php b/_inc/lib/class-jetpack-currencies.php new file mode 100644 index 000000000000..adebcf2b94d7 --- /dev/null +++ b/_inc/lib/class-jetpack-currencies.php @@ -0,0 +1,176 @@ + array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => '$', + 'decimal' => 2, + ), + 'GBP' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => '£', + 'decimal' => 2, + ), + 'JPY' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => '¥', + 'decimal' => 0, + ), + 'BRL' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => 'R$', + 'decimal' => 2, + ), + 'EUR' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => '€', + 'decimal' => 2, + ), + 'NZD' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => 'NZ$', + 'decimal' => 2, + ), + 'AUD' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => 'A$', + 'decimal' => 2, + ), + 'CAD' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => 'C$', + 'decimal' => 2, + ), + 'ILS' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => '₪', + 'decimal' => 2, + ), + 'RUB' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => '₽', + 'decimal' => 2, + ), + 'MXN' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => 'MX$', + 'decimal' => 2, + ), + 'MYR' => array( + 'format' => '%2$s%1$s', // 1: Symbol 2: currency value + 'symbol' => 'RM', + 'decimal' => 2, + ), + 'SEK' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => 'Skr', + 'decimal' => 2, + ), + 'HUF' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => 'Ft', + 'decimal' => 0, // Decimals are supported by Stripe but not by PayPal. + ), + 'CHF' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => 'CHF', + 'decimal' => 2, + ), + 'CZK' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => 'Kč', + 'decimal' => 2, + ), + 'DKK' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => 'Dkr', + 'decimal' => 2, + ), + 'HKD' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => 'HK$', + 'decimal' => 2, + ), + 'NOK' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => 'Kr', + 'decimal' => 2, + ), + 'PHP' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => '₱', + 'decimal' => 2, + ), + 'PLN' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => 'PLN', + 'decimal' => 2, + ), + 'SGD' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => 'S$', + 'decimal' => 2, + ), + 'TWD' => array( + 'format' => '%1$s%2$s', // 1: Symbol 2: currency value + 'symbol' => 'NT$', + 'decimal' => 0, // Decimals are supported by Stripe but not by PayPal. + ), + 'THB' => array( + 'format' => '%2$s%1$s', // 1: Symbol 2: currency value + 'symbol' => '฿', + 'decimal' => 2, + ), + 'INR' => array( + 'format' => '%2$s %1$s', // 1: Symbol 2: currency value + 'symbol' => '₹', + 'decimal' => 0, + ), + ); + + /** + * Format a price with currency. + * + * Uses currency-aware formatting to output a formatted price with a simple fallback. + * + * Largely inspired by WordPress.com's Store_Price::display_currency + * + * @param string $price Price. + * @param string $currency Currency. + * @param bool $symbol Whether to display the currency symbol. + * @return string Formatted price. + */ + public static function format_price( $price, $currency, $symbol = true ) { + // Fall back to unspecified currency symbol like `¤1,234.05`. + // @link https://en.wikipedia.org/wiki/Currency_sign_(typography). + if ( ! array_key_exists( $currency, self::CURRENCIES ) ) { + return '¤' . number_format_i18n( $price, 2 ); + } + + $currency_details = self::CURRENCIES[ $currency ]; + + // Ensure USD displays as 1234.56 even in non-US locales. + $amount = 'USD' === $currency + ? number_format( $price, $currency_details['decimal'], '.', ',' ) + : number_format_i18n( $price, $currency_details['decimal'] ); + + return sprintf( + $currency_details['format'], + $symbol ? $currency_details['symbol'] : '', + $amount + ); + } +} diff --git a/bin/phpcs-requirelist.js b/bin/phpcs-requirelist.js index 244d96f147cb..30926fdf56f3 100644 --- a/bin/phpcs-requirelist.js +++ b/bin/phpcs-requirelist.js @@ -23,6 +23,7 @@ module.exports = [ '_inc/lib/admin-pages/class-jetpack-about-page.php', '_inc/lib/class.media-extractor.php', '_inc/lib/class.media-summary.php', + '_inc/lib/class-jetpack-currencies.php', '_inc/lib/class-jetpack-instagram-gallery-helper.php', '_inc/lib/class-jetpack-tweetstorm-helper.php', '_inc/lib/class-jetpack-mapbox-helper.php', diff --git a/extensions/blocks/donations/attributes.js b/extensions/blocks/donations/attributes.js deleted file mode 100644 index 6c53e5672532..000000000000 --- a/extensions/blocks/donations/attributes.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -export default { - currency: { - type: 'string', - default: 'USD', - }, - oneTimeDonation: { - type: 'object', - default: { - show: true, - planId: null, - amounts: [ 5, 15, 100 ], - heading: __( 'Make a one-time donation', 'jetpack' ), - extraText: __( 'Your contribution is appreciated.', 'jetpack' ), - buttonText: __( 'Donate', 'jetpack' ), - }, - }, - monthlyDonation: { - type: 'object', - default: { - show: true, - planId: null, - amounts: [ 5, 15, 100 ], - heading: __( 'Make a monthly donation', 'jetpack' ), - extraText: __( 'Your contribution is appreciated.', 'jetpack' ), - buttonText: __( 'Donate monthly', 'jetpack' ), - }, - }, - annualDonation: { - type: 'object', - default: { - show: true, - planId: null, - amounts: [ 5, 15, 100 ], - heading: __( 'Make a yearly donation', 'jetpack' ), - extraText: __( 'Your contribution is appreciated.', 'jetpack' ), - buttonText: __( 'Donate yearly', 'jetpack' ), - }, - }, - showCustomAmount: { - type: 'boolean', - default: true, - }, - chooseAmountText: { - type: 'string', - default: __( 'Choose an amount', 'jetpack' ), - }, - customAmountText: { - type: 'string', - default: __( 'Or enter a custom amount', 'jetpack' ), - }, -}; diff --git a/extensions/blocks/donations/common.scss b/extensions/blocks/donations/common.scss index 75878343077f..7b41d68af360 100644 --- a/extensions/blocks/donations/common.scss +++ b/extensions/blocks/donations/common.scss @@ -91,8 +91,6 @@ } .donations__separator { - line-height: 8px; - height: 8px; margin-bottom: 16px; margin-top: 16px; diff --git a/extensions/blocks/donations/deprecated/v1/index.js b/extensions/blocks/donations/deprecated/v1/index.js new file mode 100644 index 000000000000..f76d36a9c04f --- /dev/null +++ b/extensions/blocks/donations/deprecated/v1/index.js @@ -0,0 +1,226 @@ +/** + * External dependencies + */ +import formatCurrency, { CURRENCIES } from '@automattic/format-currency'; + +/** + * WordPress dependencies + */ +import { RichText } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { minimumTransactionAmountForCurrency } from '../../../../shared/currencies'; + +export default { + attributes: { + currency: { + type: 'string', + default: 'USD', + }, + oneTimeDonation: { + type: 'object', + default: { + show: true, + planId: null, + amounts: [ 5, 15, 100 ], + heading: __( 'Make a one-time donation', 'jetpack' ), + extraText: __( 'Your contribution is appreciated.', 'jetpack' ), + buttonText: __( 'Donate', 'jetpack' ), + }, + }, + monthlyDonation: { + type: 'object', + default: { + show: true, + planId: null, + amounts: [ 5, 15, 100 ], + heading: __( 'Make a monthly donation', 'jetpack' ), + extraText: __( 'Your contribution is appreciated.', 'jetpack' ), + buttonText: __( 'Donate monthly', 'jetpack' ), + }, + }, + annualDonation: { + type: 'object', + default: { + show: true, + planId: null, + amounts: [ 5, 15, 100 ], + heading: __( 'Make a yearly donation', 'jetpack' ), + extraText: __( 'Your contribution is appreciated.', 'jetpack' ), + buttonText: __( 'Donate yearly', 'jetpack' ), + }, + }, + showCustomAmount: { + type: 'boolean', + default: true, + }, + chooseAmountText: { + type: 'string', + default: __( 'Choose an amount', 'jetpack' ), + }, + customAmountText: { + type: 'string', + default: __( 'Or enter a custom amount', 'jetpack' ), + }, + }, + supports: { + html: false, + }, + save: ( { attributes } ) => { + const { + currency, + oneTimeDonation, + monthlyDonation, + annualDonation, + showCustomAmount, + chooseAmountText, + customAmountText, + } = attributes; + + if ( ! oneTimeDonation || ! oneTimeDonation.show || oneTimeDonation.planId === -1 ) { + return null; + } + + const tabs = { + 'one-time': { title: __( 'One-Time', 'jetpack' ) }, + ...( monthlyDonation.show && { '1 month': { title: __( 'Monthly', 'jetpack' ) } } ), + ...( annualDonation.show && { '1 year': { title: __( 'Yearly', 'jetpack' ) } } ), + }; + + return ( +
+
+ { Object.keys( tabs ).length > 1 && ( +
+ { Object.entries( tabs ).map( ( [ interval, { title } ] ) => ( +
+ { title } +
+ ) ) } +
+ ) } +
+
+ + { monthlyDonation.show && ( + + ) } + { annualDonation.show && ( + + ) } + +
+ { oneTimeDonation.amounts.map( amount => ( +
+ { formatCurrency( amount, currency ) } +
+ ) ) } +
+ { monthlyDonation.show && ( +
+ { monthlyDonation.amounts.map( amount => ( +
+ { formatCurrency( amount, currency ) } +
+ ) ) } +
+ ) } + { annualDonation.show && ( +
+ { annualDonation.amounts.map( amount => ( +
+ { formatCurrency( amount, currency ) } +
+ ) ) } +
+ ) } + { showCustomAmount && ( + <> + +
+ { CURRENCIES[ currency ].symbol } +
+
+ + ) } +
——
+ + { monthlyDonation.show && ( + + ) } + { annualDonation.show && ( + + ) } +
+ +
+ { monthlyDonation.show && ( +
+ +
+ ) } + { annualDonation.show && ( +
+ +
+ ) } +
+
+
+
+ ); + }, +}; diff --git a/extensions/blocks/donations/donations.php b/extensions/blocks/donations/donations.php index 134e6ac077a0..a392791bf731 100644 --- a/extensions/blocks/donations/donations.php +++ b/extensions/blocks/donations/donations.php @@ -26,6 +26,60 @@ function register_block() { array( 'render_callback' => __NAMESPACE__ . '\render_block', 'plan_check' => true, + 'attributes' => array( + 'currency' => array( + 'type' => 'string', + 'default' => 'USD', + ), + 'oneTimeDonation' => array( + 'type' => 'object', + 'default' => array( + 'show' => true, + 'planId' => null, + 'amounts' => array( 5, 15, 100 ), + 'heading' => __( 'Make a one-time donation', 'jetpack' ), + 'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ), + 'buttonText' => __( 'Donate', 'jetpack' ), + ), + ), + 'monthlyDonation' => array( + 'type' => 'object', + 'default' => array( + 'show' => true, + 'planId' => null, + 'amounts' => array( 5, 15, 100 ), + 'heading' => __( 'Make a monthly donation', 'jetpack' ), + 'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ), + 'buttonText' => __( 'Donate monthly', 'jetpack' ), + ), + ), + 'annualDonation' => array( + 'type' => 'object', + 'default' => array( + 'show' => true, + 'planId' => null, + 'amounts' => array( 5, 15, 100 ), + 'heading' => __( 'Make a yearly donation', 'jetpack' ), + 'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ), + 'buttonText' => __( 'Donate yearly', 'jetpack' ), + ), + ), + 'showCustomAmount' => array( + 'type' => 'boolean', + 'default' => true, + ), + 'chooseAmountText' => array( + 'type' => 'string', + 'default' => __( 'Choose an amount', 'jetpack' ), + ), + 'customAmountText' => array( + 'type' => 'string', + 'default' => __( 'Or enter a custom amount', 'jetpack' ), + ), + 'fallbackLinkUrl' => array( + 'type' => 'string', + ), + ), ) ); } @@ -40,29 +94,163 @@ function register_block() { * @return string */ function render_block( $attr, $content ) { - Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME, array( 'thickbox' ) ); + // Keep content as-is if rendered in other contexts than frontend (i.e. feed, emails, API, etc.). + if ( ! jetpack_is_frontend() ) { + return $content; + } - require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php'; + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME, array( 'thickbox' ) ); add_thickbox(); + require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php'; + jetpack_require_lib( 'class-jetpack-currencies' ); + $donations = array( - 'one-time' => $attr['oneTimeDonation'], - 'monthly' => $attr['monthlyDonation'], - 'annual' => $attr['annualDonation'], + 'one-time' => array_merge( + array( + 'title' => __( 'One-Time', 'jetpack' ), + 'class' => 'donations__one-time-item', + ), + $attr['oneTimeDonation'] + ), ); + if ( $attr['monthlyDonation']['show'] ) { + $donations['1 month'] = array_merge( + array( + 'title' => __( 'Monthly', 'jetpack' ), + 'class' => 'donations__monthly-item', + ), + $attr['monthlyDonation'] + ); + } + if ( $attr['annualDonation']['show'] ) { + $donations['1 year'] = array_merge( + array( + 'title' => __( 'Yearly', 'jetpack' ), + 'class' => 'donations__annual-item', + ), + $attr['annualDonation'] + ); + } + + $currency = $attr['currency']; + $nav = ''; + $headings = ''; + $amounts = ''; + $extra_text = ''; + $buttons = ''; foreach ( $donations as $interval => $donation ) { - if ( ! $donation['show'] ) { - continue; - } $plan_id = (int) $donation['planId']; $plan = get_post( $plan_id ); if ( ! $plan || is_wp_error( $plan ) ) { continue; } - $url = \Jetpack_Memberships::get_instance()->get_subscription_url( $plan_id ); - $content = preg_replace( '/(donations__donate-button donations__' . $interval . '-item")/i', '$1 href="' . esc_url( $url ) . '"', $content ); + if ( count( $donations ) > 1 ) { + if ( ! $nav ) { + $nav .= '
'; + } + $nav .= sprintf( + '
%2$s
', + esc_attr( $interval ), + esc_html( $donation['title'] ) + ); + } + $headings .= sprintf( + '

%2$s

', + esc_attr( $donation['class'] ), + wp_kses_post( $donation['heading'] ) + ); + $amounts .= sprintf( + '
', + esc_attr( $donation['class'] ) + ); + foreach ( $donation['amounts'] as $amount ) { + $amounts .= sprintf( + '
%2$s
', + esc_attr( $amount ), + esc_html( \Jetpack_Currencies::format_price( $amount, $currency ) ) + ); + } + $amounts .= '
'; + $extra_text .= sprintf( + '

%2$s

', + esc_attr( $donation['class'] ), + wp_kses_post( $donation['extraText'] ) + ); + $buttons .= sprintf( + '%3$s', + esc_attr( $donation['class'] ), + esc_url( \Jetpack_Memberships::get_instance()->get_subscription_url( $plan_id ) ), + wp_kses_post( $donation['buttonText'] ) + ); + } + if ( $nav ) { + $nav .= '
'; + } + + $custom_amount = ''; + if ( $attr['showCustomAmount'] ) { + $custom_amount .= sprintf( + '

%s

', + wp_kses_post( $attr['customAmountText'] ) + ); + $default_custom_amount = \Jetpack_Memberships::SUPPORTED_CURRENCIES[ $currency ] * 100; + $custom_amount .= sprintf( + '
+ %1$s +
+
', + esc_html( \Jetpack_Currencies::CURRENCIES[ $attr['currency'] ]['symbol'] ), + esc_attr( $attr['currency'] ), + esc_attr( \Jetpack_Currencies::format_price( $default_custom_amount, $currency, false ) ) + ); } - return $content; + return sprintf( + ' +
+
+ %2$s +
+
+ %3$s +

%4$s

+ %5$s + %6$s +
+ %7$s + %8$s +
+
+
+', + esc_attr( Blocks::classes( FEATURE_NAME, $attr ) ), + $nav, + $headings, + $attr['chooseAmountText'], + $amounts, + $custom_amount, + $extra_text, + $buttons + ); +} + +/** + * Determine if AMP should be disabled on posts having Donations blocks. + * + * @param bool $skip Skipped. + * @param int $post_id Post ID. + * @param WP_Post $post Post. + * + * @return bool Whether to skip the post from AMP. + */ +function amp_skip_post( $skip, $post_id, $post ) { + // When AMP is on standard mode, there are no non-AMP posts to link to where the donation can be completed, so let's + // prevent the post from being available in AMP. + if ( function_exists( 'amp_is_canonical' ) && \amp_is_canonical() && has_block( BLOCK_NAME, $post->post_content ) ) { + return true; + } + return $skip; } +add_filter( 'amp_skip_post', __NAMESPACE__ . '\amp_skip_post', 10, 3 ); diff --git a/extensions/blocks/donations/edit.js b/extensions/blocks/donations/edit.js index acf7a91b5cd6..c0667a599be9 100644 --- a/extensions/blocks/donations/edit.js +++ b/extensions/blocks/donations/edit.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { useSelect } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -13,7 +14,7 @@ import fetchDefaultProducts from './fetch-default-products'; import fetchStatus from './fetch-status'; const Edit = props => { - const { attributes, className } = props; + const { attributes, className, setAttributes } = props; const { currency } = attributes; const [ loadingError, setLoadingError ] = useState( '' ); @@ -21,6 +22,11 @@ const Edit = props => { const [ stripeConnectUrl, setStripeConnectUrl ] = useState( false ); const [ products, setProducts ] = useState( [] ); + const post = useSelect( select => select( 'core/editor' ).getCurrentPost(), [] ); + useEffect( () => { + setAttributes( { fallbackLinkUrl: post.link } ); + }, [ post.link, setAttributes ] ); + const apiError = message => { setLoadingError( message ); }; diff --git a/extensions/blocks/donations/index.js b/extensions/blocks/donations/index.js index f5e5b0ba3237..cd4b41be5358 100644 --- a/extensions/blocks/donations/index.js +++ b/extensions/blocks/donations/index.js @@ -1,16 +1,20 @@ /** * External dependencies */ +import GridiconHeart from 'gridicons/dist/heart-outline'; + +/** + * WordPress dependencies + */ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import attributes from './attributes'; import edit from './edit'; import save from './save'; -import GridiconHeart from 'gridicons/dist/heart-outline'; import { getIconColor } from '../../shared/block-icons'; +import deprecatedV1 from './deprecated/v1'; /** * Style dependencies @@ -34,6 +38,6 @@ export const settings = { }, edit, save, - attributes, example: {}, + deprecated: [ deprecatedV1 ], }; diff --git a/extensions/blocks/donations/save.js b/extensions/blocks/donations/save.js index 482ca6607ede..b07302e278dd 100644 --- a/extensions/blocks/donations/save.js +++ b/extensions/blocks/donations/save.js @@ -1,170 +1,62 @@ -/** - * External dependencies - */ -import formatCurrency, { CURRENCIES } from '@automattic/format-currency'; - /** * WordPress dependencies */ import { RichText } from '@wordpress/block-editor'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { minimumTransactionAmountForCurrency } from '../../shared/currencies'; const Save = ( { attributes } ) => { - const { - currency, - oneTimeDonation, - monthlyDonation, - annualDonation, - showCustomAmount, - chooseAmountText, - customAmountText, - } = attributes; + const { fallbackLinkUrl, oneTimeDonation, monthlyDonation, annualDonation } = attributes; - if ( ! oneTimeDonation || ! oneTimeDonation.show || oneTimeDonation.planId === -1 ) { + if ( + ! oneTimeDonation || + ! oneTimeDonation.show || + ! oneTimeDonation.planId || + oneTimeDonation.planId === -1 + ) { return null; } - const tabs = { - 'one-time': { title: __( 'One-Time', 'jetpack' ) }, - ...( monthlyDonation.show && { '1 month': { title: __( 'Monthly', 'jetpack' ) } } ), - ...( annualDonation.show && { '1 year': { title: __( 'Yearly', 'jetpack' ) } } ), - }; - return (
-
- { Object.keys( tabs ).length > 1 && ( -
- { Object.entries( tabs ).map( ( [ interval, { title } ] ) => ( -
- { title } -
- ) ) } -
- ) } -
-
- - { monthlyDonation.show && ( - - ) } - { annualDonation.show && ( - - ) } - -
- { oneTimeDonation.amounts.map( amount => ( -
- { formatCurrency( amount, currency ) } -
- ) ) } -
- { monthlyDonation.show && ( -
- { monthlyDonation.amounts.map( amount => ( -
- { formatCurrency( amount, currency ) } -
- ) ) } -
- ) } - { annualDonation.show && ( -
- { annualDonation.amounts.map( amount => ( -
- { formatCurrency( amount, currency ) } -
- ) ) } -
- ) } - { showCustomAmount && ( - <> - -
- { CURRENCIES[ currency ].symbol } -
-
- - ) } -
——
- - { monthlyDonation.show && ( - - ) } - { annualDonation.show && ( - - ) } -
- -
- { monthlyDonation.show && ( -
- -
- ) } - { annualDonation.show && ( -
- -
- ) } -
-
-
+ + + + { monthlyDonation.show && ( + <> +
+ + + + + ) } + { annualDonation.show && ( + <> +
+ + + + + ) }
); }; diff --git a/extensions/blocks/donations/tab.js b/extensions/blocks/donations/tab.js index b231d9e0a2e8..23f4aa500df5 100644 --- a/extensions/blocks/donations/tab.js +++ b/extensions/blocks/donations/tab.js @@ -140,7 +140,7 @@ const Tab = ( { activeTab, attributes, setAttributes } ) => { /> ) } -
——
+
post_content ) ) { + if ( function_exists( 'amp_is_canonical' ) && \amp_is_canonical() && has_block( BLOCK_NAME, $post->post_content ) ) { return true; } return $skip; diff --git a/extensions/shared/currencies.js b/extensions/shared/currencies.js index 7d13b43f1db5..e638f61cd441 100644 --- a/extensions/shared/currencies.js +++ b/extensions/shared/currencies.js @@ -8,10 +8,8 @@ import { CURRENCIES } from '@automattic/format-currency'; * * @link https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts * - * List has to be in sync with the Memberships library in WP.com. - * @see Memberships_Product::SUPPORTED_CURRENCIES - * - * @type { [currency: string]: number } + * List has to be in with `Jetpack_Memberships::SUPPORTED_CURRENCIES` in modules/memberships/class-jetpack-memberships.php and + * `Memberships_Product::SUPPORTED_CURRENCIES` in the WP.com memberships library. */ export const SUPPORTED_CURRENCIES = { USD: 0.5, diff --git a/modules/memberships/class-jetpack-memberships.php b/modules/memberships/class-jetpack-memberships.php index 1cabbcde0901..cb87fda2ddf1 100644 --- a/modules/memberships/class-jetpack-memberships.php +++ b/modules/memberships/class-jetpack-memberships.php @@ -66,6 +66,34 @@ class Jetpack_Memberships { */ private static $instance; + /** + * Currencies we support and Stripe's minimum amount for a transaction in that currency. + * + * @link https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts + * + * List has to be in with `SUPPORTED_CURRENCIES` in extensions/shared/currencies.js and + * `Memberships_Product::SUPPORTED_CURRENCIES` in the WP.com memberships library. + */ + const SUPPORTED_CURRENCIES = array( + 'USD' => 0.5, + 'AUD' => 0.5, + 'BRL' => 0.5, + 'CAD' => 0.5, + 'CHF' => 0.5, + 'DKK' => 2.5, + 'EUR' => 0.5, + 'GBP' => 0.3, + 'HKD' => 4.0, + 'INR' => 0.5, + 'JPY' => 50, + 'MXN' => 10, + 'NOK' => 3.0, + 'NZD' => 0.5, + 'PLN' => 2.0, + 'SEK' => 3.0, + 'SGD' => 0.5, + ); + /** * Jetpack_Memberships constructor. */ diff --git a/modules/simple-payments/simple-payments.php b/modules/simple-payments/simple-payments.php index 66fd2a142f13..8d36a00774dc 100644 --- a/modules/simple-payments/simple-payments.php +++ b/modules/simple-payments/simple-payments.php @@ -290,28 +290,8 @@ public function output_shortcode( $data ) { * @return string Formatted price. */ private function format_price( $price, $currency ) { - $currency_details = self::get_currency( $currency ); - - if ( $currency_details ) { - // Ensure USD displays as 1234.56 even in non-US locales. - $amount = 'USD' === $currency - ? number_format( $price, $currency_details['decimal'], '.', ',' ) - : number_format_i18n( $price, $currency_details['decimal'] ); - - return sprintf( - $currency_details['format'], - $currency_details['symbol'], - $amount - ); - } - - // Fall back to unspecified currency symbol like `¤1,234.05`. - // @link https://en.wikipedia.org/wiki/Currency_sign_(typography). - if ( ! $currency ) { - return '¤' . number_format_i18n( $price, 2 ); - } - - return number_format_i18n( $price, 2 ) . ' ' . $currency; + require_once JETPACK__PLUGIN_DIR . 'class.jetpack-currencies.php'; + return Jetpack_Currencies::format_price( $price, $currency ); } /** @@ -573,128 +553,8 @@ function setup_cpts() { * @return ?array Currency object or null if not found. */ private static function get_currency( $the_currency ) { - $currencies = array( - 'USD' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => '$', - 'decimal' => 2, - ), - 'GBP' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => '£', - 'decimal' => 2, - ), - 'JPY' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => '¥', - 'decimal' => 0, - ), - 'BRL' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => 'R$', - 'decimal' => 2, - ), - 'EUR' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => '€', - 'decimal' => 2, - ), - 'NZD' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => 'NZ$', - 'decimal' => 2, - ), - 'AUD' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => 'A$', - 'decimal' => 2, - ), - 'CAD' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => 'C$', - 'decimal' => 2, - ), - 'ILS' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => '₪', - 'decimal' => 2, - ), - 'RUB' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => '₽', - 'decimal' => 2, - ), - 'MXN' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => 'MX$', - 'decimal' => 2, - ), - 'MYR' => array( - 'format' => '%2$s%1$s', // 1: Symbol 2: currency value - 'symbol' => 'RM', - 'decimal' => 2, - ), - 'SEK' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => 'Skr', - 'decimal' => 2, - ), - 'HUF' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => 'Ft', - 'decimal' => 0, // Decimals are supported by Stripe but not by PayPal. - ), - 'CHF' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => 'CHF', - 'decimal' => 2, - ), - 'CZK' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => 'Kč', - 'decimal' => 2, - ), - 'DKK' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => 'Dkr', - 'decimal' => 2, - ), - 'HKD' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => 'HK$', - 'decimal' => 2, - ), - 'NOK' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => 'Kr', - 'decimal' => 2, - ), - 'PHP' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => '₱', - 'decimal' => 2, - ), - 'PLN' => array( - 'format' => '%2$s %1$s', // 1: Symbol 2: currency value - 'symbol' => 'PLN', - 'decimal' => 2, - ), - 'SGD' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => 'S$', - 'decimal' => 2, - ), - 'TWD' => array( - 'format' => '%1$s%2$s', // 1: Symbol 2: currency value - 'symbol' => 'NT$', - 'decimal' => 0, // Decimals are supported by Stripe but not by PayPal. - ), - 'THB' => array( - 'format' => '%2$s%1$s', // 1: Symbol 2: currency value - 'symbol' => '฿', - 'decimal' => 2, - ), - ); + require_once JETPACK__PLUGIN_DIR . 'class.jetpack-currencies.php'; + $currencies = Jetpack_Currencies::CURRENCIES; if ( isset( $currencies[ $the_currency ] ) ) { return $currencies[ $the_currency ]; diff --git a/tests/php/_inc/lib/test-class-jetpack-currencies.php b/tests/php/_inc/lib/test-class-jetpack-currencies.php new file mode 100644 index 000000000000..3f7982530b1c --- /dev/null +++ b/tests/php/_inc/lib/test-class-jetpack-currencies.php @@ -0,0 +1,59 @@ +number_format; + $wp_locale->number_format = array( + 'thousands_sep' => '-', + 'decimal_point' => '|', + ); + $formatted_price = Jetpack_Currencies::format_price( '12345.67890', 'USD' ); + $this->assertEquals( '$12,345.68', $formatted_price ); + $wp_locale->number_format = $previous_number_format; + } + + /** + * Test that non-USD currencies are formatted according to the user locale. + */ + public function test_format_price_non_usd() { + global $wp_locale; + $previous_number_format = $wp_locale->number_format; + $wp_locale->number_format = array( + 'thousands_sep' => '-', + 'decimal_point' => '|', + ); + $formatted_price = Jetpack_Currencies::format_price( '12345.67890', 'EUR' ); + $this->assertEquals( '€12-345|68', $formatted_price ); + $wp_locale->number_format = $previous_number_format; + } + + /** + * Test that no currency symbol is displayed when specified. + */ + public function test_format_price_no_currency_symbol() { + $formatted_price = Jetpack_Currencies::format_price( '12345.67890', 'USD', false ); + $this->assertEquals( '12,345.68', $formatted_price ); + } + + /** + * Test that the unspecified currency symbol is displayed when the currency is not found. + */ + public function test_format_price_unspecified_currency_symbol() { + $formatted_price = Jetpack_Currencies::format_price( '12345.67890', 'TEST', false ); + $this->assertEquals( '¤12,345.68', $formatted_price ); + } +}