🎨 Added 'Changed email address' event to Member Activity (#20493)
fixes https://linear.app/tryghost/issue/ENG-1256 - when a member changes their email address, surface it in Member Activity
This commit is contained in:
parent
fca8941740
commit
7f963e9c2a
@ -11,7 +11,8 @@ const ALL_EVENT_TYPES = [
|
||||
{event: 'email_opened_event', icon: 'filter-dropdown-email-opened', name: 'Email opened', group: 'emails'},
|
||||
{event: 'email_delivered_event', icon: 'filter-dropdown-email-received', name: 'Email received', group: 'emails'},
|
||||
{event: 'email_complaint_event', icon: 'filter-dropdown-email-flagged-as-spam', name: 'Email flagged as spam', group: 'emails'},
|
||||
{event: 'email_failed_event', icon: 'filter-dropdown-email-bounced', name: 'Email bounced', group: 'emails'}
|
||||
{event: 'email_failed_event', icon: 'filter-dropdown-email-bounced', name: 'Email bounced', group: 'emails'},
|
||||
{event: 'email_change_event', icon: 'filter-dropdown-email-address-changed', name: 'Email address changed', group: 'emails'}
|
||||
];
|
||||
|
||||
export default class MembersActivityEventTypeFilter extends Component {
|
||||
|
@ -114,6 +114,10 @@ export default class ParseMemberEventHelper extends Helper {
|
||||
icon = 'subscriptions';
|
||||
}
|
||||
|
||||
if (event.type === 'email_change_event') {
|
||||
icon = 'email-changed';
|
||||
}
|
||||
|
||||
return 'event-' + icon;
|
||||
}
|
||||
|
||||
@ -208,8 +212,15 @@ export default class ParseMemberEventHelper extends Helper {
|
||||
return 'less like this';
|
||||
}
|
||||
|
||||
if (event.type === 'email_change_event') {
|
||||
if (event.data.from_email && event.data.to_email) {
|
||||
return `Email address changed from ${event.data.from_email} to ${event.data.to_email}`;
|
||||
}
|
||||
return 'Email address changed';
|
||||
}
|
||||
|
||||
if (event.type === 'donation_event') {
|
||||
return `Made a one-time payment`;
|
||||
return 'Made a one-time payment';
|
||||
}
|
||||
}
|
||||
|
||||
|
4
ghost/admin/public/assets/icons/event-email-changed.svg
Normal file
4
ghost/admin/public/assets/icons/event-email-changed.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="#6C747D" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.6 8.364v5.818c0 1.715 2.703 1.962 4.067-.356 1.157-1.963.873-4.955-.571-6.923-2.125-2.9-7.039-3.984-10.608-1.588-3.28 2.202-4.448 6.658-2.635 10.258 1.793 3.564 6.003 5.29 9.813 4.002"/>
|
||||
<path stroke="#6C747D" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.52 15.23c1.701 0 3.08-1.495 3.08-3.34 0-1.846-1.379-3.341-3.08-3.341-1.702 0-3.081 1.495-3.081 3.34 0 1.846 1.38 3.342 3.08 3.342Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 597 B |
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M10.736 4.806v5.058c0 1.56 2.371 1.907 3.613-.203 1.052-1.785.794-4.509-.52-6.302C11.894.722 7.422-.265 4.173 1.916 1.19 3.92.127 7.975 1.776 11.252c1.632 3.243 5.463 4.814 8.931 3.641"/>
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M7.882 10.94c1.57 0 2.843-1.362 2.843-3.041 0-1.68-1.273-3.04-2.843-3.04S5.04 6.218 5.04 7.898c0 1.679 1.272 3.04 2.842 3.04Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 561 B |
@ -2910,12 +2910,12 @@ Object {
|
||||
Object {
|
||||
"comped": 4,
|
||||
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"free": 4,
|
||||
"free": 5,
|
||||
"paid": 2,
|
||||
},
|
||||
],
|
||||
"resource": "members",
|
||||
"total": 10,
|
||||
"total": 11,
|
||||
}
|
||||
`;
|
||||
|
||||
@ -6323,6 +6323,130 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API can change the email address 1: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Object {
|
||||
"id": null,
|
||||
"referrer_medium": "Ghost Admin",
|
||||
"referrer_source": "Created manually",
|
||||
"referrer_url": null,
|
||||
"title": null,
|
||||
"type": null,
|
||||
"url": null,
|
||||
},
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": "jon.snow@test.com",
|
||||
"email_count": 0,
|
||||
"email_open_rate": null,
|
||||
"email_opened_count": 0,
|
||||
"email_suppression": Object {
|
||||
"info": null,
|
||||
"suppressed": false,
|
||||
},
|
||||
"geolocation": null,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"labels": Any<Array>,
|
||||
"last_seen_at": null,
|
||||
"name": "Jon Snow",
|
||||
"newsletters": Array [],
|
||||
"note": null,
|
||||
"status": "free",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Array [],
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API can change the email address 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": "665",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API can change the email address 3: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Object {
|
||||
"id": null,
|
||||
"referrer_medium": "Ghost Admin",
|
||||
"referrer_source": "Created manually",
|
||||
"referrer_url": null,
|
||||
"title": null,
|
||||
"type": null,
|
||||
"url": null,
|
||||
},
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": "aegon.targaryen@test.com",
|
||||
"email_count": 0,
|
||||
"email_open_rate": null,
|
||||
"email_opened_count": 0,
|
||||
"email_suppression": Object {
|
||||
"info": null,
|
||||
"suppressed": false,
|
||||
},
|
||||
"geolocation": null,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"labels": Any<Array>,
|
||||
"last_seen_at": null,
|
||||
"name": "Jon Snow",
|
||||
"newsletters": Array [],
|
||||
"note": null,
|
||||
"status": "free",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Array [],
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API can change the email address 4: [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": "672",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API can change the email address 5: [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": "1118",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API without Stripe Add should fail when comped flag is passed in but Stripe is not enabled 1: [body] 1`] = `
|
||||
Object {
|
||||
"errors": Array [
|
||||
|
@ -2131,6 +2131,83 @@ describe('Members API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can change the email address', async function () {
|
||||
const memberToChange = {
|
||||
name: 'Jon Snow',
|
||||
email: 'jon.snow@test.com',
|
||||
newsletters: []
|
||||
};
|
||||
|
||||
const memberChanged = {
|
||||
email: 'aegon.targaryen@test.com'
|
||||
};
|
||||
|
||||
// Create member
|
||||
const {body} = await agent
|
||||
.post(`/members/`)
|
||||
.body({members: [memberToChange]})
|
||||
.expectStatus(201)
|
||||
.matchBodySnapshot({
|
||||
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 0))
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
etag: anyEtag,
|
||||
location: anyLocationFor('members')
|
||||
});
|
||||
|
||||
// Update email address
|
||||
const newMember = body.members[0];
|
||||
await agent
|
||||
.put(`/members/${newMember.id}/`)
|
||||
.body({members: [memberChanged]})
|
||||
.expectStatus(200)
|
||||
.matchBodySnapshot({
|
||||
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 0))
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
etag: anyEtag
|
||||
});
|
||||
|
||||
// Check member events
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberEmailChangeEvent',
|
||||
memberId: newMember.id,
|
||||
asserts: [
|
||||
{
|
||||
from_email: 'jon.snow@test.com',
|
||||
to_email: 'aegon.targaryen@test.com'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Check activity feed
|
||||
const {body: eventsBody} = await agent
|
||||
.get(`/members/events?filter=data.member_id:'${newMember.id}'`)
|
||||
.body({members: [memberChanged]})
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
etag: anyEtag
|
||||
});
|
||||
|
||||
const events = eventsBody.events;
|
||||
|
||||
matchArrayWithoutOrder(events, [
|
||||
{
|
||||
type: 'email_change_event',
|
||||
data: {
|
||||
from_email: 'jon.snow@test.com',
|
||||
to_email: 'aegon.targaryen@test.com'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'signup_event'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
describe('email_disabled', function () {
|
||||
const testMemberId = '6543c13c13575e086a06b222';
|
||||
const suppressedEmail = 'suppressed@email.com';
|
||||
|
@ -122,7 +122,8 @@ module.exports = function MembersAPI({
|
||||
EmailSpamComplaintEvent,
|
||||
Comment,
|
||||
labsService,
|
||||
memberAttributionService
|
||||
memberAttributionService,
|
||||
MemberEmailChangeEvent
|
||||
});
|
||||
|
||||
const memberBREADService = new MemberBREADService({
|
||||
|
@ -31,7 +31,8 @@ module.exports = class EventRepository {
|
||||
EmailSpamComplaintEvent,
|
||||
Comment,
|
||||
labsService,
|
||||
memberAttributionService
|
||||
memberAttributionService,
|
||||
MemberEmailChangeEvent
|
||||
}) {
|
||||
this._DonationPaymentEvent = DonationPaymentEvent;
|
||||
this._MemberSubscribeEvent = MemberSubscribeEvent;
|
||||
@ -48,6 +49,7 @@ module.exports = class EventRepository {
|
||||
this._MemberFeedback = MemberFeedback;
|
||||
this._EmailSpamComplaintEvent = EmailSpamComplaintEvent;
|
||||
this._memberAttributionService = memberAttributionService;
|
||||
this._MemberEmailChangeEvent = MemberEmailChangeEvent;
|
||||
}
|
||||
|
||||
async getEventTimeline(options = {}) {
|
||||
@ -76,7 +78,8 @@ module.exports = class EventRepository {
|
||||
pageActions.push(
|
||||
{type: 'newsletter_event', action: 'getNewsletterSubscriptionEvents'},
|
||||
{type: 'login_event', action: 'getLoginEvents'},
|
||||
{type: 'payment_event', action: 'getPaymentEvents'}
|
||||
{type: 'payment_event', action: 'getPaymentEvents'},
|
||||
{type: 'email_change_event', action: 'getEmailChangeEvent'}
|
||||
);
|
||||
}
|
||||
|
||||
@ -764,6 +767,38 @@ module.exports = class EventRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async getEmailChangeEvent(options = {}, filter) {
|
||||
options = {
|
||||
...options,
|
||||
withRelated: ['member'],
|
||||
filter: 'custom:true',
|
||||
mongoTransformer: chainTransformers(
|
||||
// First set the filter manually
|
||||
replaceCustomFilterTransformer(filter),
|
||||
|
||||
// Map the used keys in that filter
|
||||
...mapKeys({
|
||||
'data.created_at': 'created_at',
|
||||
'data.member_id': 'member_id'
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
const {data: models, meta} = await this._MemberEmailChangeEvent.findPage(options);
|
||||
|
||||
const data = models.map((model) => {
|
||||
return {
|
||||
type: 'email_change_event',
|
||||
data: model.toJSON(options)
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
meta
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the filter in two parts:
|
||||
* - One with 'type' that will be applied to all the pages
|
||||
|
Loading…
Reference in New Issue
Block a user