✨ Allowed domain change for members "from" address (#1597)
refs TryGhost/Ghost#11414 - Restructures member settings in labs, from address gets its own section - Removes explicit site domain for fromAddress as we allow updating the full address - Adds new CTA to trigger magic link for updating members from address - Adds new confirmation modal for email sent to new from address - Adds notification banner for from address update redirect link
This commit is contained in:
parent
1055c2d7e4
commit
1e7c80a1da
|
@ -2,5 +2,5 @@
|
||||||
{{message.message}}
|
{{message.message}}
|
||||||
</div>
|
</div>
|
||||||
<button class="gh-alert-close" {{action "closeNotification"}} data-test-button="close-notification">
|
<button class="gh-alert-close" {{action "closeNotification"}} data-test-button="close-notification">
|
||||||
{{svg-jar "close"}}<span class="hidden">Close</span>
|
{{svg-jar "close-stroke"}}<span class="hidden">Close</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -219,74 +219,101 @@
|
||||||
<section class="bb b--whitegrey pa5">
|
<section class="bb b--whitegrey pa5">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="gh-setting-title">Email settings</h4>
|
<h4 class="gh-setting-title">From address</h4>
|
||||||
<p class="gh-setting-desc pa0 ma0">Customise signup, signin and subscription emails</p>
|
<p class="gh-setting-desc pa0 ma0">The email address your members receive newsletters from</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="gh-btn" {{action (toggle "membersEmailOpen" this)}} data-test-toggle-membersemail><span>{{if this.membersEmailOpen "Close" "Expand"}}</span></button>
|
<button type="button" class="gh-btn" {{action (toggle "membersFromOpen" this)}} data-test-toggle-membersFrom><span>{{if this.membersFromOpen "Close" "Expand"}}</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#liquid-if this.membersEmailOpen}}
|
{{#liquid-if this.membersFromOpen}}
|
||||||
<div class="flex flex-column w-100 w-50-l flex mt8">
|
<div class="flex flex-column w-100 w-50-l flex mt8">
|
||||||
<GhFormGroup>
|
|
||||||
<label class="fw6 f8">From Address</label>
|
|
||||||
<div class="flex items-center justify-center mt1 gh-input-group">
|
|
||||||
<GhTextInput
|
|
||||||
@value={{readonly this.subscriptionSettings.fromAddress}}
|
|
||||||
@input={{action "setSubscriptionSettings" "fromAddress"}}
|
|
||||||
@class="w20"
|
|
||||||
/>
|
|
||||||
<span class="gh-input-append"> @{{this.blogDomain}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="f8 fw4 midgrey mt1">Your members will receive system emails from this address</div>
|
|
||||||
</GhFormGroup>
|
|
||||||
|
|
||||||
{{#unless this.hasBulkEmailConfig}}
|
|
||||||
<div class="flex items-center">
|
|
||||||
<GhFormGroup @class="gh-labs-mailgun-region">
|
|
||||||
<label class="fw6 f8">Mailgun region</label>
|
|
||||||
<div class="mt1">
|
|
||||||
<PowerSelect
|
|
||||||
@options={{this.mailgunRegions}}
|
|
||||||
@selected={{this.mailgunRegion}}
|
|
||||||
@onChange={{action "setBulkEmailRegion"}}
|
|
||||||
@searchEnabled={{false}}
|
|
||||||
@triggerComponent="gh-power-select/trigger"
|
|
||||||
as |region|
|
|
||||||
>
|
|
||||||
{{region.flag}} {{region.name}}
|
|
||||||
</PowerSelect>
|
|
||||||
</div>
|
|
||||||
</GhFormGroup>
|
|
||||||
<GhFormGroup>
|
|
||||||
<label class="fw6 f8">Mailgun domain</label>
|
|
||||||
<GhTextInput
|
|
||||||
@value={{readonly this.bulkEmailSettings.domain}}
|
|
||||||
@input={{action "setBulkEmailSettings" "domain"}}
|
|
||||||
@class="mt1"
|
|
||||||
/>
|
|
||||||
</GhFormGroup>
|
|
||||||
</div>
|
|
||||||
<div class="nt5 mb5">
|
|
||||||
<a href="https://app.mailgun.com/app/sending/domains" target="_blank" class="mt1 fw4 f8">
|
|
||||||
Find your Mailgun region and domain here »
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<GhFormGroup>
|
<GhFormGroup>
|
||||||
<label class="fw6 f8">Mailgun API key</label>
|
<div class="flex items-center justify-center mt1">
|
||||||
<GhTextInput
|
<GhTextInput
|
||||||
@type="password"
|
@value={{readonly this.subscriptionSettings.fromAddress}}
|
||||||
@value={{readonly this.bulkEmailSettings.apiKey}}
|
@input={{action "setSubscriptionSettings" "fromAddress"}}
|
||||||
@input={{action "setBulkEmailSettings" "apiKey"}}
|
@class="w20"
|
||||||
@class="mt1 password" @autocomplete="new-password"
|
/>
|
||||||
/>
|
<GhTaskButton
|
||||||
<a href="https://app.mailgun.com/app/account/security/api_keys" target="_blank" class="mt1 fw4 f8">
|
@buttonText="Update from address"
|
||||||
Find your Mailgun API keys here »
|
@runningText="Sending..."
|
||||||
</a>
|
@successText="Confirmation Email Sent"
|
||||||
|
@task={{this.updateFromAddress}}
|
||||||
|
@class="gh-btn gh-btn-icon gh-btn-textfield-group ml2"
|
||||||
|
data-test-button="update-from-address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</GhFormGroup>
|
</GhFormGroup>
|
||||||
{{/unless}}
|
{{#if this.showFromAddressConfirmation}}
|
||||||
</div>
|
<div class="flex items-center green-d1 nt3 lh-1">
|
||||||
|
{{svg-jar "check-circle" class="w4 h4 mr1 stroke-green-d1"}} <span class="nudge-left--2">Check your inbox and click the link to confirm</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
{{/liquid-if}}
|
{{/liquid-if}}
|
||||||
</section>
|
</section>
|
||||||
|
{{#unless this.hasBulkEmailConfig}}
|
||||||
|
<section class="bb b--whitegrey pa5">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="gh-setting-title">Mailgun settings</h4>
|
||||||
|
<p class="gh-setting-desc pa0 ma0">Customise signup, signin and subscription emails</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="gh-btn" {{action (toggle "membersEmailOpen" this)}} data-test-toggle-membersemail>
|
||||||
|
<span>{{if this.membersEmailOpen "Close" "Expand"}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#liquid-if this.membersEmailOpen}}
|
||||||
|
<div class="flex flex-column w-100 w-50-l flex mt8">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<GhFormGroup @class="gh-labs-mailgun-region">
|
||||||
|
<label class="fw6 f8">Mailgun region</label>
|
||||||
|
<div class="mt1">
|
||||||
|
<PowerSelect
|
||||||
|
@options={{this.mailgunRegions}}
|
||||||
|
@selected={{this.mailgunRegion}}
|
||||||
|
@onChange={{action "setBulkEmailRegion"}}
|
||||||
|
@searchEnabled={{false}}
|
||||||
|
@triggerComponent="gh-power-select/trigger"
|
||||||
|
as |region|
|
||||||
|
>
|
||||||
|
{{region.flag}} {{region.name}}
|
||||||
|
</PowerSelect>
|
||||||
|
</div>
|
||||||
|
</GhFormGroup>
|
||||||
|
<GhFormGroup>
|
||||||
|
<label class="fw6 f8">Mailgun domain</label>
|
||||||
|
<GhTextInput
|
||||||
|
@value={{readonly this.bulkEmailSettings.domain}}
|
||||||
|
@input={{action "setBulkEmailSettings" "domain"}}
|
||||||
|
@class="mt1"
|
||||||
|
/>
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
<div class="nt5 mb5">
|
||||||
|
<a href="https://app.mailgun.com/app/sending/domains" target="_blank" class="mt1 fw4 f8">
|
||||||
|
Find your Mailgun region and domain here »
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<GhFormGroup>
|
||||||
|
<label class="fw6 f8">Mailgun API key</label>
|
||||||
|
<GhTextInput
|
||||||
|
@type="password"
|
||||||
|
@value={{readonly this.bulkEmailSettings.apiKey}}
|
||||||
|
@input={{action "setBulkEmailSettings" "apiKey"}}
|
||||||
|
@class="mt1 password" @autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
<a href="https://app.mailgun.com/app/account/security/api_keys" target="_blank" class="mt1 fw4 f8">
|
||||||
|
Find your Mailgun API keys here »
|
||||||
|
</a>
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
{{/liquid-if}}
|
||||||
|
</section>
|
||||||
|
{{/unless}}
|
||||||
</div>
|
</div>
|
|
@ -3,6 +3,7 @@ import {computed} from '@ember/object';
|
||||||
import {reads} from '@ember/object/computed';
|
import {reads} from '@ember/object/computed';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
import {set} from '@ember/object';
|
import {set} from '@ember/object';
|
||||||
|
import {task} from 'ember-concurrency';
|
||||||
|
|
||||||
const US = {flag: '🇺🇸', name: 'US', baseUrl: 'https://api.mailgun.net/v3'};
|
const US = {flag: '🇺🇸', name: 'US', baseUrl: 'https://api.mailgun.net/v3'};
|
||||||
const EU = {flag: '🇪🇺', name: 'EU', baseUrl: 'https://api.eu.mailgun.net/v3'};
|
const EU = {flag: '🇪🇺', name: 'EU', baseUrl: 'https://api.eu.mailgun.net/v3'};
|
||||||
|
@ -30,8 +31,10 @@ export default Component.extend({
|
||||||
config: service(),
|
config: service(),
|
||||||
mediaQueries: service(),
|
mediaQueries: service(),
|
||||||
ghostPaths: service(),
|
ghostPaths: service(),
|
||||||
|
ajax: service(),
|
||||||
|
|
||||||
currencies: null,
|
currencies: null,
|
||||||
|
showFromAddressConfirmation: false,
|
||||||
|
|
||||||
// passed in actions
|
// passed in actions
|
||||||
setMembersSubscriptionSettings() {},
|
setMembersSubscriptionSettings() {},
|
||||||
|
@ -45,6 +48,10 @@ export default Component.extend({
|
||||||
return CURRENCIES.findBy('value', this.get('subscriptionSettings.stripeConfig.plans.monthly.currency'));
|
return CURRENCIES.findBy('value', this.get('subscriptionSettings.stripeConfig.plans.monthly.currency'));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
disableUpdateFromAddressButton: computed('subscriptionSettings.fromAddress', function () {
|
||||||
|
return (this.originalFromAddress === this.get('subscriptionSettings.fromAddress'));
|
||||||
|
}),
|
||||||
|
|
||||||
mailgunRegion: computed('settings.bulkEmailSettings.baseUrl', function () {
|
mailgunRegion: computed('settings.bulkEmailSettings.baseUrl', function () {
|
||||||
if (!this.settings.get('bulkEmailSettings.baseUrl')) {
|
if (!this.settings.get('bulkEmailSettings.baseUrl')) {
|
||||||
return US;
|
return US;
|
||||||
|
@ -103,6 +110,10 @@ export default Component.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
toggleFromAddressConfirmation() {
|
||||||
|
this.toggleProperty('showFromAddressConfirmation');
|
||||||
|
},
|
||||||
|
|
||||||
setDefaultContentVisibility(value) {
|
setDefaultContentVisibility(value) {
|
||||||
this.setDefaultContentVisibility(value);
|
this.setDefaultContentVisibility(value);
|
||||||
},
|
},
|
||||||
|
@ -183,6 +194,22 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateFromAddress: task(function* () {
|
||||||
|
let url = this.get('ghostPaths.url').api('/settings/members/email');
|
||||||
|
try {
|
||||||
|
const response = yield this.ajax.post(url, {
|
||||||
|
data: {
|
||||||
|
from_address: this.subscriptionSettings.fromAddress
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.toggleProperty('showFromAddressConfirmation');
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
// Failed to send email, retry
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).drop(),
|
||||||
|
|
||||||
get stripeConnectAuthUrl() {
|
get stripeConnectAuthUrl() {
|
||||||
return this.ghostPaths.url.api('members/stripe_connect');
|
return this.ghostPaths.url.api('members/stripe_connect');
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<header class="modal-header">
|
||||||
|
<h1>Check your Inbox</h1>
|
||||||
|
</header>
|
||||||
|
<a class="close" href="" role="button" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
We have sent an email to confirm you own <b>{{this.fromAddress}}</b>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button {{action "closeModal"}} class="gh-btn"><span>Close</span></button>
|
||||||
|
</div>
|
|
@ -0,0 +1,8 @@
|
||||||
|
import ModalComponent from 'ghost-admin/components/modal-base';
|
||||||
|
import {alias} from '@ember/object/computed';
|
||||||
|
|
||||||
|
export default ModalComponent.extend({
|
||||||
|
|
||||||
|
confirm() {},
|
||||||
|
fromAddress: alias('model.fromAddress')
|
||||||
|
});
|
|
@ -42,6 +42,8 @@ export default Controller.extend({
|
||||||
session: service(),
|
session: service(),
|
||||||
settings: service(),
|
settings: service(),
|
||||||
|
|
||||||
|
queryParams: ['fromAddressUpdate'],
|
||||||
|
fromAddressUpdate: null,
|
||||||
importErrors: null,
|
importErrors: null,
|
||||||
importSuccessful: false,
|
importSuccessful: false,
|
||||||
showDeleteAllModal: false,
|
showDeleteAllModal: false,
|
||||||
|
@ -250,5 +252,6 @@ export default Controller.extend({
|
||||||
reset() {
|
reset() {
|
||||||
this.set('importErrors', null);
|
this.set('importErrors', null);
|
||||||
this.set('importSuccessful', false);
|
this.set('importSuccessful', false);
|
||||||
|
this.set('fromAddressUpdate', null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,12 @@ import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||||
settings: service(),
|
settings: service(),
|
||||||
|
notifications: service(),
|
||||||
|
queryParams: {
|
||||||
|
fromAddressUpdate: {
|
||||||
|
replace: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -16,6 +22,15 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||||
return this.settings.reload();
|
return this.settings.reload();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setupController(controller) {
|
||||||
|
if (controller.fromAddressUpdate === 'success') {
|
||||||
|
this.notifications.showAlert(
|
||||||
|
`Done! Newsletter “From address” has been updated`.htmlSafe(),
|
||||||
|
{type: 'success', key: 'members.settings.from-address.updated'}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
resetController(controller, isExiting) {
|
resetController(controller, isExiting) {
|
||||||
if (isExiting) {
|
if (isExiting) {
|
||||||
controller.reset();
|
controller.reset();
|
||||||
|
|
|
@ -203,7 +203,7 @@
|
||||||
.gh-alert-content {
|
.gh-alert-content {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
font-weight: 300;
|
font-weight: 400;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,14 +216,14 @@
|
||||||
.gh-alert-close {
|
.gh-alert-close {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
padding: 5px;
|
padding: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-alert-close svg {
|
.gh-alert-close svg {
|
||||||
height: 10px;
|
height: 12px;
|
||||||
width: 10px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-alert-close:hover {
|
.gh-alert-close:hover {
|
||||||
|
|
|
@ -320,6 +320,10 @@ fieldset[disabled] .gh-btn {
|
||||||
fill: color-mod(var(--midgrey) l(+15%));
|
fill: color-mod(var(--midgrey) l(+15%));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-btn:not(.gh-btn-blue):not(.gh-btn-green) svg.gh-icon-spinner rect {
|
||||||
|
fill: color-mod(var(--midgrey) l(-7%));
|
||||||
|
}
|
||||||
|
|
||||||
.gh-btn-icon-right svg,
|
.gh-btn-icon-right svg,
|
||||||
svg.gh-btn-icon-right {
|
svg.gh-btn-icon-right {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
|
@ -374,6 +378,11 @@ svg.gh-btn-icon-right {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-btn-textfield-group span {
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
/* Button Variations
|
/* Button Variations
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--lh-1-0: 1.0em;
|
||||||
--lh-1-1: 1.1em;
|
--lh-1-1: 1.1em;
|
||||||
--lh-1-3: 1.333em;
|
--lh-1-3: 1.333em;
|
||||||
--lh-1-4: 1.4em;
|
--lh-1-4: 1.4em;
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
--lh-2-0: 2.0em;
|
--lh-2-0: 2.0em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lh-1 { line-height: var(--lh-1-0); }
|
||||||
.lh-solid { line-height: var(--lh-1-1); }
|
.lh-solid { line-height: var(--lh-1-1); }
|
||||||
.lh-heading { line-height: var(--lh-1-3); }
|
.lh-heading { line-height: var(--lh-1-3); }
|
||||||
.lh-title { line-height: var(--lh-1-4); }
|
.lh-title { line-height: var(--lh-1-4); }
|
||||||
|
@ -24,6 +26,7 @@
|
||||||
.lh-zero { line-height: 0; }
|
.lh-zero { line-height: 0; }
|
||||||
|
|
||||||
@media (--breakpoint-not-small) {
|
@media (--breakpoint-not-small) {
|
||||||
|
.lh-1-ns { line-height: var(--lh-1-0); }
|
||||||
.lh-solid-ns { line-height: var(--lh-1-1); }
|
.lh-solid-ns { line-height: var(--lh-1-1); }
|
||||||
.lh-heading-ns { line-height: var(--lh-1-3); }
|
.lh-heading-ns { line-height: var(--lh-1-3); }
|
||||||
.lh-title-ns { line-height: var(--lh-1-4); }
|
.lh-title-ns { line-height: var(--lh-1-4); }
|
||||||
|
@ -34,6 +37,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (--breakpoint-medium) {
|
@media (--breakpoint-medium) {
|
||||||
|
.lh-1-m { line-height: var(--lh-1-0); }
|
||||||
.lh-solid-m { line-height: var(--lh-1-1); }
|
.lh-solid-m { line-height: var(--lh-1-1); }
|
||||||
.lh-heading-m { line-height: var(--lh-1-3); }
|
.lh-heading-m { line-height: var(--lh-1-3); }
|
||||||
.lh-title-m { line-height: var(--lh-1-4); }
|
.lh-title-m { line-height: var(--lh-1-4); }
|
||||||
|
@ -44,6 +48,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (--breakpoint-large) {
|
@media (--breakpoint-large) {
|
||||||
|
.lh-1-l { line-height: var(--lh-1-0); }
|
||||||
.lh-solid-l { line-height: var(--lh-1-1); }
|
.lh-solid-l { line-height: var(--lh-1-1); }
|
||||||
.lh-heading-l { line-height: var(--lh-1-3); }
|
.lh-heading-l { line-height: var(--lh-1-3); }
|
||||||
.lh-title-l { line-height: var(--lh-1-4); }
|
.lh-title-l { line-height: var(--lh-1-4); }
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>close</title><line class="a" x1="0.75" y1="23.249" x2="23.25" y2="0.749"/><line class="a" x1="23.25" y1="23.249" x2="0.75" y2="0.749"/></svg>
|
After Width: | Height: | Size: 332 B |
Loading…
Reference in New Issue