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:
Rishabh Garg 2020-06-09 01:52:58 +05:30 committed by GitHub
parent 1055c2d7e4
commit 1e7c80a1da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 173 additions and 66 deletions

View File

@ -2,5 +2,5 @@
{{message.message}}
</div>
<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>

View File

@ -219,74 +219,101 @@
<section class="bb b--whitegrey pa5">
<div class="flex justify-between">
<div>
<h4 class="gh-setting-title">Email settings</h4>
<p class="gh-setting-desc pa0 ma0">Customise signup, signin and subscription emails</p>
<h4 class="gh-setting-title">From address</h4>
<p class="gh-setting-desc pa0 ma0">The email address your members receive newsletters from</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>
<button type="button" class="gh-btn" {{action (toggle "membersFromOpen" this)}} data-test-toggle-membersFrom><span>{{if this.membersFromOpen "Close" "Expand"}}</span></button>
</div>
</div>
{{#liquid-if this.membersEmailOpen}}
<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 &raquo;
</a>
</div>
{{#liquid-if this.membersFromOpen}}
<div class="flex flex-column w-100 w-50-l flex mt8">
<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 &raquo;
</a>
<div class="flex items-center justify-center mt1">
<GhTextInput
@value={{readonly this.subscriptionSettings.fromAddress}}
@input={{action "setSubscriptionSettings" "fromAddress"}}
@class="w20"
/>
<GhTaskButton
@buttonText="Update from address"
@runningText="Sending..."
@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>
{{/unless}}
</div>
{{#if this.showFromAddressConfirmation}}
<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}}
</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 &raquo;
</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 &raquo;
</a>
</GhFormGroup>
</div>
{{/liquid-if}}
</section>
{{/unless}}
</div>

View File

@ -3,6 +3,7 @@ import {computed} from '@ember/object';
import {reads} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {set} from '@ember/object';
import {task} from 'ember-concurrency';
const US = {flag: '🇺🇸', name: 'US', baseUrl: 'https://api.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(),
mediaQueries: service(),
ghostPaths: service(),
ajax: service(),
currencies: null,
showFromAddressConfirmation: false,
// passed in actions
setMembersSubscriptionSettings() {},
@ -45,6 +48,10 @@ export default Component.extend({
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 () {
if (!this.settings.get('bulkEmailSettings.baseUrl')) {
return US;
@ -103,6 +110,10 @@ export default Component.extend({
},
actions: {
toggleFromAddressConfirmation() {
this.toggleProperty('showFromAddressConfirmation');
},
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() {
return this.ghostPaths.url.api('members/stripe_connect');
}

View File

@ -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>

View File

@ -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')
});

View File

@ -42,6 +42,8 @@ export default Controller.extend({
session: service(),
settings: service(),
queryParams: ['fromAddressUpdate'],
fromAddressUpdate: null,
importErrors: null,
importSuccessful: false,
showDeleteAllModal: false,
@ -250,5 +252,6 @@ export default Controller.extend({
reset() {
this.set('importErrors', null);
this.set('importSuccessful', false);
this.set('fromAddressUpdate', null);
}
});

View File

@ -4,6 +4,12 @@ import {inject as service} from '@ember/service';
export default AuthenticatedRoute.extend(CurrentUserSettings, {
settings: service(),
notifications: service(),
queryParams: {
fromAddressUpdate: {
replace: true
}
},
beforeModel() {
this._super(...arguments);
@ -16,6 +22,15 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
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) {
if (isExiting) {
controller.reset();

View File

@ -203,7 +203,7 @@
.gh-alert-content {
font-size: 1.4rem;
line-height: 1.3em;
font-weight: 300;
font-weight: 400;
user-select: text;
}
@ -216,14 +216,14 @@
.gh-alert-close {
flex-shrink: 0;
margin-left: 20px;
padding: 5px;
padding: 4px;
font-size: 10px;
line-height: 10px;
}
.gh-alert-close svg {
height: 10px;
width: 10px;
height: 12px;
width: 12px;
}
.gh-alert-close:hover {

View File

@ -320,6 +320,10 @@ fieldset[disabled] .gh-btn {
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,
svg.gh-btn-icon-right {
margin-left: 0.5em;
@ -374,6 +378,11 @@ svg.gh-btn-icon-right {
box-shadow: none;
}
.gh-btn-textfield-group span {
height: 36px;
line-height: 36px;
}
/*
/* Button Variations

View File

@ -8,6 +8,7 @@
*/
:root {
--lh-1-0: 1.0em;
--lh-1-1: 1.1em;
--lh-1-3: 1.333em;
--lh-1-4: 1.4em;
@ -15,6 +16,7 @@
--lh-2-0: 2.0em;
}
.lh-1 { line-height: var(--lh-1-0); }
.lh-solid { line-height: var(--lh-1-1); }
.lh-heading { line-height: var(--lh-1-3); }
.lh-title { line-height: var(--lh-1-4); }
@ -24,6 +26,7 @@
.lh-zero { line-height: 0; }
@media (--breakpoint-not-small) {
.lh-1-ns { line-height: var(--lh-1-0); }
.lh-solid-ns { line-height: var(--lh-1-1); }
.lh-heading-ns { line-height: var(--lh-1-3); }
.lh-title-ns { line-height: var(--lh-1-4); }
@ -34,6 +37,7 @@
}
@media (--breakpoint-medium) {
.lh-1-m { line-height: var(--lh-1-0); }
.lh-solid-m { line-height: var(--lh-1-1); }
.lh-heading-m { line-height: var(--lh-1-3); }
.lh-title-m { line-height: var(--lh-1-4); }
@ -44,6 +48,7 @@
}
@media (--breakpoint-large) {
.lh-1-l { line-height: var(--lh-1-0); }
.lh-solid-l { line-height: var(--lh-1-1); }
.lh-heading-l { line-height: var(--lh-1-3); }
.lh-title-l { line-height: var(--lh-1-4); }

View File

@ -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