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:
Sag 2023-08-04 20:17:35 +02:00 committed by GitHub
parent 299cdb4387
commit 81c3555106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 124 additions and 26 deletions

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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()

View File

@ -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;

View File

@ -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')
];

View File

@ -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 = {

View File

@ -98,6 +98,10 @@ class SettingsHelpers {
getNoReplyAddress() {
return `noreply@${this.getDefaultEmailDomain()}`;
}
areDonationsEnabled() {
return this.isStripeConnected();
}
}
module.exports = SettingsHelpers;

View File

@ -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;
},

View File

@ -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,
},
],
}
`;

View File

@ -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 = {};

View File

@ -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
});