Added logic for currency and suggested amount for Tips & Donations (#17599)
closes https://github.com/TryGhost/Product/issues/3666 - added computed setting "donations_enabled" - added logic to persist "donations_suggested_amount" and "donations_currency" - used "donations_suggested_amount" and "donations_currency" when initiating a new Stripe Checkout for donations - added copy functionality to "your link" in Tips & Donations settings
This commit is contained in:
parent
299cdb4387
commit
81c3555106
@ -20,10 +20,12 @@
|
||||
<div class="percentage">
|
||||
<input
|
||||
type="number"
|
||||
id="tips-and-donations-amount"
|
||||
id="gh-tips-and-donations-amount"
|
||||
class="gh-input"
|
||||
name="amount"
|
||||
value="0"
|
||||
min="0"
|
||||
value={{this.selectedAmount}}
|
||||
{{on "input" this.setDonationsSuggestedAmount}}
|
||||
/>
|
||||
</div>
|
||||
</GhFormGroup>
|
||||
@ -31,15 +33,14 @@
|
||||
<GhFormGroup class="no-margin">
|
||||
<span class="gh-select">
|
||||
<OneWaySelect
|
||||
@value={{this.currency}}
|
||||
@options={{this.allCurrencies}}
|
||||
id="currency"
|
||||
@value={{this.selectedCurrency}}
|
||||
id="gh-tips-and-donations-currency"
|
||||
name="currency"
|
||||
@options={{readonly this.allCurrencies}}
|
||||
@optionValuePath="value"
|
||||
@optionLabelPath="label"
|
||||
>
|
||||
<option value="">USD</option>
|
||||
</OneWaySelect>
|
||||
@update={{this.setDonationsCurrency}}
|
||||
/>
|
||||
{{svg-jar "arrow-down-small"}}
|
||||
</span>
|
||||
</GhFormGroup>
|
||||
@ -52,11 +53,11 @@
|
||||
<div class="gh-input-group">
|
||||
<GhTextInput
|
||||
data-test-input="tips-and-donations-link"
|
||||
@id="tips-and-donations-link"
|
||||
@id="gh-tips-and-donations-link"
|
||||
@name="tips-and-donations-link"
|
||||
@disabled={{true}}
|
||||
@value="https://publication.com/portal/support"
|
||||
@placeholder="https://publication.com/portal/support"
|
||||
@value="{{this.siteUrl}}/#/portal/support"
|
||||
@placeholder="{{this.siteUrl}}/#/portal/support"
|
||||
/>
|
||||
<GhTaskButton
|
||||
data-test-button="tips-and-donations-copy-link"
|
||||
@ -72,4 +73,4 @@
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,17 +1,61 @@
|
||||
import Component from '@glimmer/component';
|
||||
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
|
||||
import {action} from '@ember/object';
|
||||
import {currencies} from 'ghost-admin/utils/currency';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task, timeout} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const CURRENCIES = currencies.map((currency) => {
|
||||
return {
|
||||
value: currency.isoCode,
|
||||
label: `${currency.isoCode}`
|
||||
};
|
||||
});
|
||||
|
||||
export default class TipsAndDonations extends Component {
|
||||
@service settings;
|
||||
|
||||
@tracked currency = 'USD';
|
||||
@tracked allCurrencies = ['USD', 'RSD'];
|
||||
@inject config;
|
||||
|
||||
get allCurrencies() {
|
||||
return CURRENCIES;
|
||||
}
|
||||
|
||||
get selectedAmount() {
|
||||
return this.settings.donationsSuggestedAmount && this.settings.donationsSuggestedAmount / 100;
|
||||
}
|
||||
|
||||
get selectedCurrency() {
|
||||
return CURRENCIES.findBy('value', this.settings.donationsCurrency);
|
||||
}
|
||||
|
||||
get siteUrl() {
|
||||
return this.config.blogUrl;
|
||||
}
|
||||
|
||||
@task
|
||||
*copyTipsAndDonationsLink() {
|
||||
yield timeout(10);
|
||||
const link = document.getElementById('gh-tips-and-donations-link')?.value;
|
||||
|
||||
if (link) {
|
||||
copyTextToClipboard(link);
|
||||
yield timeout(this.isTesting ? 50 : 500);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@action
|
||||
setDonationsCurrency(event) {
|
||||
this.settings.donationsCurrency = event.value;
|
||||
}
|
||||
|
||||
@action
|
||||
setDonationsSuggestedAmount(event) {
|
||||
const amount = Math.abs(event.target.value);
|
||||
const amountInCents = Math.round(amount * 100);
|
||||
|
||||
this.settings.donationsSuggestedAmount = amountInCents;
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,13 @@ export default Model.extend(ValidationEngine, {
|
||||
pinturaJsUrl: attr('string'),
|
||||
pinturaCssUrl: attr('string'),
|
||||
|
||||
/**
|
||||
* Donations
|
||||
*/
|
||||
donationsEnabled: attr('boolean'),
|
||||
donationsCurrency: attr('string'),
|
||||
donationsSuggestedAmount: attr('number'),
|
||||
|
||||
// HACK - not a real model attribute but a workaround for Ember Data not
|
||||
// exposing meta from save responses
|
||||
_meta: attr()
|
||||
|
@ -55,7 +55,7 @@ export default class SettingsService extends Service.extend(ValidationEngine) {
|
||||
_loadSettings() {
|
||||
if (!this._loadingPromise) {
|
||||
this._loadingPromise = this.store
|
||||
.queryRecord('setting', {group: 'site,theme,private,members,portal,newsletter,email,amp,labs,slack,unsplash,views,firstpromoter,editor,comments,analytics,announcement,pintura'})
|
||||
.queryRecord('setting', {group: 'site,theme,private,members,portal,newsletter,email,amp,labs,slack,unsplash,views,firstpromoter,editor,comments,analytics,announcement,pintura,donations'})
|
||||
.then((settings) => {
|
||||
this._loadingPromise = null;
|
||||
return settings;
|
||||
|
@ -125,5 +125,9 @@ export default [
|
||||
|
||||
// EDITOR
|
||||
setting('editor', 'editor_default_email_recipients', 'visibility'),
|
||||
setting('editor', 'editor_default_email_recipients_filter', 'all')
|
||||
setting('editor', 'editor_default_email_recipients_filter', 'all'),
|
||||
|
||||
// DONATIONS
|
||||
setting('donations_suggested_amount', 'donations', 0),
|
||||
setting('donations_currency', 'donations', 'USD')
|
||||
];
|
||||
|
@ -68,7 +68,9 @@ const EDITABLE_SETTINGS = [
|
||||
'announcement_visibility',
|
||||
'pintura',
|
||||
'pintura_js_url',
|
||||
'pintura_css_url'
|
||||
'pintura_css_url',
|
||||
'donations_currency',
|
||||
'donations_suggested_amount'
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
@ -98,6 +98,10 @@ class SettingsHelpers {
|
||||
getNoReplyAddress() {
|
||||
return `noreply@${this.getDefaultEmailDomain()}`;
|
||||
}
|
||||
|
||||
areDonationsEnabled() {
|
||||
return this.isStripeConnected();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SettingsHelpers;
|
||||
|
@ -89,6 +89,7 @@ module.exports = {
|
||||
fields.push(new CalculatedField({key: 'members_invite_only', type: 'boolean', group: 'members', fn: settingsHelpers.isMembersInviteOnly.bind(settingsHelpers), dependents: ['members_signup_access']}));
|
||||
fields.push(new CalculatedField({key: 'paid_members_enabled', type: 'boolean', group: 'members', fn: settingsHelpers.arePaidMembersEnabled.bind(settingsHelpers), dependents: ['members_signup_access', 'stripe_secret_key', 'stripe_publishable_key', 'stripe_connect_secret_key', 'stripe_connect_publishable_key']}));
|
||||
fields.push(new CalculatedField({key: 'firstpromoter_account', type: 'string', group: 'firstpromoter', fn: settingsHelpers.getFirstpromoterId.bind(settingsHelpers), dependents: ['firstpromoter', 'firstpromoter_id']}));
|
||||
fields.push(new CalculatedField({key: 'donations_enabled', type: 'boolean', group: 'donations', fn: settingsHelpers.areDonationsEnabled.bind(settingsHelpers), dependents: ['stripe_secret_key', 'stripe_publishable_key', 'stripe_connect_secret_key', 'stripe_connect_publishable_key']}));
|
||||
|
||||
return fields;
|
||||
},
|
||||
|
@ -328,6 +328,10 @@ Object {
|
||||
"key": "firstpromoter_account",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"key": "donations_enabled",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -726,6 +730,10 @@ Object {
|
||||
"key": "firstpromoter_account",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"key": "donations_enabled",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -734,7 +742,7 @@ exports[`Settings API Edit Can edit a setting 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "4104",
|
||||
"content-length": "4145",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
@ -1072,6 +1080,10 @@ Object {
|
||||
"key": "firstpromoter_account",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"key": "donations_enabled",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -1417,6 +1429,10 @@ Object {
|
||||
"key": "firstpromoter_account",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"key": "donations_enabled",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -1767,6 +1783,10 @@ Object {
|
||||
"key": "firstpromoter_account",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"key": "donations_enabled",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -2205,6 +2225,10 @@ Object {
|
||||
"key": "firstpromoter_account",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"key": "donations_enabled",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -2615,6 +2639,10 @@ Object {
|
||||
"key": "firstpromoter_account",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"key": "donations_enabled",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
@ -8,7 +8,7 @@ const {stringMatching, anyEtag, anyUuid, anyContentLength, anyContentVersion} =
|
||||
const models = require('../../../core/server/models');
|
||||
const {anyErrorId} = matchers;
|
||||
|
||||
const CURRENT_SETTINGS_COUNT = 81;
|
||||
const CURRENT_SETTINGS_COUNT = 82;
|
||||
|
||||
const settingsMatcher = {};
|
||||
|
||||
|
@ -279,13 +279,18 @@ class PaymentsService {
|
||||
* @returns {Promise<{id: string}>}
|
||||
*/
|
||||
async getPriceForDonations() {
|
||||
const currency = 'usd'; // TODO: we need to use a setting here!
|
||||
const nickname = 'Support ' + this.settingsCache.get('title');
|
||||
const currency = this.settingsCache.get('donations_currency');
|
||||
const suggestedAmount = this.settingsCache.get('donations_suggested_amount');
|
||||
|
||||
// Stripe requires a minimum of 50 cents
|
||||
const amount = suggestedAmount && suggestedAmount > 50 ? suggestedAmount : 0;
|
||||
|
||||
const price = await this.StripePriceModel
|
||||
.where({
|
||||
type: 'donation',
|
||||
active: true,
|
||||
amount,
|
||||
currency
|
||||
})
|
||||
.query()
|
||||
@ -319,7 +324,8 @@ class PaymentsService {
|
||||
|
||||
const newPrice = await this.createPriceForDonations({
|
||||
nickname,
|
||||
currency
|
||||
currency,
|
||||
amount
|
||||
});
|
||||
return {
|
||||
id: newPrice.id
|
||||
@ -329,7 +335,7 @@ class PaymentsService {
|
||||
/**
|
||||
* @returns {Promise<import('stripe').default.Price>}
|
||||
*/
|
||||
async createPriceForDonations({currency, nickname}) {
|
||||
async createPriceForDonations({currency, amount, nickname}) {
|
||||
const product = await this.getProductForDonations({name: nickname});
|
||||
|
||||
// Create the price in Stripe
|
||||
@ -337,7 +343,8 @@ class PaymentsService {
|
||||
currency,
|
||||
product: product.id,
|
||||
custom_unit_amount: {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
preset: amount
|
||||
},
|
||||
nickname,
|
||||
type: 'one-time',
|
||||
@ -351,7 +358,7 @@ class PaymentsService {
|
||||
active: price.active,
|
||||
nickname: price.nickname,
|
||||
currency: price.currency,
|
||||
amount: 0,
|
||||
amount,
|
||||
type: 'donation',
|
||||
interval: null
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user