🐛 Fixed member subscription details in Admin (#20619)
fixes https://linear.app/tryghost/issue/ONC-189
- commit 4084a3d
introduced a regression that caused member subscription
details to not be rendered for active/canceled subscriptions
- with this fix, the rendering logic in Admin for member subscription
details has been fully moved to a helper and is now covered by
additional unit tests
This commit is contained in:
parent
cd17b94e9c
commit
c5bb2e5dc7
@ -111,122 +111,104 @@
|
||||
<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 gh-cp-membertier-attribution gh-membertier-subscription {{if (gt tier.subscriptions.length 1) "multiple-subs" ""}}">
|
||||
{{#each tier.subscriptions as |sub index|}}
|
||||
<div class="gh-tier-card-header flex items-center">
|
||||
<div class="gh-tier-card-price">
|
||||
<div class="flex items-start">
|
||||
<span class="currency-symbol">{{sub.price.currencySymbol}}</span>
|
||||
<span class="amount">{{format-number sub.price.nonDecimalAmount}}</span>
|
||||
<div class="gh-tier-card-header flex items-center">
|
||||
<div class="gh-tier-card-price">
|
||||
<div class="flex items-start">
|
||||
<span class="currency-symbol">{{sub.price.currencySymbol}}</span>
|
||||
<span class="amount">{{format-number sub.price.nonDecimalAmount}}</span>
|
||||
</div>
|
||||
<div class="period">{{if (eq sub.price.interval "year") "yearly" "monthly"}}</div>
|
||||
</div>
|
||||
<div class="period">{{if (eq sub.price.interval "year") "yearly" "monthly"}}</div>
|
||||
</div>
|
||||
<div style="margin-left: 16px; flex-grow: 1;">
|
||||
<h3 class="gh-membertier-name" data-test-text="tier-name" style="align-items:center !important; justify-content:flex-start !important;">
|
||||
{{tier.name}}
|
||||
{{#if (eq sub.status "canceled")}}
|
||||
<span class="gh-badge archived" data-test-text="member-subscription-status">Canceled</span>
|
||||
{{else if sub.cancel_at_period_end}}
|
||||
<span class="gh-badge archived" data-test-text="member-subscription-status">Canceled</span>
|
||||
{{else if sub.compExpiry}}
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{else if sub.trialUntil}}
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{else}}
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{/if}}
|
||||
{{#if (gt tier.subscriptions.length 1)}}
|
||||
<span class="gh-membertier-subcount">{{tier.subscriptions.length}} subscriptions</span>
|
||||
{{/if}}
|
||||
</h3>
|
||||
<div>
|
||||
{{#if sub.trialUntil}}
|
||||
<span class="gh-cp-membertier-pricelabel">Free trial </span>
|
||||
{{else}}
|
||||
{{#if (or (eq sub.price.nickname "Monthly") (eq sub.price.nickname "Yearly"))}}
|
||||
<div style="margin-left: 16px; flex-grow: 1;">
|
||||
<h3 class="gh-membertier-name" data-test-text="tier-name" style="align-items:center !important; justify-content:flex-start !important;">
|
||||
{{tier.name}}
|
||||
{{#if (eq sub.status "canceled")}}
|
||||
<span class="gh-badge archived" data-test-text="member-subscription-status">Canceled</span>
|
||||
{{else if sub.cancel_at_period_end}}
|
||||
<span class="gh-badge archived" data-test-text="member-subscription-status">Canceled</span>
|
||||
{{else if sub.compExpiry}}
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{else if sub.trialUntil}}
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{else}}
|
||||
<span class="gh-cp-membertier-pricelabel">{{sub.price.nickname}}</span>
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if sub.trialUntil}}
|
||||
<span class="gh-cp-membertier-renewal"> – </span>
|
||||
{{#if (gt tier.subscriptions.length 1)}}
|
||||
<span class="gh-membertier-subcount">{{tier.subscriptions.length}} subscriptions</span>
|
||||
{{/if}}
|
||||
</h3>
|
||||
<div>
|
||||
<span class="gh-cp-membertier-pricelabel">{{sub.priceLabel}}</span>
|
||||
<span class="gh-cp-membertier-renewal">{{sub.validityDetails}}</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if sub.compExpiry}}
|
||||
<span class="gh-cp-membertier-renewal"> – </span>
|
||||
<span class="gh-cp-membertier-renewal">{{sub.validityDetails}}</span>
|
||||
{{/if}}
|
||||
|
||||
|
||||
</div>
|
||||
<Member::SubscriptionDetailBox @sub={{sub}} @index={{index}} />
|
||||
</div>
|
||||
<Member::SubscriptionDetailBox @sub={{sub}} @index={{index}} />
|
||||
</div>
|
||||
{{#if sub.isComplimentary}}
|
||||
<span class="action-menu">
|
||||
<GhDropdownButton
|
||||
@dropdownName="subscription-menu-complimentary"
|
||||
@classNames="gh-btn gh-btn-outline gh-btn-icon fill gh-btn-subscription-action icon-only"
|
||||
@title="Actions"
|
||||
data-test-button="subscription-actions"
|
||||
>
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Subscription menu</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<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 (or tier.id tier.tier_id))}}
|
||||
data-test-button="remove-complimentary"
|
||||
>
|
||||
<span class="red">Remove complimentary subscription</span>
|
||||
</button>
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="action-menu">
|
||||
<GhDropdownButton @dropdownName="subscription-menu-{{sub.id}}" @classNames="gh-btn gh-btn-outline gh-btn-icon fill gh-btn-subscription-action icon-only" @title="Actions">
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Subscription menu</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href="https://dashboard.stripe.com/subscriptions/{{sub.id}}" target="_blank" rel="noopener noreferrer">
|
||||
View Stripe subscription
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{#if (not-eq sub.status "canceled")}}
|
||||
{{#if sub.cancel_at_period_end}}
|
||||
<button type="button" {{on "click" (fn this.continueSubscription sub.id)}}>
|
||||
<span>Continue subscription</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" {{on "click" (fn this.cancelSubscription sub.id)}}>
|
||||
<span class="red">Cancel subscription</span>
|
||||
{{#if sub.isComplimentary}}
|
||||
<span class="action-menu">
|
||||
<GhDropdownButton
|
||||
@dropdownName="subscription-menu-complimentary"
|
||||
@classNames="gh-btn gh-btn-outline gh-btn-icon fill gh-btn-subscription-action icon-only"
|
||||
@title="Actions"
|
||||
data-test-button="subscription-actions"
|
||||
>
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Subscription menu</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<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 (or tier.id tier.tier_id))}}
|
||||
data-test-button="remove-complimentary"
|
||||
>
|
||||
<span class="red">Remove complimentary subscription</span>
|
||||
</button>
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="action-menu">
|
||||
<GhDropdownButton @dropdownName="subscription-menu-{{sub.id}}" @classNames="gh-btn gh-btn-outline gh-btn-icon fill gh-btn-subscription-action icon-only" @title="Actions">
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Subscription menu</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href="https://dashboard.stripe.com/subscriptions/{{sub.id}}" target="_blank" rel="noopener noreferrer">
|
||||
View Stripe subscription
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{#if (not-eq sub.status "canceled")}}
|
||||
{{#if sub.cancel_at_period_end}}
|
||||
<button type="button" {{on "click" (fn this.continueSubscription sub.id)}}>
|
||||
<span>Continue subscription</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" {{on "click" (fn this.cancelSubscription sub.id)}}>
|
||||
<span class="red">Cancel subscription</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if (eq tier.subscriptions.length 0)}}
|
||||
|
@ -24,7 +24,8 @@ export function getSubscriptionData(sub) {
|
||||
trialUntil: trialUntil(sub)
|
||||
};
|
||||
|
||||
data.validityDetails = validityDetails(data);
|
||||
data.priceLabel = priceLabel(data);
|
||||
data.validityDetails = validityDetails(data, !!data.priceLabel);
|
||||
|
||||
return data;
|
||||
}
|
||||
@ -77,22 +78,39 @@ export function trialUntil(sub) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function validityDetails(data) {
|
||||
if (data.isComplimentary && data.compExpiry) {
|
||||
return `Expires ${data.compExpiry}`;
|
||||
export function validityDetails(data, separatorNeeded = false) {
|
||||
const separator = separatorNeeded ? ' – ' : '';
|
||||
const space = data.validUntil ? ' ' : '';
|
||||
|
||||
if (data.isComplimentary) {
|
||||
if (data.compExpiry) {
|
||||
return `${separator}Expires ${data.compExpiry}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (data.hasEnded) {
|
||||
return `Ended ${data.validUntil}`;
|
||||
return `${separator}Ended${space}${data.validUntil}`;
|
||||
}
|
||||
|
||||
if (data.willEndSoon) {
|
||||
return `Has access until ${data.validUntil}`;
|
||||
return `${separator}Has access until${space}${data.validUntil}`;
|
||||
}
|
||||
|
||||
if (data.trialUntil) {
|
||||
return `Ends ${data.trialUntil}`;
|
||||
return `${separator}Ends ${data.trialUntil}`;
|
||||
}
|
||||
|
||||
return `Renews ${data.validUntil}`;
|
||||
return `${separator}Renews${space}${data.validUntil}`;
|
||||
}
|
||||
|
||||
export function priceLabel(data) {
|
||||
if (data.trialUntil) {
|
||||
return 'Free trial';
|
||||
}
|
||||
|
||||
if (data.price.nickname && data.price.nickname.length > 0 && data.price.nickname !== 'Monthly' && data.price.nickname !== 'Yearly') {
|
||||
return data.price.nickname;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moment from 'moment-timezone';
|
||||
import {compExpiry, getSubscriptionData, isActive, isCanceled, isComplimentary, isSetToCancel, trialUntil, validUntil, validityDetails} from 'ghost-admin/utils/subscription-data';
|
||||
import {compExpiry, getSubscriptionData, isActive, isCanceled, isComplimentary, isSetToCancel, priceLabel, trialUntil, validUntil, validityDetails} from 'ghost-admin/utils/subscription-data';
|
||||
import {describe, it} from 'mocha';
|
||||
import {expect} from 'chai';
|
||||
|
||||
@ -165,6 +165,26 @@ describe('Unit: Util: subscription-data', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('priceLabel', function () {
|
||||
it('returns "Free trial" for trial subscriptions', function () {
|
||||
let data = {trialUntil: '31 May 2021'};
|
||||
expect(priceLabel(data)).to.equal('Free trial');
|
||||
});
|
||||
|
||||
it('returns nothing if the price nickname is the default "monthly" or "yearly"', function () {
|
||||
let data = {price: {nickname: 'Monthly'}};
|
||||
expect(priceLabel(data)).to.be.undefined;
|
||||
|
||||
data = {price: {nickname: 'Yearly'}};
|
||||
expect(priceLabel(data)).to.be.undefined;
|
||||
});
|
||||
|
||||
it('returns the price nickname for non-default prices', function () {
|
||||
let data = {price: {nickname: 'Custom'}};
|
||||
expect(priceLabel(data)).to.equal('Custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validityDetails', function () {
|
||||
it('returns "Expires {compExpiry}" for expired complimentary subscriptions', function () {
|
||||
let data = {
|
||||
@ -174,6 +194,14 @@ describe('Unit: Util: subscription-data', function () {
|
||||
expect(validityDetails(data)).to.equal('Expires 31 May 2021');
|
||||
});
|
||||
|
||||
it('returns "" for forever complimentary subscriptions', function () {
|
||||
let data = {
|
||||
isComplimentary: true,
|
||||
compExpiry: undefined
|
||||
};
|
||||
expect(validityDetails(data)).to.equal('');
|
||||
});
|
||||
|
||||
it('returns "Ended {validUntil}" for canceled subscriptions', function () {
|
||||
let data = {
|
||||
hasEnded: true,
|
||||
@ -228,6 +256,7 @@ describe('Unit: Util: subscription-data', function () {
|
||||
validUntil: '31 May 2021',
|
||||
willEndSoon: false,
|
||||
trialUntil: undefined,
|
||||
priceLabel: undefined,
|
||||
validityDetails: 'Renews 31 May 2021'
|
||||
});
|
||||
});
|
||||
@ -254,7 +283,8 @@ describe('Unit: Util: subscription-data', function () {
|
||||
validUntil: '31 May 2222',
|
||||
willEndSoon: false,
|
||||
trialUntil: '31 May 2222',
|
||||
validityDetails: 'Ends 31 May 2222'
|
||||
priceLabel: 'Free trial',
|
||||
validityDetails: ' – Ends 31 May 2222'
|
||||
});
|
||||
});
|
||||
|
||||
@ -280,7 +310,8 @@ describe('Unit: Util: subscription-data', function () {
|
||||
validUntil: '',
|
||||
willEndSoon: false,
|
||||
trialUntil: undefined,
|
||||
validityDetails: 'Ended '
|
||||
priceLabel: undefined,
|
||||
validityDetails: 'Ended'
|
||||
});
|
||||
});
|
||||
|
||||
@ -306,11 +337,42 @@ describe('Unit: Util: subscription-data', function () {
|
||||
validUntil: '31 May 2021',
|
||||
willEndSoon: true,
|
||||
trialUntil: undefined,
|
||||
priceLabel: undefined,
|
||||
validityDetails: 'Has access until 31 May 2021'
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct data for a complimentary subscription', function () {
|
||||
it('returns the correct data for a complimentary subscription active forever', function () {
|
||||
let sub = {
|
||||
id: null,
|
||||
status: 'active',
|
||||
cancel_at_period_end: false,
|
||||
current_period_end: '2021-05-31',
|
||||
trial_end_at: null,
|
||||
tier: {
|
||||
expiry_at: null
|
||||
},
|
||||
price: {
|
||||
currency: 'usd',
|
||||
amount: 0,
|
||||
nickname: 'Complimentary'
|
||||
}
|
||||
};
|
||||
let data = getSubscriptionData(sub);
|
||||
|
||||
expect(data).to.include({
|
||||
isComplimentary: true,
|
||||
compExpiry: undefined,
|
||||
hasEnded: false,
|
||||
validUntil: '31 May 2021',
|
||||
willEndSoon: false,
|
||||
trialUntil: undefined,
|
||||
priceLabel: 'Complimentary',
|
||||
validityDetails: ''
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct data for a complimentary subscription with an expiration date', function () {
|
||||
let sub = {
|
||||
id: null,
|
||||
status: 'active',
|
||||
@ -322,7 +384,8 @@ describe('Unit: Util: subscription-data', function () {
|
||||
},
|
||||
price: {
|
||||
currency: 'usd',
|
||||
amount: 0
|
||||
amount: 0,
|
||||
nickname: 'Complimentary'
|
||||
}
|
||||
};
|
||||
let data = getSubscriptionData(sub);
|
||||
@ -334,7 +397,8 @@ describe('Unit: Util: subscription-data', function () {
|
||||
validUntil: '31 May 2021',
|
||||
willEndSoon: false,
|
||||
trialUntil: undefined,
|
||||
validityDetails: 'Expires 31 May 2021'
|
||||
priceLabel: 'Complimentary',
|
||||
validityDetails: ' – Expires 31 May 2021'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user