Merge branch 'master' into v3

This commit is contained in:
Kevin Ansfield 2019-10-09 15:06:37 +01:00
commit facea619ee
9 changed files with 262 additions and 180 deletions

View File

@ -0,0 +1,107 @@
import Component from '@ember/component';
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
export default Component.extend({
feature: service(),
config: service(),
mediaQueries: service(),
subscriptionSettings: computed('settings.membersSubscriptionSettings', function () {
let subscriptionSettings = this.parseSubscriptionSettings(this.get('settings.membersSubscriptionSettings'));
let stripeProcessor = subscriptionSettings.paymentProcessors.find((proc) => {
return (proc.adapter === 'stripe');
});
let monthlyPlan = stripeProcessor.config.plans.find(plan => plan.interval === 'month');
let yearlyPlan = stripeProcessor.config.plans.find(plan => plan.interval === 'year');
monthlyPlan.dollarAmount = parseInt(monthlyPlan.amount) ? (monthlyPlan.amount / 100) : 0;
yearlyPlan.dollarAmount = parseInt(yearlyPlan.amount) ? (yearlyPlan.amount / 100) : 0;
stripeProcessor.config.plans = {
monthly: monthlyPlan,
yearly: yearlyPlan
};
subscriptionSettings.stripeConfig = stripeProcessor.config;
subscriptionSettings.requirePaymentForSetup = !!subscriptionSettings.requirePaymentForSetup;
subscriptionSettings.fromAddress = subscriptionSettings.fromAddress || 'noreply';
return subscriptionSettings;
}),
defaultContentVisibility: computed('settings.defaultContentVisibility', function () {
return this.get('settings.defaultContentVisibility');
}),
actions: {
setDefaultContentVisibility(value) {
this.setDefaultContentVisibility(value);
},
setSubscriptionSettings(key, event) {
let subscriptionSettings = this.parseSubscriptionSettings(this.get('settings.membersSubscriptionSettings'));
let stripeProcessor = subscriptionSettings.paymentProcessors.find((proc) => {
return (proc.adapter === 'stripe');
});
let stripeConfig = stripeProcessor.config;
stripeConfig.product = {
name: this.settings.get('title')
};
// TODO: this flag has to be removed as it doesn't serve any purpose
if (key === 'isPaid') {
subscriptionSettings.isPaid = event;
}
if (key === 'secret_token' || key === 'public_token') {
stripeConfig[key] = event.target.value;
}
if (key === 'month' || key === 'year') {
stripeConfig.plans = stripeConfig.plans.map((plan) => {
if (key === plan.interval) {
plan.amount = parseInt(event.target.value) ? (event.target.value * 100) : 0;
}
return plan;
});
}
if (key === 'requirePaymentForSignup') {
subscriptionSettings.requirePaymentForSignup = !subscriptionSettings.requirePaymentForSignup;
}
if (key === 'fromAddress') {
subscriptionSettings.fromAddress = event.target.value;
}
this.setMembersSubscriptionSettings(subscriptionSettings);
}
},
parseSubscriptionSettings(settingsString) {
try {
return JSON.parse(settingsString);
} catch (e) {
return {
isPaid: false,
requirePaymentForSignup: false,
fromAddress: 'noreply',
paymentProcessors: [{
adapter: 'stripe',
config: {
secret_token: '',
public_token: '',
product: {
name: this.settings.get('title')
},
plans: [
{
name: 'Monthly',
currency: 'usd',
interval: 'month',
amount: ''
},
{
name: 'Yearly',
currency: 'usd',
interval: 'year',
amount: ''
}
]
}
}]
};
}
}
});

View File

@ -46,6 +46,8 @@ export default Component.extend(SettingsMenuMixin, {
twitterImage: or('post.twitterImage', 'post.featureImage'),
twitterTitle: or('twitterTitleScratch', 'seoTitle'),
showVisibilityInput: or('session.user.isOwner', 'session.user.isAdmin', 'session.user.isEditor'),
seoTitle: computed('metaTitleScratch', 'post.titleScratch', function () {
return this.metaTitleScratch || this.post.titleScratch || '(Untitled)';
}),

View File

@ -460,9 +460,14 @@ export default Controller.extend({
// NOTE: `updateTags` changes `hasDirtyAttributes => true`.
// For a saved post it would otherwise be false.
post.updateTags();
this._previousTagNames = this._tagNames;
// update the scratch property if it's `null` and we get a blank mobiledoc
// back from the API - prevents "unsaved changes" modal on new+blank posts
if (!post.scratch) {
post.set('scratch', JSON.parse(JSON.stringify(post.get('mobiledoc'))));
}
// if the two "scratch" properties (title and content) match the post,
// then it's ok to set hasDirtyAttributes to false
// TODO: why is this necessary?

View File

@ -78,6 +78,7 @@ export default Controller.extend({
};
subscriptionSettings.stripeConfig = stripeProcessor.config;
subscriptionSettings.requirePaymentForSetup = !!subscriptionSettings.requirePaymentForSetup;
subscriptionSettings.fromAddress = subscriptionSettings.fromAddress || 'noreply';
return subscriptionSettings;
}),
@ -180,71 +181,11 @@ export default Controller.extend({
this.set('settings.defaultContentVisibility', value);
},
setSubscriptionSettings(key, event) {
let subscriptionSettings = this.parseSubscriptionSettings(this.get('settings.membersSubscriptionSettings'));
let stripeProcessor = subscriptionSettings.paymentProcessors.find((proc) => {
return (proc.adapter === 'stripe');
});
let stripeConfig = stripeProcessor.config;
stripeConfig.product = {
name: this.settings.get('title')
};
// TODO: this flag has to be removed as it doesn't serve any purpose
if (key === 'isPaid') {
subscriptionSettings.isPaid = event;
}
if (key === 'secret_token' || key === 'public_token') {
stripeConfig[key] = event.target.value;
}
if (key === 'month' || key === 'year') {
stripeConfig.plans = stripeConfig.plans.map((plan) => {
if (key === plan.interval) {
plan.amount = parseInt(event.target.value) ? (event.target.value * 100) : 0;
}
return plan;
});
}
if (key === 'requirePaymentForSignup') {
subscriptionSettings.requirePaymentForSignup = !subscriptionSettings.requirePaymentForSignup;
}
setMembersSubscriptionSettings(subscriptionSettings) {
this.set('settings.membersSubscriptionSettings', JSON.stringify(subscriptionSettings));
}
},
parseSubscriptionSettings(settingsString) {
try {
return JSON.parse(settingsString);
} catch (e) {
return {
isPaid: false,
paymentProcessors: [{
adapter: 'stripe',
config: {
secret_token: '',
public_token: '',
product: {
name: this.settings.get('title')
},
plans: [
{
name: 'Monthly',
currency: 'usd',
interval: 'month',
amount: ''
},
{
name: 'Yearly',
currency: 'usd',
interval: 'year',
amount: ''
}
]
}
}]
};
}
},
// TODO: convert to ember-concurrency task
_validate(file) {
// Windows doesn't have mime-types for json files by default, so we

View File

@ -38,6 +38,11 @@
margin: 0 50px 0 0;
}
.gh-members-setting-content {
width: 100%;
margin: 0;
}
.gh-setting-content--no-action {
margin: 0;
}

View File

@ -0,0 +1,106 @@
<div class="flex flex-column b--whitegrey bt">
<section class="flex flex-column bb b--whitegrey pa5">
<div class="w-50 mb4">
<label class="fw6 f6">Stripe publishable API key</label>
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.public_token)
input=(action "setSubscriptionSettings" "public_token")
class="mt1"
}}
</div>
<div class="w-50 mb4">
<label class="fw6 f6 mt4">Stripe secret API key</label>
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.secret_token)
input=(action "setSubscriptionSettings" "secret_token")
class="mt1"
}}
<a href="https://dashboard.stripe.com/account/apikeys" target="_blank" class="mt1 fw4 f8">
Where to find Stripe API keys
</a>
</div>
<div class="w-50 flex nb5">
<div class="w-50 mr3">
{{#gh-form-group}}
<label class="fw6 f6">Monthly price</label>
<div class="mt1 relative gh-labs-price-label gh-labs-monthly-price">
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.plans.monthly.dollarAmount)
type="number"
input=(action "setSubscriptionSettings" "month")
}}
</div>
{{/gh-form-group}}
</div>
<div class="w-50 ml2">
{{#gh-form-group class="description-container"}}
<label class="fw6 f6">Yearly price</label>
<div class="mt1 relative gh-labs-price-label gh-labs-yearly-price">
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.plans.yearly.dollarAmount)
type="number"
input=(action "setSubscriptionSettings" "year")
}}
</div>
{{/gh-form-group}}
</div>
</div>
</section>
<section class="flex flex-column bb b--whitegrey pa5">
<label class="dib f6 fw6 mb4">Default post access</label>
<div class="gh-radio {{if (eq settings.defaultContentVisibility "public") "active"}}"
{{action "setDefaultContentVisibility" "public" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-unpublished-option></div>
<div class="gh-radio-content">
<div class="gh-radio-label">Public</div>
</div>
</div>
<div class="gh-radio {{if (eq settings.defaultContentVisibility "members") "active"}}"
{{action "setDefaultContentVisibility" "members" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-published-option></div>
<div class="gh-radio-content">
<div class="gh-radio-label">Members only</div>
</div>
</div>
<div class="gh-radio {{if (eq settings.defaultContentVisibility "paid") "active"}}"
{{action "setDefaultContentVisibility" "paid" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-published-option></div>
<div class="gh-radio-content">
<div class="gh-radio-label">Paid-members only</div>
</div>
</div>
</section>
<div class="flex flex-column bb b--whitegrey pa5">
<div class="for-checkbox">
<label class="checkbox flex items-center" for="members-require-payment"
{{action "setSubscriptionSettings" "requirePaymentForSignup" bubbles="false"}}>
<span class="f6 fw6"> Require payment for signups</span>
<input type="checkbox" checked={{subscriptionSettings.requirePaymentForSignup}} class="gh-input"
onclick={{action "setSubscriptionSettings" "requirePaymentForSignup"}}
data-test-checkbox="members-require-payment">
<span class="input-toggle-component mt1"></span>
</label>
</div>
</div>
<div class="flex flex-column pl5 pr5 pt5">
<div class="w-50 mr3">
{{#gh-form-group}}
<label class="dib f6 fw6 mb4">Email</label>
<div class="flex items-center justify-center">
{{gh-text-input
value=(readonly subscriptionSettings.fromAddress)
input=(action "setSubscriptionSettings" "fromAddress")
}}
<span class="ml3"> @{{config.blogDomain}}</span>
</div>
<div class="f6 fw4"> "From" address for sending sign up and sign in emails</div>
{{/gh-form-group}}
</div>
</div>
</div>

View File

@ -77,12 +77,12 @@
{{#if feature.members}}
{{#unless session.user.isContributor}}
{{#if showVisibilityInput}}
<div class="form-group">
<label for="visibility-input">Post access</label>
{{gh-psm-visibility-input post=post triggerId="visibility-input"}}
</div>
{{/unless}}
{{/if}}
{{/if}}

View File

@ -86,126 +86,42 @@
</div>
</div>
</div>
<div class="gh-setting-header">Members</div>
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">
{{#if config.enableDeveloperExperiments}}
<div class="gh-setting-first gh-setting-last">
<div class="gh-setting-content">
<div class="gh-setting-title">Members</div>
<div class="gh-setting-desc">Enable free or paid member registration.</div>
{{#liquid-if feature.labs.members class="nr25"}}
<div class="flex ba br4 b--whitegrey pa5 pt4 mt5">
<section class="flex flex-column">
<label class="fw6 f8">Stripe publishable API key</label>
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.public_token)
input=(action "setSubscriptionSettings" "public_token")
class="mt1"
}}
<label class="fw6 f8 mt4">Stripe secret API key</label>
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.secret_token)
input=(action "setSubscriptionSettings" "secret_token")
class="mt1"
}}
<a href="https://dashboard.stripe.com/account/apikeys" target="_blank" class="mt1 fw4 f8">Where to find Stripe
API keys</a>
<div class="mt5 flex nb5">
<div class="w-50 mr3">
{{#gh-form-group}}
<label class="fw6 f8">Monthly price</label>
<div class="mt1 relative gh-labs-price-label gh-labs-monthly-price">
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.plans.monthly.dollarAmount)
type="number"
input=(action "setSubscriptionSettings" "month")
}}
</div>
{{/gh-form-group}}
</div>
<div class="w-50 ml2">
{{#gh-form-group class="description-container"}}
<label class="fw6 f8">Yearly price</label>
<div class="mt1 relative gh-labs-price-label gh-labs-yearly-price">
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.plans.yearly.dollarAmount)
type="number"
input=(action "setSubscriptionSettings" "year")
}}
</div>
{{/gh-form-group}}
</div>
</div>
<div class="mt5 flex nb5">
{{#gh-form-group class="require-payment-container"}}
<div class="form-group for-checkbox">
<label class="checkbox" for="members-require-payment" {{action "setSubscriptionSettings" "requirePaymentForSignup" bubbles="false"}}>
<input
type="checkbox"
checked={{subscriptionSettings.requirePaymentForSignup}}
class="gh-input"
onclick={{action "setSubscriptionSettings" "requirePaymentForSignup"}}
data-test-checkbox="members-require-payment"
>
<span class="input-toggle-component"></span>
<p> Require payment before signing up members</p>
</label>
</div>
{{/gh-form-group}}
</div>
</section>
<section class="gh-visibility-menu-content w-50 ml10">
<label class="dib f8 fw6 mb4">Default post access</label>
<div class="gh-radio {{if (eq settings.defaultContentVisibility "public") "active"}}"
{{action "setDefaultContentVisibility" "public" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-unpublished-option></div>
<div class="gh-radio-content">
<div class="gh-radio-label">Public</div>
</div>
</div>
<div class="gh-radio {{if (eq settings.defaultContentVisibility "members") "active"}}"
{{action "setDefaultContentVisibility" "members" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-published-option></div>
<div class="gh-radio-content">
<div class="gh-radio-label">Members only</div>
</div>
</div>
<div class="gh-radio {{if (eq settings.defaultContentVisibility "paid") "active"}}"
{{action "setDefaultContentVisibility" "paid" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-published-option></div>
<div class="gh-radio-content">
<div class="gh-radio-label">Paid-members only</div>
</div>
</div>
</section>
{{#if config.enableDeveloperExperiments}}
<div class="gh-setting-header">Members (BETA) </div>
<div class="flex flex-column br3 shadow-1 bg-grouped-table mt2">
<div class="gh-setting-first gh-setting-last">
<div class="gh-members-setting-content">
<div class="flex">
<div class="flex flex-column flex-grow-1">
<div class="gh-setting-title pl5 pt5">Members</div>
<div class="gh-setting-desc pl5 pb5">Enable free or paid member registration.</div>
</div>
<div class="gh-setting-action">
<div class="for-switch pa5">{{gh-feature-flag "members"}}</div>
</div>
</div>
<div class="mb2 mt5">
{{gh-task-button "Save members settings"
task=saveSettings
successText="Saved"
runningText="Saving"
class="gh-btn gh-btn-blue gh-btn-icon"
{{#liquid-if feature.labs.members}}
{{gh-members-lab-setting
settings=settings
setDefaultContentVisibility=(action "setDefaultContentVisibility")
setMembersSubscriptionSettings=(action "setMembersSubscriptionSettings")
}}
</div>
{{/liquid-if}}
<div class="mb2 mt5 pl5 pr5 pb5">
{{gh-task-button "Save members settings"
task=saveSettings
successText="Saved"
runningText="Saving"
class="gh-btn gh-btn-blue gh-btn-icon"
}}
</div>
{{/liquid-if}}
</div>
</div>
<div class="gh-setting-action">
<div class="for-switch">{{gh-feature-flag "members"}}</div>
</div>
</div>
{{/if}}
</div>
</div>
{{/if}}
<div class="gh-setting-header">Beta features</div>
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">

View File

@ -177,7 +177,7 @@ export default [
id: 23,
type: 'members',
key: 'members_subscription_settings',
value: '{"isPaid":false,"paymentProcessors":[{"adapter":"stripe","config":{"secret_token":"","public_token":"","product":{"name":"Ghost Subscription"},"plans":[{"name":"Monthly","currency":"usd","interval":"month","amount":""},{"name":"Yearly","currency":"usd","interval":"year","amount":""}]}}]}',
value: '{"isPaid":false,"requirePaymentForSignup":false,"fromAddress":"noreply","paymentProcessors":[{"adapter":"stripe","config":{"secret_token":"","public_token":"","product":{"name":"Ghost Subscription"},"plans":[{"name":"Monthly","currency":"usd","interval":"month","amount":""},{"name":"Yearly","currency":"usd","interval":"year","amount":""}]}}]}',
created_at: '2019-10-09T09:49:00.000Z',
created_by: 1,
updated_at: '2019-10-09T09:49:00.000Z',