mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 19:02:29 +03:00
🐛 Fixed missing validation of offer amounts in the admin panel (#16022)
closes https://github.com/TryGhost/Team/issues/2380 - improved offer validation for `amount` field to cover all type/amount cases - added validate-on-blur to the amount field to match our standard validation behaviour - added re-validation of the amount field when the type is changed and the amount gets reset - removed the internal parsing of a decimal trial days entry to an integer so the field value matches what is set internally and we let the user know that partial trial days are not supported Non-user-facing refactors: - renamed `_saveOfferProperty` to `_updateOfferProperty` to better reflect what it does - fixed missing indentation for conditional blocks in the offer template
This commit is contained in:
parent
235446b034
commit
581f0b34b4
@ -1101,3 +1101,5 @@ remove|ember-template-lint|no-passed-in-event-handlers|21|12|21|12|4069dec45ac2a
|
|||||||
remove|ember-template-lint|no-passed-in-event-handlers|22|12|22|12|e53f64794fdd0fe8c8b027d1831942d7c78c503b|1662681600000|1673053200000|1678237200000|app/components/gh-benefit-item.hbs
|
remove|ember-template-lint|no-passed-in-event-handlers|22|12|22|12|e53f64794fdd0fe8c8b027d1831942d7c78c503b|1662681600000|1673053200000|1678237200000|app/components/gh-benefit-item.hbs
|
||||||
add|ember-template-lint|no-passed-in-event-handlers|23|16|23|16|d47dcf0c8eea7584639b48d5a99b7db07436c259|1670976000000|1681340400000|1686524400000|app/components/gh-benefit-item.hbs
|
add|ember-template-lint|no-passed-in-event-handlers|23|16|23|16|d47dcf0c8eea7584639b48d5a99b7db07436c259|1670976000000|1681340400000|1686524400000|app/components/gh-benefit-item.hbs
|
||||||
add|ember-template-lint|no-passed-in-event-handlers|24|16|24|16|14a806b3f993ec777b1a5ff7e00887e5840bbb77|1670976000000|1681340400000|1686524400000|app/components/gh-benefit-item.hbs
|
add|ember-template-lint|no-passed-in-event-handlers|24|16|24|16|14a806b3f993ec777b1a5ff7e00887e5840bbb77|1670976000000|1681340400000|1686524400000|app/components/gh-benefit-item.hbs
|
||||||
|
remove|ember-template-lint|no-passed-in-event-handlers|150|56|150|56|37bf29e93ffc35c71cdddd0ab98edeb60097e826|1662681600000|1673053200000|1678237200000|app/templates/offer.hbs
|
||||||
|
remove|ember-template-lint|no-passed-in-event-handlers|161|56|161|56|37bf29e93ffc35c71cdddd0ab98edeb60097e826|1662681600000|1673053200000|1678237200000|app/templates/offer.hbs
|
||||||
|
@ -263,19 +263,34 @@ export default class OffersController extends Controller {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
setProperty(propKey, value) {
|
setProperty(propKey, value) {
|
||||||
this._saveOfferProperty(propKey, value);
|
this._updateOfferProperty(propKey, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
validateProperty(property) {
|
||||||
|
this.offer.validate({property});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
clearPropertyValidations(property) {
|
||||||
|
this.offer.errors.remove(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setDiscountType(discountType) {
|
setDiscountType(discountType) {
|
||||||
if (!this.isDiscountSectionDisabled) {
|
if (!this.isDiscountSectionDisabled) {
|
||||||
const amount = this.offer.amount || 0;
|
const amount = this.offer.amount || 0;
|
||||||
this._saveOfferProperty('type', discountType);
|
|
||||||
|
this._updateOfferProperty('type', discountType);
|
||||||
|
|
||||||
if (this.offer.type === 'fixed' && this.offer.amount !== '') {
|
if (this.offer.type === 'fixed' && this.offer.amount !== '') {
|
||||||
this.offer.amount = amount * 100;
|
this.offer.amount = amount * 100;
|
||||||
} else if (this.offer.amount !== '') {
|
} else if (this.offer.amount !== '') {
|
||||||
this.offer.amount = amount / 100;
|
this.offer.amount = amount / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.validateProperty('amount');
|
||||||
|
|
||||||
this.updatePortalPreview({forceRefresh: false});
|
this.updatePortalPreview({forceRefresh: false});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -283,53 +298,52 @@ export default class OffersController extends Controller {
|
|||||||
@action
|
@action
|
||||||
setDiscountAmount(e) {
|
setDiscountAmount(e) {
|
||||||
let amount = e.target.value;
|
let amount = e.target.value;
|
||||||
|
|
||||||
if (this.offer.type === 'fixed' && amount !== '') {
|
if (this.offer.type === 'fixed' && amount !== '') {
|
||||||
amount = parseFloat(amount) * 100;
|
amount = parseFloat(amount) * 100;
|
||||||
}
|
}
|
||||||
this._saveOfferProperty('amount', amount);
|
|
||||||
|
this._updateOfferProperty('amount', amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setTrialDuration(e) {
|
setTrialDuration(e) {
|
||||||
let amount = e.target.value;
|
let amount = e.target.value;
|
||||||
if (amount !== '') {
|
this._updateOfferProperty('amount', amount);
|
||||||
amount = parseInt(amount);
|
|
||||||
}
|
|
||||||
this._saveOfferProperty('amount', amount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setOfferName(e) {
|
setOfferName(e) {
|
||||||
this._saveOfferProperty('name', e.target.value);
|
this._updateOfferProperty('name', e.target.value);
|
||||||
if (!this.isDisplayTitleEdited && this.offer.isNew) {
|
if (!this.isDisplayTitleEdited && this.offer.isNew) {
|
||||||
this._saveOfferProperty('displayTitle', e.target.value);
|
this._updateOfferProperty('displayTitle', e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isOfferCodeEdited && this.offer.isNew) {
|
if (!this.isOfferCodeEdited && this.offer.isNew) {
|
||||||
this._saveOfferProperty('code', slugify(e.target.value));
|
this._updateOfferProperty('code', slugify(e.target.value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setPortalTitle(e) {
|
setPortalTitle(e) {
|
||||||
this.isDisplayTitleEdited = true;
|
this.isDisplayTitleEdited = true;
|
||||||
this._saveOfferProperty('displayTitle', e.target.value);
|
this._updateOfferProperty('displayTitle', e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setPortalDescription(e) {
|
setPortalDescription(e) {
|
||||||
this._saveOfferProperty('displayDescription', e.target.value);
|
this._updateOfferProperty('displayDescription', e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setOfferCode(e) {
|
setOfferCode(e) {
|
||||||
this.isOfferCodeEdited = true;
|
this.isOfferCodeEdited = true;
|
||||||
this._saveOfferProperty('code', e.target.value);
|
this._updateOfferProperty('code', e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setDurationInMonths(e) {
|
setDurationInMonths(e) {
|
||||||
this._saveOfferProperty('durationInMonths', e.target.value);
|
this._updateOfferProperty('durationInMonths', e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -403,7 +417,7 @@ export default class OffersController extends Controller {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
if (this.offer.duration === 'repeating') {
|
if (this.offer.duration === 'repeating') {
|
||||||
this._saveOfferProperty('duration', 'once');
|
this._updateOfferProperty('duration', 'once');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -412,13 +426,17 @@ export default class OffersController extends Controller {
|
|||||||
@action
|
@action
|
||||||
changeType(type) {
|
changeType(type) {
|
||||||
if (type === 'trial') {
|
if (type === 'trial') {
|
||||||
this._saveOfferProperty('type', 'trial');
|
this._updateOfferProperty('type', 'trial');
|
||||||
this._saveOfferProperty('amount', 7);
|
this._updateOfferProperty('amount', 7);
|
||||||
this._saveOfferProperty('duration', 'trial');
|
this._updateOfferProperty('duration', 'trial');
|
||||||
|
|
||||||
|
this.validateProperty('amount');
|
||||||
} else {
|
} else {
|
||||||
this._saveOfferProperty('type', 'percent');
|
this._updateOfferProperty('type', 'percent');
|
||||||
this._saveOfferProperty('amount', 0);
|
this._updateOfferProperty('amount', 0);
|
||||||
this._saveOfferProperty('duration', 'once');
|
this._updateOfferProperty('duration', 'once');
|
||||||
|
|
||||||
|
this.clearPropertyValidations('amount');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -449,12 +467,12 @@ export default class OffersController extends Controller {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
updateDuration(duration) {
|
updateDuration(duration) {
|
||||||
this._saveOfferProperty('duration', duration);
|
this._updateOfferProperty('duration', duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private -----------------------------------------------------------------
|
// Private -----------------------------------------------------------------
|
||||||
|
|
||||||
_saveOfferProperty(propKey, newValue) {
|
_updateOfferProperty(propKey, newValue) {
|
||||||
let currentValue = this.offer[propKey];
|
let currentValue = this.offer[propKey];
|
||||||
|
|
||||||
// avoid modifying empty values and triggering inadvertant unsaved changes modals
|
// avoid modifying empty values and triggering inadvertant unsaved changes modals
|
||||||
|
@ -74,6 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GhFormGroup>
|
</GhFormGroup>
|
||||||
|
|
||||||
{{#if this.isTrialOffer}}
|
{{#if this.isTrialOffer}}
|
||||||
<div class="gh-offer-tier-and-trial">
|
<div class="gh-offer-tier-and-trial">
|
||||||
<GhFormGroup @errors={{this.errors}} @property="product-cadence">
|
<GhFormGroup @errors={{this.errors}} @property="product-cadence">
|
||||||
@ -97,15 +98,15 @@
|
|||||||
<GhFormGroup @errors={{this.offer.errors}} @property="trialDuration">
|
<GhFormGroup @errors={{this.offer.errors}} @property="trialDuration">
|
||||||
<label for="trial-duration" class="fw6">Trial duration</label>
|
<label for="trial-duration" class="fw6">Trial duration</label>
|
||||||
<div class="trial-duration">
|
<div class="trial-duration">
|
||||||
<GhTextInput
|
<input
|
||||||
@name="trial-duration"
|
type="number"
|
||||||
@type="number"
|
id="trial-duration"
|
||||||
@placeholder=""
|
class="gh-input"
|
||||||
@disabled={{this.isDiscountSectionDisabled}}
|
name="trial-duration"
|
||||||
@value={{readonly this.offer.amount}}
|
value={{this.offer.amount}}
|
||||||
|
disabled={{this.isDiscountSectionDisabled}}
|
||||||
{{on "input" this.setTrialDuration}}
|
{{on "input" this.setTrialDuration}}
|
||||||
@id="trial-duration"
|
{{on "blur" (fn this.validateProperty "amount")}}
|
||||||
@class="gh-input"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="error">
|
<span class="error">
|
||||||
@ -139,29 +140,16 @@
|
|||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<GhFormGroup @errors={{this.offer.errors}} @property="amount" @hasValidated={{this.offer.hasValidated}}>
|
<GhFormGroup @errors={{this.offer.errors}} @property="amount" @hasValidated={{this.offer.hasValidated}}>
|
||||||
<div class="gh-offer-value percentage">
|
<div class="gh-offer-value percentage">
|
||||||
{{#if (eq this.offer.type 'fixed')}}
|
<input
|
||||||
<GhTextInput
|
type="number"
|
||||||
@name="amount"
|
id="amount"
|
||||||
@type="number"
|
class="gh-input"
|
||||||
@placeholder=""
|
name="amount"
|
||||||
@disabled={{this.isDiscountSectionDisabled}}
|
value={{if (eq this.offer.type 'fixed') (gh-price-amount this.offer.amount) this.offer.amount}}
|
||||||
@value={{readonly (gh-price-amount this.offer.amount)}}
|
disabled={{this.isDiscountSectionDisabled}}
|
||||||
@input={{this.setDiscountAmount}}
|
{{on "input" this.setDiscountAmount}}
|
||||||
@id="amount"
|
{{on "blur" (fn this.validateProperty "amount")}}
|
||||||
@class="gh-input"
|
|
||||||
/>
|
/>
|
||||||
{{else}}
|
|
||||||
<GhTextInput
|
|
||||||
@name="amount"
|
|
||||||
@type="number"
|
|
||||||
@placeholder=""
|
|
||||||
@disabled={{this.isDiscountSectionDisabled}}
|
|
||||||
@value={{readonly this.offer.amount}}
|
|
||||||
@input={{this.setDiscountAmount}}
|
|
||||||
@id="amount"
|
|
||||||
@class="gh-input"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
<span class="error">
|
<span class="error">
|
||||||
<GhErrorMessage @errors={{this.offer.errors}} @property="amount" />
|
<GhErrorMessage @errors={{this.offer.errors}} @property="amount" />
|
||||||
|
@ -22,10 +22,39 @@ export default BaseValidator.create({
|
|||||||
} else {
|
} else {
|
||||||
model.errors.add('amount', 'Please enter the amount.');
|
model.errors.add('amount', 'Please enter the amount.');
|
||||||
}
|
}
|
||||||
this.invalidate();
|
|
||||||
} else if (model.type === 'trial' && model.amount < 0) {
|
return this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.type === 'trial') {
|
||||||
|
if (model.amount < 1) {
|
||||||
model.errors.add('amount', 'Free trial must be at least 1 day.');
|
model.errors.add('amount', 'Free trial must be at least 1 day.');
|
||||||
this.invalidate();
|
return this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!model.amount.toString().match(/^\d+$/)) {
|
||||||
|
model.errors.add('amount', 'Trial days must be a whole number.');
|
||||||
|
return this.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.type === 'percent') {
|
||||||
|
if (model.amount < 0 || model.amount > 100) {
|
||||||
|
model.errors.add('amount', 'Amount must be between 0 and 100%.');
|
||||||
|
return this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!model.amount.toString().match(/^\d+$/)) {
|
||||||
|
model.errors.add('amount', 'Amount must be a whole number.');
|
||||||
|
return this.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.type === 'fixed') {
|
||||||
|
if (model.amount < 0) {
|
||||||
|
model.errors.add('amount', 'Amount must be greater than 0.');
|
||||||
|
return this.invalidate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user