mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 10:53:34 +03:00
Renamed products to tiers (#2372)
refs: https://github.com/TryGhost/Team/issues/1145 - this should allow us to remove the /products endpoint in v5 It avoids: - `kg-product-card`, that really is meant to say product - `product-cadence` on offers Co-authored-by: Rishabh <zrishabhgarg@gmail.com>
This commit is contained in:
parent
e852c29699
commit
affe6743e5
@ -1088,3 +1088,51 @@ add|ember-template-lint|require-valid-alt-text|3|44|3|44|a7f0566c430150bae4153e0
|
||||
add|ember-template-lint|require-valid-alt-text|8|20|8|20|9d0c591086dc9139ff38a7b385c3367a83438786|1652054400000|1662422400000|1665014400000|lib/koenig-editor/addon/components/koenig-card-embed/nft.hbs
|
||||
add|ember-template-lint|require-input-label|10|12|10|12|8c3c0ea315ff4da828363989a45fa11256a78796|1652054400000|1662422400000|1665014400000|lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.hbs
|
||||
remove|ember-template-lint|require-valid-alt-text|5|4|5|4|527936d4c6b3d34855a99669f0b8ae690094bc8e|1652054400000|1662422400000|1665014400000|app/components/gh-member-avatar.hbs
|
||||
add|ember-template-lint|no-action|38|107|38|107|79d2eeaed67e929e989261416abd3cd2dbbf4861|1652140800000|1662508800000|1667692800000|app/components/gh-membership-tiers-alpha.hbs
|
||||
add|ember-template-lint|no-action|125|48|125|48|01f579966c325cf66d3a87799e8cea7dd1fded81|1652140800000|1662508800000|1667692800000|app/components/gh-portal-links.hbs
|
||||
add|ember-template-lint|no-action|148|54|148|54|884d0df74365d794c99bb098e72dc267bd0fbc46|1652140800000|1662508800000|1667692800000|app/components/gh-portal-links.hbs
|
||||
add|ember-template-lint|no-action|175|54|175|54|b51c80e0c3de828988f74017ab7e383552f48648|1652140800000|1662508800000|1667692800000|app/components/gh-portal-links.hbs
|
||||
remove|ember-template-lint|no-action|125|48|125|48|c4ecbbcf9092307d8bb2cd5c9e386d2e549a9f26|1652054400000|1662422400000|1665014400000|app/components/gh-portal-links.hbs
|
||||
remove|ember-template-lint|no-action|148|54|148|54|e7798f471d8eb8d396fa02de55fc9710ad783158|1652054400000|1662422400000|1665014400000|app/components/gh-portal-links.hbs
|
||||
remove|ember-template-lint|no-action|175|54|175|54|0f158251c4558c601318d60d1237d5e2c3d2bd60|1652054400000|1662422400000|1665014400000|app/components/gh-portal-links.hbs
|
||||
add|ember-template-lint|no-invalid-interactive|28|24|28|24|42a29ae16e22270f0590c9ce5caa7bfec541ca0b|1652140800000|1662508800000|1667692800000|app/components/modal-member-tier.hbs
|
||||
add|ember-template-lint|no-action|88|77|88|77|437199817818c66e4539bbc8501bd73148b521fd|1652140800000|1662508800000|1667692800000|app/components/modal-portal-settings.hbs
|
||||
remove|ember-template-lint|no-action|88|77|88|77|9e5e50275cb198c163f5da82335957b69255ac1a|1652054400000|1662422400000|1665014400000|app/components/modal-portal-settings.hbs
|
||||
add|ember-template-lint|no-action|4|55|4|55|141d456b03124abca146e58e4ae15825fdd040bb|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-action|4|79|4|79|d465b362b15b90cf42a093e72895155f49cdf6f2|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-action|16|27|16|27|64b13853afb27bd49f8fc782b7d19786a502e8d1|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-action|26|27|26|27|8291585f81599303bc73e45cb65d725f054ae414|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-action|41|35|41|35|0d52869a7137b90cd482896151c974aff53287f8|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-action|56|35|56|35|9d61f56816a4b85a97cf9ba7af28bbcf08f29d05|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-action|65|34|65|34|40e7337ccf8501ef3b2fea94812d4de8435286da|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-action|78|71|78|71|141d456b03124abca146e58e4ae15825fdd040bb|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-action|80|8|80|8|d465b362b15b90cf42a093e72895155f49cdf6f2|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-down-event-binding|4|112|4|112|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-down-event-binding|80|41|80|41|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-passed-in-event-handlers|16|20|16|20|22bf28ec6b8ce5c52ffdb872a465b684319bb7c3|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-passed-in-event-handlers|26|20|26|20|4b2258e98403a4296a1796a6c7f2639f8bb5fb9c|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-passed-in-event-handlers|41|28|41|28|c74704c059987f5454146dd0efc9cbc9e0279b7c|1652140800000|1662508800000|1667692800000|app/components/modal-tier-price.hbs
|
||||
add|ember-template-lint|no-action|21|39|21|39|c1c116347fcaf9f69d60fa7ae58d746e9994cdbd|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|36|39|36|39|7307d2c5d78fa732b959ced6823b3d82dcc07446|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|45|39|45|39|7307d2c5d78fa732b959ced6823b3d82dcc07446|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|73|52|73|52|fa36149ee581ce634903cef877ccfdc133aa67ec|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|85|47|85|47|dcc09bb23a476d5b83b273b693cd8cb2aba68365|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|95|47|95|47|a80dd18e18dda6fb6f1f97d87bef2b8c2ce3d847|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|135|47|135|47|73ac7d3892fcbcf15c3d5c44fca14dd21016daea|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|141|53|141|53|c76b92d7bdb6ed498238b647928748aa4146dc24|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|142|55|142|55|02efc45b808f4cfdbe5f7b72b784337aef4e98a7|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|143|56|143|56|4227b643fe44a6c343b819bbee7eecdfb7916ccc|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|144|57|144|57|115cc8adc4c0f98e2d93a59ff8efbee711541336|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|152|41|152|41|c76b92d7bdb6ed498238b647928748aa4146dc24|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|153|44|153|44|4227b643fe44a6c343b819bbee7eecdfb7916ccc|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|154|45|154|45|115cc8adc4c0f98e2d93a59ff8efbee711541336|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|269|71|269|71|141d456b03124abca146e58e4ae15825fdd040bb|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|271|8|271|8|d465b362b15b90cf42a093e72895155f49cdf6f2|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-down-event-binding|271|41|271|41|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-passed-in-event-handlers|21|32|21|32|f69395e36c890a23e3f603ad3fd2cb384932af93|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-passed-in-event-handlers|36|32|36|32|86b5983929a27ca8d458ff051c95a50a406fbe57|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-passed-in-event-handlers|45|32|45|32|86b5983929a27ca8d458ff051c95a50a406fbe57|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-passed-in-event-handlers|85|40|85|40|dcb4785647a50814bcfce82f8d68ac8dd8f54ec2|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-passed-in-event-handlers|95|40|95|40|70487c008d7dda453fef82f0140699ee93c0055c|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|style-concatenation|205|54|205|54|23293f0c3838b23432d2b2daaf04b34504896d91|1652140800000|1662508800000|1667692800000|app/components/modal-tier.hbs
|
||||
add|ember-template-lint|no-action|12|16|12|16|40e7337ccf8501ef3b2fea94812d4de8435286da|1652227200000|1662595200000|1667779200000|app/components/gh-tiers-price-billingperiod.hbs
|
||||
|
@ -2,7 +2,8 @@ import ApplicationAdapter from 'ghost-admin/adapters/application';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
export default class Product extends ApplicationAdapter {
|
||||
|
||||
export default class Tier extends ApplicationAdapter {
|
||||
queryRecord(store, type, query) {
|
||||
if (query && query.id) {
|
||||
let {id} = query;
|
@ -31,16 +31,16 @@ export default class DashboardMembersGraphs extends Component {
|
||||
}
|
||||
|
||||
async loadMRRStats() {
|
||||
const products = await this.store.query('product', {
|
||||
const tiers = await this.store.query('tier', {
|
||||
filter: 'type:paid', include: 'monthly_price,yearly_price', limit: 'all'
|
||||
});
|
||||
const defaultProduct = products?.firstObject;
|
||||
const defaultTier = tiers?.firstObject;
|
||||
|
||||
this.mrrStatsLoading = true;
|
||||
this.membersStats.fetchMRR().then((stats) => {
|
||||
this.mrrStatsLoading = false;
|
||||
const statsData = stats.data || [];
|
||||
const defaultCurrency = defaultProduct?.monthlyPrice?.currency || 'usd';
|
||||
const defaultCurrency = defaultTier?.monthlyPrice?.currency || 'usd';
|
||||
let currencyStats = statsData.find((stat) => {
|
||||
return stat.currency === defaultCurrency;
|
||||
});
|
||||
|
@ -16,7 +16,7 @@
|
||||
>
|
||||
<GhTrimFocusInput
|
||||
@shouldFocus={{this.benefitItem.last}}
|
||||
@placeholder={{if this.isFreeProduct "Access to all public posts" "Expert analysis"}}
|
||||
@placeholder={{if this.isFreeTier "Access to all public posts" "Expert analysis"}}
|
||||
@value={{readonly this.name}}
|
||||
@input={{action "updateLabel" value="target.value"}}
|
||||
@keyPress={{action "clearLabelErrors"}}
|
||||
|
@ -81,17 +81,17 @@ export default class GhLaunchWizardConnectStripeComponent extends Component {
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*saveProduct() {
|
||||
*saveTier() {
|
||||
let pollTimeout = 0;
|
||||
while (pollTimeout < RETRY_PRODUCT_SAVE_MAX_POLL) {
|
||||
yield timeout(RETRY_PRODUCT_SAVE_POLL_LENGTH);
|
||||
|
||||
try {
|
||||
const updatedProduct = yield this.product.save();
|
||||
const updatedTier = yield this.tier.save();
|
||||
|
||||
yield this.settings.save();
|
||||
|
||||
return updatedProduct;
|
||||
return updatedTier;
|
||||
} catch (error) {
|
||||
if (error.payload?.errors && error.payload.errors[0].code === 'STRIPE_NOT_CONFIGURED') {
|
||||
pollTimeout += RETRY_PRODUCT_SAVE_POLL_LENGTH;
|
||||
@ -102,7 +102,7 @@ export default class GhLaunchWizardConnectStripeComponent extends Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.product;
|
||||
return this.tier;
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
@ -161,12 +161,12 @@ export default class GhLaunchWizardConnectStripeComponent extends Component {
|
||||
try {
|
||||
yield this.settings.save();
|
||||
|
||||
const products = yield this.store.query('product', {filter: 'type:paid', include: 'monthly_price,yearly_price'});
|
||||
this.product = products.firstObject;
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', include: 'monthly_price,yearly_price'});
|
||||
this.tier = tiers.firstObject;
|
||||
|
||||
if (this.product) {
|
||||
if (this.tier) {
|
||||
const yearlyDiscount = this.calculateDiscount(5, 50);
|
||||
this.product.set('monthlyPrice', {
|
||||
this.tier.set('monthlyPrice', {
|
||||
nickname: 'Monthly',
|
||||
amount: 500,
|
||||
active: 1,
|
||||
@ -175,7 +175,7 @@ export default class GhLaunchWizardConnectStripeComponent extends Component {
|
||||
interval: 'month',
|
||||
type: 'recurring'
|
||||
});
|
||||
this.product.set('yearlyPrice', {
|
||||
this.tier.set('yearlyPrice', {
|
||||
nickname: 'Yearly',
|
||||
amount: 5000,
|
||||
active: 1,
|
||||
@ -184,7 +184,7 @@ export default class GhLaunchWizardConnectStripeComponent extends Component {
|
||||
interval: 'year',
|
||||
type: 'recurring'
|
||||
});
|
||||
yield this.saveProduct.perform();
|
||||
yield this.saveTier.perform();
|
||||
this.settings.set('portalPlans', ['free', 'monthly', 'yearly']);
|
||||
yield this.settings.save();
|
||||
}
|
||||
|
@ -15,10 +15,10 @@ export default class GhLaunchWizardFinaliseComponent extends Component {
|
||||
this.settings.rollbackAttributes();
|
||||
}
|
||||
|
||||
async saveProduct() {
|
||||
async saveTier() {
|
||||
const data = this.args.getData();
|
||||
this.product = data?.product;
|
||||
if (this.product) {
|
||||
this.tier = data?.tier;
|
||||
if (this.tier) {
|
||||
const monthlyAmount = Math.round(data.monthlyAmount * 100);
|
||||
const yearlyAmount = Math.round(data.yearlyAmount * 100);
|
||||
const currency = data.currency;
|
||||
@ -38,18 +38,18 @@ export default class GhLaunchWizardFinaliseComponent extends Component {
|
||||
interval: 'year',
|
||||
type: 'recurring'
|
||||
};
|
||||
this.product.set('monthlyPrice', monthlyPrice);
|
||||
this.product.set('yearlyPrice', yearlyPrice);
|
||||
const savedProduct = await this.product.save();
|
||||
return savedProduct;
|
||||
this.tier.set('monthlyPrice', monthlyPrice);
|
||||
this.tier.set('yearlyPrice', yearlyPrice);
|
||||
const savedTier = await this.tier.save();
|
||||
return savedTier;
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
*finaliseTask() {
|
||||
const data = this.args.getData();
|
||||
if (data?.product) {
|
||||
yield this.saveProduct();
|
||||
if (data?.tier) {
|
||||
yield this.saveTier();
|
||||
this.settings.set('editorIsLaunchComplete', true);
|
||||
yield this.settings.save();
|
||||
}
|
||||
|
@ -29,8 +29,8 @@ export default class GhLaunchWizardSetPricingComponent extends Component {
|
||||
@tracked isMonthlyChecked = true;
|
||||
@tracked isYearlyChecked = true;
|
||||
@tracked stripePlanError = '';
|
||||
@tracked product;
|
||||
@tracked loadingProduct = false;
|
||||
@tracked tier;
|
||||
@tracked loadingTier = false;
|
||||
|
||||
get selectedCurrency() {
|
||||
return this.currencies.findBy('value', this.currency);
|
||||
@ -62,17 +62,17 @@ export default class GhLaunchWizardSetPricingComponent extends Component {
|
||||
|
||||
@action
|
||||
setup() {
|
||||
this.fetchDefaultProduct.perform();
|
||||
this.fetchDefaultTier.perform();
|
||||
this.updatePreviewUrl();
|
||||
}
|
||||
|
||||
@action
|
||||
backStep() {
|
||||
const product = this.product;
|
||||
const tier = this.tier;
|
||||
const data = this.args.getData() || {};
|
||||
this.args.storeData({
|
||||
...data,
|
||||
product,
|
||||
tier,
|
||||
isFreeChecked: this.isFreeChecked,
|
||||
isMonthlyChecked: this.isMonthlyChecked,
|
||||
isYearlyChecked: this.isYearlyChecked,
|
||||
@ -136,11 +136,11 @@ export default class GhLaunchWizardSetPricingComponent extends Component {
|
||||
if (this.stripePlanError) {
|
||||
return false;
|
||||
}
|
||||
const product = this.product;
|
||||
const tier = this.tier;
|
||||
const data = this.args.getData() || {};
|
||||
this.args.storeData({
|
||||
...data,
|
||||
product,
|
||||
tier,
|
||||
isFreeChecked: this.isFreeChecked,
|
||||
isMonthlyChecked: this.isMonthlyChecked,
|
||||
isYearlyChecked: this.isYearlyChecked,
|
||||
@ -153,10 +153,10 @@ export default class GhLaunchWizardSetPricingComponent extends Component {
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*fetchDefaultProduct() {
|
||||
*fetchDefaultTier() {
|
||||
const storedData = this.args.getData();
|
||||
if (storedData?.product) {
|
||||
this.product = storedData.product;
|
||||
if (storedData?.tier) {
|
||||
this.tier = storedData.tier;
|
||||
|
||||
if (storedData.isMonthlyChecked !== undefined) {
|
||||
this.isMonthlyChecked = storedData.isMonthlyChecked;
|
||||
@ -173,8 +173,8 @@ export default class GhLaunchWizardSetPricingComponent extends Component {
|
||||
this.stripeMonthlyAmount = storedData.monthlyAmount;
|
||||
this.stripeYearlyAmount = storedData.yearlyAmount;
|
||||
} else {
|
||||
const products = yield this.store.query('product', {filter: 'type:paid', include: 'monthly_price,yearly_price'});
|
||||
this.product = products.firstObject;
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', include: 'monthly_price,yearly_price'});
|
||||
this.tier = tiers.firstObject;
|
||||
|
||||
let portalPlans = this.settings.get('portalPlans') || [];
|
||||
|
||||
@ -182,8 +182,8 @@ export default class GhLaunchWizardSetPricingComponent extends Component {
|
||||
this.isYearlyChecked = portalPlans.includes('yearly');
|
||||
this.isFreeChecked = portalPlans.includes('free');
|
||||
|
||||
const monthlyPrice = this.product.get('monthlyPrice');
|
||||
const yearlyPrice = this.product.get('yearlyPrice');
|
||||
const monthlyPrice = this.tier.get('monthlyPrice');
|
||||
const yearlyPrice = this.tier.get('yearlyPrice');
|
||||
if (monthlyPrice && monthlyPrice.amount) {
|
||||
this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
|
||||
this.currency = monthlyPrice.currency;
|
||||
|
@ -103,9 +103,9 @@
|
||||
{{#if this.isStripeConnected}}
|
||||
<h4 class="gh-main-section-header small bn">Subscriptions</h4>
|
||||
|
||||
{{#unless this.products}}
|
||||
{{#unless this.tiers}}
|
||||
<div class="gh-main-section-content grey">
|
||||
<div class="gh-cp-memberproduct-noproduct">
|
||||
<div class="gh-cp-membertier-notier">
|
||||
{{#unless this.isCreatingComplimentary}}
|
||||
<div class="gh-members-no-data gh-members-no-subs">
|
||||
<span class="lightgrey">{{svg-jar "no-data-subscription"}}</span>
|
||||
@ -119,8 +119,8 @@
|
||||
{{else}}
|
||||
<button
|
||||
type="button"
|
||||
class="gh-btn gh-btn-text green gh-btn-icon gh-btn-addproduct"
|
||||
{{on "click" (toggle-action "showMemberProductModal" this)}}
|
||||
class="gh-btn gh-btn-text green gh-btn-icon gh-btn-addtier"
|
||||
{{on "click" (toggle-action "showMemberTierModal" this)}}
|
||||
data-test-button="add-complimentary"
|
||||
>
|
||||
<span>{{svg-jar "add"}} Add complimentary subscription</span>
|
||||
@ -131,39 +131,39 @@
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#each this.products as |product|}}
|
||||
<div class="gh-main-section-content grey gh-member-product-container" data-test-product={{product.id}}>
|
||||
<div class="gh-main-content-card gh-cp-memberproduct {{if (gt product.subscriptions.length 1) "multiple-subs" ""}}">
|
||||
<h3 class="gh-memberproduct-name" data-test-text="product-name">
|
||||
{{product.name}}
|
||||
{{#if (gt product.subscriptions.length 1)}}
|
||||
<span class="gh-memberproduct-subcount">{{product.subscriptions.length}} subscriptions</span>
|
||||
{{#each this.tiers as |tier|}}
|
||||
<div class="gh-main-section-content grey gh-member-tier-container" data-test-tier={{tier.id}}>
|
||||
<div class="gh-main-content-card gh-cp-membertier {{if (gt tier.subscriptions.length 1) "multiple-subs" ""}}">
|
||||
<h3 class="gh-membertier-name" data-test-text="tier-name">
|
||||
{{tier.name}}
|
||||
{{#if (gt tier.subscriptions.length 1)}}
|
||||
<span class="gh-membertier-subcount">{{tier.subscriptions.length}} subscriptions</span>
|
||||
{{/if}}
|
||||
</h3>
|
||||
|
||||
{{#each product.subscriptions as |sub index|}}
|
||||
<div class="gh-memberproduct-subscription" data-test-subscription={{index}}>
|
||||
{{#each tier.subscriptions as |sub index|}}
|
||||
<div class="gh-membertier-subscription" data-test-subscription={{index}}>
|
||||
<div>
|
||||
<div>
|
||||
<span class="gh-cp-memberproduct-pricelabel">{{sub.price.nickname}}</span>
|
||||
<span class="gh-cp-membertier-pricelabel">{{sub.price.nickname}}</span>
|
||||
–
|
||||
{{#if (eq sub.status "canceled")}}
|
||||
<span class="gh-cp-memberproduct-renewal">Ended {{sub.validUntil}}</span>
|
||||
<span class="gh-cp-membertier-renewal">Ended {{sub.validUntil}}</span>
|
||||
<span class="gh-badge archived" data-test-text="member-subscription-status">Cancelled</span>
|
||||
{{else if sub.cancel_at_period_end}}
|
||||
<span class="gh-cp-memberproduct-renewal">Has access until {{sub.validUntil}}</span>
|
||||
<span class="gh-cp-membertier-renewal">Has access until {{sub.validUntil}}</span>
|
||||
<span class="gh-badge archived" data-test-text="member-subscription-status">Cancelled</span>
|
||||
{{else}}
|
||||
<span class="gh-cp-memberproduct-renewal">Renews {{sub.validUntil}}</span>
|
||||
<span class="gh-cp-membertier-renewal">Renews {{sub.validUntil}}</span>
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if sub.cancellationReason}}
|
||||
<div class="gh-memberproduct-cancelreason"><span class="fw6">Cancellation reason:</span> {{sub.cancellationReason}}</div>
|
||||
<div class="gh-membertier-cancelreason"><span class="fw6">Cancellation reason:</span> {{sub.cancellationReason}}</div>
|
||||
{{/if}}
|
||||
{{#if sub.offer}}
|
||||
<div>
|
||||
<span class="gh-cp-memberproduct-pricelabel"> {{sub.offer.name}} </span>
|
||||
<span class="gh-cp-membertier-pricelabel"> {{sub.offer.name}} </span>
|
||||
offer
|
||||
{{#if (eq sub.offer.type 'fixed')}}
|
||||
({{currency-symbol sub.offer.currency}}{{gh-price-amount sub.offer.amount}} off)
|
||||
@ -173,13 +173,13 @@
|
||||
applied to subscription
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="gh-memberproduct-created">
|
||||
<div class="gh-membertier-created">
|
||||
Created on {{sub.startDate}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gh-memberproduct-price-container">
|
||||
<div class="gh-product-card-price">
|
||||
<div class="gh-membertier-price-container">
|
||||
<div class="gh-tier-card-price">
|
||||
<div class="flex items-start">
|
||||
<span class="currency-symbol">{{sub.price.currencySymbol}}</span>
|
||||
<span class="amount">{{sub.price.nonDecimalAmount}}</span>
|
||||
@ -203,12 +203,12 @@
|
||||
<GhDropdown
|
||||
@name="subscription-menu-complimentary"
|
||||
@tagName="ul"
|
||||
@classNames="product-actions-menu dropdown-menu dropdown-align-right"
|
||||
@classNames="tier-actions-menu dropdown-menu dropdown-align-right"
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
{{on "click" (fn this.removeComplimentary (or product.id product.product_id))}}
|
||||
{{on "click" (fn this.removeComplimentary (or tier.id tier.tier_id))}}
|
||||
data-test-button="remove-complimentary"
|
||||
>
|
||||
<span class="red">Remove complimentary subscription</span>
|
||||
@ -224,7 +224,7 @@
|
||||
<span class="hidden">Subscription menu</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown @name="subscription-menu-{{sub.id}}" @tagName="ul" @classNames="product-actions-menu dropdown-menu dropdown-align-right">
|
||||
<GhDropdown @name="subscription-menu-{{sub.id}}" @tagName="ul" @classNames="tier-actions-menu dropdown-menu dropdown-align-right">
|
||||
<li>
|
||||
<a href="https://dashboard.stripe.com/customers/{{sub.customer.id}}" target="_blank" rel="noopener noreferrer">
|
||||
View Stripe customer
|
||||
@ -256,17 +256,17 @@
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if (eq product.subscriptions.length 0)}}
|
||||
<div class="gh-memberproduct-subscription">
|
||||
{{#if (eq tier.subscriptions.length 0)}}
|
||||
<div class="gh-membertier-subscription">
|
||||
<div>
|
||||
<div>
|
||||
<span class="gh-cp-memberproduct-pricelabel">Complimentary</span>
|
||||
<span class="gh-cp-membertier-pricelabel">Complimentary</span>
|
||||
<span class="gh-badge active">Active</span>
|
||||
</div>
|
||||
<div class="gh-memberproduct-created">Created on</div>
|
||||
<div class="gh-membertier-created">Created on</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="gh-product-card-price">
|
||||
<div class="gh-tier-card-price">
|
||||
<div class="flex items-start">
|
||||
<span class="currency-symbol">$</span>
|
||||
<span class="amount">0</span>
|
||||
@ -280,9 +280,9 @@
|
||||
<span class="hidden">Subscription menu</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown @name="subscription-menu-complimentary" @tagName="ul" @classNames="product-actions-menu dropdown-menu dropdown-align-right">
|
||||
<GhDropdown @name="subscription-menu-complimentary" @tagName="ul" @classNames="tier-actions-menu dropdown-menu dropdown-align-right">
|
||||
<li>
|
||||
<button type="button" {{on "click" (fn this.removeComplimentary product.id)}}>
|
||||
<button type="button" {{on "click" (fn this.removeComplimentary tier.id)}}>
|
||||
<span class="red">Remove complimentary subscription</span>
|
||||
</button>
|
||||
</li>
|
||||
@ -295,15 +295,15 @@
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if (and this.products this.isAddComplimentaryAllowed)}}
|
||||
<div class="gh-memberproduct-list-footer {{if this.isCreatingComplimentary "min-height" ""}}">
|
||||
{{#if (and this.tiers this.isAddComplimentaryAllowed)}}
|
||||
<div class="gh-membertier-list-footer {{if this.isCreatingComplimentary "min-height" ""}}">
|
||||
{{#if this.isCreatingComplimentary}}
|
||||
<GhLoadingSpinner />
|
||||
{{else}}
|
||||
<button
|
||||
type="button"
|
||||
class="gh-btn gh-btn-text green gh-btn-icon gh-btn-addproduct"
|
||||
{{on "click" (toggle-action "showMemberProductModal" this)}}
|
||||
class="gh-btn gh-btn-text green gh-btn-icon gh-btn-addtier"
|
||||
{{on "click" (toggle-action "showMemberTierModal" this)}}
|
||||
data-test-button="add-complimentary"
|
||||
>
|
||||
<span>{{svg-jar "add"}} Add complimentary subscription</span>
|
||||
@ -320,11 +320,11 @@
|
||||
|
||||
</div>
|
||||
|
||||
{{#if this.showMemberProductModal}}
|
||||
<GhFullscreenModal @modifier="action wide member-product">
|
||||
<ModalMemberProduct
|
||||
{{#if this.showMemberTierModal}}
|
||||
<GhFullscreenModal @modifier="action wide member-tier">
|
||||
<ModalMemberTier
|
||||
@model={{this.member}}
|
||||
@confirm={{this.addProduct}}
|
||||
@closeModal={{this.closeMemberProductModal}} />
|
||||
@confirm={{this.addTier}}
|
||||
@closeModal={{this.closeMemberTierModal}} />
|
||||
</GhFullscreenModal>
|
||||
{{/if}}
|
||||
|
@ -20,8 +20,8 @@ export default class extends Component {
|
||||
this.scratchMember = this.args.scratchMember;
|
||||
}
|
||||
|
||||
@tracked showMemberProductModal = false;
|
||||
@tracked productsList;
|
||||
@tracked showMemberTierModal = false;
|
||||
@tracked tiersList;
|
||||
@tracked newslettersList;
|
||||
|
||||
get canShowStripeInfo() {
|
||||
@ -37,16 +37,16 @@ export default class extends Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.member.get('products')?.length > 0) {
|
||||
if (this.member.get('tiers')?.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// complimentary subscriptions are assigned to products so it only
|
||||
// complimentary subscriptions are assigned to tiers so it only
|
||||
// makes sense to show the "add complimentary" buttons when there's a
|
||||
// product to assign the complimentary subscription to
|
||||
const hasAnActivePaidProduct = !!this.productsList?.length;
|
||||
// tier to assign the complimentary subscription to
|
||||
const hasAnActivePaidTier = !!this.tiersList?.length;
|
||||
|
||||
return hasAnActivePaidProduct;
|
||||
return hasAnActivePaidTier;
|
||||
}
|
||||
|
||||
get hasSingleNewsletter() {
|
||||
@ -64,15 +64,15 @@ export default class extends Component {
|
||||
return this.args.isSaveRunning;
|
||||
}
|
||||
|
||||
get products() {
|
||||
get tiers() {
|
||||
let subscriptions = this.member.get('subscriptions') || [];
|
||||
|
||||
// Create the products from `subscriptions.price.product`
|
||||
let products = subscriptions
|
||||
.map(subscription => (subscription.tier || subscription.price.product))
|
||||
// Create the tiers from `subscriptions.price.tier`
|
||||
let tiers = subscriptions
|
||||
.map(subscription => (subscription.tier || subscription.price.tier))
|
||||
.filter((value, index, self) => {
|
||||
// Deduplicate by taking the first object by `id`
|
||||
return typeof value.id !== 'undefined' && self.findIndex(element => (element.product_id || element.id) === (value.product_id || value.id)) === index;
|
||||
return typeof value.id !== 'undefined' && self.findIndex(element => (element.tier_id || element.id) === (value.tier_id || value.id)) === index;
|
||||
});
|
||||
|
||||
let subscriptionData = subscriptions.filter((sub) => {
|
||||
@ -91,13 +91,13 @@ export default class extends Component {
|
||||
isComplimentary: !sub.id
|
||||
};
|
||||
});
|
||||
return products.map((product) => {
|
||||
let productSubscriptions = subscriptionData.filter((subscription) => {
|
||||
return subscription?.price?.product?.product_id === (product.product_id || product.id);
|
||||
return tiers.map((tier) => {
|
||||
let tierSubscriptions = subscriptionData.filter((subscription) => {
|
||||
return subscription?.price?.tier?.tier_id === (tier.tier_id || tier.id);
|
||||
});
|
||||
return {
|
||||
...product,
|
||||
subscriptions: productSubscriptions
|
||||
...tier,
|
||||
subscriptions: tierSubscriptions
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -131,7 +131,7 @@ export default class extends Component {
|
||||
|
||||
@action
|
||||
setup() {
|
||||
this.fetchProducts.perform();
|
||||
this.fetchTiers.perform();
|
||||
if (this.feature.get('multipleNewsletters')) {
|
||||
this.fetchNewsletters.perform();
|
||||
}
|
||||
@ -153,8 +153,8 @@ export default class extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
closeMemberProductModal() {
|
||||
this.showMemberProductModal = false;
|
||||
closeMemberTierModal() {
|
||||
this.showMemberTierModal = false;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -163,8 +163,8 @@ export default class extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
removeComplimentary(productId) {
|
||||
this.removeComplimentaryTask.perform(productId);
|
||||
removeComplimentary(tierId) {
|
||||
this.removeComplimentaryTask.perform(tierId);
|
||||
}
|
||||
|
||||
@action
|
||||
@ -187,20 +187,20 @@ export default class extends Component {
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*removeComplimentaryTask(productId) {
|
||||
*removeComplimentaryTask(tierId) {
|
||||
let url = this.ghostPaths.url.api(`members/${this.member.get('id')}`);
|
||||
let products = this.member.get('products') || [];
|
||||
let tiers = this.member.get('tiers') || [];
|
||||
|
||||
const updatedProducts = products
|
||||
.filter(product => product.id !== productId)
|
||||
.map(product => ({id: product.id}));
|
||||
const updatedTiers = tiers
|
||||
.filter(tier => tier.id !== tierId)
|
||||
.map(tier => ({id: tier.id}));
|
||||
|
||||
let response = yield this.ajax.put(url, {
|
||||
data: {
|
||||
members: [{
|
||||
id: this.member.get('id'),
|
||||
email: this.member.get('email'),
|
||||
products: updatedProducts
|
||||
tiers: updatedTiers
|
||||
}]
|
||||
}
|
||||
});
|
||||
@ -224,8 +224,8 @@ export default class extends Component {
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*fetchProducts() {
|
||||
this.productsList = yield this.store.query('product', {filter: 'type:paid+active:true', include: 'monthly_price,yearly_price'});
|
||||
*fetchTiers() {
|
||||
this.tiersList = yield this.store.query('tier', {filter: 'type:paid+active:true', include: 'monthly_price,yearly_price'});
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
|
@ -3,9 +3,9 @@
|
||||
<span class="gh-members-list-labels">{{this.labels}}</span>
|
||||
</LinkTo>
|
||||
|
||||
{{else if (eq @filterColumn 'product')}}
|
||||
{{else if (eq @filterColumn 'tier')}}
|
||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data wrap middarkgrey f8" data-test-table-data={{@filterColumn}}>
|
||||
<span class="gh-members-list-labels">{{this.products}}</span>
|
||||
<span class="gh-members-list-labels">{{this.tiers}}</span>
|
||||
</LinkTo>
|
||||
|
||||
{{else if (eq @filterColumn 'status')}}
|
||||
|
@ -12,9 +12,9 @@ export default class GhMembersListItemColumn extends Component {
|
||||
return labelData.map(label => label.name).join(', ');
|
||||
}
|
||||
|
||||
get products() {
|
||||
const productData = get(this.args.member, 'products') || [];
|
||||
return productData.map(product => product.name).join(', ');
|
||||
get tiers() {
|
||||
const tierData = get(this.args.member, 'tiers') || [];
|
||||
return tierData.map(tier => tier.name).join(', ');
|
||||
}
|
||||
|
||||
get mostRecentSubscription() {
|
||||
|
@ -23,14 +23,14 @@
|
||||
</div>
|
||||
</LinkTo>
|
||||
{{#if (feature "membersTableStatus")}}
|
||||
{{#if this.hasMultipleProducts}}
|
||||
{{#if this.hasMultipleTiers}}
|
||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data">
|
||||
{{#if (not (is-empty @member.status))}}
|
||||
<span>{{capitalize @member.status}}</span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">-</span>
|
||||
{{/if}}
|
||||
<div class="midgrey">{{this.products}}</div>
|
||||
<div class="midgrey">{{this.tiers}}</div>
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data" data-test-table-data="status">
|
||||
|
@ -9,12 +9,12 @@ export default class GhMembersListItem extends Component {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
get hasMultipleProducts() {
|
||||
return this.store.peekAll('product')?.length > 1;
|
||||
get hasMultipleTiers() {
|
||||
return this.store.peekAll('tier')?.length > 1;
|
||||
}
|
||||
|
||||
get products() {
|
||||
const productData = get(this.args.member, 'products') || [];
|
||||
return productData.map(product => product.name).join(', ');
|
||||
get tiers() {
|
||||
const tierData = get(this.args.member, 'tiers') || [];
|
||||
return tierData.map(tier => tier.name).join(', ');
|
||||
}
|
||||
}
|
||||
|
@ -250,12 +250,12 @@ export default Component.extend({
|
||||
});
|
||||
},
|
||||
|
||||
saveProduct: task(function* () {
|
||||
const products = yield this.store.query('product', {filter: 'type:paid', include: 'monthly_price, yearly_price'});
|
||||
this.product = products.firstObject;
|
||||
if (this.product) {
|
||||
saveTier: task(function* () {
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', include: 'monthly_price, yearly_price'});
|
||||
this.tier = tiers.firstObject;
|
||||
if (this.tier) {
|
||||
const yearlyDiscount = this.calculateDiscount(5, 50);
|
||||
this.product.set('monthlyPrice', {
|
||||
this.tier.set('monthlyPrice', {
|
||||
nickname: 'Monthly',
|
||||
amount: 500,
|
||||
active: 1,
|
||||
@ -264,7 +264,7 @@ export default Component.extend({
|
||||
interval: 'month',
|
||||
type: 'recurring'
|
||||
});
|
||||
this.product.set('yearlyPrice', {
|
||||
this.tier.set('yearlyPrice', {
|
||||
nickname: 'Yearly',
|
||||
amount: 5000,
|
||||
active: 1,
|
||||
@ -275,13 +275,13 @@ export default Component.extend({
|
||||
});
|
||||
|
||||
let pollTimeout = 0;
|
||||
/** To allow Stripe config to be ready in backend, we poll the save product request */
|
||||
/** To allow Stripe config to be ready in backend, we poll the save tier request */
|
||||
while (pollTimeout < RETRY_PRODUCT_SAVE_MAX_POLL) {
|
||||
yield timeout(RETRY_PRODUCT_SAVE_POLL_LENGTH);
|
||||
|
||||
try {
|
||||
const updatedProduct = yield this.product.save();
|
||||
return updatedProduct;
|
||||
const updatedTier = yield this.tier.save();
|
||||
return updatedTier;
|
||||
} catch (error) {
|
||||
if (error.payload?.errors && error.payload.errors[0].code === 'STRIPE_NOT_CONFIGURED') {
|
||||
pollTimeout += RETRY_PRODUCT_SAVE_POLL_LENGTH;
|
||||
@ -293,7 +293,7 @@ export default Component.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.product;
|
||||
return this.tier;
|
||||
}),
|
||||
|
||||
saveStripeSettings: task(function* () {
|
||||
@ -303,7 +303,7 @@ export default Component.extend({
|
||||
try {
|
||||
let response = yield this.settings.save();
|
||||
|
||||
yield this.saveProduct.perform();
|
||||
yield this.saveTier.perform();
|
||||
this.settings.set('portalPlans', ['free', 'monthly', 'yearly']);
|
||||
|
||||
response = yield this.settings.save();
|
||||
|
@ -151,26 +151,26 @@ export default class GhMembersRecipientSelect extends Component {
|
||||
|
||||
options.push(labelsGroup);
|
||||
}
|
||||
// fetch all products w̶i̶t̶h̶ c̶o̶u̶n̶t̶s̶
|
||||
// fetch all tiers w̶i̶t̶h̶ c̶o̶u̶n̶t̶s̶
|
||||
// TODO: add `include: 'count.members` to query once API supports
|
||||
const products = yield this.store.query('product', {filter: 'type:paid', limit: 'all'});
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', limit: 'all'});
|
||||
|
||||
if (products.length > 1) {
|
||||
const productsGroup = {
|
||||
if (tiers.length > 1) {
|
||||
const tiersGroup = {
|
||||
groupName: 'Tiers',
|
||||
options: []
|
||||
};
|
||||
|
||||
products.forEach((product) => {
|
||||
productsGroup.options.push({
|
||||
name: product.name,
|
||||
segment: `product:${product.slug}`,
|
||||
count: product.count?.members,
|
||||
class: 'segment-product'
|
||||
tiers.forEach((tier) => {
|
||||
tiersGroup.options.push({
|
||||
name: tier.name,
|
||||
segment: `tier:${tier.slug}`,
|
||||
count: tier.count?.members,
|
||||
class: 'segment-tier'
|
||||
});
|
||||
});
|
||||
|
||||
options.push(productsGroup);
|
||||
options.push(tiersGroup);
|
||||
}
|
||||
|
||||
this.specificOptions = options;
|
||||
|
@ -74,28 +74,28 @@ export default class GhMembersSegmentSelect extends Component {
|
||||
}
|
||||
|
||||
if (this.feature.get('multipleProducts')) {
|
||||
// fetch all products w̶i̶t̶h̶ c̶o̶u̶n̶t̶s̶
|
||||
// fetch all tiers w̶i̶t̶h̶ c̶o̶u̶n̶t̶s̶
|
||||
// TODO: add `include: 'count.members` to query once API supports
|
||||
const products = yield this.store.query('product', {filter: 'type:paid', limit: 'all', include: 'monthly_price,yearly_price,benefits'});
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', limit: 'all', include: 'monthly_price,yearly_price,benefits'});
|
||||
|
||||
if (products.length > 0) {
|
||||
const productsGroup = {
|
||||
if (tiers.length > 0) {
|
||||
const tiersGroup = {
|
||||
groupName: 'Tiers',
|
||||
options: []
|
||||
};
|
||||
|
||||
products.forEach((product) => {
|
||||
productsGroup.options.push({
|
||||
name: product.name,
|
||||
segment: `product:${product.slug}`,
|
||||
count: product.count?.members,
|
||||
class: 'segment-product'
|
||||
tiers.forEach((tier) => {
|
||||
tiersGroup.options.push({
|
||||
name: tier.name,
|
||||
segment: `tier:${tier.slug}`,
|
||||
count: tier.count?.members,
|
||||
class: 'segment-tier'
|
||||
});
|
||||
});
|
||||
|
||||
options.push(productsGroup);
|
||||
if (this.args.selectDefaultProduct && !this.args.segment) {
|
||||
this.args.onChange?.(productsGroup.options[0].segment);
|
||||
options.push(tiersGroup);
|
||||
if (this.args.selectDefaultTier && !this.args.segment) {
|
||||
this.args.onChange?.(tiersGroup.options[0].segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,37 +18,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-product-cards">
|
||||
<div class="gh-tier-cards">
|
||||
{{#if this.isEmptyList}}
|
||||
<div class="gh-main-content-card gh-product-card-empty-state">
|
||||
<div class="gh-main-content-card gh-tier-card-empty-state">
|
||||
<p>You have no {{this.selectedType.value}} tiers.</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#each this.products as |product|}}
|
||||
<GhProductCard
|
||||
@product={{product}}
|
||||
@openEditProduct={{this.openEditProduct}}
|
||||
{{#each this.tiers as |tier|}}
|
||||
<GhTierCard
|
||||
@tier={{tier}}
|
||||
@openEditTier={{this.openEditTier}}
|
||||
@onUnarchive={{this.onUnarchive}}
|
||||
@onArchive={{this.onArchive}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{#if (eq this.type "active" )}}
|
||||
<div class="gh-product-cards-footer">
|
||||
<button class="gh-btn gh-btn-link gh-btn-text gh-btn-icon gh-btn-add-product green" type="button" {{action "openNewProduct" this.product}}>
|
||||
<div class="gh-tier-cards-footer">
|
||||
<button class="gh-btn gh-btn-link gh-btn-text gh-btn-icon gh-btn-add-tier green" type="button" {{action "openNewTier" this.tier}}>
|
||||
<span>{{svg-jar "add-stroke" class="stroke-green"}}Add tier</span>
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.showProductModal}}
|
||||
{{#if this.showTierModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="product"
|
||||
@modal="tier"
|
||||
@model={{hash
|
||||
product=this.productModel
|
||||
tier=this.tierModel
|
||||
}}
|
||||
@confirm={{this.confirmProductSave}}
|
||||
@close={{this.closeProductModal}}
|
||||
@modifier="edit-product action wide" />
|
||||
@confirm={{this.confirmTierSave}}
|
||||
@close={{this.closeTierModal}}
|
||||
@modifier="edit-tier action wide" />
|
||||
{{/if}}
|
@ -18,16 +18,16 @@ export default class extends Component {
|
||||
@service store;
|
||||
@service config;
|
||||
|
||||
@tracked showProductModal = false;
|
||||
@tracked productModel = null;
|
||||
@tracked showTierModal = false;
|
||||
@tracked tierModel = null;
|
||||
@tracked type = 'active';
|
||||
|
||||
get products() {
|
||||
return this.args.products.filter((product) => {
|
||||
get tiers() {
|
||||
return this.args.tiers.filter((tier) => {
|
||||
if (this.type === 'active') {
|
||||
return !!product.active;
|
||||
return !!tier.active;
|
||||
} else if (this.type === 'archived') {
|
||||
return !product.active;
|
||||
return !tier.active;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -43,7 +43,7 @@ export default class extends Component {
|
||||
}
|
||||
|
||||
get isEmptyList() {
|
||||
return this.products.length === 0;
|
||||
return this.tiers.length === 0;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -52,9 +52,9 @@ export default class extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
async openEditProduct(product) {
|
||||
this.productModel = product;
|
||||
this.showProductModal = true;
|
||||
async openEditTier(tier) {
|
||||
this.tierModel = tier;
|
||||
this.showTierModal = true;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -69,18 +69,18 @@ export default class extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
async openNewProduct() {
|
||||
this.productModel = this.store.createRecord('product');
|
||||
this.showProductModal = true;
|
||||
async openNewTier() {
|
||||
this.tierModel = this.store.createRecord('tier');
|
||||
this.showTierModal = true;
|
||||
}
|
||||
|
||||
@action
|
||||
closeProductModal() {
|
||||
this.showProductModal = false;
|
||||
closeTierModal() {
|
||||
this.showTierModal = false;
|
||||
}
|
||||
|
||||
@action
|
||||
confirmProductSave() {
|
||||
this.args.confirmProductSave();
|
||||
confirmTierSave() {
|
||||
this.args.confirmTierSave();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<div class="gh-portal-links-container" {{did-insert (perform this.fetchProducts)}}>
|
||||
<div class="gh-portal-links-container" {{did-insert (perform this.fetchTiers)}}>
|
||||
<div class="gh-portal-links-main">
|
||||
<h2>Links</h2>
|
||||
<p>Use these {{if this.isLink "links" "data attributes"}} in your theme to show pages of Portal.</p>
|
||||
@ -24,14 +24,14 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal"
|
||||
disabled="true"
|
||||
aria-label="Default Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="data-portal"
|
||||
type="text"
|
||||
value="data-portal"
|
||||
disabled="true"
|
||||
aria-label="Default Portal data attribute">
|
||||
{{/if}}
|
||||
@ -55,13 +55,13 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signin"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signin"
|
||||
disabled="true"
|
||||
aria-label="Sign in Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
type="text"
|
||||
value='data-portal="signin"'
|
||||
disabled="true"
|
||||
aria-label="Sign in Portal data attribute">
|
||||
@ -82,13 +82,13 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup"
|
||||
disabled="true"
|
||||
aria-label="Sign up Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
type="text"
|
||||
value='data-portal="signup"'
|
||||
disabled="true"
|
||||
aria-label="Sign up Portal data attribute">
|
||||
@ -103,7 +103,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{#if (and (feature "multipleProducts") (gt this.products.length 1))}}
|
||||
{{#if (and (feature "multipleProducts") (gt this.tiers.length 1))}}
|
||||
<tr>
|
||||
<td colspan="2"><hr class="gh-portal-links-group-divider" /></td>
|
||||
</tr>
|
||||
@ -116,13 +116,13 @@
|
||||
tabindex="0"
|
||||
>
|
||||
<OneWaySelect
|
||||
@id="portal-product-link"
|
||||
@name="portal[product-link]"
|
||||
@options={{this.productOptions}}
|
||||
@id="portal-tier-link"
|
||||
@name="portal[tier-link]"
|
||||
@options={{this.tierOptions}}
|
||||
@optionValuePath="name"
|
||||
@optionLabelPath="label"
|
||||
@value={{this.selectedProduct}}
|
||||
@update={{action "setSelectedProduct"}}
|
||||
@value={{this.selectedTier}}
|
||||
@update={{action "setSelectedTier"}}
|
||||
/>
|
||||
{{svg-jar "arrow-down-small"}}
|
||||
</span>
|
||||
@ -134,19 +134,19 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup{{this.selectedProductIdPath}}/monthly"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup{{this.selectedTierIdPath}}/monthly"
|
||||
disabled="true"
|
||||
aria-label="Monthly sign up Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value='data-portal="signup{{this.selectedProductIdPath}}/monthly"'
|
||||
type="text"
|
||||
value='data-portal="signup{{this.selectedTierIdPath}}/monthly"'
|
||||
disabled="true"
|
||||
aria-label="Monthly sign up Portal data attribute">
|
||||
{{/if}}
|
||||
<button type="button" {{action (perform this.copyProductSignupLink "monthly")}} class="gh-portal-setting-copy">
|
||||
{{#if (and this.copyProductSignupLink.isRunning (eq this.copiedSignupInterval "monthly"))}}
|
||||
<button type="button" {{action (perform this.copyTierSignupLink "monthly")}} class="gh-portal-setting-copy">
|
||||
{{#if (and this.copyTierSignupLink.isRunning (eq this.copiedSignupInterval "monthly"))}}
|
||||
{{svg-jar "check-circle" class="w3 v-mid mr2 stroke-darkgrey"}} Copied
|
||||
{{else}}
|
||||
<span data-tooltip="Copy">{{svg-jar "copy" class="w4 v-mid fill-darkgrey"}}</span>
|
||||
@ -161,19 +161,19 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup{{this.selectedProductIdPath}}/yearly"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup{{this.selectedTierIdPath}}/yearly"
|
||||
disabled="true"
|
||||
aria-label="Yearly sign up Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value='data-portal="signup{{this.selectedProductIdPath}}/yearly"'
|
||||
type="text"
|
||||
value='data-portal="signup{{this.selectedTierIdPath}}/yearly"'
|
||||
disabled="true"
|
||||
aria-label="Yearly sign up Portal data attribute">
|
||||
{{/if}}
|
||||
<button type="button" {{action (perform this.copyProductSignupLink "yearly")}} class="gh-portal-setting-copy">
|
||||
{{#if (and this.copyProductSignupLink.isRunning (eq this.copiedSignupInterval "yearly"))}}
|
||||
<button type="button" {{action (perform this.copyTierSignupLink "yearly")}} class="gh-portal-setting-copy">
|
||||
{{#if (and this.copyTierSignupLink.isRunning (eq this.copiedSignupInterval "yearly"))}}
|
||||
{{svg-jar "check-circle" class="w3 v-mid mr2 stroke-darkgrey"}} Copied
|
||||
{{else}}
|
||||
<span data-tooltip="Copy">{{svg-jar "copy" class="w4 v-mid fill-darkgrey"}}</span>
|
||||
@ -188,13 +188,13 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup/free"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup/free"
|
||||
disabled="true"
|
||||
aria-label="Free sign up Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
type="text"
|
||||
value='data-portal="signup/free"'
|
||||
disabled="true"
|
||||
aria-label="Free sign up Portal data attribute">
|
||||
@ -216,13 +216,13 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup/monthly"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup/monthly"
|
||||
disabled="true"
|
||||
aria-label="Monthly sign up Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
type="text"
|
||||
value='data-portal="signup/monthly"'
|
||||
disabled="true"
|
||||
aria-label="Monthly sign up Portal data attribute">
|
||||
@ -243,13 +243,13 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup/yearly"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup/yearly"
|
||||
disabled="true"
|
||||
aria-label="Yearly sign up Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
type="text"
|
||||
value='data-portal="signup/yearly"'
|
||||
disabled="true"
|
||||
aria-label="Yearly sign up Portal data attribute">
|
||||
@ -270,13 +270,13 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup/free"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/signup/free"
|
||||
disabled="true"
|
||||
aria-label="Free sign up Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
type="text"
|
||||
value='data-portal="signup/free"'
|
||||
disabled="true"
|
||||
aria-label="Free sign up Portal data attribute">
|
||||
@ -301,13 +301,13 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/account"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/account"
|
||||
disabled="true"
|
||||
aria-label="Account Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
type="text"
|
||||
value='data-portal="account"'
|
||||
disabled="true"
|
||||
aria-label="Account Portal data attribute">
|
||||
@ -328,13 +328,13 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/account/plans"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/account/plans"
|
||||
disabled="true"
|
||||
aria-label="Account/Plans Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
type="text"
|
||||
value='data-portal="account/plans"'
|
||||
disabled="true"
|
||||
aria-label="Account/Plans Portal data attribute">
|
||||
@ -355,13 +355,13 @@
|
||||
<div class="gh-portal-page-url-container">
|
||||
{{#if this.isLink}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/account/profile"
|
||||
type="text"
|
||||
value="{{this.siteUrl}}/#/portal/account/profile"
|
||||
disabled="true"
|
||||
aria-label="Account/Profile Portal link">
|
||||
{{else}}
|
||||
<input class="gh-input page-url-field"
|
||||
type="text"
|
||||
type="text"
|
||||
value='data-portal="account/profile"'
|
||||
disabled="true"
|
||||
aria-label="Account/Profile Portal data attribute">
|
||||
|
@ -17,8 +17,8 @@ export default class GhPortalLinks extends Component {
|
||||
prices = null;
|
||||
copiedPrice = null;
|
||||
copiedSignupInterval = null;
|
||||
selectedProduct = null;
|
||||
products = null;
|
||||
selectedTier = null;
|
||||
tiers = null;
|
||||
|
||||
@computed('isLink')
|
||||
get toggleValue() {
|
||||
@ -30,22 +30,22 @@ export default class GhPortalLinks extends Component {
|
||||
return this.isLink ? 'Link' : 'Data attribute';
|
||||
}
|
||||
|
||||
@computed('selectedProduct')
|
||||
get selectedProductIdPath() {
|
||||
const selectedProduct = this.selectedProduct;
|
||||
if (selectedProduct) {
|
||||
return `/${selectedProduct.name}`;
|
||||
@computed('selectedTier')
|
||||
get selectedTierIdPath() {
|
||||
const selectedTier = this.selectedTier;
|
||||
if (selectedTier) {
|
||||
return `/${selectedTier.name}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@computed('products.[]')
|
||||
get productOptions() {
|
||||
if (this.products) {
|
||||
return this.products.map((product) => {
|
||||
@computed('tiers.[]')
|
||||
get tierOptions() {
|
||||
if (this.tiers) {
|
||||
return this.tiers.map((tier) => {
|
||||
return {
|
||||
label: product.name,
|
||||
name: product.id
|
||||
label: tier.name,
|
||||
name: tier.id
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -63,21 +63,21 @@ export default class GhPortalLinks extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
setSelectedProduct(product) {
|
||||
this.set('selectedProduct', product);
|
||||
setSelectedTier(tier) {
|
||||
this.set('selectedTier', tier);
|
||||
}
|
||||
|
||||
@task(function* () {
|
||||
const products = yield this.store.query('product', {filter: 'type:paid', include: 'monthly_price,yearly_price'}) || [];
|
||||
this.set('products', products);
|
||||
if (products.length > 0) {
|
||||
this.set('selectedProduct', {
|
||||
name: products.firstObject.id,
|
||||
label: products.firstObject.name
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', include: 'monthly_price,yearly_price'}) || [];
|
||||
this.set('tiers', tiers);
|
||||
if (tiers.length > 0) {
|
||||
this.set('selectedTier', {
|
||||
name: tiers.firstObject.id,
|
||||
label: tiers.firstObject.name
|
||||
});
|
||||
}
|
||||
})
|
||||
fetchProducts;
|
||||
fetchTiers;
|
||||
|
||||
@task(function* (id) {
|
||||
this.set('copiedPrice', id);
|
||||
@ -97,13 +97,13 @@ export default class GhPortalLinks extends Component {
|
||||
this.set('copiedSignupInterval', interval);
|
||||
let data = '';
|
||||
if (this.isLink) {
|
||||
data = `#/portal/signup${this.selectedProductIdPath}/${interval}`;
|
||||
data = `#/portal/signup${this.selectedTierIdPath}/${interval}`;
|
||||
data = this.siteUrl + `/` + data;
|
||||
} else {
|
||||
data = `data-portal="signup${this.selectedProductIdPath}/${interval}"`;
|
||||
data = `data-portal="signup${this.selectedTierIdPath}/${interval}"`;
|
||||
}
|
||||
copyTextToClipboard(data);
|
||||
yield timeout(this.isTesting ? 50 : 3000);
|
||||
})
|
||||
copyProductSignupLink;
|
||||
copyTierSignupLink;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ export default class VisibilitySegmentSelect extends Component {
|
||||
@service feature;
|
||||
|
||||
@tracked _options = [];
|
||||
@tracked products = [];
|
||||
@tracked tiers = [];
|
||||
|
||||
get renderInPlace() {
|
||||
return this.args.renderInPlace === undefined ? false : this.args.renderInPlace;
|
||||
@ -41,9 +41,9 @@ export default class VisibilitySegmentSelect extends Component {
|
||||
}
|
||||
|
||||
get selectedOptions() {
|
||||
const tierList = this.args.tiers.map((product) => {
|
||||
return this.products.find((p) => {
|
||||
return p.id === product.id;
|
||||
const tierList = this.args.tiers.map((tier) => {
|
||||
return this.tiers.find((p) => {
|
||||
return p.id === tier.id;
|
||||
});
|
||||
}).filter(d => !!d);
|
||||
const tierIdList = tierList.map(d => d.id);
|
||||
@ -53,13 +53,13 @@ export default class VisibilitySegmentSelect extends Component {
|
||||
@action
|
||||
setSegment(options) {
|
||||
let ids = options.mapBy('id').map((id) => {
|
||||
let product = this.products.find((p) => {
|
||||
let tier = this.tiers.find((p) => {
|
||||
return p.id === id;
|
||||
});
|
||||
return {
|
||||
id: product.id,
|
||||
slug: product.slug,
|
||||
name: product.name
|
||||
id: tier.id,
|
||||
slug: tier.slug,
|
||||
name: tier.name
|
||||
};
|
||||
}) || [];
|
||||
this.args.onChange?.(ids);
|
||||
@ -70,29 +70,29 @@ export default class VisibilitySegmentSelect extends Component {
|
||||
const options = yield [];
|
||||
|
||||
if (this.feature.get('multipleProducts')) {
|
||||
// fetch all products with count
|
||||
// fetch all tiers with count
|
||||
// TODO: add `include: 'count.members` to query once API supports
|
||||
const products = yield this.store.query('product', {filter: 'type:paid', limit: 'all', include: 'monthly_price,yearly_price,benefits'});
|
||||
this.products = products;
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', limit: 'all', include: 'monthly_price,yearly_price,benefits'});
|
||||
this.tiers = tiers;
|
||||
|
||||
if (products.length > 0) {
|
||||
const productsGroup = {
|
||||
if (tiers.length > 0) {
|
||||
const tiersGroup = {
|
||||
groupName: 'Tiers',
|
||||
options: []
|
||||
};
|
||||
|
||||
products.forEach((product) => {
|
||||
productsGroup.options.push({
|
||||
name: product.name,
|
||||
id: product.id,
|
||||
count: product.count?.members,
|
||||
class: 'segment-product'
|
||||
tiers.forEach((tier) => {
|
||||
tiersGroup.options.push({
|
||||
name: tier.name,
|
||||
id: tier.id,
|
||||
count: tier.count?.members,
|
||||
class: 'segment-tier'
|
||||
});
|
||||
});
|
||||
|
||||
options.push(productsGroup);
|
||||
if (this.args.selectDefaultProduct && !this.args.tiers) {
|
||||
this.setSegment([productsGroup.options[0]]);
|
||||
options.push(tiersGroup);
|
||||
if (this.args.selectDefaultTier && !this.args.tiers) {
|
||||
this.setSegment([tiersGroup.options[0]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class extends Component {
|
||||
@service membersUtils;
|
||||
@service ghostPaths;
|
||||
@service ajax;
|
||||
@service store;
|
||||
@service config;
|
||||
|
||||
@tracked showProductModal = false;
|
||||
|
||||
get product() {
|
||||
return this.args.product;
|
||||
}
|
||||
|
||||
get showArchiveOption() {
|
||||
return this.product.type === 'paid' && !!this.product.monthlyPrice;
|
||||
}
|
||||
|
||||
get productCurrency() {
|
||||
if (this.isFreeProduct) {
|
||||
const firstPaidProduct = this.args.products.find((product) => {
|
||||
return product.type === 'paid';
|
||||
});
|
||||
return firstPaidProduct?.monthlyPrice?.currency || 'usd';
|
||||
} else {
|
||||
return this.product?.monthlyPrice?.currency;
|
||||
}
|
||||
}
|
||||
|
||||
get isPaidProduct() {
|
||||
return this.product.type === 'paid';
|
||||
}
|
||||
|
||||
get hasCurrencySymbol() {
|
||||
const currencySymbol = getSymbol(this.product?.monthlyPrice?.currency);
|
||||
return currencySymbol?.length !== 3;
|
||||
}
|
||||
|
||||
get isFreeProduct() {
|
||||
return this.product.type === 'free';
|
||||
}
|
||||
|
||||
@action
|
||||
async openEditProduct(product) {
|
||||
this.args.openEditProduct(product);
|
||||
}
|
||||
}
|
@ -1,66 +1,66 @@
|
||||
<div class="gh-main-content-card gh-product-card" data-test-product-card={{@product.slug}}>
|
||||
<div class="gh-product-card-block title-block">
|
||||
<h3 class="gh-product-card-name" data-test-name>
|
||||
{{@product.name}}
|
||||
<div class="gh-main-content-card gh-tier-card" data-test-tier-card={{@tier.slug}}>
|
||||
<div class="gh-tier-card-block title-block">
|
||||
<h3 class="gh-tier-card-name" data-test-name>
|
||||
{{@tier.name}}
|
||||
</h3>
|
||||
<p class="gh-product-card-description" data-test-description>
|
||||
{{#if @product.description.length}}
|
||||
{{@product.description}}
|
||||
<p class="gh-tier-card-description" data-test-description>
|
||||
{{#if @tier.description.length}}
|
||||
{{@tier.description}}
|
||||
{{else}}
|
||||
No description added for this tier.
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="gh-product-card-block benefits-block" data-test-benefits>
|
||||
<h4>Benefits <span class="counter">({{or @product.benefits.length "0"}})</span></h4>
|
||||
{{#if @product.benefits.length}}
|
||||
<div class="gh-tier-card-block benefits-block" data-test-benefits>
|
||||
<h4>Benefits <span class="counter">({{or @tier.benefits.length "0"}})</span></h4>
|
||||
{{#if @tier.benefits.length}}
|
||||
<ul class="benefits">
|
||||
{{#each @product.benefits as |benefit|}}
|
||||
{{#each @tier.benefits as |benefit|}}
|
||||
<li>{{svg-jar "check"}} {{benefit.name}} </li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p class="gh-product-card-description">No benefits added for this tier.</p>
|
||||
<p class="gh-tier-card-description">No benefits added for this tier.</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if (eq @product.type "free" )}}
|
||||
<div class="gh-product-card-block">
|
||||
<div class="gh-product-price-container">
|
||||
<div class="gh-product-card-price" data-test-free-price>
|
||||
{{#if (eq @tier.type "free" )}}
|
||||
<div class="gh-tier-card-block">
|
||||
<div class="gh-tier-price-container">
|
||||
<div class="gh-tier-card-price" data-test-free-price>
|
||||
<div class="flex items-start">
|
||||
<span class="currency">{{currency-symbol this.productCurrency}}</span>
|
||||
<span class="currency">{{currency-symbol this.tierCurrency}}</span>
|
||||
<span class="amount">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (eq @product.type "paid" )}}
|
||||
<div class="gh-product-card-block">
|
||||
<div class="gh-product-price-container">
|
||||
<div class="gh-product-card-price" data-test-monthly-price>
|
||||
{{#if (eq @tier.type "paid" )}}
|
||||
<div class="gh-tier-card-block">
|
||||
<div class="gh-tier-price-container">
|
||||
<div class="gh-tier-card-price" data-test-monthly-price>
|
||||
<div class="flex items-start">
|
||||
<span class="currency">{{currency-symbol this.productCurrency}}</span>
|
||||
<span class="amount">{{gh-price-amount @product.monthlyPrice.amount}}</span>
|
||||
<span class="currency">{{currency-symbol this.tierCurrency}}</span>
|
||||
<span class="amount">{{gh-price-amount @tier.monthlyPrice.amount}}</span>
|
||||
</div>
|
||||
<div class="period">Monthly</div>
|
||||
</div>
|
||||
<div class="gh-product-card-price" data-test-yearly-price>
|
||||
<div class="gh-tier-card-price" data-test-yearly-price>
|
||||
<div class="flex items-start">
|
||||
<span class="currency">{{currency-symbol this.productCurrency}}</span>
|
||||
<span class="amount">{{gh-price-amount @product.yearlyPrice.amount}}</span>
|
||||
<span class="currency">{{currency-symbol this.tierCurrency}}</span>
|
||||
<span class="amount">{{gh-price-amount @tier.yearlyPrice.amount}}</span>
|
||||
</div>
|
||||
<div class="period">Yearly</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (eq @product.type "paid" )}}
|
||||
<div class="gh-product-card-button-container">
|
||||
{{#if (eq @tier.type "paid" )}}
|
||||
<div class="gh-tier-card-button-container">
|
||||
<span class="dropdown">
|
||||
<GhDropdownButton
|
||||
@dropdownName="tiers-actions-menu-{{@product.name}}"
|
||||
@classNames="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-product-card-actions-button icon-only"
|
||||
@dropdownName="tiers-actions-menu-{{@tier.name}}"
|
||||
@classNames="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-tier-card-actions-button icon-only"
|
||||
@title="Tiers Actions"
|
||||
data-test-button="tiers-actions"
|
||||
>
|
||||
@ -70,19 +70,19 @@
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown
|
||||
@name="tiers-actions-menu-{{@product.name}}"
|
||||
@name="tiers-actions-menu-{{@tier.name}}"
|
||||
@tagName="ul"
|
||||
@classNames="gh-tier-actions-menu dropdown-menu dropdown-triangle-top-right"
|
||||
>
|
||||
<li>
|
||||
<button class="mr2" type="button" {{on "click" (fn this.openEditProduct @product)}}>
|
||||
<button class="mr2" type="button" {{on "click" (fn this.openEditTier @tier)}}>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
</li>
|
||||
{{#if this.showArchiveOption}}
|
||||
<li>
|
||||
<Settings::Members::ArchiveTier
|
||||
@product={{@product}}
|
||||
@tier={{@tier}}
|
||||
@onUnarchive={{@onUnarchive}}
|
||||
@onArchive={{@onArchive}}
|
||||
/>
|
||||
@ -92,8 +92,8 @@
|
||||
</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-product-card-button-container">
|
||||
<button type="button" {{on "click" (fn this.openEditProduct @product)}} class="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-product-card-edit-button icon-only" data-test-button="edit-product">
|
||||
<div class="gh-tier-card-button-container">
|
||||
<button type="button" {{on "click" (fn this.openEditTier @tier)}} class="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-tier-card-edit-button icon-only" data-test-button="edit-tier">
|
||||
<span>
|
||||
{{svg-jar "pen"}}
|
||||
</span>
|
52
ghost/admin/app/components/gh-tier-card.js
Normal file
52
ghost/admin/app/components/gh-tier-card.js
Normal file
@ -0,0 +1,52 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class extends Component {
|
||||
@service membersUtils;
|
||||
@service ghostPaths;
|
||||
@service ajax;
|
||||
@service store;
|
||||
@service config;
|
||||
|
||||
@tracked showTierModal = false;
|
||||
|
||||
get tier() {
|
||||
return this.args.tier;
|
||||
}
|
||||
|
||||
get showArchiveOption() {
|
||||
return this.tier.type === 'paid' && !!this.tier.monthlyPrice;
|
||||
}
|
||||
|
||||
get tierCurrency() {
|
||||
if (this.isFreeTier) {
|
||||
const firstPaidTier = this.args.tiers.find((tier) => {
|
||||
return tier.type === 'paid';
|
||||
});
|
||||
return firstPaidTier?.monthlyPrice?.currency || 'usd';
|
||||
} else {
|
||||
return this.tier?.monthlyPrice?.currency;
|
||||
}
|
||||
}
|
||||
|
||||
get isPaidTier() {
|
||||
return this.tier.type === 'paid';
|
||||
}
|
||||
|
||||
get hasCurrencySymbol() {
|
||||
const currencySymbol = getSymbol(this.tier?.monthlyPrice?.currency);
|
||||
return currencySymbol?.length !== 3;
|
||||
}
|
||||
|
||||
get isFreeTier() {
|
||||
return this.tier.type === 'free';
|
||||
}
|
||||
|
||||
@action
|
||||
async openEditTier(tier) {
|
||||
this.args.openEditTier(tier);
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ const PERIODS = [
|
||||
{label: 'Yearly', period: 'year'}
|
||||
];
|
||||
|
||||
export default class GhProductsPriceBillingPeriodComponent extends Component {
|
||||
export default class GhTiersPriceBillingPeriodComponent extends Component {
|
||||
@service feature;
|
||||
@service session;
|
||||
@service settings;
|
@ -32,11 +32,11 @@
|
||||
@allowEdit={{true}}
|
||||
/>
|
||||
|
||||
{{else if (eq @filter.type 'product')}}
|
||||
{{else if (eq @filter.type 'tier')}}
|
||||
<div class="relative">
|
||||
<Tiers::SegmentSelect
|
||||
@onChange={{fn this.setProductsFilterValue @filter}}
|
||||
@tiers={{this.productFilterValue}}
|
||||
@onChange={{fn this.setTiersFilterValue @filter}}
|
||||
@tiers={{this.tierFilterValue}}
|
||||
@renderInPlace={{true}}
|
||||
@hideOptionsWhenAllSelected={{true}}
|
||||
/>
|
||||
|
@ -36,8 +36,8 @@ export default class MembersFilterValue extends Component {
|
||||
this.filterValue = this.args.filter.value;
|
||||
}
|
||||
|
||||
get productFilterValue() {
|
||||
if (this.args.filter?.type === 'product') {
|
||||
get tierFilterValue() {
|
||||
if (this.args.filter?.type === 'tier') {
|
||||
const tiers = this.args.filter?.value || [];
|
||||
return tiers.map((tier) => {
|
||||
return {
|
||||
@ -75,7 +75,7 @@ export default class MembersFilterValue extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
setProductsFilterValue(filter, tiers) {
|
||||
setTiersFilterValue(filter, tiers) {
|
||||
this.args.setFilterValue(filter, tiers.map(tier => tier.slug));
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ const FILTER_PROPERTIES = [
|
||||
{label: 'Created', name: 'created_at', group: 'Basic', valueType: 'date'},
|
||||
|
||||
// Member subscription
|
||||
{label: 'Membership tier', name: 'product', group: 'Subscription', valueType: 'array', feature: 'multipleProducts'},
|
||||
{label: 'Membership tier', name: 'tier', group: 'Subscription', valueType: 'array', feature: 'multipleProducts'},
|
||||
{label: 'Member status', name: 'status', group: 'Subscription'},
|
||||
{label: 'Billing period', name: 'subscriptions.plan_interval', group: 'Subscription'},
|
||||
{label: 'Stripe subscription status', name: 'subscriptions.status', group: 'Subscription'},
|
||||
@ -71,7 +71,7 @@ const FILTER_RELATIONS_OPTIONS = {
|
||||
name: CONTAINS_RELATION_OPTIONS,
|
||||
email: CONTAINS_RELATION_OPTIONS,
|
||||
label: MATCH_RELATION_OPTIONS,
|
||||
product: MATCH_RELATION_OPTIONS,
|
||||
tier: MATCH_RELATION_OPTIONS,
|
||||
subscribed: MATCH_RELATION_OPTIONS,
|
||||
last_seen_at: DATE_RELATION_OPTIONS,
|
||||
created_at: DATE_RELATION_OPTIONS,
|
||||
@ -155,7 +155,7 @@ export default class MembersFilter extends Component {
|
||||
|
||||
get availableFilterProperties() {
|
||||
let availableFilters = FILTER_PROPERTIES;
|
||||
const hasMultipleProducts = this.store.peekAll('product').length > 1;
|
||||
const hasMultipleTiers = this.store.peekAll('tier').length > 1;
|
||||
|
||||
// exclude any filters that are behind disabled feature flags
|
||||
availableFilters = availableFilters.filter(prop => !prop.feature || this.feature[prop.feature]);
|
||||
@ -163,7 +163,7 @@ export default class MembersFilter extends Component {
|
||||
// exclude tiers filter if site has only single tier
|
||||
availableFilters = availableFilters
|
||||
.filter((filter) => {
|
||||
return filter.name === 'product' ? hasMultipleProducts : true;
|
||||
return filter.name === 'tier' ? hasMultipleTiers : true;
|
||||
});
|
||||
|
||||
// exclude subscription filters if Stripe isn't connected
|
||||
@ -190,7 +190,7 @@ export default class MembersFilter extends Component {
|
||||
this.parseNqlFilter(this.args.defaultFilterParam);
|
||||
}
|
||||
|
||||
this.fetchProducts.perform();
|
||||
this.fetchTiers.perform();
|
||||
}
|
||||
|
||||
@action
|
||||
@ -436,7 +436,7 @@ export default class MembersFilter extends Component {
|
||||
this.applySoftFilter();
|
||||
}
|
||||
|
||||
if (newType !== 'product' && defaultValue) {
|
||||
if (newType !== 'tier' && defaultValue) {
|
||||
this.applySoftFilter();
|
||||
}
|
||||
}
|
||||
@ -459,7 +459,7 @@ export default class MembersFilter extends Component {
|
||||
if (filter.type === 'label') {
|
||||
return filter.value?.length;
|
||||
}
|
||||
if (filter.type === 'product') {
|
||||
if (filter.type === 'tier') {
|
||||
return filter.value?.length;
|
||||
}
|
||||
return filter.value;
|
||||
@ -471,7 +471,7 @@ export default class MembersFilter extends Component {
|
||||
@action
|
||||
applyFilter() {
|
||||
const validFilters = this.filters.filter((filter) => {
|
||||
if (['label', 'product'].includes(filter.type)) {
|
||||
if (['label', 'tier'].includes(filter.type)) {
|
||||
return filter.value?.length;
|
||||
}
|
||||
return filter.value;
|
||||
@ -497,8 +497,8 @@ export default class MembersFilter extends Component {
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*fetchProducts() {
|
||||
const response = yield this.store.query('product', {filter: 'type:paid'});
|
||||
this.productsList = response;
|
||||
*fetchTiers() {
|
||||
const response = yield this.store.query('tier', {filter: 'type:paid'});
|
||||
this.tiersList = response;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
<form>
|
||||
<div class="modal-body">
|
||||
<div class="gh-main-section-block">
|
||||
<div class="gh-main-section-content grey gh-product-priceform-block">
|
||||
<div class="gh-main-section-content grey gh-tier-priceform-block">
|
||||
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="name">
|
||||
<label for="name" class="fw6">Portal display name</label>
|
||||
<GhTextInput
|
||||
|
@ -1,4 +1,4 @@
|
||||
<header class="modal-header" data-test-modal="member-product" {{did-insert this.setup}}>
|
||||
<header class="modal-header" data-test-modal="member-tier" {{did-insert this.setup}}>
|
||||
<h1>Add subscription</h1>
|
||||
</header>
|
||||
<button type="button" class="close" title="Close" {{on "click" this.close}}>
|
||||
@ -16,23 +16,23 @@
|
||||
Adding a complimentary subscription cancels all existing subscriptions of this member.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{#if this.loadingProducts}}
|
||||
{{#if this.loadingTiers}}
|
||||
<div class="flex justify-center flex-auto">
|
||||
<div class="gh-loading-spinner"> </div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="form-rich-radio">
|
||||
{{#each this.products as |product|}}
|
||||
{{#each this.tiers as |tier|}}
|
||||
<div
|
||||
class="gh-radio {{if (eq this.selectedProduct product.id) "active"}}"
|
||||
{{on "click" (fn this.setProduct product.id)}}
|
||||
data-test-tier-option={{product.id}}
|
||||
class="gh-radio {{if (eq this.selectedTier tier.id) "active"}}"
|
||||
{{on "click" (fn this.setTier tier.id)}}
|
||||
data-test-tier-option={{tier.id}}
|
||||
>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">
|
||||
<div class="description" data-test-text="tier-desc">
|
||||
<h4>{{product.name}}</h4>
|
||||
<p>{{product.description}}</p>
|
||||
<h4>{{tier.name}}</h4>
|
||||
<p>{{tier.description}}</p>
|
||||
</div>
|
||||
{{svg-jar "check" class="check"}}
|
||||
</div>
|
||||
@ -61,7 +61,7 @@
|
||||
|
||||
<GhTaskButton @buttonText="Add subscription"
|
||||
@successText={{"Added"}}
|
||||
@task={{this.addProduct}}
|
||||
@class="gh-btn gh-btn-green gh-btn-icon gh-btn-add-memberproduct"
|
||||
data-test-button="save-comp-product" />
|
||||
@task={{this.addTier}}
|
||||
@class="gh-btn gh-btn-green gh-btn-icon gh-btn-add-membertier"
|
||||
data-test-button="save-comp-tier" />
|
||||
</div>
|
@ -4,24 +4,24 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class ModalMemberProduct extends ModalComponent {
|
||||
export default class ModalMemberTier extends ModalComponent {
|
||||
@service store;
|
||||
@service ghostPaths;
|
||||
@service ajax;
|
||||
|
||||
@tracked price;
|
||||
@tracked product;
|
||||
@tracked products = [];
|
||||
@tracked selectedProduct = null;
|
||||
@tracked loadingProducts = false;
|
||||
@tracked tier;
|
||||
@tracked tiers = [];
|
||||
@tracked selectedTier = null;
|
||||
@tracked loadingTiers = false;
|
||||
|
||||
@task({drop: true})
|
||||
*fetchProducts() {
|
||||
this.products = yield this.store.query('product', {filter: 'type:paid+active:true', include: 'monthly_price,yearly_price,benefits'});
|
||||
*fetchTiers() {
|
||||
this.tiers = yield this.store.query('tier', {filter: 'type:paid+active:true', include: 'monthly_price,yearly_price,benefits'});
|
||||
|
||||
this.loadingProducts = false;
|
||||
if (this.products.length > 0) {
|
||||
this.selectedProduct = this.products.firstObject.id;
|
||||
this.loadingTiers = false;
|
||||
if (this.tiers.length > 0) {
|
||||
this.selectedTier = this.tiers.firstObject.id;
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,13 +42,13 @@ export default class ModalMemberProduct extends ModalComponent {
|
||||
|
||||
@action
|
||||
setup() {
|
||||
this.loadingProducts = true;
|
||||
this.fetchProducts.perform();
|
||||
this.loadingTiers = true;
|
||||
this.fetchTiers.perform();
|
||||
}
|
||||
|
||||
@action
|
||||
setProduct(productId) {
|
||||
this.selectedProduct = productId;
|
||||
setTier(tierId) {
|
||||
this.selectedTier = tierId;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -58,7 +58,7 @@ export default class ModalMemberProduct extends ModalComponent {
|
||||
|
||||
@action
|
||||
confirmAction() {
|
||||
return this.addProduct.perform();
|
||||
return this.addTier.perform();
|
||||
}
|
||||
|
||||
@action
|
||||
@ -68,8 +68,8 @@ export default class ModalMemberProduct extends ModalComponent {
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*addProduct() {
|
||||
const url = `${this.ghostPaths.url.api(`members/${this.member.get('id')}`)}?include=products`;
|
||||
*addTier() {
|
||||
const url = `${this.ghostPaths.url.api(`members/${this.member.get('id')}`)}?include=tiers`;
|
||||
|
||||
// Cancel existing active subscriptions for member
|
||||
for (let i = 0; i < this.activeSubscriptions.length; i++) {
|
||||
@ -87,8 +87,8 @@ export default class ModalMemberProduct extends ModalComponent {
|
||||
members: [{
|
||||
id: this.member.get('id'),
|
||||
email: this.member.get('email'),
|
||||
products: [{
|
||||
id: this.selectedProduct
|
||||
tiers: [{
|
||||
id: this.selectedTier
|
||||
}]
|
||||
}]
|
||||
}
|
@ -69,26 +69,26 @@
|
||||
<p>Free</p>
|
||||
</label>
|
||||
</div>
|
||||
{{#each this.products as |product|}}
|
||||
{{#each this.tiers as |tier|}}
|
||||
<div class="form-group mb0 for-checkbox">
|
||||
<label
|
||||
class="checkbox"
|
||||
for={{product.id}}
|
||||
for={{tier.id}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={{product.id}}
|
||||
name={{product.id}}
|
||||
checked={{product.checked}}
|
||||
id={{tier.id}}
|
||||
name={{tier.id}}
|
||||
checked={{tier.checked}}
|
||||
disabled={{or
|
||||
(not this.membersUtils.isStripeEnabled)
|
||||
(not-eq this.settings.membersSignupAccess "all")
|
||||
}}
|
||||
class="gh-input post-settings-featured"
|
||||
{{on "click" (action "toggleProduct" product.id)}}
|
||||
{{on "click" (action "toggleTier" tier.id)}}
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
<p>{{product.name}}</p>
|
||||
<p>{{tier.name}}</p>
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
@ -24,7 +24,7 @@ export default ModalComponent.extend({
|
||||
showLinksPage: false,
|
||||
showLeaveSettingsModal: false,
|
||||
isPreloading: true,
|
||||
changedProducts: null,
|
||||
changedTiers: null,
|
||||
openSection: null,
|
||||
portalPreviewGuid: 'modal-portal-settings',
|
||||
|
||||
@ -50,19 +50,19 @@ export default ModalComponent.extend({
|
||||
return `data-portal`;
|
||||
}),
|
||||
|
||||
portalPreviewUrl: computed('page', 'model.products.[]', 'changedProducts.[]', 'membersUtils.{isFreeChecked,isMonthlyChecked,isYearlyChecked}', 'settings.{portalName,portalButton,portalButtonIcon,portalButtonSignupText,portalButtonStyle,accentColor,portalPlans.[]}', function () {
|
||||
portalPreviewUrl: computed('page', 'model.tiers.[]', 'changedTiers.[]', 'membersUtils.{isFreeChecked,isMonthlyChecked,isYearlyChecked}', 'settings.{portalName,portalButton,portalButtonIcon,portalButtonSignupText,portalButtonStyle,accentColor,portalPlans.[]}', function () {
|
||||
const options = this.getProperties(['page']);
|
||||
options.portalProducts = this.model.products?.filter((product) => {
|
||||
return product.get('visibility') === 'public'
|
||||
&& product.get('active') === true
|
||||
&& product.get('type') === 'paid';
|
||||
}).map((product) => {
|
||||
return product.id;
|
||||
options.portalTiers = this.model.tiers?.filter((tier) => {
|
||||
return tier.get('visibility') === 'public'
|
||||
&& tier.get('active') === true
|
||||
&& tier.get('type') === 'paid';
|
||||
}).map((tier) => {
|
||||
return tier.id;
|
||||
});
|
||||
const freeProduct = this.model.products?.find((product) => {
|
||||
return product.type === 'free';
|
||||
const freeTier = this.model.tiers?.find((tier) => {
|
||||
return tier.type === 'free';
|
||||
});
|
||||
options.isFreeChecked = freeProduct?.visibility === 'public';
|
||||
options.isFreeChecked = freeTier?.visibility === 'public';
|
||||
return this.membersUtils.getPortalPreviewUrl(options);
|
||||
}),
|
||||
|
||||
@ -94,39 +94,39 @@ export default ModalComponent.extend({
|
||||
const allowedPlans = this.settings.get('portalPlans') || [];
|
||||
return (this.membersUtils.isStripeEnabled && allowedPlans.includes('yearly'));
|
||||
}),
|
||||
products: computed('model.products.[]', 'changedProducts.[]', 'isPreloading', function () {
|
||||
const paidProducts = this.model.products?.filter(product => product.type === 'paid' && product.active === true);
|
||||
if (this.isPreloading || !paidProducts?.length) {
|
||||
tiers: computed('model.tiers.[]', 'changedTiers.[]', 'isPreloading', function () {
|
||||
const paidTiers = this.model.tiers?.filter(tier => tier.type === 'paid' && tier.active === true);
|
||||
if (this.isPreloading || !paidTiers?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const products = paidProducts.map((product) => {
|
||||
const tiers = paidTiers.map((tier) => {
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
checked: product.visibility === 'public'
|
||||
id: tier.id,
|
||||
name: tier.name,
|
||||
checked: tier.visibility === 'public'
|
||||
};
|
||||
});
|
||||
return products;
|
||||
return tiers;
|
||||
}),
|
||||
|
||||
showPortalTiers: computed('products', 'feature.multipleProducts', function () {
|
||||
showPortalTiers: computed('tiers', 'feature.multipleProducts', function () {
|
||||
if (this.feature.get('multipleProducts')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
|
||||
showPortalPrices: computed('products', 'feature.multipleProducts', function () {
|
||||
showPortalPrices: computed('tiers', 'feature.multipleProducts', function () {
|
||||
if (!this.feature.get('multipleProducts')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const visibleProducts = this.model.products?.filter((product) => {
|
||||
return product.visibility === 'public' && product.type === 'paid';
|
||||
const visibleTiers = this.model.tiers?.filter((tier) => {
|
||||
return tier.visibility === 'public' && tier.type === 'paid';
|
||||
});
|
||||
|
||||
return !!visibleProducts?.length;
|
||||
return !!visibleTiers?.length;
|
||||
}),
|
||||
|
||||
init() {
|
||||
@ -147,7 +147,7 @@ export default ModalComponent.extend({
|
||||
label: 'Links'
|
||||
}];
|
||||
this.iconExtensions = ICON_EXTENSIONS;
|
||||
this.changedProducts = [];
|
||||
this.changedTiers = [];
|
||||
this.set('supportAddress', this.parseEmailAddress(this.settings.get('membersSupportAddress')));
|
||||
},
|
||||
|
||||
@ -163,8 +163,8 @@ export default ModalComponent.extend({
|
||||
togglePlan(plan, event) {
|
||||
this.updateAllowedPlan(plan, event.target.checked);
|
||||
},
|
||||
toggleProduct(productId, event) {
|
||||
this.updateAllowedProduct(productId, event.target.checked);
|
||||
toggleTier(tierId, event) {
|
||||
this.updateAllowedTier(tierId, event.target.checked);
|
||||
},
|
||||
togglePortalButton(showButton) {
|
||||
this.settings.set('portalButton', showButton);
|
||||
@ -288,33 +288,33 @@ export default ModalComponent.extend({
|
||||
updateAllowedPlan(plan, isChecked) {
|
||||
const portalPlans = this.settings.get('portalPlans') || [];
|
||||
const allowedPlans = [...portalPlans];
|
||||
const freeProduct = this.model.products.find(p => p.type === 'free');
|
||||
const freeTier = this.model.tiers.find(p => p.type === 'free');
|
||||
|
||||
if (!isChecked) {
|
||||
this.settings.set('portalPlans', allowedPlans.filter(p => p !== plan));
|
||||
if (plan === 'free') {
|
||||
freeProduct.set('visibility', 'none');
|
||||
freeTier.set('visibility', 'none');
|
||||
}
|
||||
} else {
|
||||
allowedPlans.push(plan);
|
||||
this.settings.set('portalPlans', allowedPlans);
|
||||
if (plan === 'free') {
|
||||
freeProduct.set('visibility', 'public');
|
||||
freeTier.set('visibility', 'public');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateAllowedProduct(productId, isChecked) {
|
||||
const product = this.model.products.find(p => p.id === productId);
|
||||
updateAllowedTier(tierId, isChecked) {
|
||||
const tier = this.model.tiers.find(p => p.id === tierId);
|
||||
if (!isChecked) {
|
||||
product.set('visibility', 'none');
|
||||
tier.set('visibility', 'none');
|
||||
} else {
|
||||
product.set('visibility', 'public');
|
||||
tier.set('visibility', 'public');
|
||||
}
|
||||
let portalProducts = this.model.products.filter((p) => {
|
||||
let portalTiers = this.model.tiers.filter((p) => {
|
||||
return p.visibility === 'public';
|
||||
}).map(p => p.id);
|
||||
this.set('changedProducts', portalProducts);
|
||||
this.set('changedTiers', portalTiers);
|
||||
},
|
||||
|
||||
_validateSignupRedirect(url, type) {
|
||||
@ -378,11 +378,11 @@ export default ModalComponent.extend({
|
||||
|
||||
// Save tier visibility if changed
|
||||
yield Promise.all(
|
||||
this.model.products.filter((product) => {
|
||||
const changedAttrs = product.changedAttributes();
|
||||
this.model.tiers.filter((tier) => {
|
||||
const changedAttrs = tier.changedAttributes();
|
||||
return !!changedAttrs.visibility;
|
||||
}).map((product) => {
|
||||
return product.save();
|
||||
}).map((tier) => {
|
||||
return tier.save();
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
<form>
|
||||
<div class="modal-body">
|
||||
<div class="gh-main-section-block">
|
||||
<div class="gh-main-section-content grey gh-product-priceform-block">
|
||||
<div class="gh-main-section-content grey gh-tier-priceform-block">
|
||||
<GhFormGroup @errors={{this.errors}} @property="name">
|
||||
<label for="name" class="fw6">Name</label>
|
||||
<GhTextInput
|
||||
@ -29,7 +29,7 @@
|
||||
@class="gh-input" />
|
||||
<GhErrorMessage @errors={{this.errors}} @property="description" />
|
||||
</GhFormGroup>
|
||||
<div class="gh-product-priceform-pricecurrency">
|
||||
<div class="gh-tier-priceform-pricecurrency">
|
||||
<GhFormGroup @errors={{this.errors}} @property="amount">
|
||||
<label for="amount" class="fw6">Price</label>
|
||||
<div class="flex items-center justify-center gh-labs-price-label">
|
||||
@ -61,7 +61,7 @@
|
||||
</div>
|
||||
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="billing-period">
|
||||
<label for="billing-period" class="fw6">Billing period</label>
|
||||
<GhProductsPriceBillingperiod
|
||||
<GhTiersPriceBillingperiod
|
||||
@updatePeriod={{action "updatePeriod"}}
|
||||
@triggerId="period-input"
|
||||
@value={{this.price.interval}} @disabled={{this.isExistingPrice}}
|
@ -8,7 +8,7 @@ import {tracked} from '@glimmer/tracking';
|
||||
|
||||
// TODO: update modals to work fully with Glimmer components
|
||||
@classic
|
||||
export default class ModalProductPrice extends ModalBase {
|
||||
export default class ModalTierPrice extends ModalBase {
|
||||
@tracked model;
|
||||
@tracked price;
|
||||
@tracked currencyVal;
|
@ -2,57 +2,57 @@
|
||||
{{svg-jar "close"}}
|
||||
</button>
|
||||
|
||||
<div class="gh-product-modal-content" data-test-modal="edit-product">
|
||||
<div class="gh-tier-modal-content" data-test-modal="edit-tier">
|
||||
<header class="modal-header">
|
||||
<h1 data-test-text="title">{{this.title}}</h1>
|
||||
</header>
|
||||
|
||||
<form>
|
||||
<div class="modal-body gh-form-edit-product">
|
||||
<div class="modal-body gh-form-edit-tier">
|
||||
<div class="gh-main-section columns-3">
|
||||
<div class="gh-main-section-block span-2">
|
||||
<h4 class="gh-main-section-header small bn">Basic</h4>
|
||||
<div class="gh-main-section-content grey gh-product-priceform-block">
|
||||
{{#unless this.isFreeProduct}}
|
||||
<div class="gh-main-section-content grey gh-tier-priceform-block">
|
||||
{{#unless this.isFreeTier}}
|
||||
<GhFormGroup @errors={{this.errors}} @property="name" data-test-formgroup="name">
|
||||
<label for="name" class="fw6">Name</label>
|
||||
<GhTextInput
|
||||
@value={{readonly this.product.name}}
|
||||
@input={{action (mut this.product.name) value="target.value"}}
|
||||
@value={{readonly this.tier.name}}
|
||||
@input={{action (mut this.tier.name) value="target.value"}}
|
||||
@name="name"
|
||||
@placeholder="Bronze"
|
||||
@id="name"
|
||||
@class="gh-input"
|
||||
data-test-input="product-name" />
|
||||
data-test-input="tier-name" />
|
||||
<GhErrorMessage @errors={{this.errors}} @property="name" />
|
||||
</GhFormGroup>
|
||||
{{/unless}}
|
||||
|
||||
<GhFormGroup @errors={{this.errors}} @property="description" data-test-formgroup="description">
|
||||
<label for="description" class="fw6">Description</label>
|
||||
{{#if this.isFreeProduct}}
|
||||
{{#if this.isFreeTier}}
|
||||
<GhTextInput
|
||||
@value={{readonly this.product.description}}
|
||||
@input={{action (mut this.product.description) value="target.value"}}
|
||||
@value={{readonly this.tier.description}}
|
||||
@input={{action (mut this.tier.description) value="target.value"}}
|
||||
@name="description"
|
||||
@placeholder="Free preview of {{this.settings.title}}"
|
||||
@id="description"
|
||||
@class="gh-input"
|
||||
data-test-input="free-product-description" />
|
||||
data-test-input="free-tier-description" />
|
||||
{{else}}
|
||||
<GhTextInput
|
||||
@value={{readonly this.product.description}}
|
||||
@input={{action (mut this.product.description) value="target.value"}}
|
||||
@value={{readonly this.tier.description}}
|
||||
@input={{action (mut this.tier.description) value="target.value"}}
|
||||
@name="description"
|
||||
@placeholder="Full access to premium content"
|
||||
@id="description"
|
||||
@class="gh-input"
|
||||
data-test-input="product-description" />
|
||||
data-test-input="tier-description" />
|
||||
{{/if}}
|
||||
<GhErrorMessage @errors={{this.errors}} @property="description" />
|
||||
</GhFormGroup>
|
||||
|
||||
{{#unless this.isFreeProduct}}
|
||||
{{#unless this.isFreeTier}}
|
||||
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="prices" data-test-formgroup="prices">
|
||||
<div class="gh-settings-members-pricelabelcont">
|
||||
<label for="monthlyPrice">Prices</label>
|
||||
@ -109,13 +109,13 @@
|
||||
<label for="welcomePage" class="fw6">Welcome page</label>
|
||||
<GhUrlInput
|
||||
@id="welcomePage"
|
||||
@value={{readonly this.model.product.welcomePageURL}}
|
||||
@value={{readonly this.model.tier.welcomePageURL}}
|
||||
@baseUrl={{readonly this.siteUrl}}
|
||||
@setResult={{this.setWelcomePageURL}}
|
||||
@validateUrl={{this.validateWelcomePageURL}}
|
||||
@placeholder={{readonly this.siteUrl}}
|
||||
/>
|
||||
{{#if this.isFreeProduct}}
|
||||
{{#if this.isFreeTier}}
|
||||
<p>Redirect to this URL after signup for a free membership</p>
|
||||
{{else}}
|
||||
<p>Redirect to this URL after signup for premium membership</p>
|
||||
@ -126,8 +126,8 @@
|
||||
</div>
|
||||
|
||||
<h4 class="gh-main-section-header small bn">Benefits</h4>
|
||||
<div class="gh-main-section-content grey gh-product-form-benefits">
|
||||
<div class="gh-product-benefits">
|
||||
<div class="gh-main-section-content grey gh-tier-form-benefits">
|
||||
<div class="gh-tier-benefits">
|
||||
<div class="gh-blognav">
|
||||
<SortableObjects
|
||||
@sortableObjectList={{this.benefits}}
|
||||
@ -147,7 +147,7 @@
|
||||
{{/each}}
|
||||
</SortableObjects>
|
||||
<GhBenefitItem
|
||||
@isFreeProduct={{this.isFreeProduct}}
|
||||
@isFreeTier={{this.isFreeTier}}
|
||||
@benefitItem={{this.newBenefit}}
|
||||
@addItem={{action "addBenefit"}}
|
||||
@deleteItem={{action "deleteBenefit"}}
|
||||
@ -157,12 +157,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-main-section-block gh-product-form-tierpreview" data-test-tierpreview>
|
||||
<div class="gh-product-form-tierpreview-content">
|
||||
{{#if this.isFreeProduct}}
|
||||
<div class="gh-main-section-block gh-tier-form-tierpreview" data-test-tierpreview>
|
||||
<div class="gh-tier-form-tierpreview-content">
|
||||
{{#if this.isFreeTier}}
|
||||
<h4 class="gh-main-section-header small bn" data-test-tierpreview-title>Free Membership Preview</h4>
|
||||
{{else}}
|
||||
<div class="gh-product-form-tierpreivew-cadence">
|
||||
<div class="gh-tier-form-tierpreivew-cadence">
|
||||
<h4 class="gh-main-section-header small bn" data-test-tierpreview-title>Tier Preview</h4>
|
||||
<div>
|
||||
<button class="gh-btn {{if (eq this.previewCadence "monthly") "selected"}}" type="button" {{on "click" (fn this.changeCadence "monthly")}}><span>Monthly</span></button>
|
||||
@ -171,22 +171,22 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="gh-main-section-content">
|
||||
<div class="gh-portal-product-card-header">
|
||||
{{#if this.product.name}}
|
||||
<h4 class="gh-portal-product-name" data-test-tierpreview-name style={{this.accentColorStyle}}>{{this.product.name}}</h4>
|
||||
<div class="gh-portal-tier-card-header">
|
||||
{{#if this.tier.name}}
|
||||
<h4 class="gh-portal-tier-name" data-test-tierpreview-name style={{this.accentColorStyle}}>{{this.tier.name}}</h4>
|
||||
{{else}}
|
||||
<h4 class="placeholder gh-portal-product-name" style={{this.accentColorStyle}} data-test-tierpreview-name>Bronze</h4>
|
||||
<h4 class="placeholder gh-portal-tier-name" style={{this.accentColorStyle}} data-test-tierpreview-name>Bronze</h4>
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-portal-product-card-pricecontainer" data-test-tierpreview-price>
|
||||
{{#if this.isFreeProduct}}
|
||||
<div class="gh-portal-product-price">
|
||||
<div class="gh-portal-tier-card-pricecontainer" data-test-tierpreview-price>
|
||||
{{#if this.isFreeTier}}
|
||||
<div class="gh-portal-tier-price">
|
||||
<span class="currency-sign">{{currency-symbol this.currency}}</span>
|
||||
<span class="amount">0</span>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if this.stripeYearlyAmount}}
|
||||
<div class="gh-portal-product-price">
|
||||
<div class="gh-portal-tier-price">
|
||||
<span class="currency-sign">{{currency-symbol this.currency}}</span>
|
||||
{{#if (eq this.previewCadence "monthly")}}
|
||||
<span class="amount">
|
||||
@ -206,7 +206,7 @@
|
||||
{{this.discountValue}}% discount</span>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="gh-portal-product-price placeholder">
|
||||
<div class="gh-portal-tier-price placeholder">
|
||||
<span class="currency-sign">{{currency-symbol this.currency}}</span>
|
||||
<span class="amount">0</span>
|
||||
<span class="billing-period">/year</span>
|
||||
@ -217,36 +217,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gh-portal-product-card-details">
|
||||
<div class="gh-portal-product-card-detaildata">
|
||||
{{#if this.product.description}}
|
||||
<div class="gh-portal-product-description" data-test-tierpreview-description>{{this.product.description}}</div>
|
||||
<div class="gh-portal-tier-card-details">
|
||||
<div class="gh-portal-tier-card-detaildata">
|
||||
{{#if this.tier.description}}
|
||||
<div class="gh-portal-tier-description" data-test-tierpreview-description>{{this.tier.description}}</div>
|
||||
{{else}}
|
||||
{{#if this.isFreeProduct}}
|
||||
<div class="placeholder gh-portal-product-description" data-test-tierpreview-description>Free preview of {{this.settings.title}}</div>
|
||||
{{#if this.isFreeTier}}
|
||||
<div class="placeholder gh-portal-tier-description" data-test-tierpreview-description>Free preview of {{this.settings.title}}</div>
|
||||
{{else}}
|
||||
<div class="placeholder gh-portal-product-description" data-test-tierpreview-description>Full access to premium content</div>
|
||||
<div class="placeholder gh-portal-tier-description" data-test-tierpreview-description>Full access to premium content</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.benefits}}
|
||||
<div class="gh-portal-product-benefits" data-test-tierpreview-benefits>
|
||||
<div class="gh-portal-tier-benefits" data-test-tierpreview-benefits>
|
||||
{{#each this.benefits as |benefitItem|}}
|
||||
<div class="gh-portal-product-benefit">{{svg-jar "check-2"}}
|
||||
<div class="gh-portal-tier-benefit">{{svg-jar "check-2"}}
|
||||
<div class="gh-portal-benefit-title">{{benefitItem.name}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="placeholder gh-portal-product-benefits" data-test-tierpreview-benefits>
|
||||
{{#if this.isFreeProduct}}
|
||||
<div class="gh-portal-product-benefit">{{svg-jar "check-2"}}
|
||||
<div class="placeholder gh-portal-tier-benefits" data-test-tierpreview-benefits>
|
||||
{{#if this.isFreeTier}}
|
||||
<div class="gh-portal-tier-benefit">{{svg-jar "check-2"}}
|
||||
<div class="gh-portal-benefit-title">
|
||||
Access to all public posts
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-portal-product-benefit">
|
||||
<div class="gh-portal-tier-benefit">
|
||||
{{svg-jar "check-2"}}
|
||||
<div class="gh-portal-benefit-title">Expert analysis</div>
|
||||
</div>
|
||||
@ -272,10 +272,10 @@
|
||||
>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
<GhTaskButton @buttonText="{{if this.isExistingProduct "Save" "Add tier"}}"
|
||||
<GhTaskButton @buttonText="{{if this.isExistingTier "Save" "Add tier"}}"
|
||||
@successText={{this.successText}}
|
||||
@task={{this.saveProduct}}
|
||||
@task={{this.saveTier}}
|
||||
@idleClass="gh-btn-primary"
|
||||
@class="gh-btn {{if this.isExistingProduct "gh-btn-black" "gh-btn-green"}} gh-btn-icon"
|
||||
data-test-button="save-product" />
|
||||
@class="gh-btn {{if this.isExistingTier "gh-btn-black" "gh-btn-green"}} gh-btn-icon"
|
||||
data-test-button="save-tier" />
|
||||
</div>
|
@ -1,6 +1,6 @@
|
||||
import EmberObject, {action} from '@ember/object';
|
||||
import ModalBase from 'ghost-admin/components/modal-base';
|
||||
import ProductBenefitItem from '../models/product-benefit-item';
|
||||
import TierBenefitItem from '../models/tier-benefit-item';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {A as emberA} from '@ember/array';
|
||||
@ -20,11 +20,11 @@ const CURRENCIES = currencies.map((currency) => {
|
||||
|
||||
// TODO: update modals to work fully with Glimmer components
|
||||
@classic
|
||||
export default class ModalProductPrice extends ModalBase {
|
||||
export default class ModalTierPrice extends ModalBase {
|
||||
@service settings;
|
||||
@service config;
|
||||
@tracked model;
|
||||
@tracked product;
|
||||
@tracked tier;
|
||||
@tracked periodVal;
|
||||
@tracked stripeMonthlyAmount = 5;
|
||||
@tracked stripeYearlyAmount = 50;
|
||||
@ -41,22 +41,22 @@ export default class ModalProductPrice extends ModalBase {
|
||||
|
||||
confirm() {}
|
||||
|
||||
get isFreeProduct() {
|
||||
return this.product.type === 'free';
|
||||
get isFreeTier() {
|
||||
return this.tier.type === 'free';
|
||||
}
|
||||
|
||||
get allCurrencies() {
|
||||
return getCurrencyOptions();
|
||||
}
|
||||
|
||||
get productCurrency() {
|
||||
if (this.isFreeProduct) {
|
||||
const firstPaidProduct = this.model.products?.find((product) => {
|
||||
return product.type === 'paid';
|
||||
get tierCurrency() {
|
||||
if (this.isFreeTier) {
|
||||
const firstPaidTier = this.model.tiers?.find((tier) => {
|
||||
return tier.type === 'paid';
|
||||
});
|
||||
return firstPaidProduct?.monthlyPrice?.currency || 'usd';
|
||||
return firstPaidTier?.monthlyPrice?.currency || 'usd';
|
||||
} else {
|
||||
return this.product?.monthlyPrice?.currency;
|
||||
return this.tier?.monthlyPrice?.currency;
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,18 +66,18 @@ export default class ModalProductPrice extends ModalBase {
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
this.product = this.model.product;
|
||||
const monthlyPrice = this.product.get('monthlyPrice');
|
||||
const yearlyPrice = this.product.get('yearlyPrice');
|
||||
this.tier = this.model.tier;
|
||||
const monthlyPrice = this.tier.get('monthlyPrice');
|
||||
const yearlyPrice = this.tier.get('yearlyPrice');
|
||||
if (monthlyPrice) {
|
||||
this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
|
||||
}
|
||||
if (yearlyPrice) {
|
||||
this.stripeYearlyAmount = (yearlyPrice.amount / 100);
|
||||
}
|
||||
this.currency = this.productCurrency || 'usd';
|
||||
this.benefits = this.product.get('benefits') || emberA([]);
|
||||
this.newBenefit = ProductBenefitItem.create({
|
||||
this.currency = this.tierCurrency || 'usd';
|
||||
this.benefits = this.tier.get('benefits') || emberA([]);
|
||||
this.newBenefit = TierBenefitItem.create({
|
||||
isNew: true,
|
||||
name: ''
|
||||
});
|
||||
@ -97,9 +97,9 @@ export default class ModalProductPrice extends ModalBase {
|
||||
|
||||
if (this.welcomePageURL.href.startsWith(siteUrl)) {
|
||||
const path = this.welcomePageURL.href.replace(siteUrl, '');
|
||||
this.model.product.welcomePageURL = path;
|
||||
this.model.tier.welcomePageURL = path;
|
||||
} else {
|
||||
this.model.product.welcomePageURL = this.welcomePageURL.href;
|
||||
this.model.tier.welcomePageURL = this.welcomePageURL.href;
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,12 +109,12 @@ export default class ModalProductPrice extends ModalBase {
|
||||
|
||||
// eslint-disable-next-line no-dupe-class-members
|
||||
get welcomePageURL() {
|
||||
return this.model.product.welcomePageURL;
|
||||
return this.model.tier.welcomePageURL;
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (this.isExistingProduct) {
|
||||
if (this.isFreeProduct) {
|
||||
if (this.isExistingTier) {
|
||||
if (this.isFreeTier) {
|
||||
return `Edit free membership`;
|
||||
}
|
||||
return `Edit tier`;
|
||||
@ -122,8 +122,8 @@ export default class ModalProductPrice extends ModalBase {
|
||||
return 'New tier';
|
||||
}
|
||||
|
||||
get isExistingProduct() {
|
||||
return !this.model.product.isNew;
|
||||
get isExistingTier() {
|
||||
return !this.model.tier.isNew;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -143,13 +143,13 @@ export default class ModalProductPrice extends ModalBase {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.newBenefit = ProductBenefitItem.create({isNew: true, name: ''});
|
||||
const savedBenefits = this.product.benefits?.filter(benefit => !!benefit.id) || emberA([]);
|
||||
this.product.set('benefits', savedBenefits);
|
||||
this.newBenefit = TierBenefitItem.create({isNew: true, name: ''});
|
||||
const savedBenefits = this.tier.benefits?.filter(benefit => !!benefit.id) || emberA([]);
|
||||
this.tier.set('benefits', savedBenefits);
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*saveProduct() {
|
||||
*saveTier() {
|
||||
this.validatePrices();
|
||||
if (!isEmpty(this.errors) && Object.keys(this.errors).length > 0) {
|
||||
return;
|
||||
@ -162,10 +162,10 @@ export default class ModalProductPrice extends ModalBase {
|
||||
yield this.send('addBenefit', this.newBenefit);
|
||||
}
|
||||
|
||||
if (!this.isFreeProduct) {
|
||||
if (!this.isFreeTier) {
|
||||
const monthlyAmount = Math.round(this.stripeMonthlyAmount * 100);
|
||||
const yearlyAmount = Math.round(this.stripeYearlyAmount * 100);
|
||||
this.product.set('monthlyPrice', {
|
||||
this.tier.set('monthlyPrice', {
|
||||
nickname: 'Monthly',
|
||||
amount: monthlyAmount,
|
||||
active: true,
|
||||
@ -173,7 +173,7 @@ export default class ModalProductPrice extends ModalBase {
|
||||
interval: 'month',
|
||||
type: 'recurring'
|
||||
});
|
||||
this.product.set('yearlyPrice', {
|
||||
this.tier.set('yearlyPrice', {
|
||||
nickname: 'Yearly',
|
||||
amount: yearlyAmount,
|
||||
active: true,
|
||||
@ -182,8 +182,8 @@ export default class ModalProductPrice extends ModalBase {
|
||||
type: 'recurring'
|
||||
});
|
||||
}
|
||||
this.product.set('benefits', this.benefits.filter(benefit => !benefit.get('isBlank')));
|
||||
yield this.product.save();
|
||||
this.tier.set('benefits', this.benefits.filter(benefit => !benefit.get('isBlank')));
|
||||
yield this.tier.save();
|
||||
|
||||
yield this.confirm();
|
||||
this.send('closeModal');
|
||||
@ -208,7 +208,7 @@ export default class ModalProductPrice extends ModalBase {
|
||||
item.set('isNew', false);
|
||||
this.benefits.pushObject(item);
|
||||
|
||||
this.newBenefit = ProductBenefitItem.create({isNew: true, name: ''});
|
||||
this.newBenefit = TierBenefitItem.create({isNew: true, name: ''});
|
||||
}
|
||||
|
||||
calculateDiscount() {
|
||||
@ -254,7 +254,7 @@ export default class ModalProductPrice extends ModalBase {
|
||||
this.benefits.removeObject(item);
|
||||
},
|
||||
reorderItems() {
|
||||
this.product.set('benefits', this.benefits);
|
||||
this.tier.set('benefits', this.benefits);
|
||||
},
|
||||
updateLabel(label, benefitItem) {
|
||||
if (!benefitItem) {
|
@ -7,7 +7,7 @@
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Members will no longer be able to subscribe to <strong>{{@data.product.name}}</strong> and it will be removed from the list of available tiers in portal.
|
||||
Members will no longer be able to subscribe to <strong>{{@data.tier.name}}</strong> and it will be removed from the list of available tiers in portal.
|
||||
</p>
|
||||
<p>
|
||||
Existing members on this tier will remain unchanged.
|
||||
|
@ -7,20 +7,20 @@ export default class ArchiveTierModal extends Component {
|
||||
@service router;
|
||||
|
||||
get isActive() {
|
||||
const {product} = this.args.data;
|
||||
return !!product.active;
|
||||
const {tier} = this.args.data;
|
||||
return !!tier.active;
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*archiveTierTask() {
|
||||
const {product, onArchive} = this.args.data;
|
||||
product.active = false;
|
||||
product.visibility = 'none';
|
||||
const {tier, onArchive} = this.args.data;
|
||||
tier.active = false;
|
||||
tier.visibility = 'none';
|
||||
|
||||
try {
|
||||
yield product.save();
|
||||
yield tier.save();
|
||||
onArchive?.();
|
||||
return product;
|
||||
return tier;
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
this.notifications.showAPIError(error, {key: 'tier.archive.failed'});
|
||||
|
@ -7,10 +7,10 @@
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Reactivating <strong>{{@data.product.name}}</strong> will re-enable it as an option in portal and allow new members to subscribe to this tier.
|
||||
Reactivating <strong>{{@data.tier.name}}</strong> will re-enable it as an option in portal and allow new members to subscribe to this tier.
|
||||
</p>
|
||||
<p>
|
||||
Existing members will remain unchanged.
|
||||
Existing members will remain unchanged.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -7,19 +7,19 @@ export default class UnarchiveTierModal extends Component {
|
||||
@service router;
|
||||
|
||||
get isActive() {
|
||||
const {product} = this.args.data;
|
||||
return !!product.active;
|
||||
const {tier} = this.args.data;
|
||||
return !!tier.active;
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*unarchiveTask() {
|
||||
const {product, onUnarchive} = this.args.data;
|
||||
product.active = true;
|
||||
const {tier, onUnarchive} = this.args.data;
|
||||
tier.active = true;
|
||||
|
||||
try {
|
||||
yield product.save();
|
||||
yield tier.save();
|
||||
onUnarchive?.();
|
||||
return product;
|
||||
return tier;
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
this.notifications.showAPIError(error, {key: 'tier.unarchive.failed'});
|
||||
|
@ -26,7 +26,7 @@
|
||||
{{#if this.hasVisibilityFilter}}
|
||||
<div class="mt2" data-test-default-post-access-tiers>
|
||||
<GhPostSettingsMenu::VisibilitySegmentSelect
|
||||
@selectDefaultProduct={{true}}
|
||||
@selectDefaultTier={{true}}
|
||||
@tiers={{this.visibilityTiers}}
|
||||
@onChange={{action "setVisibility"}}
|
||||
@renderInPlace={{true}}
|
||||
|
@ -62,11 +62,11 @@ export default class SettingsMembersDefaultPostAccess extends Component {
|
||||
@action
|
||||
setVisibility(segment) {
|
||||
if (segment) {
|
||||
const productIds = segment?.map((product) => {
|
||||
return product.id;
|
||||
const tierIds = segment?.map((tier) => {
|
||||
return tier.id;
|
||||
});
|
||||
this.settings.set('defaultContentVisibility', 'tiers');
|
||||
this.settings.set('defaultContentVisibilityTiers', productIds);
|
||||
this.settings.set('defaultContentVisibilityTiers', tierIds);
|
||||
this.showSegmentError = false;
|
||||
} else {
|
||||
this.settings.set('defaultContentVisibility', '');
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{#if this.product.active}}
|
||||
{{#unless this.product.isNew}}
|
||||
{{#if this.tier.active}}
|
||||
{{#unless this.tier.isNew}}
|
||||
<button
|
||||
type="button"
|
||||
{{on "click" this.handleArchiveTier}}
|
||||
|
@ -8,19 +8,19 @@ export default class ArchiveTierComponent extends Component {
|
||||
@service modals;
|
||||
|
||||
get isActive() {
|
||||
const {product} = this.args;
|
||||
return !!product.active;
|
||||
const {tier} = this.args;
|
||||
return !!tier.active;
|
||||
}
|
||||
|
||||
get product() {
|
||||
return this.args.product;
|
||||
get tier() {
|
||||
return this.args.tier;
|
||||
}
|
||||
|
||||
@action
|
||||
handleArchiveTier() {
|
||||
if (!this.product.isNew) {
|
||||
if (!this.tier.isNew) {
|
||||
this.modals.open('modals/tiers/archive', {
|
||||
product: this.product,
|
||||
tier: this.tier,
|
||||
onArchive: this.args.onArchive
|
||||
}, {
|
||||
className: 'fullscreen-modal fullscreen-modal-action fullscreen-modal-wide'
|
||||
@ -30,9 +30,9 @@ export default class ArchiveTierComponent extends Component {
|
||||
|
||||
@action
|
||||
handleUnarchiveTier() {
|
||||
if (!this.product.isNew) {
|
||||
if (!this.tier.isNew) {
|
||||
this.modals.open('modals/tiers/unarchive', {
|
||||
product: this.product,
|
||||
tier: this.tier,
|
||||
onUnarchive: this.args.onUnarchive
|
||||
}, {
|
||||
className: 'fullscreen-modal fullscreen-modal-action fullscreen-modal-wide'
|
||||
|
@ -90,7 +90,7 @@
|
||||
<LinkTo @route="settings.newsletters.edit-newsletter" @model={{newsletter.id}} class="gh-btn gh-btn-green" data-test-button="customize-newsletter"><span>Customize →</span></LinkTo>
|
||||
{{else}}
|
||||
<GhBasicDropdown @verticalPosition="below" @horizontalPosition="right" @renderInPlace={{true}} as |dd|>
|
||||
<dd.Trigger class="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-product-card-actions-button icon-only">
|
||||
<dd.Trigger class="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-tier-card-actions-button icon-only">
|
||||
<span data-test-newsletter-menu-trigger>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Actions</span>
|
||||
|
@ -9,7 +9,7 @@ export default class TiersSegmentSelect extends Component {
|
||||
@service feature;
|
||||
|
||||
@tracked _options = [];
|
||||
@tracked products = [];
|
||||
@tracked tiers = [];
|
||||
|
||||
get renderInPlace() {
|
||||
return this.args.renderInPlace === undefined ? false : this.args.renderInPlace;
|
||||
@ -41,9 +41,9 @@ export default class TiersSegmentSelect extends Component {
|
||||
}
|
||||
|
||||
get selectedOptions() {
|
||||
const tierList = (this.args.tiers || []).map((product) => {
|
||||
return this.products.find((p) => {
|
||||
return p.id === product.id || p.slug === product.slug;
|
||||
const tierList = (this.args.tiers || []).map((tier) => {
|
||||
return this.tiers.find((p) => {
|
||||
return p.id === tier.id || p.slug === tier.slug;
|
||||
});
|
||||
}).filter(d => !!d);
|
||||
const tierIdList = tierList.map(d => d.id);
|
||||
@ -53,13 +53,13 @@ export default class TiersSegmentSelect extends Component {
|
||||
@action
|
||||
setSegment(options) {
|
||||
let ids = options.mapBy('id').map((id) => {
|
||||
let product = this.products.find((p) => {
|
||||
let tier = this.tiers.find((p) => {
|
||||
return p.id === id;
|
||||
});
|
||||
return {
|
||||
id: product.id,
|
||||
slug: product.slug,
|
||||
name: product.name
|
||||
id: tier.id,
|
||||
slug: tier.slug,
|
||||
name: tier.name
|
||||
};
|
||||
}) || [];
|
||||
this.args.onChange?.(ids);
|
||||
@ -70,29 +70,29 @@ export default class TiersSegmentSelect extends Component {
|
||||
const options = yield [];
|
||||
|
||||
if (this.feature.get('multipleProducts')) {
|
||||
// fetch all products with count
|
||||
// fetch all tiers with count
|
||||
// TODO: add `include: 'count.members` to query once API supports
|
||||
const products = yield this.store.query('product', {filter: 'type:paid', limit: 'all', include: 'monthly_price,yearly_price,benefits'});
|
||||
this.products = products;
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', limit: 'all', include: 'monthly_price,yearly_price,benefits'});
|
||||
this.tiers = tiers;
|
||||
|
||||
if (products.length > 0) {
|
||||
const productsGroup = {
|
||||
if (tiers.length > 0) {
|
||||
const tiersGroup = {
|
||||
groupName: 'Tiers',
|
||||
options: []
|
||||
};
|
||||
|
||||
products.forEach((product) => {
|
||||
productsGroup.options.push({
|
||||
name: product.name,
|
||||
id: product.id,
|
||||
count: product.count?.members,
|
||||
class: 'segment-product'
|
||||
tiers.forEach((tier) => {
|
||||
tiersGroup.options.push({
|
||||
name: tier.name,
|
||||
id: tier.id,
|
||||
count: tier.count?.members,
|
||||
class: 'segment-tier'
|
||||
});
|
||||
});
|
||||
|
||||
options.push(productsGroup);
|
||||
if (this.args.selectDefaultProduct && !this.args.tiers) {
|
||||
this.setSegment([productsGroup.options[0]]);
|
||||
options.push(tiersGroup);
|
||||
if (this.args.selectDefaultTier && !this.args.tiers) {
|
||||
this.setSegment([tiersGroup.options[0]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ export default class MemberController extends Controller {
|
||||
|
||||
this.member = yield this.store.queryRecord('member', {
|
||||
id: memberId,
|
||||
include: 'products'
|
||||
include: 'tiers'
|
||||
});
|
||||
|
||||
this.isLoading = false;
|
||||
|
@ -163,7 +163,7 @@ export default class MembersController extends Controller {
|
||||
get filterColumns() {
|
||||
const defaultColumns = ['name', 'email', 'email_open_rate', 'created_at'];
|
||||
if (this.feature.get('membersTableStatus')) {
|
||||
defaultColumns.push('status', 'product');
|
||||
defaultColumns.push('status', 'tier');
|
||||
}
|
||||
const availableFilters = this.filters.length ? this.filters : this.softFilters;
|
||||
return availableFilters.map((filter) => {
|
||||
@ -180,7 +180,7 @@ export default class MembersController extends Controller {
|
||||
'subscriptions.status': 'Subscription Status',
|
||||
'subscriptions.start_date': 'Paid start date',
|
||||
'subscriptions.current_period_end': 'Next billing date',
|
||||
product: 'Membership tier'
|
||||
tier: 'Membership tier'
|
||||
};
|
||||
return this.filterColumns.map((d) => {
|
||||
return {
|
||||
@ -190,10 +190,10 @@ export default class MembersController extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
includeProductQuery() {
|
||||
includeTierQuery() {
|
||||
const availableFilters = this.filters.length ? this.filters : this.softFilters;
|
||||
return availableFilters.some((f) => {
|
||||
return f.type === 'product';
|
||||
return f.type === 'tier';
|
||||
});
|
||||
}
|
||||
|
||||
@ -408,7 +408,7 @@ export default class MembersController extends Controller {
|
||||
extraFilters: [`created_at:<='${moment.utc(this._startDate).format('YYYY-MM-DD HH:mm:ss')}'`]
|
||||
});
|
||||
const order = orderParam ? `${orderParam} desc` : `created_at desc`;
|
||||
const includes = ['labels', 'products'];
|
||||
const includes = ['labels', 'tiers'];
|
||||
|
||||
query = Object.assign({
|
||||
include: includes.join(','),
|
||||
|
@ -23,7 +23,7 @@ export default class OffersController extends Controller {
|
||||
@service notifications;
|
||||
|
||||
@tracked cadences = [];
|
||||
@tracked products = [];
|
||||
@tracked tiers = [];
|
||||
@tracked portalPreviewUrl = '';
|
||||
@tracked showUnsavedChangesModal = false;
|
||||
|
||||
@ -84,11 +84,11 @@ export default class OffersController extends Controller {
|
||||
|
||||
get cadence() {
|
||||
if (this.offer.tier && this.offer.cadence) {
|
||||
const product = this.products.findBy('id', this.offer.tier.id);
|
||||
return `${this.offer.tier.id}-${this.offer.cadence}-${product?.monthlyPrice?.currency}`;
|
||||
const tier = this.tiers.findBy('id', this.offer.tier.id);
|
||||
return `${this.offer.tier.id}-${this.offer.cadence}-${tier?.monthlyPrice?.currency}`;
|
||||
} else if (this.defaultProps) {
|
||||
const product = this.products.findBy('id', this.defaultProps.tier.id);
|
||||
return `${this.defaultProps.tier.id}-${this.defaultProps.cadence}-${product?.monthlyPrice?.currency}`;
|
||||
const tier = this.tiers.findBy('id', this.defaultProps.tier.id);
|
||||
return `${this.defaultProps.tier.id}-${this.defaultProps.cadence}-${tier?.monthlyPrice?.currency}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@ -100,33 +100,33 @@ export default class OffersController extends Controller {
|
||||
// Tasks -------------------------------------------------------------------
|
||||
|
||||
@task({drop: true})
|
||||
*fetchProducts() {
|
||||
this.products = yield this.store.query('product', {filter: 'type:paid+active:true', include: 'monthly_price,yearly_price'});
|
||||
this.products = this.products.filter((d) => {
|
||||
*fetchTiers() {
|
||||
this.tiers = yield this.store.query('tier', {filter: 'type:paid+active:true', include: 'monthly_price,yearly_price'});
|
||||
this.tiers = this.tiers.filter((d) => {
|
||||
return d.monthlyPrice && d.yearlyPrice;
|
||||
});
|
||||
const cadences = [];
|
||||
this.products.forEach((product) => {
|
||||
this.tiers.forEach((tier) => {
|
||||
let monthlyLabel;
|
||||
let yearlyLabel;
|
||||
const productCurrency = product.monthlyPrice.currency;
|
||||
const productCurrencySymbol = productCurrency.toUpperCase();
|
||||
const tierCurrency = tier.monthlyPrice.currency;
|
||||
const tierCurrencySymbol = tierCurrency.toUpperCase();
|
||||
if (this.feature.get('multipleProducts')) {
|
||||
monthlyLabel = `${product.name} - Monthly (${ghPriceAmount(product.monthlyPrice.amount)} ${productCurrencySymbol})`;
|
||||
yearlyLabel = `${product.name} - Yearly (${ghPriceAmount(product.yearlyPrice.amount)} ${productCurrencySymbol})`;
|
||||
monthlyLabel = `${tier.name} - Monthly (${ghPriceAmount(tier.monthlyPrice.amount)} ${tierCurrencySymbol})`;
|
||||
yearlyLabel = `${tier.name} - Yearly (${ghPriceAmount(tier.yearlyPrice.amount)} ${tierCurrencySymbol})`;
|
||||
} else {
|
||||
monthlyLabel = `Monthly (${ghPriceAmount(product.monthlyPrice.amount)} ${productCurrencySymbol})`;
|
||||
yearlyLabel = `Yearly (${ghPriceAmount(product.yearlyPrice.amount)} ${productCurrencySymbol})`;
|
||||
monthlyLabel = `Monthly (${ghPriceAmount(tier.monthlyPrice.amount)} ${tierCurrencySymbol})`;
|
||||
yearlyLabel = `Yearly (${ghPriceAmount(tier.yearlyPrice.amount)} ${tierCurrencySymbol})`;
|
||||
}
|
||||
|
||||
cadences.push({
|
||||
label: monthlyLabel,
|
||||
name: `${product.id}-month-${productCurrency}`
|
||||
name: `${tier.id}-month-${tierCurrency}`
|
||||
});
|
||||
|
||||
cadences.push({
|
||||
label: yearlyLabel,
|
||||
name: `${product.id}-year-${productCurrency}`
|
||||
name: `${tier.id}-year-${tierCurrency}`
|
||||
});
|
||||
});
|
||||
this.cadences = cadences;
|
||||
@ -286,7 +286,7 @@ export default class OffersController extends Controller {
|
||||
|
||||
@action
|
||||
setup() {
|
||||
this.fetchProducts.perform();
|
||||
this.fetchTiers.perform();
|
||||
}
|
||||
|
||||
@action
|
||||
@ -378,9 +378,9 @@ export default class OffersController extends Controller {
|
||||
if (!tierId) {
|
||||
return '$';
|
||||
}
|
||||
const product = this.products.findBy('id', tierId);
|
||||
const productCurrency = product?.monthlyPrice?.currency || 'usd';
|
||||
return getSymbol(productCurrency);
|
||||
const tier = this.tiers.findBy('id', tierId);
|
||||
const tierCurrency = tier?.monthlyPrice?.currency || 'usd';
|
||||
return getSymbol(tierCurrency);
|
||||
}
|
||||
|
||||
get currencyLength() {
|
||||
|
@ -18,7 +18,7 @@ export default class MembersController extends Controller {
|
||||
@service router;
|
||||
|
||||
@tracked offers = [];
|
||||
@tracked products = [];
|
||||
@tracked tiers = [];
|
||||
@tracked type = 'active';
|
||||
|
||||
queryParams = [
|
||||
@ -32,16 +32,16 @@ export default class MembersController extends Controller {
|
||||
|
||||
get filteredOffers() {
|
||||
return this.offers.filter((offer) => {
|
||||
const product = this.products.find((p) => {
|
||||
const tier = this.tiers.find((p) => {
|
||||
return p.id === offer.tier.id;
|
||||
});
|
||||
const price = offer.cadence === 'month' ? product.monthlyPrice : product.yearlyPrice;
|
||||
const price = offer.cadence === 'month' ? tier.monthlyPrice : tier.yearlyPrice;
|
||||
return offer.status === this.type && !!price;
|
||||
}).map((offer) => {
|
||||
const product = this.products.find((p) => {
|
||||
const tier = this.tiers.find((p) => {
|
||||
return p.id === offer.tier.id;
|
||||
});
|
||||
const price = offer.cadence === 'month' ? product.monthlyPrice : product.yearlyPrice;
|
||||
const price = offer.cadence === 'month' ? tier.monthlyPrice : tier.yearlyPrice;
|
||||
offer.finalCurrency = offer.currency || price.currency;
|
||||
offer.originalPrice = price.amount;
|
||||
offer.updatedPrice = offer.type === 'fixed' ? (price.amount - offer.amount) : (price.amount - ((price.amount * offer.amount) / 100));
|
||||
@ -73,7 +73,7 @@ export default class MembersController extends Controller {
|
||||
|
||||
@task({restartable: true})
|
||||
*fetchOffersTask() {
|
||||
this.products = yield this.store.query('product', {
|
||||
this.tiers = yield this.store.query('tier', {
|
||||
filter: 'type:paid', include: 'monthly_price,yearly_price'
|
||||
});
|
||||
this.offers = yield this.store.query('offer', {limit: 'all'});
|
||||
|
@ -26,11 +26,11 @@ export default class MembersAccessController extends Controller {
|
||||
@tracked showLeaveRouteModal = false;
|
||||
@tracked showPortalSettings = false;
|
||||
@tracked showStripeConnect = false;
|
||||
@tracked showProductModal = false;
|
||||
@tracked showTierModal = false;
|
||||
|
||||
@tracked product = null;
|
||||
@tracked products = null;
|
||||
@tracked productModel = null;
|
||||
@tracked tier = null;
|
||||
@tracked tiers = null;
|
||||
@tracked tierModel = null;
|
||||
@tracked paidSignupRedirect;
|
||||
@tracked freeSignupRedirect;
|
||||
@tracked welcomePageURL;
|
||||
@ -45,12 +45,12 @@ export default class MembersAccessController extends Controller {
|
||||
|
||||
queryParams = ['showPortalSettings'];
|
||||
|
||||
get freeProduct() {
|
||||
return this.products?.find(product => product.type === 'free');
|
||||
get freeTier() {
|
||||
return this.tiers?.find(tier => tier.type === 'free');
|
||||
}
|
||||
|
||||
get paidProducts() {
|
||||
return this.products?.filter(product => product.type === 'paid');
|
||||
get paidTiers() {
|
||||
return this.tiers?.filter(tier => tier.type === 'paid');
|
||||
}
|
||||
|
||||
get allCurrencies() {
|
||||
@ -71,9 +71,9 @@ export default class MembersAccessController extends Controller {
|
||||
}
|
||||
|
||||
get hasChangedPrices() {
|
||||
if (this.product) {
|
||||
const monthlyPrice = this.product.get('monthlyPrice');
|
||||
const yearlyPrice = this.product.get('yearlyPrice');
|
||||
if (this.tier) {
|
||||
const monthlyPrice = this.tier.get('monthlyPrice');
|
||||
const yearlyPrice = this.tier.get('yearlyPrice');
|
||||
|
||||
if (monthlyPrice?.amount && parseFloat(this.stripeMonthlyAmount) !== (monthlyPrice.amount / 100)) {
|
||||
return true;
|
||||
@ -88,7 +88,7 @@ export default class MembersAccessController extends Controller {
|
||||
|
||||
@action
|
||||
setup() {
|
||||
this.fetchProducts.perform();
|
||||
this.fetchTiers.perform();
|
||||
this.updatePortalPreview();
|
||||
}
|
||||
|
||||
@ -169,9 +169,9 @@ export default class MembersAccessController extends Controller {
|
||||
|
||||
if (this.welcomePageURL.href.startsWith(siteUrl)) {
|
||||
const path = this.welcomePageURL.href.replace(siteUrl, '');
|
||||
this.freeProduct.welcomePageURL = path;
|
||||
this.freeTier.welcomePageURL = path;
|
||||
} else {
|
||||
this.freeProduct.welcomePageURL = this.welcomePageURL.href;
|
||||
this.freeTier.welcomePageURL = this.welcomePageURL.href;
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,20 +210,20 @@ export default class MembersAccessController extends Controller {
|
||||
}
|
||||
|
||||
@action
|
||||
async openEditProduct(product) {
|
||||
this.productModel = product;
|
||||
this.showProductModal = true;
|
||||
async openEditTier(tier) {
|
||||
this.tierModel = tier;
|
||||
this.showTierModal = true;
|
||||
}
|
||||
|
||||
@action
|
||||
async openNewProduct() {
|
||||
this.productModel = this.store.createRecord('product');
|
||||
this.showProductModal = true;
|
||||
async openNewTier() {
|
||||
this.tierModel = this.store.createRecord('tier');
|
||||
this.showTierModal = true;
|
||||
}
|
||||
|
||||
@action
|
||||
closeProductModal() {
|
||||
this.showProductModal = false;
|
||||
closeTierModal() {
|
||||
this.showTierModal = false;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -266,20 +266,20 @@ export default class MembersAccessController extends Controller {
|
||||
let isMonthlyChecked = portalPlans.includes('monthly');
|
||||
let isYearlyChecked = portalPlans.includes('yearly');
|
||||
|
||||
const products = this.store.peekAll('product');
|
||||
const portalProducts = products?.filter((product) => {
|
||||
return product.get('visibility') === 'public'
|
||||
&& product.get('active') === true
|
||||
&& product.get('type') === 'paid';
|
||||
}).map((product) => {
|
||||
return product.id;
|
||||
const tiers = this.store.peekAll('tier');
|
||||
const portalTiers = tiers?.filter((tier) => {
|
||||
return tier.get('visibility') === 'public'
|
||||
&& tier.get('active') === true
|
||||
&& tier.get('type') === 'paid';
|
||||
}).map((tier) => {
|
||||
return tier.id;
|
||||
});
|
||||
|
||||
const newUrl = new URL(this.membersUtils.getPortalPreviewUrl({
|
||||
button: false,
|
||||
monthlyPrice,
|
||||
yearlyPrice,
|
||||
portalProducts,
|
||||
portalTiers,
|
||||
currency: this.currency,
|
||||
isMonthlyChecked,
|
||||
isYearlyChecked,
|
||||
@ -325,9 +325,9 @@ export default class MembersAccessController extends Controller {
|
||||
}
|
||||
|
||||
@action
|
||||
confirmProductSave() {
|
||||
confirmTierSave() {
|
||||
this.updatePortalPreview({forceRefresh: true});
|
||||
return this.fetchProducts.perform();
|
||||
return this.fetchTiers.perform();
|
||||
}
|
||||
|
||||
@task
|
||||
@ -335,10 +335,10 @@ export default class MembersAccessController extends Controller {
|
||||
return yield this.saveSettingsTask.perform({forceRefresh: true});
|
||||
}
|
||||
|
||||
setupPortalProduct(product) {
|
||||
if (product) {
|
||||
const monthlyPrice = product.get('monthlyPrice');
|
||||
const yearlyPrice = product.get('yearlyPrice');
|
||||
setupPortalTier(tier) {
|
||||
if (tier) {
|
||||
const monthlyPrice = tier.get('monthlyPrice');
|
||||
const yearlyPrice = tier.get('yearlyPrice');
|
||||
if (monthlyPrice && monthlyPrice.amount) {
|
||||
this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
|
||||
this.currency = monthlyPrice.currency;
|
||||
@ -351,12 +351,12 @@ export default class MembersAccessController extends Controller {
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*fetchProducts() {
|
||||
this.products = yield this.store.query('product', {
|
||||
*fetchTiers() {
|
||||
this.tiers = yield this.store.query('tier', {
|
||||
include: 'monthly_price,yearly_price,benefits'
|
||||
});
|
||||
this.product = this.paidProducts.firstObject;
|
||||
this.setupPortalProduct(this.product);
|
||||
this.tier = this.paidTiers.firstObject;
|
||||
this.setupPortalTier(this.tier);
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
@ -372,7 +372,7 @@ export default class MembersAccessController extends Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
yield this.saveProduct();
|
||||
yield this.saveTier();
|
||||
const result = yield this.settings.save();
|
||||
|
||||
this.updatePortalPreview(options);
|
||||
@ -387,19 +387,19 @@ export default class MembersAccessController extends Controller {
|
||||
return;
|
||||
}
|
||||
const result = yield this.settings.save();
|
||||
yield this.freeProduct.save();
|
||||
yield this.freeTier.save();
|
||||
this.updatePortalPreview(options);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
async saveProduct() {
|
||||
async saveTier() {
|
||||
const isStripeConnected = this.settings.get('stripeConnectAccountId');
|
||||
if (this.product && isStripeConnected) {
|
||||
if (this.tier && isStripeConnected) {
|
||||
const monthlyAmount = Math.round(this.stripeMonthlyAmount * 100);
|
||||
const yearlyAmount = Math.round(this.stripeYearlyAmount * 100);
|
||||
|
||||
this.product.set('monthlyPrice', {
|
||||
this.tier.set('monthlyPrice', {
|
||||
nickname: 'Monthly',
|
||||
amount: monthlyAmount,
|
||||
active: true,
|
||||
@ -407,7 +407,7 @@ export default class MembersAccessController extends Controller {
|
||||
interval: 'month',
|
||||
type: 'recurring'
|
||||
});
|
||||
this.product.set('yearlyPrice', {
|
||||
this.tier.set('yearlyPrice', {
|
||||
nickname: 'Yearly',
|
||||
amount: yearlyAmount,
|
||||
active: true,
|
||||
@ -416,14 +416,14 @@ export default class MembersAccessController extends Controller {
|
||||
type: 'recurring'
|
||||
});
|
||||
|
||||
const savedProduct = await this.product.save();
|
||||
return savedProduct;
|
||||
const savedTier = await this.tier.save();
|
||||
return savedTier;
|
||||
}
|
||||
}
|
||||
|
||||
resetPrices() {
|
||||
const monthlyPrice = this.product.get('monthlyPrice');
|
||||
const yearlyPrice = this.product.get('yearlyPrice');
|
||||
const monthlyPrice = this.tier.get('monthlyPrice');
|
||||
const yearlyPrice = this.tier.get('yearlyPrice');
|
||||
|
||||
this.stripeMonthlyAmount = monthlyPrice ? (monthlyPrice.amount / 100) : 5;
|
||||
this.stripeYearlyAmount = yearlyPrice ? (yearlyPrice.amount / 100) : 50;
|
||||
|
@ -4,7 +4,7 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class ProductController extends Controller {
|
||||
export default class TierController extends Controller {
|
||||
@service config;
|
||||
@service membersUtils;
|
||||
@service settings;
|
||||
@ -20,7 +20,7 @@ export default class ProductController extends Controller {
|
||||
this.siteUrl = this.config.get('blogUrl');
|
||||
}
|
||||
|
||||
get product() {
|
||||
get tier() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export default class ProductController extends Controller {
|
||||
}
|
||||
|
||||
get noOfPrices() {
|
||||
return (this.product.stripePrices || []).length;
|
||||
return (this.tier.stripePrices || []).length;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -71,7 +71,7 @@ export default class ProductController extends Controller {
|
||||
|
||||
@action
|
||||
leaveScreen() {
|
||||
this.product.rollbackAttributes();
|
||||
this.tier.rollbackAttributes();
|
||||
return this.leaveScreenTransition.retry();
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ export default class ProductController extends Controller {
|
||||
|
||||
@action
|
||||
savePrice(price) {
|
||||
const stripePrices = this.product.stripePrices.map((d) => {
|
||||
const stripePrices = this.tier.stripePrices.map((d) => {
|
||||
if (d.id === price.id) {
|
||||
return EmberObject.create({
|
||||
...price,
|
||||
@ -144,7 +144,7 @@ export default class ProductController extends Controller {
|
||||
active: !!price.active
|
||||
}));
|
||||
}
|
||||
this.product.set('stripePrices', stripePrices);
|
||||
this.tier.set('stripePrices', stripePrices);
|
||||
this.saveTask.perform();
|
||||
}
|
||||
|
||||
@ -166,15 +166,15 @@ export default class ProductController extends Controller {
|
||||
@task({restartable: true})
|
||||
*saveTask() {
|
||||
this.send('validatePaidSignupRedirect');
|
||||
this.product.validate();
|
||||
if (this.product.get('errors').length !== 0) {
|
||||
this.tier.validate();
|
||||
if (this.tier.get('errors').length !== 0) {
|
||||
return;
|
||||
}
|
||||
if (this.settings.get('errors').length !== 0) {
|
||||
return;
|
||||
}
|
||||
yield this.settings.save();
|
||||
const response = yield this.product.save();
|
||||
const response = yield this.tier.save();
|
||||
if (this.showPriceModal) {
|
||||
this.closePriceModal();
|
||||
}
|
@ -4,7 +4,7 @@ import {htmlSafe} from '@ember/template';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class ProductsController extends Controller {
|
||||
export default class TiersController extends Controller {
|
||||
@service settings;
|
||||
@service config;
|
||||
|
||||
@ -16,7 +16,7 @@ export default class ProductsController extends Controller {
|
||||
this.iconStyle = this.setIconStyle();
|
||||
}
|
||||
|
||||
get products() {
|
||||
get tiers() {
|
||||
return this.model.sortBy('name');
|
||||
}
|
||||
|
@ -12,8 +12,6 @@ import NavItemValidator from 'ghost-admin/validators/nav-item';
|
||||
import NewsletterValidator from 'ghost-admin/validators/newsletter';
|
||||
import OfferValidator from 'ghost-admin/validators/offer';
|
||||
import PostValidator from 'ghost-admin/validators/post';
|
||||
import ProductBenefitItemValidator from 'ghost-admin/validators/product-benefit-item';
|
||||
import ProductValidator from 'ghost-admin/validators/product';
|
||||
import RSVP from 'rsvp';
|
||||
import ResetValidator from 'ghost-admin/validators/reset';
|
||||
import SettingValidator from 'ghost-admin/validators/setting';
|
||||
@ -23,6 +21,8 @@ import SignupValidator from 'ghost-admin/validators/signup';
|
||||
import SlackIntegrationValidator from 'ghost-admin/validators/slack-integration';
|
||||
import SnippetValidator from 'ghost-admin/validators/snippet';
|
||||
import TagSettingsValidator from 'ghost-admin/validators/tag-settings';
|
||||
import TierBenefitItemValidator from 'ghost-admin/validators/tier-benefit-item';
|
||||
import TierValidator from 'ghost-admin/validators/tier';
|
||||
import UserValidator from 'ghost-admin/validators/user';
|
||||
import WebhookValidator from 'ghost-admin/validators/webhook';
|
||||
import {A as emberA, isArray as isEmberArray} from '@ember/array';
|
||||
@ -60,7 +60,7 @@ export default Mixin.create({
|
||||
customView: CustomViewValidator,
|
||||
inviteUser: InviteUserValidator,
|
||||
navItem: NavItemValidator,
|
||||
productBenefitItem: ProductBenefitItemValidator,
|
||||
tierBenefitItem: TierBenefitItemValidator,
|
||||
post: PostValidator,
|
||||
reset: ResetValidator,
|
||||
setting: SettingValidator,
|
||||
@ -75,7 +75,7 @@ export default Mixin.create({
|
||||
webhook: WebhookValidator,
|
||||
label: LabelValidator,
|
||||
snippet: SnippetValidator,
|
||||
product: ProductValidator,
|
||||
tier: TierValidator,
|
||||
offer: OfferValidator,
|
||||
newsletter: NewsletterValidator
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import EmberObject from '@ember/object';
|
||||
|
||||
export default EmberObject.extend({
|
||||
name: 'Name of the product',
|
||||
slug: 'Slug for the product'
|
||||
name: 'Name of the tier',
|
||||
slug: 'Slug for the tier'
|
||||
});
|
@ -21,7 +21,7 @@ export default Model.extend(ValidationEngine, {
|
||||
emailOpenRate: attr('number'),
|
||||
avatarImage: attr('string'),
|
||||
|
||||
products: attr('member-product'),
|
||||
tiers: attr('member-tier'),
|
||||
newsletters: hasMany('newsletter', {embedded: 'always', async: false}),
|
||||
|
||||
labels: hasMany('label', {embedded: 'always', async: false}),
|
||||
|
@ -145,7 +145,7 @@ export default Model.extend(Comparable, ValidationEngine, {
|
||||
ogTitleScratch: boundOneWay('ogTitle'),
|
||||
twitterDescriptionScratch: boundOneWay('twitterDescription'),
|
||||
twitterTitleScratch: boundOneWay('twitterTitle'),
|
||||
tiers: attr('member-product'),
|
||||
tiers: attr('member-tier'),
|
||||
emailSubjectScratch: boundOneWay('emailSubject'),
|
||||
|
||||
isPublished: equal('status', 'published'),
|
||||
@ -192,7 +192,7 @@ export default Model.extend(Comparable, ValidationEngine, {
|
||||
}
|
||||
if (this.visibility === 'tiers' && this.tiers) {
|
||||
let filter = this.tiers.map((tier) => {
|
||||
return `product:${tier.slug}`;
|
||||
return `tier:${tier.slug}`;
|
||||
}).join(',');
|
||||
return filter;
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ export default Model.extend(ValidationEngine, {
|
||||
portalButton: attr('boolean'),
|
||||
portalName: attr('boolean'),
|
||||
portalPlans: attr('json-string'),
|
||||
portalProducts: attr('json-string'),
|
||||
portalTiers: attr('json-string'),
|
||||
portalButtonStyle: attr('string'),
|
||||
portalButtonIcon: attr('string'),
|
||||
portalButtonSignupText: attr('string'),
|
||||
|
@ -6,7 +6,7 @@ export default EmberObject.extend(ValidationEngine, {
|
||||
name: '',
|
||||
isNew: false,
|
||||
|
||||
validationType: 'productBenefitItem',
|
||||
validationType: 'tierBenefitItem',
|
||||
|
||||
isComplete: computed('name', function () {
|
||||
let {name} = this;
|
@ -2,7 +2,7 @@ import Model, {attr} from '@ember-data/model';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
|
||||
export default Model.extend(ValidationEngine, {
|
||||
validationType: 'product',
|
||||
validationType: 'tier',
|
||||
|
||||
name: attr('string'),
|
||||
description: attr('string'),
|
||||
@ -13,5 +13,5 @@ export default Model.extend(ValidationEngine, {
|
||||
type: attr('string', {defaultValue: 'paid'}),
|
||||
monthlyPrice: attr('stripe-price'),
|
||||
yearlyPrice: attr('stripe-price'),
|
||||
benefits: attr('product-benefits')
|
||||
benefits: attr('tier-benefits')
|
||||
});
|
@ -19,7 +19,7 @@ export default class MembersRoute extends AdminRoute {
|
||||
this._requiresBackgroundRefresh = false;
|
||||
|
||||
if (params.member_id) {
|
||||
return this.store.queryRecord('member', {id: params.member_id, include: 'products'});
|
||||
return this.store.queryRecord('member', {id: params.member_id, include: 'tiers'});
|
||||
} else {
|
||||
return this.store.createRecord('member');
|
||||
}
|
||||
|
@ -1,67 +0,0 @@
|
||||
import AdminRoute from 'ghost-admin/routes/admin';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class ProductRoute extends AdminRoute {
|
||||
@service store;
|
||||
@service router;
|
||||
|
||||
_requiresBackgroundRefresh = true;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.router.on('routeWillChange', (transition) => {
|
||||
this.showUnsavedChangesModal(transition);
|
||||
});
|
||||
}
|
||||
|
||||
model(params) {
|
||||
if (params.product_id) {
|
||||
return this.store.queryRecord('product', {id: params.product_id, include: 'stripe_prices'});
|
||||
} else {
|
||||
return this.store.createRecord('product');
|
||||
}
|
||||
}
|
||||
|
||||
setupController(controller, product) {
|
||||
super.setupController(...arguments);
|
||||
if (this._requiresBackgroundRefresh) {
|
||||
if (product.get('id')) {
|
||||
return this.store.queryRecord('product', {id: product.get('id'), include: 'stripe_prices'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
super.deactivate(...arguments);
|
||||
// clean up newly created records and revert unsaved changes to existing
|
||||
this.controller.product.rollbackAttributes();
|
||||
this._requiresBackgroundRefresh = true;
|
||||
}
|
||||
|
||||
@action
|
||||
save() {
|
||||
this.controller.save();
|
||||
}
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Settings - Products'
|
||||
};
|
||||
}
|
||||
|
||||
showUnsavedChangesModal(transition) {
|
||||
if (transition.from && transition.from.name === this.routeName && transition.targetName) {
|
||||
let {controller} = this;
|
||||
|
||||
// product.changedAttributes is always true for new products but number of changed attrs is reliable
|
||||
let isChanged = Object.keys(controller.product.changedAttributes()).length > 0;
|
||||
|
||||
if (!controller.product.isDeleted && isChanged) {
|
||||
transition.abort();
|
||||
controller.toggleUnsavedChangesModal(transition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import ProductRoute from '../product';
|
||||
|
||||
export default class NewProductRoute extends ProductRoute {
|
||||
controllerName = 'settings.product';
|
||||
templateName = 'settings.product';
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import AdminRoute from 'ghost-admin/routes/admin';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class ProductsRoute extends AdminRoute {
|
||||
@service store;
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Settings - Products'
|
||||
};
|
||||
}
|
||||
|
||||
model() {
|
||||
return this.store.findAll('product', {include: 'stripe_prices'});
|
||||
}
|
||||
}
|
6
ghost/admin/app/routes/settings/tier/new.js
Normal file
6
ghost/admin/app/routes/settings/tier/new.js
Normal file
@ -0,0 +1,6 @@
|
||||
import TierRoute from '../tier';
|
||||
|
||||
export default class NewTierRoute extends TierRoute {
|
||||
controllerName = 'settings.tier';
|
||||
templateName = 'settings.tier';
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class ProductSerializer extends ApplicationSerializer {
|
||||
export default class TierSerializer extends ApplicationSerializer {
|
||||
serialize() {
|
||||
let json = super.serialize(...arguments);
|
||||
|
@ -143,8 +143,8 @@ export default class DashboardStatsService extends Service {
|
||||
*/
|
||||
@tracked lastSeenFilterStatus = 'total';
|
||||
|
||||
paidProducts = null;
|
||||
|
||||
paidTiers = null;
|
||||
|
||||
/**
|
||||
* @type {?MemberCounts}
|
||||
*/
|
||||
@ -193,7 +193,7 @@ export default class DashboardStatsService extends Service {
|
||||
paid: stat.paid + stat.comped,
|
||||
free: stat.free
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have any statistic from more than x days ago.
|
||||
@ -221,7 +221,7 @@ export default class DashboardStatsService extends Service {
|
||||
const stat = this.mrrStats[index];
|
||||
if (stat.date <= searchDate) {
|
||||
return stat.mrr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have any statistic from more than x days ago.
|
||||
@ -293,14 +293,14 @@ export default class DashboardStatsService extends Service {
|
||||
}
|
||||
|
||||
if (this.membersUtils.isStripeEnabled) {
|
||||
yield this.loadPaidProducts();
|
||||
yield this.loadPaidTiers();
|
||||
}
|
||||
|
||||
const hasPaidTiers = this.membersUtils.isStripeEnabled && this.paidProducts && this.paidProducts.length > 0;
|
||||
const hasPaidTiers = this.membersUtils.isStripeEnabled && this.paidTiers && this.paidTiers.length > 0;
|
||||
|
||||
this.siteStatus = {
|
||||
hasPaidTiers,
|
||||
hasMultipleTiers: hasPaidTiers && this.paidProducts.length > 1,
|
||||
hasMultipleTiers: hasPaidTiers && this.paidTiers.length > 1,
|
||||
newslettersEnabled: this.settings.get('editorDefaultEmailRecipients') !== 'disabled',
|
||||
membersEnabled: this.membersUtils.isMembersEnabled
|
||||
};
|
||||
@ -358,15 +358,15 @@ export default class DashboardStatsService extends Service {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
yield this.loadPaidProducts();
|
||||
yield this.loadPaidTiers();
|
||||
|
||||
const paidMembersByTier = [];
|
||||
|
||||
for (const tier of result.meta.tiers) {
|
||||
const product = this.paidProducts.find(x => x.id === tier);
|
||||
const _tier = this.paidTiers.find(x => x.id === tier);
|
||||
paidMembersByTier.push({
|
||||
tier: {
|
||||
name: product.name
|
||||
name: _tier.name
|
||||
},
|
||||
members: result.meta.totals.reduce((sum, total) => {
|
||||
if (total.tier !== tier) {
|
||||
@ -536,24 +536,24 @@ export default class DashboardStatsService extends Service {
|
||||
this.membersLastSeen7d = result7d;
|
||||
}
|
||||
|
||||
loadPaidProducts() {
|
||||
if (this.paidProducts !== null) {
|
||||
loadPaidTiers() {
|
||||
if (this.paidTiers !== null) {
|
||||
return;
|
||||
}
|
||||
if (this._loadPaidProducts.isRunning) {
|
||||
if (this._loadPaidTiers.isRunning) {
|
||||
// We need to explicitly wait for the already running task instead of dropping it and returning immediately
|
||||
return this._loadPaidProducts.last;
|
||||
return this._loadPaidTiers.last;
|
||||
}
|
||||
return this._loadPaidProducts.perform();
|
||||
return this._loadPaidTiers.perform();
|
||||
}
|
||||
|
||||
@task
|
||||
*_loadPaidProducts() {
|
||||
const data = yield this.store.query('product', {
|
||||
*_loadPaidTiers() {
|
||||
const data = yield this.store.query('tier', {
|
||||
filter: 'type:paid+active:true',
|
||||
limit: 'all'
|
||||
});
|
||||
this.paidProducts = data.toArray();
|
||||
this.paidTiers = data.toArray();
|
||||
}
|
||||
|
||||
loadNewsletterSubscribers() {
|
||||
@ -573,7 +573,7 @@ export default class DashboardStatsService extends Service {
|
||||
this.newsletterSubscribers = this.dashboardMocks.newsletterSubscribers;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const [paid, free] = yield Promise.all([
|
||||
this.membersCountCache.count('newsletters.status:active+status:-free'),
|
||||
this.membersCountCache.count('newsletters.status:active+status:free')
|
||||
@ -603,7 +603,7 @@ export default class DashboardStatsService extends Service {
|
||||
this.emailsSent30d = this.dashboardMocks.emailsSent30d;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const start30d = new Date(Date.now() - 30 * 86400 * 1000);
|
||||
const result = yield this.store.query('email', {limit: 100, filter: 'submitted_at:>' + start30d.toISOString()});
|
||||
this.emailsSent30d = result.reduce((c, email) => c + email.emailCount, 0);
|
||||
|
@ -89,14 +89,14 @@ export default class MembersUtilsService extends Service {
|
||||
monthlyPrice,
|
||||
yearlyPrice,
|
||||
portalPlans = this.settings.get('portalPlans'),
|
||||
portalProducts,
|
||||
portalTiers,
|
||||
currency,
|
||||
membersSignupAccess = this.settings.get('membersSignupAccess')
|
||||
} = overrides;
|
||||
|
||||
const tiers = this.store.peekAll('product') || [];
|
||||
const tiers = this.store.peekAll('tier') || [];
|
||||
|
||||
portalProducts = portalProducts || tiers.filter((t) => {
|
||||
portalTiers = portalTiers || tiers.filter((t) => {
|
||||
return t.visibility === 'public' && t.type === 'paid';
|
||||
}).map(t => t.id);
|
||||
|
||||
@ -121,8 +121,8 @@ export default class MembersUtilsService extends Service {
|
||||
settingsParam.append('portalPrices', encodeURIComponent(portalPlans));
|
||||
}
|
||||
|
||||
if (portalProducts && this.feature.get('multipleProducts')) {
|
||||
settingsParam.append('portalProducts', encodeURIComponent(portalProducts));
|
||||
if (portalTiers && this.feature.get('multipleProducts')) {
|
||||
settingsParam.append('portalProducts', encodeURIComponent(portalTiers));
|
||||
}
|
||||
|
||||
if (this.settings.get('accentColor') === '' || this.settings.get('accentColor')) {
|
||||
|
@ -70,7 +70,7 @@
|
||||
@import "layouts/post-preview.css";
|
||||
@import "layouts/dashboard.css";
|
||||
@import "layouts/dashboard-v5.css";
|
||||
@import "layouts/products.css";
|
||||
@import "layouts/tiers.css";
|
||||
@import "layouts/offers.css";
|
||||
|
||||
|
||||
@ -179,7 +179,7 @@ input:focus,
|
||||
.gh-mobile-nav-bar a.active {
|
||||
background: #111213;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gh-nav-search .ember-power-select-trigger {
|
||||
@ -856,12 +856,12 @@ input:focus,
|
||||
background: var(--pink-d1);
|
||||
}
|
||||
|
||||
.token-segment-product {
|
||||
.token-segment-tier {
|
||||
background: var(--darkgrey);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.token-segment-product svg path {
|
||||
.token-segment-tier svg path {
|
||||
stroke: var(--lightgrey);
|
||||
fill: var(--lightgrey);
|
||||
}
|
||||
|
@ -70,7 +70,7 @@
|
||||
@import "layouts/post-preview.css";
|
||||
@import "layouts/dashboard.css";
|
||||
@import "layouts/dashboard-v5.css";
|
||||
@import "layouts/products.css";
|
||||
@import "layouts/tiers.css";
|
||||
@import "layouts/offers.css"
|
||||
|
||||
|
||||
|
@ -116,11 +116,11 @@
|
||||
margin: 2px !important;
|
||||
}
|
||||
|
||||
.gh-filter-block .token-segment-product {
|
||||
.gh-filter-block .token-segment-tier {
|
||||
margin: 2px !important;
|
||||
}
|
||||
|
||||
.gh-filter-block .token-segment-product .ember-power-select-multiple-remove-btn svg {
|
||||
.gh-filter-block .token-segment-tier .ember-power-select-multiple-remove-btn svg {
|
||||
margin-right: 0!important;
|
||||
}
|
||||
|
||||
|
@ -316,17 +316,17 @@
|
||||
fill: var(--middarkgrey);
|
||||
}
|
||||
|
||||
.token-segment-product {
|
||||
.token-segment-tier {
|
||||
background: var(--black);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.token-segment-status-free svg path,
|
||||
.token-segment-status-paid svg path,
|
||||
.token-segment-product svg path {
|
||||
.token-segment-tier svg path {
|
||||
stroke: var(--white);
|
||||
fill: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline input */
|
||||
.ember-power-select-inline {
|
||||
|
@ -2042,7 +2042,7 @@ p.gh-members-import-errordetail:first-of-type {
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom product member details */
|
||||
/* Custom tier member details */
|
||||
.gh-cp-member-email-name {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@ -2075,20 +2075,20 @@ p.gh-members-import-errordetail:first-of-type {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
/* Member's product list */
|
||||
.gh-member-product-container {
|
||||
/* Member's tier list */
|
||||
.gh-member-tier-container {
|
||||
grid-row-gap: 24px;
|
||||
}
|
||||
|
||||
.gh-member-settings .gh-member-product-container + .gh-member-product-container {
|
||||
.gh-member-settings .gh-member-tier-container + .gh-member-tier-container {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct {
|
||||
.gh-cp-membertier {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.gh-memberproduct-name {
|
||||
.gh-membertier-name {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 1.65rem !important;
|
||||
@ -2096,97 +2096,97 @@ p.gh-members-import-errordetail:first-of-type {
|
||||
margin-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct.multiple-subs .gh-memberproduct-name {
|
||||
.gh-cp-membertier.multiple-subs .gh-membertier-name {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.gh-memberproduct-subcount {
|
||||
.gh-membertier-subcount {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
color: var(--midgrey);
|
||||
}
|
||||
|
||||
.gh-memberproduct-list .gh-list-row:hover {
|
||||
.gh-membertier-list .gh-list-row:hover {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct-pricelabel {
|
||||
.gh-cp-membertier-pricelabel {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gh-memberproduct-subscription span.archived {
|
||||
.gh-membertier-subscription span.archived {
|
||||
background: var(--lightgrey-l2);
|
||||
color: var(--midgrey);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct.multiple-subs .gh-memberproduct-subscription {
|
||||
.gh-cp-membertier.multiple-subs .gh-membertier-subscription {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--whitegrey);
|
||||
}
|
||||
|
||||
.gh-memberproduct-created {
|
||||
.gh-membertier-created {
|
||||
color: var(--midgrey-d1);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.gh-memberproduct-archived .gh-memberproduct-name {
|
||||
.gh-membertier-archived .gh-membertier-name {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.gh-memberproduct-list-footer {
|
||||
.gh-membertier-list-footer {
|
||||
position:relative;
|
||||
margin-top: 8px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.gh-memberproduct-list-footer.min-height {
|
||||
.gh-membertier-list-footer.min-height {
|
||||
min-height: 74px;
|
||||
}
|
||||
|
||||
.gh-memberproduct-list-footer .gh-loading-content {
|
||||
.gh-membertier-list-footer .gh-loading-content {
|
||||
padding-bottom: unset;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.gh-memberproduct-cancelreason {
|
||||
.gh-membertier-cancelreason {
|
||||
line-height: 1.45em;
|
||||
margin: 3px 0 5px;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.gh-btn-addproduct svg {
|
||||
.gh-btn-addtier svg {
|
||||
width: .8em;
|
||||
height: .8em;
|
||||
}
|
||||
|
||||
.gh-btn-addproduct svg path {
|
||||
.gh-btn-addtier svg path {
|
||||
fill: var(--green);
|
||||
}
|
||||
|
||||
.gh-member-product-memberdetails {
|
||||
.gh-member-tier-memberdetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 12px 0 24px;
|
||||
}
|
||||
|
||||
.gh-member-product-memberdetails .gh-member-gravatar {
|
||||
.gh-member-tier-memberdetails .gh-member-gravatar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gh-member-product-memberdetails h3 {
|
||||
.gh-member-tier-memberdetails h3 {
|
||||
margin: 12px 0 0;
|
||||
font-size: 1.9rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.gh-member-product-memberdetails p {
|
||||
.gh-member-tier-memberdetails p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct-noproduct {
|
||||
.gh-cp-membertier-notier {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -2196,73 +2196,73 @@ p.gh-members-import-errordetail:first-of-type {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct-noproduct .gh-loading-content {
|
||||
.gh-cp-membertier-notier .gh-loading-content {
|
||||
padding-bottom: unset;
|
||||
}
|
||||
|
||||
.gh-btn-add-memberproduct[disabled],
|
||||
.gh-btn-add-memberproduct[disabled]:hover {
|
||||
.gh-btn-add-membertier[disabled],
|
||||
.gh-btn-add-membertier[disabled]:hover {
|
||||
background: var(--lightgrey-l1) !important;
|
||||
}
|
||||
|
||||
.gh-btn-add-memberproduct[disabled] span {
|
||||
.gh-btn-add-membertier[disabled] span {
|
||||
color: var(--midgrey);
|
||||
}
|
||||
|
||||
.gh-memberproduct-subscription {
|
||||
.gh-membertier-subscription {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct-renewal {
|
||||
.gh-cp-membertier-renewal {
|
||||
color: var(--darkgrey-l1);
|
||||
}
|
||||
|
||||
.gh-memberproduct-price-container {
|
||||
.gh-membertier-price-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct:not(.multiple-subs) .gh-memberproduct-price-container {
|
||||
.gh-cp-membertier:not(.multiple-subs) .gh-membertier-price-container {
|
||||
margin-top: -19px;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct .gh-product-card-price {
|
||||
.gh-cp-membertier .gh-tier-card-price {
|
||||
padding: 10px 18px;
|
||||
}
|
||||
|
||||
.gh-cp-memberproduct:not(.multiple-subs) .gh-product-card-price {
|
||||
.gh-cp-membertier:not(.multiple-subs) .gh-tier-card-price {
|
||||
padding: 15px 18px;
|
||||
}
|
||||
|
||||
.product-actions-menu {
|
||||
.tier-actions-menu {
|
||||
top: calc(100% - 36px);
|
||||
right: 0px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.gh-memberproduct-subscription .action-menu .gh-btn-subscription-action:not(:hover) {
|
||||
.gh-membertier-subscription .action-menu .gh-btn-subscription-action:not(:hover) {
|
||||
border: 1px solid var(--whitegrey);
|
||||
background: var(--main-bg-color) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gh-memberproduct-subscription .action-menu .gh-btn-subscription-action.open {
|
||||
.gh-membertier-subscription .action-menu .gh-btn-subscription-action.open {
|
||||
border: 1px solid var(--lightgrey-l1);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.gh-memberproduct-subscription .action-menu > .gh-btn span {
|
||||
.gh-membertier-subscription .action-menu > .gh-btn span {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.gh-memberproduct-subscription .action-menu > .gh-btn svg {
|
||||
.gh-membertier-subscription .action-menu > .gh-btn svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gh-member-product-form-block .form-group:last-of-type {
|
||||
.gh-member-tier-form-block .form-group:last-of-type {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
/* Product list */
|
||||
.gh-product-list {
|
||||
/* Tier list */
|
||||
.gh-tier-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-gap: 32px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.gh-product-list {
|
||||
.gh-tier-list {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
}
|
||||
@ -21,11 +21,11 @@
|
||||
color: #394047 !important;
|
||||
}
|
||||
|
||||
.gh-product-cards {
|
||||
.gh-tier-cards {
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.gh-product-card {
|
||||
.gh-tier-card {
|
||||
position:relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@ -33,28 +33,28 @@
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.gh-product-card {
|
||||
.gh-tier-card {
|
||||
padding: 4vmin 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.gh-product-card-button-container {
|
||||
.gh-tier-card-button-container {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: 24px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.gh-product-card-actions-button, .gh-product-card-edit-button {
|
||||
.gh-tier-card-actions-button, .gh-tier-card-edit-button {
|
||||
margin-right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gh-product-card-actions-button.gh-btn span, .gh-product-card-edit-button.gh-btn span {
|
||||
.gh-tier-card-actions-button.gh-btn span, .gh-tier-card-edit-button.gh-btn span {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.gh-product-card-edit-button svg {
|
||||
.gh-tier-card-edit-button svg {
|
||||
fill: #15171a;
|
||||
color: transparent;
|
||||
}
|
||||
@ -65,64 +65,64 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.gh-product-card-block {
|
||||
.gh-tier-card-block {
|
||||
flex-basis: 30%;
|
||||
}
|
||||
|
||||
.gh-product-card-block:not(:first-of-type) {
|
||||
.gh-tier-card-block:not(:first-of-type) {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.gh-product-card-block h4 {
|
||||
.gh-tier-card-block h4 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gh-product-card-block h4 .counter {
|
||||
.gh-tier-card-block h4 .counter {
|
||||
font-weight: 400;
|
||||
color: var(--midgrey);
|
||||
}
|
||||
|
||||
.gh-product-card-name {
|
||||
.gh-tier-card-name {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gh-product-card-empty-state {
|
||||
.gh-tier-card-empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gh-product-card-empty-state p {
|
||||
.gh-tier-card-empty-state p {
|
||||
margin-bottom: 0;
|
||||
padding: 3.2rem;
|
||||
color: var(--midgrey);
|
||||
}
|
||||
|
||||
.gh-product-card-description {
|
||||
.gh-tier-card-description {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.45em;
|
||||
margin: 4px 20px 4px 0;
|
||||
color: var(--midgrey);
|
||||
}
|
||||
|
||||
.gh-product-card-block.title-block {
|
||||
.gh-tier-card-block.title-block {
|
||||
flex-basis: 40%;
|
||||
}
|
||||
|
||||
.gh-product-card-block.benefits-block .gh-product-card-description {
|
||||
.gh-tier-card-block.benefits-block .gh-tier-card-description {
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.gh-product-card-block ul.benefits {
|
||||
.gh-tier-card-block ul.benefits {
|
||||
list-style: none;
|
||||
margin: 10px 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gh-product-card-block ul.benefits li {
|
||||
.gh-tier-card-block ul.benefits li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 1.3rem;
|
||||
@ -130,7 +130,7 @@
|
||||
color: var(--middarkgrey);
|
||||
}
|
||||
|
||||
.gh-product-card-block ul.benefits li svg {
|
||||
.gh-tier-card-block ul.benefits li svg {
|
||||
flex-basis: 18px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
@ -140,17 +140,17 @@
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.gh-product-card-block ul.benefits li span {
|
||||
.gh-tier-card-block ul.benefits li span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.gh-product-price-container {
|
||||
.gh-tier-price-container {
|
||||
display: flex;
|
||||
margin: 0 60px 0 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.gh-product-card-price {
|
||||
.gh-tier-card-price {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -165,19 +165,19 @@
|
||||
min-height: 66px;
|
||||
}
|
||||
|
||||
.gh-product-card-price .currency-symbol,
|
||||
.gh-product-card-price .amount,
|
||||
.gh-product-card-price .currency,
|
||||
.gh-product-card-price .currency-code {
|
||||
.gh-tier-card-price .currency-symbol,
|
||||
.gh-tier-card-price .amount,
|
||||
.gh-tier-card-price .currency,
|
||||
.gh-tier-card-price .currency-code {
|
||||
font-weight: 600;
|
||||
color: var(--darkgrey);
|
||||
}
|
||||
|
||||
.gh-product-card-price .currency-symbol {
|
||||
.gh-tier-card-price .currency-symbol {
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.gh-product-card-price .amount {
|
||||
.gh-tier-card-price .amount {
|
||||
letter-spacing: -.2px;
|
||||
line-height: 1;
|
||||
margin-right: 2px;
|
||||
@ -185,7 +185,7 @@
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.gh-product-card-price .currency {
|
||||
.gh-tier-card-price .currency {
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
@ -194,7 +194,7 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.gh-product-card-price .currency-code {
|
||||
.gh-tier-card-price .currency-code {
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
top: 0;
|
||||
@ -204,14 +204,14 @@
|
||||
letter-spacing: -.2px;
|
||||
}
|
||||
|
||||
.gh-product-card-price .period {
|
||||
.gh-tier-card-price .period {
|
||||
font-size: 1.25rem;
|
||||
text-transform: lowercase;
|
||||
line-height: 1.2em;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.gh-product-cards-footer {
|
||||
.gh-tier-cards-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -7px;
|
||||
@ -219,23 +219,23 @@
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.gh-btn-add-product,
|
||||
.gh-btn-add-product:hover {
|
||||
.gh-btn-add-tier,
|
||||
.gh-btn-add-tier:hover {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.gh-btn-add-product svg {
|
||||
.gh-btn-add-tier svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 1px 4px 0 0;
|
||||
}
|
||||
|
||||
.gh-btn-icon.gh-btn-add-product svg path {
|
||||
.gh-btn-icon.gh-btn-add-tier svg path {
|
||||
stroke: var(--green-d1);
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.gh-product-list-icon {
|
||||
.gh-tier-list-icon {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
@ -244,12 +244,12 @@
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.gh-product-list-icon svg {
|
||||
.gh-tier-list-icon svg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.gh-product-list-siteicon {
|
||||
.gh-tier-list-siteicon {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
background-color: transparent;
|
||||
@ -258,32 +258,32 @@
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.gh-product-list-icon svg circle,
|
||||
.gh-product-list-icon svg path {
|
||||
.gh-tier-list-icon svg circle,
|
||||
.gh-tier-list-icon svg path {
|
||||
stroke-width: 1px !important;
|
||||
}
|
||||
|
||||
/* Product details */
|
||||
.gh-product-details {
|
||||
/* Tier details */
|
||||
.gh-tier-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 32px;
|
||||
margin-bottom: 3vw;
|
||||
}
|
||||
|
||||
.gh-product-details-form {
|
||||
.gh-tier-details-form {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: 20px !important;
|
||||
}
|
||||
|
||||
.gh-product-icon-container {
|
||||
.gh-tier-icon-container {
|
||||
width: unset;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gh-product-icon {
|
||||
.gh-tier-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -295,32 +295,32 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gh-product-details-fields {
|
||||
.gh-tier-details-fields {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gh-product-details-fields .max-width {
|
||||
.gh-tier-details-fields .max-width {
|
||||
max-width: 840px;
|
||||
}
|
||||
|
||||
.gh-product-details-fields .form-group:last-of-type {
|
||||
.gh-tier-details-fields .form-group:last-of-type {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gh-product-details section {
|
||||
.gh-tier-details section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
/* Product stats */
|
||||
.gh-product-stat-container {
|
||||
/* Tier stats */
|
||||
.gh-tier-stat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gh-product-stat-details .data {
|
||||
.gh-tier-stat-details .data {
|
||||
white-space: nowrap;
|
||||
font-size: 3.1rem;
|
||||
line-height: 1em;
|
||||
@ -330,13 +330,13 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gh-product-stat-details .info {
|
||||
.gh-tier-stat-details .info {
|
||||
color: var(--midgrey);
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gh-product-chart {
|
||||
.gh-tier-chart {
|
||||
color: var(--whitegrey);
|
||||
border: 1px solid var(--whitegrey);
|
||||
border-top-color: transparent;
|
||||
@ -428,67 +428,67 @@
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.product-actions-menu.fade-out {
|
||||
.tier-actions-menu.fade-out {
|
||||
animation-duration: 0.01s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Add/edit product modal */
|
||||
.fullscreen-modal-edit-product {
|
||||
/* Add/edit tier modal */
|
||||
.fullscreen-modal-edit-tier {
|
||||
max-width: 1080px;
|
||||
}
|
||||
|
||||
.gh-product-modal-content {
|
||||
.gh-tier-modal-content {
|
||||
margin: -32px -32px 0;
|
||||
padding: 32px 32px 0;
|
||||
max-height: calc(100vh - 16vw);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.gh-form-edit-product .gh-main-section {
|
||||
.gh-form-edit-tier .gh-main-section {
|
||||
margin-bottom: 32px;
|
||||
grid-template-columns: 1fr 0.8fr 1.2fr;
|
||||
}
|
||||
|
||||
.gh-form-edit-product .gh-main-section-block {
|
||||
.gh-form-edit-tier .gh-main-section-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gh-form-edit-product .gh-main-section-content {
|
||||
.gh-form-edit-tier .gh-main-section-content {
|
||||
padding-top: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gh-product-priceform-block {
|
||||
.gh-tier-priceform-block {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.gh-product-priceform-block .form-group:last-of-type {
|
||||
.gh-tier-priceform-block .form-group:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gh-product-priceform-pricecurrency {
|
||||
.gh-tier-priceform-pricecurrency {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-gap: 20px;
|
||||
}
|
||||
|
||||
.gh-form-edit-product .gh-main-section-content.gh-product-form-benefits {
|
||||
.gh-form-edit-tier .gh-main-section-content.gh-tier-form-benefits {
|
||||
padding-left: 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-input {
|
||||
.gh-tier-benefits .gh-input {
|
||||
padding: 6px 28px 6px 30px;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-line {
|
||||
.gh-tier-benefits .gh-blognav-line {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-line svg {
|
||||
.gh-tier-benefits .gh-blognav-line svg {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
@ -496,20 +496,20 @@
|
||||
left: 11px;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-line.placeholder {
|
||||
.gh-tier-benefits .gh-blognav-line.placeholder {
|
||||
color: var(--midlightgrey);
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-line svg path {
|
||||
.gh-tier-benefits .gh-blognav-line svg path {
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-item {
|
||||
.gh-tier-benefits .gh-blognav-item {
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-item.gh-blognav-item--error {
|
||||
.gh-tier-benefits .gh-blognav-item.gh-blognav-item--error {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@ -517,42 +517,42 @@
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-label {
|
||||
.gh-tier-benefits .gh-blognav-label {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-label .response {
|
||||
.gh-tier-benefits .gh-blognav-label .response {
|
||||
position: relative;
|
||||
font-size: 1.25rem;
|
||||
margin: 2px 0 6px;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-delete {
|
||||
.gh-tier-benefits .gh-blognav-delete {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-add {
|
||||
.gh-tier-benefits .gh-blognav-add {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-grab {
|
||||
.gh-tier-benefits .gh-blognav-grab {
|
||||
text-indent: 0px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-item:hover .gh-blognav-delete,
|
||||
.gh-product-benefits .gh-blognav-item:hover .gh-blognav-grab {
|
||||
.gh-tier-benefits .gh-blognav-item:hover .gh-blognav-delete,
|
||||
.gh-tier-benefits .gh-blognav-item:hover .gh-blognav-grab {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gh-product-benefits .gh-blognav-item:not(.gh-blognav-item--sortable):not(:last-of-type) {
|
||||
.gh-tier-benefits .gh-blognav-item:not(.gh-blognav-item--sortable):not(:last-of-type) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.gh-product-benefit-hint {
|
||||
.gh-tier-benefit-hint {
|
||||
color: var(--midgrey-d2);
|
||||
font-size: 1.25rem !important;
|
||||
font-weight: 400;
|
||||
@ -560,13 +560,13 @@
|
||||
margin-top: -12px;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview-content {
|
||||
.gh-tier-form-tierpreview-content {
|
||||
position: sticky;
|
||||
top: 45px;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .gh-main-section-content {
|
||||
.gh-tier-form-tierpreview .gh-main-section-content {
|
||||
flex: 1;
|
||||
max-width: 420px;
|
||||
min-width: 320px;
|
||||
@ -583,12 +583,12 @@
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.gh-portal-product-card-header {
|
||||
.gh-portal-tier-card-header {
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .gh-main-section-content .gh-portal-product-name {
|
||||
.gh-tier-form-tierpreview .gh-main-section-content .gh-portal-tier-name {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3em;
|
||||
@ -599,7 +599,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .gh-main-section-content .gh-portal-product-description {
|
||||
.gh-tier-form-tierpreview .gh-main-section-content .gh-portal-tier-description {
|
||||
font-size: 1.55rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4em;
|
||||
@ -608,7 +608,7 @@
|
||||
color: #3d3d3d;
|
||||
}
|
||||
|
||||
.gh-portal-product-card-pricecontainer {
|
||||
.gh-portal-tier-card-pricecontainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
@ -620,38 +620,38 @@
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.gh-portal-product-price {
|
||||
.gh-portal-tier-price {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: #1d1d1d;
|
||||
}
|
||||
|
||||
.gh-portal-product-card-details {
|
||||
.gh-portal-tier-card-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gh-portal-product-card-detaildata {
|
||||
.gh-portal-tier-card-detaildata {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.gh-portal-product-price .amount {
|
||||
.gh-portal-tier-price .amount {
|
||||
font-size: 3.4rem;
|
||||
font-weight: 700;
|
||||
line-height: 1em;
|
||||
letter-spacing: -1.3px;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreivew-cadence {
|
||||
.gh-tier-form-tierpreivew-cadence {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreivew-cadence .gh-btn,
|
||||
.gh-product-form-tierpreivew-cadence .gh-btn span {
|
||||
.gh-tier-form-tierpreivew-cadence .gh-btn,
|
||||
.gh-tier-form-tierpreivew-cadence .gh-btn span {
|
||||
background: transparent !important;
|
||||
padding: 0;
|
||||
line-height: 1em;
|
||||
@ -663,16 +663,16 @@
|
||||
overflow: unset;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreivew-cadence .gh-btn:hover span {
|
||||
.gh-tier-form-tierpreivew-cadence .gh-btn:hover span {
|
||||
color: var(--middarkgrey);
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreivew-cadence .gh-btn.selected span {
|
||||
.gh-tier-form-tierpreivew-cadence .gh-btn.selected span {
|
||||
font-weight: 500;
|
||||
color: var(--darkgrey);
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .monthly-price {
|
||||
.gh-tier-form-tierpreview .monthly-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-size: 3.3rem;
|
||||
@ -681,7 +681,7 @@
|
||||
color: #3d3d3d;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .currency-sign {
|
||||
.gh-tier-form-tierpreview .currency-sign {
|
||||
align-self: flex-start;
|
||||
font-size: 2.7rem;
|
||||
font-weight: 700;
|
||||
@ -689,7 +689,7 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .billing-period {
|
||||
.gh-tier-form-tierpreview .billing-period {
|
||||
align-self: flex-end;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.4em;
|
||||
@ -727,21 +727,21 @@
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .gh-portal-product-benefits {
|
||||
.gh-tier-form-tierpreview .gh-portal-tier-benefits {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.4em;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .gh-portal-product-benefit {
|
||||
.gh-tier-form-tierpreview .gh-portal-tier-benefit {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
color: #3d3d3d;
|
||||
}
|
||||
|
||||
.gh-portal-product-benefit svg {
|
||||
.gh-portal-tier-benefit svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
min-width: 14px;
|
||||
@ -749,16 +749,16 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .gh-portal-product-benefit polyline,
|
||||
.gh-product-form-tierpreview .gh-portal-product-benefit g,
|
||||
.gh-product-form-tierpreview .gh-portal-product-benefit path {
|
||||
.gh-tier-form-tierpreview .gh-portal-tier-benefit polyline,
|
||||
.gh-tier-form-tierpreview .gh-portal-tier-benefit g,
|
||||
.gh-tier-form-tierpreview .gh-portal-tier-benefit path {
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .gh-portal-benefit-title {
|
||||
.gh-tier-form-tierpreview .gh-portal-benefit-title {
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.gh-product-form-tierpreview .placeholder {
|
||||
.gh-tier-form-tierpreview .placeholder {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
@ -92,10 +92,10 @@
|
||||
{{#liquid-if this.freeOpen}}
|
||||
<div class="gh-setting-content-extended" data-test-free-settings-expanded>
|
||||
{{#if (feature "multipleProducts")}}
|
||||
<GhProductCard
|
||||
@product={{this.freeProduct}}
|
||||
@products={{this.products}}
|
||||
@openEditProduct={{this.openEditProduct}}
|
||||
<GhTierCard
|
||||
@tier={{this.freeTier}}
|
||||
@tiers={{this.tiers}}
|
||||
@openEditTier={{this.openEditTier}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (or (not (feature "tierWelcomePages")) (not (feature "multipleProducts")))}}
|
||||
@ -121,7 +121,7 @@
|
||||
<label for="welcomePage" class="fw6">Welcome page</label>
|
||||
<GhUrlInput
|
||||
@id="welcomePage"
|
||||
@value={{readonly this.freeProduct.welcomePageURL}}
|
||||
@value={{readonly this.freeTier.welcomePageURL}}
|
||||
@baseUrl={{readonly this.siteUrl}}
|
||||
@setResult={{this.setWelcomePageURL}}
|
||||
@validateUrl={{this.validateWelcomePageURL}}
|
||||
@ -164,14 +164,14 @@
|
||||
<div class="gh-expandable-content">
|
||||
{{#liquid-if this.paidOpen}}
|
||||
<div class="gh-setting-content-extended">
|
||||
{{#if this.fetchDefaultProduct.isRunning}}
|
||||
{{#if this.fetchDefaultTier.isRunning}}
|
||||
Loading...
|
||||
{{else}}
|
||||
{{#if (feature "multipleProducts")}}
|
||||
<GhMembershipProductsAlpha
|
||||
<GhMembershipTiersAlpha
|
||||
@updatePortalPreview={{this.updatePortalPreview}}
|
||||
@products={{this.paidProducts}}
|
||||
@confirmProductSave={{this.confirmProductSave}}
|
||||
@tiers={{this.paidTiers}}
|
||||
@confirmTierSave={{this.confirmTierSave}}
|
||||
/>
|
||||
{{else}}
|
||||
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="prices">
|
||||
@ -266,7 +266,7 @@
|
||||
@model={{hash
|
||||
preloadTask=this.saveSettingsTask
|
||||
openStripeSettings=this.openStripeConnect
|
||||
products=this.products
|
||||
tiers=this.tiers
|
||||
}}
|
||||
@close={{this.closePortalSettings}}
|
||||
@modifier="full-overlay portal-settings" />
|
||||
@ -287,15 +287,15 @@
|
||||
@close={{this.closeStripeConnect}}
|
||||
@modifier="action wide stripe-connect" />
|
||||
{{/if}}
|
||||
{{#if this.showProductModal}}
|
||||
{{#if this.showTierModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="product"
|
||||
@modal="tier"
|
||||
@model={{hash
|
||||
product=this.productModel
|
||||
products=this.products
|
||||
tier=this.tierModel
|
||||
tiers=this.tiers
|
||||
}}
|
||||
@confirm={{this.confirmProductSave}}
|
||||
@close={{this.closeProductModal}}
|
||||
@modifier="edit-product action wide" />
|
||||
@confirm={{this.confirmTierSave}}
|
||||
@close={{this.closeTierModal}}
|
||||
@modifier="edit-tier action wide" />
|
||||
{{/if}}
|
||||
</section>
|
||||
|
@ -1,158 +0,0 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.products">Products</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
{{#if this.product.name}}
|
||||
{{this.product.name}}
|
||||
{{else}}
|
||||
<span class="midlightgrey">New product</span>
|
||||
{{/if}}
|
||||
</h2>
|
||||
<section class="view-actions">
|
||||
<GhTaskButton @buttonText="Save product"
|
||||
@task={{this.saveTask}}
|
||||
@successText="Saved"
|
||||
@runningText="Saving"
|
||||
@class="gh-btn gh-btn-primary gh-btn-icon"
|
||||
data-test-button="save-settings"
|
||||
/>
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<form>
|
||||
|
||||
<section class="gh-main-section">
|
||||
<div class="gh-main-section-block">
|
||||
<h4 class="gh-main-section-header small bn">Product details</h4>
|
||||
<div class="gh-main-section-content grey gh-product-details-form">
|
||||
<div class="gh-product-details-fields">
|
||||
<GhFormGroup @errors={{this.product.errors}} @hasValidated={{this.product.hasValidated}} @property="name" @classNames="max-width">
|
||||
<label for="product-name">Product name</label>
|
||||
<GhTextInput data-test-input="product-name" @id="product-name" @value={{this.product.name}} />
|
||||
<GhErrorMessage @errors={{this.product.errors}} @property="name" />
|
||||
</GhFormGroup>
|
||||
|
||||
<GhFormGroup @property="description" @classNames="max-width">
|
||||
<label for="product-description">Description</label>
|
||||
<GhTextInput data-test-input="product-description" @value={{this.product.description}} />
|
||||
<GhErrorMessage @property="description" />
|
||||
</GhFormGroup>
|
||||
|
||||
<GhFormGroup @property="_welcome-page" @classNames="max-width">
|
||||
<label for="_welcome-page">Welcome page</label>
|
||||
<GhUrlInput
|
||||
@value={{readonly this.settings.membersPaidSignupRedirect}}
|
||||
@baseUrl={{readonly this.siteUrl}}
|
||||
@setResult={{action "setPaidSignupRedirect"}}
|
||||
@validateUrl={{action "validatePaidSignupRedirect"}}
|
||||
@placeholder={{readonly this.siteUrl}}
|
||||
/>
|
||||
<p>Redirect to this URL after signup for this product</p>
|
||||
<GhErrorMessage
|
||||
@errors={{this.settings.errors}}
|
||||
@property="membersPaidSignupRedirect"
|
||||
/>
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<div class="gh-main-section">
|
||||
<div class="gh-main-section-block p0">
|
||||
<ol class="gh-price-list gh-list">
|
||||
<li class="gh-list-row header">
|
||||
<div class="gh-list-header">Prices({{this.noOfPrices}})</div>
|
||||
<div class="gh-list-header">Price</div>
|
||||
<div class="gh-list-header"></div>
|
||||
</li>
|
||||
{{#unless this.stripePrices}}
|
||||
<tr class="gh-list-row {{if this.price.active "" "gh-price-list-archived"}}">
|
||||
<td colspan="4" class="gh-list-data">
|
||||
<div class="gh-price-list-noprices">
|
||||
<div class="mb2">There are no prices for this product</div>
|
||||
{{#if this.membersUtils.isStripeEnabled}}
|
||||
{{#unless this.product.isNew}}
|
||||
<button type="button" class="gh-btn gh-btn-green" {{action "openNewPrice"}}
|
||||
disabled={{this.saveTask.isRunning}} >
|
||||
<span>Add price</span>
|
||||
</button>
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
You need to <button class="b gh-setting-group" type="button" {{on "click" this.openStripeConnect}}>connect to Stripe</button> to add prices
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/unless}}
|
||||
{{#each this.stripePrices as |price|}}
|
||||
<li class="gh-list-row {{if price.active "" "gh-price-list-archived"}}">
|
||||
<div class="gh-list-data gh-price-list-title">
|
||||
<h3 class="gh-price-list-name">
|
||||
<span class="name">{{price.nickname}}</span>
|
||||
{{#unless price.active}}
|
||||
<span class="gh-badge archived">Archived</span>
|
||||
{{/unless}}
|
||||
</h3>
|
||||
<p class="ma0 pa0 f8 midgrey gh-price-list-description">
|
||||
{{price.description}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="gh-list-data gh-price-list-price">
|
||||
<span>{{currency-symbol price.currency}}{{price.amount}} / {{price.interval}}</span>
|
||||
</div>
|
||||
|
||||
<div class="gh-list-data gh-price-list-actions">
|
||||
<div class="gh-price-list-actionlist">
|
||||
<button class="gh-btn gh-btn-link" type="button" {{action "openEditPrice" price}}>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
{{#if price.active}}
|
||||
<button class="gh-btn gh-btn-link gh-btn-archive-toggle archived" disabled={{this.saveTask.isRunning}} type="button" {{action "archivePrice" price}}>
|
||||
<span>Archive</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button class="gh-btn gh-btn-link gh-btn-archive-toggle" disabled={{this.saveTask.isRunning}} type="button" {{action "activatePrice" price}}>
|
||||
<span>Activate</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
|
||||
{{#if this.stripePrices}}
|
||||
{{#unless this.product.isNew}}
|
||||
<button type="button" class="gh-btn gh-btn-green" {{action "openNewPrice"}}>
|
||||
<span>Add price</span>
|
||||
</button>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.showPriceModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="product-price"
|
||||
@model={{hash
|
||||
price=this.priceModel
|
||||
}}
|
||||
@confirm={{action "savePrice"}}
|
||||
@close={{this.closePriceModal}}
|
||||
@modifier="action wide product-ssprice" />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showUnsavedChangesModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="leave-settings"
|
||||
@confirm={{this.leaveScreen}}
|
||||
@close={{this.toggleUnsavedChangesModal}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
</section>
|
@ -1,48 +0,0 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
Products
|
||||
</h2>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<section class="view-container">
|
||||
<div class="gh-product-list">
|
||||
<div class="gh-product-card">
|
||||
<span class="gh-product-list-icon">{{svg-jar "members"}}</span>
|
||||
<h3 class="gh-product-card-name">
|
||||
Free membership
|
||||
</h3>
|
||||
<p class="gh-product-card-description">
|
||||
{{this.settings.membersFreePriceDescription}}
|
||||
</p>
|
||||
<LinkTo @route="settings.products" class="gh-btn" {{action (toggle "showFreeMembershipModal" this)}}>
|
||||
<span>Customize</span>
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
{{#each this.products as |product|}}
|
||||
<div class="gh-product-card">
|
||||
<span class="gh-product-list-icon"><div class="gh-product-list-siteicon" style={{this.iconStyle}}></div></span>
|
||||
<h3 class="gh-product-card-name">
|
||||
{{product.name}}
|
||||
</h3>
|
||||
<p class="gh-product-card-description">
|
||||
{{product.description}}
|
||||
</p>
|
||||
<LinkTo @route="settings.product" @model={{product}} class="gh-btn">
|
||||
<span>Customize</span>
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{{#if this.showFreeMembershipModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="free-membership-settings"
|
||||
@close={{this.closeFreeMembershipModal}}
|
||||
@modifier="action wide product-ssprice" />
|
||||
{{/if}}
|
@ -1,14 +1,14 @@
|
||||
import MemberProduct from 'ghost-admin/models/member-product';
|
||||
import MemberTier from 'ghost-admin/models/member-tier';
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import {A as emberA, isArray as isEmberArray} from '@ember/array';
|
||||
|
||||
export default class MemberProductTransform extends Transform {
|
||||
export default class MemberTierTransform extends Transform {
|
||||
deserialize(serialized) {
|
||||
let subscriptions, subscriptionArray;
|
||||
|
||||
subscriptionArray = serialized || [];
|
||||
|
||||
subscriptions = subscriptionArray.map(itemDetails => MemberProduct.create(itemDetails));
|
||||
subscriptions = subscriptionArray.map(itemDetails => MemberTier.create(itemDetails));
|
||||
|
||||
return emberA(subscriptions);
|
||||
}
|
||||
@ -27,4 +27,3 @@ export default class MemberProductTransform extends Transform {
|
||||
return subscriptionArray;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
import ProductBenefitItem from '../models/product-benefit-item';
|
||||
import TierBenefitItem from '../models/tier-benefit-item';
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import {A as emberA, isArray as isEmberArray} from '@ember/array';
|
||||
|
||||
export default class ProductBenefits extends Transform {
|
||||
export default class TierBenefits extends Transform {
|
||||
deserialize(serialized) {
|
||||
let benefitsItems, benefitsArray;
|
||||
|
||||
benefitsArray = serialized || [];
|
||||
|
||||
benefitsItems = benefitsArray.map((itemDetails) => {
|
||||
return ProductBenefitItem.create(itemDetails);
|
||||
return TierBenefitItem.create(itemDetails);
|
||||
});
|
||||
|
||||
return emberA(benefitsItems);
|
@ -12,7 +12,6 @@ import mockNewsletters from './config/newsletters';
|
||||
import mockOffers from './config/offers';
|
||||
import mockPages from './config/pages';
|
||||
import mockPosts from './config/posts';
|
||||
import mockProducts from './config/products';
|
||||
import mockRoles from './config/roles';
|
||||
import mockSettings from './config/settings';
|
||||
import mockSite from './config/site';
|
||||
@ -20,6 +19,7 @@ import mockSlugs from './config/slugs';
|
||||
import mockSnippets from './config/snippets';
|
||||
import mockTags from './config/tags';
|
||||
import mockThemes from './config/themes';
|
||||
import mockTiers from './config/tiers';
|
||||
import mockUploads from './config/uploads';
|
||||
import mockUsers from './config/users';
|
||||
import mockWebhooks from './config/webhooks';
|
||||
@ -77,7 +77,7 @@ export function testConfig() {
|
||||
mockUploads(this);
|
||||
mockUsers(this);
|
||||
mockWebhooks(this);
|
||||
mockProducts(this);
|
||||
mockTiers(this);
|
||||
mockOffers(this);
|
||||
mockSnippets(this);
|
||||
mockNewsletters(this);
|
||||
|
@ -81,8 +81,8 @@ export default function mockMembers(server) {
|
||||
replacement: 'labels.slug'
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
replacement: 'products.slug'
|
||||
key: 'tier',
|
||||
replacement: 'tiers.slug'
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -97,7 +97,7 @@ export default function mockMembers(server) {
|
||||
});
|
||||
|
||||
// similar deal for associated models
|
||||
['labels', 'products', 'subscriptions', 'newsletters'].forEach((association) => {
|
||||
['labels', 'tiers', 'subscriptions', 'newsletters'].forEach((association) => {
|
||||
serializedMember[association] = [];
|
||||
|
||||
member[association].models.forEach((associatedModel) => {
|
||||
@ -177,22 +177,22 @@ export default function mockMembers(server) {
|
||||
});
|
||||
});
|
||||
|
||||
server.put('/members/:id/', function ({members, products, subscriptions}, {params}) {
|
||||
server.put('/members/:id/', function ({members, tiers, subscriptions}, {params}) {
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
const member = members.find(params.id);
|
||||
|
||||
// API accepts `products: [{id: 'x'}]` which isn't handled natively by mirage
|
||||
if (attrs.products.length > 0) {
|
||||
attrs.products.forEach((p) => {
|
||||
const product = products.find(p.id);
|
||||
// API accepts `tiers: [{id: 'x'}]` which isn't handled natively by mirage
|
||||
if (attrs.tiers.length > 0) {
|
||||
attrs.tiers.forEach((p) => {
|
||||
const tier = tiers.find(p.id);
|
||||
|
||||
if (!member.products.includes(product)) {
|
||||
// TODO: serialize products through _active_ subscriptions
|
||||
member.products.add(product);
|
||||
if (!member.tiers.includes(tier)) {
|
||||
// TODO: serialize tiers through _active_ subscriptions
|
||||
member.tiers.add(tier);
|
||||
|
||||
subscriptions.create({
|
||||
member,
|
||||
product,
|
||||
tier,
|
||||
comped: true,
|
||||
plan: {
|
||||
id: '',
|
||||
@ -215,9 +215,9 @@ export default function mockMembers(server) {
|
||||
interval: 'year',
|
||||
type: 'recurring',
|
||||
currency: 'USD',
|
||||
product: {
|
||||
tier: {
|
||||
id: '',
|
||||
product_id: product.id
|
||||
tier_id: tier.id
|
||||
}
|
||||
},
|
||||
offer: null
|
||||
@ -228,20 +228,20 @@ export default function mockMembers(server) {
|
||||
});
|
||||
}
|
||||
|
||||
const productIds = (attrs.products || []).map(p => p.id);
|
||||
const tierIds = (attrs.tiers || []).map(p => p.id);
|
||||
|
||||
member.products.models.forEach((product) => {
|
||||
if (!productIds.includes(product.id)) {
|
||||
member.subscriptions.models.filter(sub => sub.product.id === product.id).forEach((sub) => {
|
||||
member.tiers.models.forEach((tier) => {
|
||||
if (!tierIds.includes(tier.id)) {
|
||||
member.subscriptions.models.filter(sub => sub.tier.id === tier.id).forEach((sub) => {
|
||||
member.subscriptions.remove(sub);
|
||||
});
|
||||
|
||||
member.products.remove(product);
|
||||
member.tiers.remove(tier);
|
||||
}
|
||||
});
|
||||
|
||||
// these are read-only properties so make sure we don't overwrite data
|
||||
delete attrs.products;
|
||||
delete attrs.tiers;
|
||||
delete attrs.subscriptions;
|
||||
|
||||
return member.update(attrs);
|
||||
|
@ -7,9 +7,9 @@ export default function mockOffers(server) {
|
||||
|
||||
server.get('/offers/:id/', function ({offers}, {params}) {
|
||||
let {id} = params;
|
||||
let product = offers.find(id);
|
||||
let tier = offers.find(id);
|
||||
|
||||
return product || new Response(404, {}, {
|
||||
return tier || new Response(404, {}, {
|
||||
errors: [{
|
||||
type: 'NotFoundError',
|
||||
message: 'Offer not found.'
|
||||
|
@ -1,44 +0,0 @@
|
||||
import {paginatedResponse} from '../utils';
|
||||
|
||||
export default function mockProducts(server) {
|
||||
server.post('/products/');
|
||||
|
||||
server.get('/products/', paginatedResponse('products'));
|
||||
|
||||
server.get('/products/:id/', function ({products}, {params}) {
|
||||
let {id} = params;
|
||||
let product = products.find(id);
|
||||
|
||||
return product || new Response(404, {}, {
|
||||
errors: [{
|
||||
type: 'NotFoundError',
|
||||
message: 'Product not found.'
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
server.put('/products/:id/', function ({products, productBenefits}, {params}) {
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
const product = products.find(params.id);
|
||||
|
||||
const benefitAttrs = attrs.benefits;
|
||||
delete attrs.benefits;
|
||||
|
||||
product.update(attrs);
|
||||
|
||||
benefitAttrs.forEach((benefit) => {
|
||||
if (benefit.id) {
|
||||
const productBenefit = productBenefits.find(benefit.id);
|
||||
productBenefit.product = product;
|
||||
productBenefit.save();
|
||||
} else {
|
||||
product.createProductBenefit(benefit);
|
||||
product.save();
|
||||
}
|
||||
});
|
||||
|
||||
return product.save();
|
||||
});
|
||||
|
||||
server.del('/products/:id/');
|
||||
}
|
44
ghost/admin/mirage/config/tiers.js
Normal file
44
ghost/admin/mirage/config/tiers.js
Normal file
@ -0,0 +1,44 @@
|
||||
import {paginatedResponse} from '../utils';
|
||||
|
||||
export default function mockTiers(server) {
|
||||
server.post('/tiers/');
|
||||
|
||||
server.get('/tiers/', paginatedResponse('tiers'));
|
||||
|
||||
server.get('/tiers/:id/', function ({tiers}, {params}) {
|
||||
let {id} = params;
|
||||
let tier = tiers.find(id);
|
||||
|
||||
return tier || new Response(404, {}, {
|
||||
errors: [{
|
||||
type: 'NotFoundError',
|
||||
message: 'Tier not found.'
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
server.put('/tiers/:id/', function ({tiers, tierBenefits}, {params}) {
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
const tier = tiers.find(params.id);
|
||||
|
||||
const benefitAttrs = attrs.benefits;
|
||||
delete attrs.benefits;
|
||||
|
||||
tier.update(attrs);
|
||||
|
||||
benefitAttrs.forEach((benefit) => {
|
||||
if (benefit.id) {
|
||||
const tierBenefit = tierBenefits.find(benefit.id);
|
||||
tierBenefit.tier = tier;
|
||||
tierBenefit.save();
|
||||
} else {
|
||||
tier.createTierBenefit(benefit);
|
||||
tier.save();
|
||||
}
|
||||
});
|
||||
|
||||
return tier.save();
|
||||
});
|
||||
|
||||
server.del('/tiers/:id/');
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import {Factory} from 'miragejs';
|
||||
|
||||
export default Factory.extend({
|
||||
name(i) { return `Product ${i}`; },
|
||||
description(i) { return `Description for product ${i}`; },
|
||||
name(i) { return `Tier ${i}`; },
|
||||
description(i) { return `Description for tier ${i}`; },
|
||||
active: true,
|
||||
slug(i) { return `product-${i}`;},
|
||||
slug(i) { return `tier-${i}`;},
|
||||
type: 'paid',
|
||||
visibility: 'none',
|
||||
monthly_price() {
|
@ -177,7 +177,7 @@ export default [
|
||||
id: 23,
|
||||
group: 'members',
|
||||
key: 'members_subscription_settings',
|
||||
value: '{"allowSelfSignup":true,"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":""}]}}]}',
|
||||
value: '{"allowSelfSignup":true,"fromAddress":"noreply","paymentProcessors":[{"adapter":"stripe","config":{"secret_token":"","public_token":"","tier":{"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',
|
||||
|
@ -22,8 +22,8 @@ export default [
|
||||
createdAt: '2022-02-04T13:11:40.000Z',
|
||||
description: null,
|
||||
monthlyPrice: null,
|
||||
name: 'Default Product',
|
||||
slug: 'default-product',
|
||||
name: 'Default Tier',
|
||||
slug: 'default-tier',
|
||||
type: 'paid',
|
||||
visibility: 'public',
|
||||
updatedAt: '2022-02-04T13:11:40.000Z',
|
@ -3,7 +3,7 @@ import {Model, hasMany} from 'miragejs';
|
||||
export default Model.extend({
|
||||
labels: hasMany(),
|
||||
emailRecipients: hasMany(),
|
||||
products: hasMany(),
|
||||
tiers: hasMany(),
|
||||
newsletters: hasMany(),
|
||||
subscriptions: hasMany()
|
||||
});
|
||||
|
@ -2,5 +2,5 @@ import {Model, belongsTo} from 'miragejs';
|
||||
|
||||
export default Model.extend({
|
||||
member: belongsTo(),
|
||||
product: belongsTo()
|
||||
tier: belongsTo()
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Model, belongsTo} from 'miragejs';
|
||||
|
||||
export default Model.extend({
|
||||
product: belongsTo('product')
|
||||
tier: belongsTo('tier')
|
||||
});
|
@ -3,6 +3,6 @@ import {Model, hasMany} from 'miragejs';
|
||||
export default Model.extend({
|
||||
// ran into odd relationship bugs when called `benefits`
|
||||
// serializer will rename to `benefits`
|
||||
productBenefits: hasMany(),
|
||||
tierBenefits: hasMany(),
|
||||
members: hasMany()
|
||||
});
|
@ -7,13 +7,13 @@ export default BaseSerializer.extend({
|
||||
include(/*request*/) {
|
||||
let includes = [];
|
||||
|
||||
includes.push('product');
|
||||
includes.push('tier');
|
||||
|
||||
return includes;
|
||||
},
|
||||
|
||||
keyForEmbeddedRelationship(relationshipName) {
|
||||
if (relationshipName === 'product') {
|
||||
if (relationshipName === 'tier') {
|
||||
return 'tier';
|
||||
}
|
||||
|
||||
|
@ -7,13 +7,13 @@ export default BaseSerializer.extend({
|
||||
include(/*request*/) {
|
||||
let includes = [];
|
||||
|
||||
includes.push('productBenefits');
|
||||
includes.push('tierBenefits');
|
||||
|
||||
return includes;
|
||||
},
|
||||
|
||||
keyForEmbeddedRelationship(relationshipName) {
|
||||
if (relationshipName === 'productBenefits') {
|
||||
if (relationshipName === 'tierBenefits') {
|
||||
return 'benefits';
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ describe('Acceptance: Member details', function () {
|
||||
setupMirage(hooks);
|
||||
|
||||
let clock;
|
||||
let product;
|
||||
let tier;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.server.loadFixtures('configs');
|
||||
@ -25,8 +25,8 @@ describe('Acceptance: Member details', function () {
|
||||
enableStripe(this.server);
|
||||
enableNewsletters(this.server, true);
|
||||
|
||||
// add a default product that complimentary plans can be assigned to
|
||||
product = this.server.create('product', {
|
||||
// add a default tier that complimentary plans can be assigned to
|
||||
tier = this.server.create('tier', {
|
||||
id: '6213b3f6cb39ebdb03ebd810',
|
||||
name: 'Ghost Subscription',
|
||||
slug: 'ghost-subscription',
|
||||
@ -82,17 +82,17 @@ describe('Acceptance: Member details', function () {
|
||||
interval: 'month',
|
||||
type: 'recurring',
|
||||
currency: 'USD',
|
||||
product: {
|
||||
tier: {
|
||||
id: 'prod_LFmAAmCnnbzrvL',
|
||||
name: 'Ghost Subscription',
|
||||
product_id: product.id
|
||||
tier_id: tier.id
|
||||
}
|
||||
},
|
||||
offer: null
|
||||
}),
|
||||
this.server.create('subscription', {
|
||||
id: 'sub_1KZGi6EGb07FFvyNDjZq98g8',
|
||||
product,
|
||||
tier,
|
||||
customer: {
|
||||
id: 'cus_LFmGicpX4BkQKH',
|
||||
name: '123',
|
||||
@ -119,17 +119,17 @@ describe('Acceptance: Member details', function () {
|
||||
interval: 'month',
|
||||
type: 'recurring',
|
||||
currency: 'USD',
|
||||
product: {
|
||||
tier: {
|
||||
id: 'prod_LFmAAmCnnbzrvL',
|
||||
name: 'Ghost Subscription',
|
||||
product_id: product.id
|
||||
tier_id: tier.id
|
||||
}
|
||||
},
|
||||
offer: null
|
||||
})
|
||||
],
|
||||
products: [
|
||||
product
|
||||
tiers: [
|
||||
tier
|
||||
]
|
||||
});
|
||||
|
||||
@ -149,7 +149,7 @@ describe('Acceptance: Member details', function () {
|
||||
subscriptions: [
|
||||
this.server.create('subscription', {
|
||||
id: 'sub_1KZGcmEGb07FFvyN9jwrwbKu',
|
||||
product,
|
||||
tier,
|
||||
customer: {
|
||||
id: 'cus_LFmBWoSkB84lnr',
|
||||
name: 'test',
|
||||
@ -176,16 +176,16 @@ describe('Acceptance: Member details', function () {
|
||||
interval: 'month',
|
||||
type: 'recurring',
|
||||
currency: 'USD',
|
||||
product: {
|
||||
tier: {
|
||||
id: 'prod_LFmAAmCnnbzrvL',
|
||||
name: 'Ghost Subscription',
|
||||
product_id: '6213b3f6cb39ebdb03ebd810'
|
||||
tier_id: '6213b3f6cb39ebdb03ebd810'
|
||||
}
|
||||
},
|
||||
offer: null
|
||||
})
|
||||
],
|
||||
products: []
|
||||
tiers: []
|
||||
});
|
||||
|
||||
await visit(`/members/${member.id}`);
|
||||
@ -205,17 +205,17 @@ describe('Acceptance: Member details', function () {
|
||||
.to.equal(1);
|
||||
|
||||
await click('[data-test-button="add-complimentary"]');
|
||||
expect(find('[data-test-modal="member-product"]'), 'select product modal').to.exist;
|
||||
expect(find('[data-test-modal="member-tier"]'), 'select tier modal').to.exist;
|
||||
expect(find('[data-test-text="select-tier-desc"]')).to.contain.text('Comp Member Test');
|
||||
expect(find('[data-test-tier-option="6213b3f6cb39ebdb03ebd810"]')).to.have.exist;
|
||||
expect(find('[data-test-tier-option="6213b3f6cb39ebdb03ebd810"]')).to.have.class('active');
|
||||
await click('[data-test-button="save-comp-product"]');
|
||||
await click('[data-test-button="save-comp-tier"]');
|
||||
|
||||
expect(findAll('[data-test-subscription]').length, '# of subscription blocks - after add comped')
|
||||
.to.equal(1);
|
||||
|
||||
await click('[data-test-product="6213b3f6cb39ebdb03ebd810"] [data-test-button="subscription-actions"]');
|
||||
await click('[data-test-product="6213b3f6cb39ebdb03ebd810"] [data-test-button="remove-complimentary"]');
|
||||
await click('[data-test-tier="6213b3f6cb39ebdb03ebd810"] [data-test-button="subscription-actions"]');
|
||||
await click('[data-test-tier="6213b3f6cb39ebdb03ebd810"] [data-test-button="remove-complimentary"]');
|
||||
|
||||
expect(findAll('[data-test-subscription]').length, '# of subscription blocks - after remove comped')
|
||||
.to.equal(0);
|
||||
@ -226,13 +226,13 @@ describe('Acceptance: Member details', function () {
|
||||
name: 'Comped for canceled sub test',
|
||||
subscriptions: [
|
||||
this.server.create('subscription', {
|
||||
// product, // _Not_ included as `tier` when subscription is canceled
|
||||
// tier, // _Not_ included as `tier` when subscription is canceled
|
||||
status: 'canceled',
|
||||
price: {
|
||||
id: 'price_1',
|
||||
product: {
|
||||
tier: {
|
||||
id: 'prod_1',
|
||||
product_id: product.id
|
||||
tier_id: tier.id
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -245,7 +245,7 @@ describe('Acceptance: Member details', function () {
|
||||
.to.equal(1);
|
||||
|
||||
await click('[data-test-button="add-complimentary"]');
|
||||
await click('[data-test-button="save-comp-product"]');
|
||||
await click('[data-test-button="save-comp-tier"]');
|
||||
|
||||
expect(findAll('[data-test-subscription]').length, '# of subscription blocks - after add comped')
|
||||
.to.equal(2);
|
||||
@ -253,10 +253,10 @@ describe('Acceptance: Member details', function () {
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
it('handles multiple products', async function () {
|
||||
const product2 = this.server.create('product', {
|
||||
name: 'Second product',
|
||||
slug: 'second-product',
|
||||
it('handles multiple tiers', async function () {
|
||||
const tier2 = this.server.create('tier', {
|
||||
name: 'Second tier',
|
||||
slug: 'second-tier',
|
||||
created_at: '2022-02-21T16:47:02.000Z',
|
||||
updated_at: '2022-03-03T15:37:02.000Z',
|
||||
description: null,
|
||||
@ -267,32 +267,32 @@ describe('Acceptance: Member details', function () {
|
||||
welcome_page_url: '/'
|
||||
});
|
||||
|
||||
const member = this.server.create('member', {name: 'Multiple product test'});
|
||||
const member = this.server.create('member', {name: 'Multiple tier test'});
|
||||
|
||||
this.server.create('subscription', {member, product, status: 'canceled', price: {id: '1', product: {product_id: product.id}}});
|
||||
this.server.create('subscription', {member, product, status: 'canceled', price: {id: '1', product: {product_id: product.id}}});
|
||||
this.server.create('subscription', {member, product: product2, status: 'canceled', price: {id: '1', product: {product_id: product2.id}}});
|
||||
this.server.create('subscription', {member, tier, status: 'canceled', price: {id: '1', tier: {tier_id: tier.id}}});
|
||||
this.server.create('subscription', {member, tier, status: 'canceled', price: {id: '1', tier: {tier_id: tier.id}}});
|
||||
this.server.create('subscription', {member, tier: tier2, status: 'canceled', price: {id: '1', tier: {tier_id: tier2.id}}});
|
||||
|
||||
await visit(`/members/${member.id}`);
|
||||
|
||||
// all products and subscriptions are shown
|
||||
expect(findAll('[data-test-product]').length, '# of product blocks').to.equal(2);
|
||||
// all tiers and subscriptions are shown
|
||||
expect(findAll('[data-test-tier]').length, '# of tier blocks').to.equal(2);
|
||||
|
||||
const p1 = `[data-test-product="${product.id}"]`;
|
||||
const p2 = `[data-test-product="${product2.id}"]`;
|
||||
const p1 = `[data-test-tier="${tier.id}"]`;
|
||||
const p2 = `[data-test-tier="${tier2.id}"]`;
|
||||
|
||||
expect(find(`${p1} [data-test-text="product-name"]`)).to.contain.text('Ghost Subscription');
|
||||
expect(findAll(`${p1} [data-test-subscription]`).length, '# of product 1 subscription blocks').to.equal(2);
|
||||
expect(find(`${p1} [data-test-text="tier-name"]`)).to.contain.text('Ghost Subscription');
|
||||
expect(findAll(`${p1} [data-test-subscription]`).length, '# of tier 1 subscription blocks').to.equal(2);
|
||||
|
||||
expect(find(`${p2} [data-test-text="product-name"]`)).to.contain.text('Second product');
|
||||
expect(findAll(`${p2} [data-test-subscription]`).length, '# of product 2 subscription blocks').to.equal(1);
|
||||
expect(find(`${p2} [data-test-text="tier-name"]`)).to.contain.text('Second tier');
|
||||
expect(findAll(`${p2} [data-test-subscription]`).length, '# of tier 2 subscription blocks').to.equal(1);
|
||||
|
||||
// can add complimentary
|
||||
expect(findAll('[data-test-button="add-complimentary"]').length, '# of add-complimentary buttons').to.equal(1);
|
||||
await click('[data-test-button="add-complimentary"]');
|
||||
await click(`[data-test-tier-option="${product2.id}"]`);
|
||||
await click('[data-test-button="save-comp-product"]');
|
||||
await click(`[data-test-tier-option="${tier2.id}"]`);
|
||||
await click('[data-test-button="save-comp-tier"]');
|
||||
|
||||
expect(findAll(`${p2} [data-test-subscription]`).length, '# of product 2 subscription blocks after comp added').to.equal(2);
|
||||
expect(findAll(`${p2} [data-test-subscription]`).length, '# of tier 2 subscription blocks after comp added').to.equal(2);
|
||||
});
|
||||
});
|
||||
|
@ -116,11 +116,11 @@ describe('Acceptance: Members filtering', function () {
|
||||
it('can filter by tier', async function () {
|
||||
// add some labels to test the selection dropdown
|
||||
const newsletter = this.server.create('newsletter', {status: 'active'});
|
||||
this.server.createList('product', 4);
|
||||
this.server.createList('tier', 4);
|
||||
|
||||
// add a labelled member so we can test the filter includes correctly
|
||||
const product = this.server.create('product');
|
||||
this.server.createList('member', 3, {products: [product], newsletters: [newsletter]});
|
||||
const tier = this.server.create('tier');
|
||||
this.server.createList('member', 3, {tiers: [tier], newsletters: [newsletter]});
|
||||
|
||||
// add some non-labelled members so we can see the filter excludes correctly
|
||||
this.server.createList('member', 4, {newsletters: [newsletter]});
|
||||
@ -133,7 +133,7 @@ describe('Acceptance: Members filtering', function () {
|
||||
|
||||
const filterSelector = `[data-test-members-filter="0"]`;
|
||||
|
||||
await fillIn(`${filterSelector} [data-test-select="members-filter"]`, 'product');
|
||||
await fillIn(`${filterSelector} [data-test-select="members-filter"]`, 'tier');
|
||||
|
||||
// has the right operators
|
||||
const operatorOptions = findAll(`${filterSelector} [data-test-select="members-filter-operator"] option`);
|
||||
@ -146,14 +146,14 @@ describe('Acceptance: Members filtering', function () {
|
||||
expect(findAll(`${filterSelector} [data-test-tiers-segment]`).length, '# of label options').to.equal(5);
|
||||
|
||||
// selecting a value updates table
|
||||
await selectChoose(`${filterSelector} .gh-tier-token-input`, product.name);
|
||||
await selectChoose(`${filterSelector} .gh-tier-token-input`, tier.name);
|
||||
|
||||
expect(findAll('[data-test-list="members-list-item"]').length, `# of filtered member rows - ${product.name}`)
|
||||
expect(findAll('[data-test-list="members-list-item"]').length, `# of filtered member rows - ${tier.name}`)
|
||||
.to.equal(3);
|
||||
// table shows labels column+data
|
||||
expect(find('[data-test-table-column="product"]')).to.exist;
|
||||
expect(findAll('[data-test-table-data="product"]').length).to.equal(3);
|
||||
expect(find('[data-test-table-data="product"]')).to.contain.text(product.name);
|
||||
expect(find('[data-test-table-column="tier"]')).to.exist;
|
||||
expect(findAll('[data-test-table-data="tier"]').length).to.equal(3);
|
||||
expect(find('[data-test-table-data="tier"]')).to.contain.text(tier.name);
|
||||
|
||||
// can delete filter
|
||||
await click('[data-test-delete-members-filter="0"]');
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user