From 81c35551063a299f82e310468dde8fffb7597204 Mon Sep 17 00:00:00 2001 From: Sag Date: Fri, 4 Aug 2023 20:17:35 +0200 Subject: [PATCH] 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 --- .../settings/tips-and-donations.hbs | 25 ++++----- .../components/settings/tips-and-donations.js | 52 +++++++++++++++++-- ghost/admin/app/models/setting.js | 7 +++ ghost/admin/app/services/settings.js | 2 +- ghost/admin/mirage/fixtures/settings.js | 6 ++- .../utils/serializers/input/settings.js | 4 +- .../settings-helpers/SettingsHelpers.js | 4 ++ .../services/settings/settings-service.js | 1 + .../admin/__snapshots__/settings.test.js.snap | 30 ++++++++++- .../core/test/e2e-api/admin/settings.test.js | 2 +- ghost/payments/lib/PaymentsService.js | 17 ++++-- 11 files changed, 124 insertions(+), 26 deletions(-) diff --git a/ghost/admin/app/components/settings/tips-and-donations.hbs b/ghost/admin/app/components/settings/tips-and-donations.hbs index c036b08931..3c112e20c1 100644 --- a/ghost/admin/app/components/settings/tips-and-donations.hbs +++ b/ghost/admin/app/components/settings/tips-and-donations.hbs @@ -20,10 +20,12 @@
@@ -31,15 +33,14 @@ - - + @update={{this.setDonationsCurrency}} + /> {{svg-jar "arrow-down-small"}} @@ -52,11 +53,11 @@
{{/liquid-if}}
- \ No newline at end of file + diff --git a/ghost/admin/app/components/settings/tips-and-donations.js b/ghost/admin/app/components/settings/tips-and-donations.js index 9cfa317356..873032cbe8 100644 --- a/ghost/admin/app/components/settings/tips-and-donations.js +++ b/ghost/admin/app/components/settings/tips-and-donations.js @@ -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; + } } diff --git a/ghost/admin/app/models/setting.js b/ghost/admin/app/models/setting.js index 43b2add759..f4869ccc11 100644 --- a/ghost/admin/app/models/setting.js +++ b/ghost/admin/app/models/setting.js @@ -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() diff --git a/ghost/admin/app/services/settings.js b/ghost/admin/app/services/settings.js index 97965561a1..7c3294dc6d 100644 --- a/ghost/admin/app/services/settings.js +++ b/ghost/admin/app/services/settings.js @@ -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; diff --git a/ghost/admin/mirage/fixtures/settings.js b/ghost/admin/mirage/fixtures/settings.js index c015d87b6a..d15d4dc712 100644 --- a/ghost/admin/mirage/fixtures/settings.js +++ b/ghost/admin/mirage/fixtures/settings.js @@ -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') ]; diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js b/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js index 42ec53d33b..7cffdbe36a 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js @@ -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 = { diff --git a/ghost/core/core/server/services/settings-helpers/SettingsHelpers.js b/ghost/core/core/server/services/settings-helpers/SettingsHelpers.js index c095d58ecd..a6337ad0cb 100644 --- a/ghost/core/core/server/services/settings-helpers/SettingsHelpers.js +++ b/ghost/core/core/server/services/settings-helpers/SettingsHelpers.js @@ -98,6 +98,10 @@ class SettingsHelpers { getNoReplyAddress() { return `noreply@${this.getDefaultEmailDomain()}`; } + + areDonationsEnabled() { + return this.isStripeConnected(); + } } module.exports = SettingsHelpers; diff --git a/ghost/core/core/server/services/settings/settings-service.js b/ghost/core/core/server/services/settings/settings-service.js index 596c0f75fe..3f5fbc0445 100644 --- a/ghost/core/core/server/services/settings/settings-service.js +++ b/ghost/core/core/server/services/settings/settings-service.js @@ -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; }, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap index baf27521f3..10e8fa5fa2 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap @@ -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, + }, ], } `; diff --git a/ghost/core/test/e2e-api/admin/settings.test.js b/ghost/core/test/e2e-api/admin/settings.test.js index 40a5cd703d..1bcb2e135a 100644 --- a/ghost/core/test/e2e-api/admin/settings.test.js +++ b/ghost/core/test/e2e-api/admin/settings.test.js @@ -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 = {}; diff --git a/ghost/payments/lib/PaymentsService.js b/ghost/payments/lib/PaymentsService.js index 4dd06ade26..e6a95c3bc6 100644 --- a/ghost/payments/lib/PaymentsService.js +++ b/ghost/payments/lib/PaymentsService.js @@ -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} */ - 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 });