mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
Added members feature to labs
no issue - Added new members settings/toggle to labs
This commit is contained in:
parent
779a6aba18
commit
57f9663b6e
@ -1,5 +1,6 @@
|
||||
import Component from '@ember/component';
|
||||
import {computed} from '@ember/object';
|
||||
import {computed, defineProperty} from '@ember/object';
|
||||
import {readOnly} from '@ember/object/computed';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const FeatureFlagComponent = Component.extend({
|
||||
@ -7,14 +8,21 @@ const FeatureFlagComponent = Component.extend({
|
||||
|
||||
tagName: 'label',
|
||||
classNames: 'checkbox',
|
||||
attributeBindings: ['for'],
|
||||
_flagValue: null,
|
||||
|
||||
attributeBindings: ['for', 'disabled'],
|
||||
disabled: computed('_disabled', function () {
|
||||
if (this.get('_disabled')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
value: computed('_flagValue', {
|
||||
get() {
|
||||
return this.get('_flagValue');
|
||||
},
|
||||
set(key, value) {
|
||||
if (this.get('flag') === 'members' && value === true) {
|
||||
this.set(`feature.subscribers`, false);
|
||||
}
|
||||
return this.set(`feature.${this.get('flag')}`, value);
|
||||
}
|
||||
}),
|
||||
@ -30,12 +38,14 @@ const FeatureFlagComponent = Component.extend({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.set('_flagValue', this.get(`feature.${this.get('flag')}`));
|
||||
defineProperty(this, '_flagValue', readOnly(`feature.${this.get('flag')}`), function () {
|
||||
return this.get(`feature.${this.get('flag')}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
FeatureFlagComponent.reopenClass({
|
||||
positionalParams: ['flag']
|
||||
positionalParams: ['flag', '_disabled']
|
||||
});
|
||||
|
||||
export default FeatureFlagComponent;
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
isRequestEntityTooLargeError,
|
||||
isUnsupportedMediaTypeError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
import {computed} from '@ember/object';
|
||||
import {isBlank} from '@ember/utils';
|
||||
import {isArray as isEmberArray} from '@ember/array';
|
||||
import {run} from '@ember/runloop';
|
||||
@ -45,6 +46,7 @@ export default Controller.extend({
|
||||
importErrors: null,
|
||||
importSuccessful: false,
|
||||
showDeleteAllModal: false,
|
||||
showMemberConfig: false,
|
||||
submitting: false,
|
||||
uploadButtonText: 'Import',
|
||||
|
||||
@ -53,7 +55,6 @@ export default Controller.extend({
|
||||
jsonMimeType: null,
|
||||
yamlExtension: null,
|
||||
yamlMimeType: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.importMimeType = IMPORT_MIME_TYPES;
|
||||
@ -63,6 +64,23 @@ export default Controller.extend({
|
||||
this.yamlMimeType = YAML_MIME_TYPE;
|
||||
},
|
||||
|
||||
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 = (monthlyPlan.amount / 100);
|
||||
yearlyPlan.dollarAmount = (yearlyPlan.amount / 100);
|
||||
stripeProcessor.config.plans = {
|
||||
monthly: monthlyPlan,
|
||||
yearly: yearlyPlan
|
||||
};
|
||||
subscriptionSettings.stripeConfig = stripeProcessor.config;
|
||||
return subscriptionSettings;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
onUpload(file) {
|
||||
let formData = new FormData();
|
||||
@ -143,6 +161,10 @@ export default Controller.extend({
|
||||
this.toggleProperty('showDeleteAllModal');
|
||||
},
|
||||
|
||||
toggleMemberConfig() {
|
||||
this.toggleProperty('showMemberConfig');
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a file selection dialog - Triggered by "Upload x" buttons,
|
||||
* searches for the hidden file input within the .gh-setting element
|
||||
@ -156,6 +178,66 @@ export default Controller.extend({
|
||||
.closest('.gh-setting-action')
|
||||
.find('input[type="file"]')
|
||||
.click();
|
||||
},
|
||||
|
||||
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.get('settings').get('title')
|
||||
};
|
||||
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 = event.target.value * 100;
|
||||
}
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
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.get('settings').get('title')
|
||||
},
|
||||
plans: [
|
||||
{
|
||||
name: 'Monthly',
|
||||
currency: 'usd',
|
||||
interval: 'month',
|
||||
amount: ''
|
||||
},
|
||||
{
|
||||
name: 'Yearly',
|
||||
currency: 'usd',
|
||||
interval: 'year',
|
||||
amount: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@ -213,6 +295,14 @@ export default Controller.extend({
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
saveSettings: task(function* () {
|
||||
try {
|
||||
return yield this.get('settings').save();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
redirectUploadResult: task(function* (success) {
|
||||
this.set('redirectSuccess', success);
|
||||
this.set('redirectFailure', !success);
|
||||
|
@ -29,5 +29,6 @@ export default Model.extend(ValidationEngine, {
|
||||
defaultValue() {
|
||||
return {isActive: true};
|
||||
}
|
||||
})
|
||||
}),
|
||||
membersSubscriptionSettings: attr('string')
|
||||
});
|
||||
|
@ -52,6 +52,7 @@ export default Service.extend({
|
||||
|
||||
publicAPI: feature('publicAPI'),
|
||||
subscribers: feature('subscribers'),
|
||||
members: feature('members'),
|
||||
nightShift: feature('nightShift', {user: true, onChange: '_setAdminTheme'}),
|
||||
|
||||
_user: null,
|
||||
|
@ -27,7 +27,7 @@ export default Service.extend(_ProxyMixin, ValidationEngine, {
|
||||
_loadSettings() {
|
||||
if (!this._loadingPromise) {
|
||||
this._loadingPromise = this.get('store')
|
||||
.queryRecord('setting', {type: 'blog,theme,private'})
|
||||
.queryRecord('setting', {type: 'blog,theme,private,members'})
|
||||
.then((settings) => {
|
||||
this._loadingPromise = null;
|
||||
return settings;
|
||||
|
@ -55,6 +55,7 @@
|
||||
@import "layouts/apps.css";
|
||||
@import "layouts/packages.css";
|
||||
@import "layouts/subscribers.css";
|
||||
@import "layouts/labs.css";
|
||||
|
||||
|
||||
:root {
|
||||
|
@ -55,6 +55,7 @@
|
||||
@import "layouts/apps.css";
|
||||
@import "layouts/packages.css";
|
||||
@import "layouts/subscribers.css";
|
||||
@import "layouts/labs.css";
|
||||
|
||||
|
||||
/* ---------------------------✈️----------------------------- */
|
||||
|
70
ghost/admin/app/styles/layouts/labs.css
Normal file
70
ghost/admin/app/styles/layouts/labs.css
Normal file
@ -0,0 +1,70 @@
|
||||
.gh-labs-price-label input {
|
||||
padding-right: 96px;
|
||||
}
|
||||
|
||||
.gh-labs-price-label input::-webkit-outer-spin-button, .gh-labs-price-label input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gh-labs-price-label input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
.gh-labs-price-label::after {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
color: var(--midlightgrey);
|
||||
}
|
||||
|
||||
.gh-labs-monthly-price::after {
|
||||
content: "USD/month";
|
||||
}
|
||||
|
||||
.gh-labs-yearly-price::after {
|
||||
content: "USD/year";
|
||||
}
|
||||
|
||||
.gh-labs-toggle-wrapper {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-btn-labs-toggle {
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.gh-btn-labs-toggle svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.gh-btn-labs-toggle svg path {
|
||||
stroke: var(--blue);
|
||||
}
|
||||
|
||||
.gh-labs-members-radio {
|
||||
cursor: pointer;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.gh-labs-members-radio.active {
|
||||
background: color-mod(var(--blue) alpha(6%));
|
||||
border-color: var(--blue);
|
||||
}
|
||||
|
||||
.gh-labs-disabled .gh-setting-content, .gh-labs-disabled .gh-setting-action {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.gh-labs-disabled .for-checkbox label {
|
||||
cursor: default;
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
<input type="checkbox" checked={{value}} id={{for}} name={{name}} onclick={{action (mut value) value="target.checked"}}>
|
||||
<input type="checkbox" checked={{value}} disabled={{disabled}} id={{for}} name={{name}} onclick={{action (mut value) value="target.checked"}}>
|
||||
<span class="input-toggle-component"></span>
|
||||
{{{yield}}}
|
||||
|
@ -102,15 +102,130 @@
|
||||
<div class="for-checkbox">{{gh-feature-flag "nightShift"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-setting">
|
||||
<div class="gh-setting {{if feature.members "gh-labs-disabled"}}">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Subscribers</div>
|
||||
<div class="gh-setting-desc">Collect email addresses from your readers, more info in <a href="https://docs.ghost.org/faq/enable-subscribers-feature/">the docs</a></div>
|
||||
<div class="gh-setting-desc">Collect email addresses from your readers, more info in <a
|
||||
href="https://docs.ghost.org/faq/enable-subscribers-feature/">the docs</a></div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<div class="for-checkbox">{{gh-feature-flag "subscribers"}}</div>
|
||||
{{#if feature.members}}
|
||||
<div class="for-checkbox">{{gh-feature-flag "subscribers" "disabled"}}</div>
|
||||
{{else}}
|
||||
<div class="for-checkbox">{{gh-feature-flag "subscribers" }}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if config.enableDeveloperExperiments}}
|
||||
<div class="gh-setting">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Members</div>
|
||||
<div class="gh-setting-desc">Enable free or paid member registration. Restrict content by using <span class="dib blue ba br2 b--blue pa1 pt0 pb0 tag-token--internal">#members</span> hashtag on posts</div>
|
||||
|
||||
{{#liquid-if feature.labs.members class="nr20"}}
|
||||
|
||||
<button type="button" class="gh-btn gh-btn-labs-toggle" {{action "toggleMemberConfig" ""}}>
|
||||
{{#if showMemberConfig}}
|
||||
{{svg-jar "arrow-down-small"}}
|
||||
{{else}}
|
||||
{{svg-jar "arrow-right-small"}}
|
||||
{{/if}}
|
||||
Configure
|
||||
</button>
|
||||
|
||||
{{#liquid-if showMemberConfig}}
|
||||
<div class="flex nl2 nr2 mt5">
|
||||
<div class="gh-publishmenu-radio {{if (eq subscriptionSettings.isPaid false) "active"}} flex-auto w-50 ba br4 b--whitegrey pa5 gh-labs-members-radio"
|
||||
{{action "setSubscriptionSettings" "isPaid" false on="click"}}>
|
||||
<div class="gh-publishmenu-radio-button"></div>
|
||||
<div class="gh-publishmenu-radio-content">
|
||||
<div class="gh-publishmenu-radio-label f3"><span class="fw6 f7 darkgrey">Free</span></div>
|
||||
<div class="gh-publishmenu-radio-desc">Access to members-only posts require free user registration</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-publishmenu-radio {{if (eq subscriptionSettings.isPaid true) "active"}} flex-auto w-50 ba br4 b--whitegrey pa5 ml2 gh-labs-members-radio">
|
||||
<div class="gh-publishmenu-radio-button" {{action "setSubscriptionSettings" "isPaid" true on="click"}}></div>
|
||||
<div class="gh-publishmenu-radio-content">
|
||||
<div {{action "setSubscriptionSettings" "isPaid" true on="click"}}>
|
||||
<div class="gh-publishmenu-radio-label"><span class="fw6 f7 darkgrey">Paid</span></div>
|
||||
<div class="gh-publishmenu-radio-desc">Set up paid subscriptions using Stripe</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#liquid-if (eq subscriptionSettings.isPaid true)}}
|
||||
|
||||
<div class="ba br4 b--whitegrey pa5 mt5 bg-whitegrey-l2">
|
||||
<div class="flex flex-column">
|
||||
<div class="flex">
|
||||
<div class="w-50 mr2">
|
||||
<label class="fw6">Stripe publishable API key</label>
|
||||
{{gh-text-input
|
||||
value=(readonly subscriptionSettings.stripeConfig.public_token)
|
||||
input=(action "setSubscriptionSettings" "public_token")
|
||||
class="mt1"
|
||||
placeholder="pk_..."
|
||||
}}
|
||||
</div>
|
||||
<div class="w-50 ml2">
|
||||
<label class="fw6">Stripe secret API key</label>
|
||||
{{gh-text-input
|
||||
value=(readonly subscriptionSettings.stripeConfig.secret_token)
|
||||
input=(action "setSubscriptionSettings" "secret_token")
|
||||
class="mt1"
|
||||
placeholder="sk_..."
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://stripe.com/docs/keys" target="_blank" class="mt1 self-end fw3">How to find Stripe API keys</a>
|
||||
</div>
|
||||
|
||||
<div class="mt4 flex nb5">
|
||||
<div class="w-50 mr2">
|
||||
{{#gh-form-group}}
|
||||
<label class="fw6">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">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>
|
||||
{{/liquid-if}}
|
||||
|
||||
<div class="mb5 mt5">
|
||||
{{gh-task-button "Save"
|
||||
task=saveSettings
|
||||
successText="Saved"
|
||||
runningText="Saving"
|
||||
class="gh-btn gh-btn-blue gh-btn-icon"
|
||||
}}
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<div class="for-checkbox">{{gh-feature-flag "members"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="gh-setting">
|
||||
{{#gh-uploader
|
||||
extensions=jsonExtension
|
||||
|
Loading…
Reference in New Issue
Block a user