Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: enhancement

Donations Block: Add Security inspector panel with configurable minimum and maximum donation amounts to help prevent fraudulent transactions.
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@
"type": "string",
"enum": [ "", "left", "center", "right" ],
"default": ""
},
"minimumAmount": {
"type": "number"
},
"maximumAmount": {
"type": "number"
}
},
"example": {}
Expand Down
57 changes: 55 additions & 2 deletions projects/plugins/jetpack/extensions/blocks/donations/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
ToolbarButton,
} from '@wordpress/components';
import { useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { DOWN } from '@wordpress/keycodes';
import {
getDefaultDonationAmountsForCurrency,
Expand All @@ -45,9 +45,12 @@ const Controls = props => {
contentAlignment,
defaultInterval,
customAmountPlaceholder,
minimumAmount,
maximumAmount,
} = attributes;

const computedCustomAmountPlaceholder = minimumTransactionAmountForCurrency( currency ) * 100;
const stripeMin = minimumTransactionAmountForCurrency( currency );
const computedCustomAmountPlaceholder = stripeMin * 100;
const effectiveCustomAmountPlaceholder =
customAmountPlaceholder ?? computedCustomAmountPlaceholder;

Expand Down Expand Up @@ -130,6 +133,21 @@ const Controls = props => {
[ setAttributes ]
);

let maximumHelp;
if (
minimumAmount !== undefined &&
maximumAmount !== undefined &&
maximumAmount < minimumAmount
) {
maximumHelp = __( 'Maximum must be greater than the minimum amount.', 'jetpack' );
} else if ( maximumAmount !== undefined && maximumAmount < stripeMin ) {
maximumHelp = sprintf(
/* translators: %s: minimum donation amount formatted with currency symbol */
__( 'Maximum must be at least %s, the minimum amount Stripe can process.', 'jetpack' ),
formatCurrency( stripeMin, currency )
);
}

const changeDefaultDonationAmounts = ccy => {
const defaultAmounts = getDefaultDonationAmountsForCurrency( ccy );

Expand Down Expand Up @@ -331,6 +349,41 @@ const Controls = props => {
</ExternalLink>
</p>
</PanelBody>
<PanelBody title={ __( 'Security', 'jetpack' ) } initialOpen={ false }>
<p style={ { marginTop: 0, marginBottom: 16, fontSize: 13 } }>
{ __(
'Setting minimum and maximum donation amounts can help prevent fraudulent transactions.',
'jetpack'
) }
</p>
<TextControl
type="number"
label={ __( 'Minimum amount', 'jetpack' ) }
value={ minimumAmount ?? '' }
onChange={ value =>
setAttributes( {
minimumAmount: value === '' ? undefined : Number( value ),
} )
}
min={ stripeMin }
step={ 0.01 }
__nextHasNoMarginBottom={ true }
/>
<TextControl
type="number"
label={ __( 'Maximum amount', 'jetpack' ) }
value={ maximumAmount ?? '' }
onChange={ value =>
setAttributes( {
maximumAmount: value === '' ? undefined : Number( value ),
} )
}
min={ minimumAmount ?? stripeMin }
step={ 0.01 }
help={ maximumHelp }
__nextHasNoMarginBottom={ true }
/>
</PanelBody>
</InspectorControls>
</>
);
Expand Down
46 changes: 44 additions & 2 deletions projects/plugins/jetpack/extensions/blocks/donations/donations.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,9 @@
if ( $default_interval ) {
$wrapper_attr_array['data-default-interval'] = $default_interval;
}
$wrapper_attrs = get_block_wrapper_attributes( $wrapper_attr_array );
$custom_styles = build_custom_styles( $attr, '.' . $instance_id );
$wrapper_attr_array = array_merge( $wrapper_attr_array, build_security_data_attrs( $attr, $currency ) );
$wrapper_attrs = get_block_wrapper_attributes( $wrapper_attr_array );
$custom_styles = build_custom_styles( $attr, '.' . $instance_id );

$choose_amount_html = wp_kses_post( $choose_amount_text );
$choose_amount_block = '' !== trim( $choose_amount_html ) ? '<p>' . $choose_amount_html . '</p>' : '';
Expand All @@ -271,6 +272,7 @@
%4$s
%5$s
%6$s
<div class="donations__range-error"></div>
<hr class="donations__separator">
%7$s
%8$s
Expand All @@ -292,6 +294,46 @@
);
}

/**
* Build data-attributes array for security (min/max amount) constraints.
*
* Extracted so it can be tested independently of the full render pipeline.
*
* @since $$next-version$$
*
* @param array $attr Block attributes.
* @param string $currency Currency code (e.g. 'USD').
* @return array Associative array of data-attribute name => value.
*/
function build_security_data_attrs( $attr, $currency ) {
$attrs = array();
$min_amount = isset( $attr['minimumAmount'] ) ? (float) $attr['minimumAmount'] : null;
$max_amount = isset( $attr['maximumAmount'] ) ? (float) $attr['maximumAmount'] : null;
if ( null !== $min_amount ) {
$attrs['data-min-amount'] = $min_amount;
$attrs['data-min-error'] = sprintf(
/* translators: %s: minimum donation amount formatted with currency symbol */
__( 'The minimum donation amount is %s.', 'jetpack' ),
\Jetpack_Currencies::format_price( $min_amount, $currency )

Check failure on line 317 in projects/plugins/jetpack/extensions/blocks/donations/donations.php

View workflow job for this annotation

GitHub Actions / Static analysis

TypeError PhanTypeMismatchArgument Argument 1 ($price) is $min_amount of type float but \Jetpack_Currencies::format_price() takes string defined at _inc/lib/class-jetpack-currencies.php:156 FAQ on Phan issues: pdWQjU-Jb-p2
);
}
if ( null !== $max_amount ) {
$attrs['data-max-amount'] = $max_amount;
$attrs['data-max-error'] = sprintf(
/* translators: %s: maximum donation amount formatted with currency symbol */
__( 'The maximum donation amount is %s.', 'jetpack' ),
\Jetpack_Currencies::format_price( $max_amount, $currency )

Check failure on line 325 in projects/plugins/jetpack/extensions/blocks/donations/donations.php

View workflow job for this annotation

GitHub Actions / Static analysis

TypeError PhanTypeMismatchArgument Argument 1 ($price) is $max_amount of type float but \Jetpack_Currencies::format_price() takes string defined at _inc/lib/class-jetpack-currencies.php:156 FAQ on Phan issues: pdWQjU-Jb-p2
);
}
$stripe_min = \Jetpack_Memberships::SUPPORTED_CURRENCIES[ $currency ] ?? 1;
$attrs['data-stripe-min-error'] = sprintf(
/* translators: %s: payment processor minimum donation amount formatted with currency symbol */
_x( 'The minimum donation amount is %s.', 'payment processor minimum', 'jetpack' ),
\Jetpack_Currencies::format_price( $stripe_min, $currency )

Check failure on line 332 in projects/plugins/jetpack/extensions/blocks/donations/donations.php

View workflow job for this annotation

GitHub Actions / Static analysis

TypeError PhanTypeMismatchArgument Argument 1 ($price) is $stripe_min of type 0|0.3|0.5|1|2.0|2.5|3.0|4.0|10|10.0|15.0|50|175.0 but \Jetpack_Currencies::format_price() takes string defined at _inc/lib/class-jetpack-currencies.php:156 FAQ on Phan issues: pdWQjU-Jb-p2
);
return $attrs;
}

/**
* Build a CSS string scoping per-state and tab-level style rules to a single
* block instance.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { checkAmountRange, firstShownInterval } from '../utils';

describe( 'firstShownInterval', () => {
test( 'returns one-time when it is shown', () => {
expect( firstShownInterval( true, true, true ) ).toBe( 'one-time' );
expect( firstShownInterval( true, false, false ) ).toBe( 'one-time' );
} );

test( 'skips one-time and returns monthly when one-time is hidden', () => {
expect( firstShownInterval( false, true, true ) ).toBe( '1 month' );
expect( firstShownInterval( false, true, false ) ).toBe( '1 month' );
} );

test( 'returns annual when only annual is shown', () => {
expect( firstShownInterval( false, false, true ) ).toBe( '1 year' );
} );

test( 'returns null when all are hidden', () => {
expect( firstShownInterval( false, false, false ) ).toBeNull();
} );
} );

describe( 'checkAmountRange', () => {
const MIN_ERROR = 'Minimum is $10.';
const MAX_ERROR = 'Maximum is $100.';

test( 'returns null when no limits are set', () => {
expect( checkAmountRange( 50, null, null, MIN_ERROR, MAX_ERROR ) ).toBeNull();
} );

test( 'returns null when amount is within range', () => {
expect( checkAmountRange( 50, 10, 100, MIN_ERROR, MAX_ERROR ) ).toBeNull();
} );

test( 'returns null when amount equals the minimum', () => {
expect( checkAmountRange( 10, 10, 100, MIN_ERROR, MAX_ERROR ) ).toBeNull();
} );

test( 'returns null when amount equals the maximum', () => {
expect( checkAmountRange( 100, 10, 100, MIN_ERROR, MAX_ERROR ) ).toBeNull();
} );

test( 'returns minError when amount is below minimum', () => {
expect( checkAmountRange( 5, 10, 100, MIN_ERROR, MAX_ERROR ) ).toBe( MIN_ERROR );
} );

test( 'returns maxError when amount is above maximum', () => {
expect( checkAmountRange( 150, 10, 100, MIN_ERROR, MAX_ERROR ) ).toBe( MAX_ERROR );
} );

test( 'returns minError with only a minimum set', () => {
expect( checkAmountRange( 1, 10, null, MIN_ERROR, MAX_ERROR ) ).toBe( MIN_ERROR );
expect( checkAmountRange( 10, 10, null, MIN_ERROR, MAX_ERROR ) ).toBeNull();
} );

test( 'returns maxError with only a maximum set', () => {
expect( checkAmountRange( 200, null, 100, MIN_ERROR, MAX_ERROR ) ).toBe( MAX_ERROR );
expect( checkAmountRange( 100, null, 100, MIN_ERROR, MAX_ERROR ) ).toBeNull();
} );
} );
20 changes: 20 additions & 0 deletions projects/plugins/jetpack/extensions/blocks/donations/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@ export function firstShownInterval( oneTimeShown, monthlyShown, annualShown ) {
return null;
}

/**
* Returns an error message if amount falls outside the configured min/max range, or null if valid.
*
* @param {number} amount - The donation amount to check.
* @param {number|null} minAmount - Admin-configured minimum (null = no limit).
* @param {number|null} maxAmount - Admin-configured maximum (null = no limit).
* @param {string} minError - Pre-translated error string for below-minimum.
* @param {string} maxError - Pre-translated error string for above-maximum.
* @return {string|null} Error message, or null if amount is within range.
*/
export function checkAmountRange( amount, minAmount, maxAmount, minError, maxError ) {
if ( minAmount !== null && amount < minAmount ) {
return minError;
}
if ( maxAmount !== null && amount > maxAmount ) {
return maxError;
}
return null;
}

/**
* Return the default texts defined in `donations.php` and injected client side by assigning them
* to the `Jetpack_DonationsBlock` attribute of the window object.
Expand Down
Loading
Loading