Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ghost/admin/app/adapters/member.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
27 changes: 27 additions & 0 deletions ghost/admin/app/components/gh-member-settings-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,33 @@
/>
{{/if}}

{{#if this.member.isNew}}
<h4 class="gh-main-section-header small bn">Welcome email</h4>
<div class="gh-main-section-content bordered gh-member-newsletter-section">
<div class="gh-member-newsletter-row" data-test-member-settings-switch>
<div>
<h4 class="gh-member-newsletter-title">Send welcome email</h4>
</div>
<div class="for-switch xs">
<label class="switch" for="member-send-welcome-email">
<Input
@checked={{@sendWelcomeEmail}}
@type="checkbox"
id="member-send-welcome-email"
name="send-welcome-email"
data-test-checkbox="send-welcome-email"
{{on "change" @onSendWelcomeEmailToggle}}
/>
<span class="input-toggle-component"></span>
</label>
</div>
</div>
<div class="gh-member-newsletter-footer middarkgrey">
If enabled, member will receive the welcome email when they're added.
</div>
</div>
{{/if}}

{{#if this.membersUtils.paidMembersEnabled}}
<h4 class="gh-main-section-header small bn">Subscriptions</h4>

Expand Down
36 changes: 36 additions & 0 deletions ghost/admin/app/components/members/modals/send-welcome-email.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class="modal-content" data-test-modal="send-welcome-email">
<header class="modal-header">
<h1>{{if this.hasBeenSent "Resend welcome email?" "Send welcome email?"}}</h1>
</header>
<button type="button" class="close" title="Close" {{on "click" (fn @close false)}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>

<div class="modal-body">
{{#if this.hasBeenSent}}
<p class="mb6">
<strong>{{or @data.member.name @data.member.email}}</strong> already received a welcome email on {{moment-format @data.member.welcomeEmailSentAtUTC "D MMM YYYY"}}. Send another?
</p>
{{else}}
<p class="mb6">
Send the welcome email to <strong>{{or @data.member.name @data.member.email}}</strong>?
</p>
{{/if}}
</div>

<div class="modal-footer">
<button
type="button"
class="gh-btn"
{{on "click" (fn @close false)}}
data-test-button="cancel"
>
<span>Cancel</span>
</button>
<GhTaskButton
@buttonText={{if this.hasBeenSent "Resend" "Send"}}
@successText="Sent"
@task={{this.sendTask}}
@class="gh-btn gh-btn-primary gh-btn-icon"
data-test-button="confirm"
/>
</div>
</div>
36 changes: 36 additions & 0 deletions ghost/admin/app/components/members/modals/send-welcome-email.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
23 changes: 22 additions & 1 deletion ghost/admin/app/controllers/member.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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})
Expand All @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions ghost/admin/app/models/member.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}),
Expand All @@ -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
Expand All @@ -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()
});
5 changes: 5 additions & 0 deletions ghost/admin/app/routes/member/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 2 additions & 0 deletions ghost/admin/app/serializers/member.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
};
Expand All @@ -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),
Expand Down
12 changes: 12 additions & 0 deletions ghost/admin/app/templates/member.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@
<span>Sign out of all devices</span>
</button>
</li>
<li>
<button
type="button"
class="mr2"
{{on "click" this.confirmSendWelcomeEmail}}
data-test-button="send-welcome-email"
>
<span>{{if this.member.welcomeEmailSentAtUTC "Resend welcome email" "Send welcome email"}}</span>
</button>
</li>
<li>
{{#if (not-eq this.member.canComment false)}}
<button
Expand Down Expand Up @@ -123,6 +133,8 @@
@saveMember={{this.save}}
@isSaveRunning={{this.saveTask.isRunning}}
@isLoading={{this.isLoading}}
@sendWelcomeEmail={{this.sendWelcomeEmailOnCreate}}
@onSendWelcomeEmailToggle={{this.toggleSendWelcomeEmailOnCreate}}
@onEnableCommenting={{this.confirmEnableCommenting}} />
</form>
</div>
Expand Down
34 changes: 33 additions & 1 deletion ghost/core/core/server/api/endpoints/members.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ const controller = {
},
options: [
'send_email',
'email_type'
'email_type',
'send_welcome_email'
],
validation: {
data: {
Expand All @@ -120,6 +121,7 @@ const controller = {
if (await membersService.verificationTrigger.checkVerificationRequired()) {
logging.warn(tpl(messages.notSendingWelcomeEmail));
frame.options.send_email = false;
frame.options.send_welcome_email = false;
}
const member = await membersService.api.memberBREADService.add(frame.data.members[0], frame.options);

Expand Down Expand Up @@ -175,6 +177,36 @@ const controller = {
}
},

sendWelcomeEmail: {
statusCode: 200,
headers: {
cacheInvalidate: false
},
options: [
'id'
],
validation: {
options: {
id: {
required: true
}
}
},
permissions: {
method: 'edit'
},
async query(frame) {
if (await membersService.verificationTrigger.checkVerificationRequired()) {
throw new errors.HostLimitError({
message: tpl(messages.notSendingWelcomeEmail)
});
}
const member = await membersService.api.memberBREADService.sendWelcomeEmail(frame.options.id, frame.options);

return member;
}
},

editSubscription: {
statusCode: 200,
headers: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {

editSubscription: createSerializer('editSubscription', singleMember),
createSubscription: createSerializer('createSubscription', singleMember),
sendWelcomeEmail: createSerializer('sendWelcomeEmail', singleMember),
bulkDestroy: createSerializer('bulkDestroy', passthrough),
bulkEdit: createSerializer('bulkEdit', bulkAction),
exportCSV: createSerializer('exportCSV', exportCSV),
Expand Down Expand Up @@ -186,7 +187,8 @@ function serializeMember(member, options) {
attribution: serializeAttribution(json.attribution),
unsubscribe_url: json.unsubscribe_url,
can_comment: json.can_comment,
commenting: json.commenting
commenting: json.commenting,
welcome_email_sent_at: json.welcome_email_sent_at
};

if (json.products) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ module.exports = function MembersAPI({
settingsHelpers,
nextPaymentCalculator,
commentsService,
giftService
giftService,
AutomatedEmailRecipient,
Automation
});

const geolocationService = new GeolocationService();
Expand Down
Loading