diff --git a/ghost/admin/app/adapters/member.js b/ghost/admin/app/adapters/member.js index b9f5b6c1d31..81bdcea4958 100644 --- a/ghost/admin/app/adapters/member.js +++ b/ghost/admin/app/adapters/member.js @@ -22,4 +22,16 @@ export default class Member extends ApplicationAdapter { return parsedUrl.toString(); } + + urlForCreateRecord(modelName, snapshot) { + let url = super.urlForCreateRecord(...arguments); + + if (snapshot && snapshot.adapterOptions && snapshot.adapterOptions.sendWelcomeEmail) { + let parsedUrl = new URL(url); + parsedUrl.searchParams.set('send_welcome_email', 'true'); + return parsedUrl.toString(); + } + + return url; + } } diff --git a/ghost/admin/app/components/gh-member-settings-form.hbs b/ghost/admin/app/components/gh-member-settings-form.hbs index 085995188d1..b5280a39b1a 100644 --- a/ghost/admin/app/components/gh-member-settings-form.hbs +++ b/ghost/admin/app/components/gh-member-settings-form.hbs @@ -79,6 +79,33 @@ /> {{/if}} + {{#if this.member.isNew}} +

Welcome email

+
+
+
+

Send welcome email

+
+
+ +
+
+ +
+ {{/if}} + {{#if this.membersUtils.paidMembersEnabled}}

Subscriptions

diff --git a/ghost/admin/app/components/members/modals/send-welcome-email.hbs b/ghost/admin/app/components/members/modals/send-welcome-email.hbs new file mode 100644 index 00000000000..c8c8f6dc046 --- /dev/null +++ b/ghost/admin/app/components/members/modals/send-welcome-email.hbs @@ -0,0 +1,36 @@ + diff --git a/ghost/admin/app/components/members/modals/send-welcome-email.js b/ghost/admin/app/components/members/modals/send-welcome-email.js new file mode 100644 index 00000000000..7fe49a30d7a --- /dev/null +++ b/ghost/admin/app/components/members/modals/send-welcome-email.js @@ -0,0 +1,36 @@ +import Component from '@glimmer/component'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; + +export default class SendWelcomeEmailModal extends Component { + @service notifications; + + get member() { + return this.args.data.member; + } + + get hasBeenSent() { + return !!this.member.welcomeEmailSentAtUTC; + } + + @task({drop: true}) + *sendTask() { + try { + yield this.member.sendWelcomeEmail.perform(); + + this.args.data.afterSend?.(); + this.notifications.showNotification( + `Welcome email sent to ${this.member.name || this.member.email}.`, + {type: 'success'} + ); + this.args.close(true); + return true; + } catch (e) { + // Keep the modal open on failure so GhTaskButton can show its + // error/retry pip — closing here destroys the button before it + // renders that state. The notification already informs the user. + this.notifications.showAPIError(e, {key: 'member.send-welcome-email'}); + throw e; + } + } +} diff --git a/ghost/admin/app/controllers/member.js b/ghost/admin/app/controllers/member.js index 2e002e76ffa..bbb912a911b 100644 --- a/ghost/admin/app/controllers/member.js +++ b/ghost/admin/app/controllers/member.js @@ -3,6 +3,7 @@ import DeleteMemberModal from '../components/members/modals/delete-member'; import DisableCommentingModal from '../components/members/modals/disable-commenting'; import EmberObject, {action, defineProperty} from '@ember/object'; import LogoutMemberModal from '../components/members/modals/logout-member'; +import SendWelcomeEmailModal from '../components/members/modals/send-welcome-email'; import boundOneWay from 'ghost-admin/utils/bound-one-way'; import moment from 'moment-timezone'; import {inject as service} from '@ember/service'; @@ -35,6 +36,7 @@ export default class MemberController extends Controller { @tracked showImpersonateMemberModal = false; @tracked modalLabel = null; @tracked showLabelModal = false; + @tracked sendWelcomeEmailOnCreate = false; _previousLabels = null; _previousNewsletters = null; @@ -238,6 +240,21 @@ export default class MemberController extends Controller { return this.saveTask.perform(); } + @action + toggleSendWelcomeEmailOnCreate(event) { + this.sendWelcomeEmailOnCreate = event.target.checked; + } + + @action + confirmSendWelcomeEmail() { + this.modals.open(SendWelcomeEmailModal, { + member: this.member, + afterSend: () => { + this.invalidateMembersCache(); + } + }); + } + // Tasks ------------------------------------------------------------------- @task({drop: true}) @@ -251,13 +268,17 @@ export default class MemberController extends Controller { try { const clearCountCache = member.isNew; // clear cache for adding new members so the count is updated without waiting for a refresh + const saveOptions = member.isNew && this.sendWelcomeEmailOnCreate + ? {adapterOptions: {sendWelcomeEmail: true}} + : undefined; - yield member.save(); + yield member.save(saveOptions); member.updateLabels(); member.labels.forEach(label => this.labelsManager.addLabel(label)); this.invalidateMembersCache(); this.setInitialRelationshipValues(); + this.sendWelcomeEmailOnCreate = false; if (clearCountCache) { this.membersCountCache.clear(); diff --git a/ghost/admin/app/models/member.js b/ghost/admin/app/models/member.js index a1cccccc0ee..d55da28ab99 100644 --- a/ghost/admin/app/models/member.js +++ b/ghost/admin/app/models/member.js @@ -12,6 +12,7 @@ export default Model.extend(ValidationEngine, { status: attr('string'), createdAtUTC: attr('moment-utc'), lastSeenAtUTC: attr('moment-utc'), + welcomeEmailSentAtUTC: attr('moment-utc'), subscriptions: attr('member-subscription'), attribution: attr(), subscribed: attr('boolean', {defaultValue: true}), @@ -32,6 +33,7 @@ export default Model.extend(ValidationEngine, { ghostPaths: service(), ajax: service(), + store: service(), // remove client-generated labels, which have `id: null`. // Ember Data won't recognize/update them automatically @@ -56,5 +58,12 @@ export default Model.extend(ValidationEngine, { logoutAllDevices: task(function* () { let url = this.get('ghostPaths.url').api('members', this.id, 'signout'); yield this.ajax.post(url); + }).drop(), + + sendWelcomeEmail: task(function* () { + let url = this.get('ghostPaths.url').api('members', this.id, 'welcome_email'); + let response = yield this.ajax.post(url); + this.store.pushPayload('member', response); + return response; }).drop() }); diff --git a/ghost/admin/app/routes/member/new.js b/ghost/admin/app/routes/member/new.js index 30c27fc4aa0..439a7c7f962 100644 --- a/ghost/admin/app/routes/member/new.js +++ b/ghost/admin/app/routes/member/new.js @@ -3,4 +3,9 @@ import MemberRoute from '../member'; export default class NewMemberRoute extends MemberRoute { controllerName = 'member'; templateName = 'member'; + + setupController(controller) { + super.setupController(...arguments); + controller.sendWelcomeEmailOnCreate = false; + } } diff --git a/ghost/admin/app/serializers/member.js b/ghost/admin/app/serializers/member.js index 08de9e1d9c9..abc12cac578 100644 --- a/ghost/admin/app/serializers/member.js +++ b/ghost/admin/app/serializers/member.js @@ -5,6 +5,7 @@ export default class MemberSerializer extends ApplicationSerializer.extend(Embed attrs = { createdAtUTC: {key: 'created_at'}, lastSeenAtUTC: {key: 'last_seen_at'}, + welcomeEmailSentAtUTC: {key: 'welcome_email_sent_at'}, labels: {embedded: 'always'}, newsletters: {embedded: 'always'} }; @@ -17,6 +18,7 @@ export default class MemberSerializer extends ApplicationSerializer.extend(Embed delete json.geolocation; delete json.status; delete json.last_seen_at; + delete json.welcome_email_sent_at; delete json.comped; // Tiers are managed via direct API calls in gh-member-settings-form.js // (removeComplimentaryTask) and modal-member-tier.js (addTier task), diff --git a/ghost/admin/app/templates/member.hbs b/ghost/admin/app/templates/member.hbs index b8d3a0e2f21..f5cdc3f18df 100644 --- a/ghost/admin/app/templates/member.hbs +++ b/ghost/admin/app/templates/member.hbs @@ -73,6 +73,16 @@ Sign out of all devices +
  • + +
  • {{#if (not-eq this.member.canComment false)}}