feat: subscription changes for program details page (#32060)

* feat: subscription ui changes on program detail page (#31846)
* test: update tests for subscription changes on program details (#32020)
* feat: subscription api changes on program details (#32059)
This commit is contained in:
Nawfal Ahmed
2023-05-08 20:19:25 +05:00
committed by GitHub
parent 9d9bd63926
commit a5987e73d0
20 changed files with 593 additions and 44 deletions

View File

@@ -0,0 +1,4 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 0V2H11.59L0 13.59L1.41 15L13 3.41V10H15V0H5Z" fill="#454545
"/>
</svg>

After

Width:  |  Height:  |  Size: 179 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 8L15 12H18C18 15.31 15.31 18 12 18C10.99 18 10.03 17.75 9.2 17.3L7.74 18.76C8.97 19.54 10.43 20 12 20C16.42 20 20 16.42 20 12H23L19 8ZM6 12C6 8.69 8.69 6 12 6C13.01 6 13.97 6.25 14.8 6.7L16.26 5.24C15.03 4.46 13.57 4 12 4C7.58 4 4 7.58 4 12H1L5 16L9 12H6Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 388 B

View File

@@ -0,0 +1,88 @@
import Backbone from 'backbone';
import moment from 'moment';
import DateUtils from 'edx-ui-toolkit/js/utils/date-utils';
/**
* Model for Program Subscription Data.
*/
class ProgramSubscriptionModel extends Backbone.Model {
constructor({ context }, ...args) {
const {
subscriptionData: [data = {}],
programData: { subscription_prices },
urls,
userPreferences,
} = context;
const priceInUSD = subscription_prices?.find(({ currency }) => currency === 'USD')?.price;
const trialMoment = moment(
DateUtils.localizeTime(
DateUtils.stringToMoment(data.trial_end),
'UTC'
)
);
const subscriptionState = data.subscription_state?.toLowerCase() ?? '';
const subscriptionPrice = '$' + parseFloat(priceInUSD);
const subscriptionUrl =
subscriptionState === 'active'
? urls.manage_subscription_url
: urls.buy_subscription_url;
const hasActiveTrial =
subscriptionState === 'active' && data.trial_end
? trialMoment.isAfter(moment.utc())
: false;
const [nextPaymentDate] = ProgramSubscriptionModel.formatDate(
data.next_payment_date,
userPreferences
);
const [trialEndDate, trialEndTime] = ProgramSubscriptionModel.formatDate(
data.trial_end,
userPreferences
);
const trialLength = 7;
super(
{
hasActiveTrial,
nextPaymentDate,
subscriptionPrice,
subscriptionState,
subscriptionUrl,
trialEndDate,
trialEndTime,
trialLength,
},
...args
);
}
static formatDate(date, userPreferences) {
if (!date) {
return ['', ''];
}
const userTimezone = userPreferences?.time_zone || 'UTC';
const userLanguage = userPreferences?.['pref-lang'] || 'en';
const context = {
datetime: date,
timezone: userTimezone,
language: userLanguage,
format: DateUtils.dateFormatEnum.shortDate,
};
const localDate = DateUtils.localize(context);
const localTime = DateUtils.localizeTime(
DateUtils.stringToMoment(date),
userTimezone
).format('HH:mm');
return [localDate, localTime];
}
}
export default ProgramSubscriptionModel;

View File

@@ -13,8 +13,13 @@ describe('Course Card View', () => {
const setupView = (data, isEnrolled, collectionCourseStatus) => {
const programData = $.extend({}, data);
const context = {
courseData: {},
programData,
collectionCourseStatus,
courseData: {},
subscriptionData: [],
urls: {},
userPreferences: {},
isSubscriptionEligible: false,
};
if (typeof collectionCourseStatus === 'undefined') {
@@ -31,7 +36,7 @@ describe('Course Card View', () => {
};
const validateCourseInfoDisplay = () => {
// DRY validation for course card in enrolled state
// DRY validation for course card in enrolled state
expect(view.$('.course-details .course-title-link').text().trim()).toEqual(course.title);
expect(view.$('.course-details .course-title-link').attr('href')).toEqual(
course.course_runs[0].marketing_url,
@@ -42,8 +47,8 @@ describe('Course Card View', () => {
};
beforeEach(() => {
// NOTE: This data is redefined prior to each test case so that tests
// can't break each other by modifying data copied by reference.
// NOTE: This data is redefined prior to each test case so that tests
// can't break each other by modifying data copied by reference.
course = {
key: 'WageningenX+FFESx',
uuid: '9f8562eb-f99b-45c7-b437-799fd0c15b6a',

View File

@@ -45,6 +45,16 @@ describe('Program Details Header View', () => {
},
],
},
subscriptionData: [
{
trial_end: '1970-01-01T03:25:45Z',
next_payment_date: '1970-06-03T07:12:04Z',
price: '100.00',
currency: 'USD',
subscription_state: 'active',
},
],
isSubscriptionEligible: true,
};
beforeEach(() => {
@@ -71,4 +81,8 @@ describe('Program Details Header View', () => {
expect(view.$('.org-logo').attr('alt'))
.toEqual(`${context.programData.authoring_organizations[0].name}'s logo`);
});
it('should render the subscription badge if subscription is active', () => {
expect(view.$('.meta-info .badge').html().trim()).toEqual('Subscribed');
});
});

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,17 @@
/* globals setFixtures */
import moment from 'moment';
import ProgramDetailsView from '../views/program_details_view';
describe('Program Details Header View', () => {
describe('Program Details View', () => {
let view = null;
const options = {
programData: {
subscription_eligible: false,
subscription_prices: [{
price: '100.00',
currency: 'USD',
}],
subtitle: '',
overview: '',
weeks_to_complete: null,
@@ -462,11 +468,23 @@ describe('Program Details Header View', () => {
},
],
},
subscriptionData: [
{
trial_end: '1970-01-01T03:25:45Z',
next_payment_date: '1970-06-03T07:12:04Z',
price: '100.00',
currency: 'USD',
subscription_state: 'pre',
},
],
urls: {
program_listing_url: '/dashboard/programs/',
commerce_api_url: '/api/commerce/v0/baskets/',
track_selection_url: '/course_modes/choose/',
program_record_url: 'http://credentials.example.com/records/programs/UUID',
buy_subscription_url: '/subscriptions',
manage_subscription_url: '/orders',
subscriptions_learner_help_center_url: '/learner',
},
userPreferences: {
'pref-lang': 'en',
@@ -493,10 +511,36 @@ describe('Program Details Header View', () => {
destination_url: 'industry.com',
},
],
programTabViewEnabled: false
programTabViewEnabled: false,
isUserB2CSubscriptionsEnabled: false,
};
const data = options.programData;
const testSubscriptionState = (state, heading, body, trial = false) => {
const subscriptionData = {
...options.subscriptionData[0],
subscription_state: state,
};
if (trial) {
subscriptionData.trial_end = moment().add(3, 'days').utc().format(
'YYYY-MM-DDTHH:mm:ss[Z]'
);
}
view = initView({
programData: $.extend({}, options.programData, {
subscription_eligible: true,
}),
isUserB2CSubscriptionsEnabled: true,
subscriptionData: [subscriptionData],
});
view.render();
expect(view.$('.upgrade-subscription')[0]).toBeInDOM();
expect(view.$('.upgrade-subscription .upgrade-button'))
.toContainText(heading);
expect(view.$('.upgrade-subscription .subscription-info-brief'))
.toContainText(body);
};
const initView = (updates) => {
const viewOptions = $.extend({}, options, updates);
@@ -535,9 +579,9 @@ describe('Program Details Header View', () => {
expect(view.$('.program-heading-title').text()).toEqual('Your Program Journey');
expect(view.$('.program-heading-message').text().trim()
.replace(/\s+/g, ' ')).toEqual(
'Track and plan your progress through the 3 courses in this program. '
+ 'To complete the program, you must earn a verified certificate for each course.',
);
'Track and plan your progress through the 3 courses in this program. '
+ 'To complete the program, you must earn a verified certificate for each course.',
);
});
it('should render the program heading congratulations message if all courses completed', () => {
@@ -553,8 +597,8 @@ describe('Program Details Header View', () => {
expect(view.$('.program-heading-title').text()).toEqual('Congratulations!');
expect(view.$('.program-heading-message').text().trim()
.replace(/\s+/g, ' ')).toEqual(
'You have successfully completed all the requirements for the Test Course Title Test.',
);
'You have successfully completed all the requirements for the Test Course Title Test.',
);
});
it('should render the course list headings', () => {
@@ -645,4 +689,37 @@ describe('Program Details Header View', () => {
properties,
);
});
it('should render the get subscription link if program is subscription eligible', () => {
testSubscriptionState(
'pre',
'Start 7-Day free trial',
'$100/month subscription after trial ends. Cancel anytime.'
);
});
it('should render appropriate subscription text when subscription is active with trial', () => {
testSubscriptionState(
'active',
'Manage my subscription',
'Active trial ends',
true
);
});
it('should render appropriate subscription text when subscription is active', () => {
testSubscriptionState(
'active',
'Manage my subscription',
'Your next billing date is'
);
});
it('should render appropriate subscription text when subscription is inactive', () => {
testSubscriptionState(
'inactive',
'Restart my subscription',
'Unlock verified access to all courses for $100/month. Cancel anytime.'
);
});
});

View File

@@ -9,6 +9,8 @@ import ExpiredNotificationView from './expired_notification_view';
import CourseEnrollView from './course_enroll_view';
import EntitlementView from './course_entitlement_view';
import SubscriptionModel from '../models/program_subscription_model';
import pageTpl from '../../../templates/learner_dashboard/course_card.underscore';
class CourseCardView extends Backbone.View {
@@ -24,6 +26,9 @@ class CourseCardView extends Backbone.View {
this.enrollModel = new EnrollModel();
if (options.context) {
this.urlModel = new Backbone.Model(options.context.urls);
this.subscriptionModel = new SubscriptionModel({
context: options.context,
});
this.enrollModel.urlRoot = this.urlModel.get('commerce_api_url');
}
this.context = options.context || {};
@@ -86,6 +91,8 @@ class CourseCardView extends Backbone.View {
this.upgradeMessage = new UpgradeMessageView({
$el: $upgradeMessage,
model: this.model,
subscriptionModel: this.subscriptionModel,
isSubscriptionEligible: this.context.isSubscriptionEligible,
});
$certStatus.remove();

View File

@@ -8,6 +8,7 @@ import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
import CertificateView from './certificate_list_view';
import ProgramProgressView from './progress_circle_view';
import arrowUprightIcon from '../../../images/arrow-upright-icon.svg';
import sidebarTpl from '../../../templates/learner_dashboard/program_details_sidebar.underscore';
class ProgramDetailsSidebarView extends Backbone.View {
@@ -25,23 +26,32 @@ class ProgramDetailsSidebarView extends Backbone.View {
this.courseModel = options.courseModel || {};
this.certificateCollection = options.certificateCollection || [];
this.programCertificate = this.getProgramCertificate();
this.programRecordUrl = options.programRecordUrl;
this.industryPathways = options.industryPathways;
this.creditPathways = options.creditPathways;
this.programModel = options.model;
this.subscriptionModel = options.subscriptionModel;
this.programTabViewEnabled = options.programTabViewEnabled;
this.isSubscriptionEligible = options.isSubscriptionEligible;
this.urls = options.urls;
this.render();
}
render() {
const data = $.extend({}, this.model.toJSON(), {
programCertificate: this.programCertificate
? this.programCertificate.toJSON() : {},
programRecordUrl: this.programRecordUrl,
industryPathways: this.industryPathways,
creditPathways: this.creditPathways,
programTabViewEnabled: this.programTabViewEnabled
});
const data = $.extend(
{},
this.model.toJSON(),
this.subscriptionModel.toJSON(),
{
programCertificate: this.programCertificate
? this.programCertificate.toJSON() : {},
industryPathways: this.industryPathways,
creditPathways: this.creditPathways,
programTabViewEnabled: this.programTabViewEnabled,
isSubscriptionEligible: this.isSubscriptionEligible,
arrowUprightIcon,
...this.urls,
},
);
HtmlUtils.setHtml(this.$el, this.tpl(data));
this.postRender();

View File

@@ -10,6 +10,9 @@ import CourseCardView from './course_card_view';
import HeaderView from './program_header_view';
import SidebarView from './program_details_sidebar_view';
import SubscriptionModel from '../models/program_subscription_model';
import restartIcon from '../../../images/restart-icon.svg';
import pageTpl from '../../../templates/learner_dashboard/program_details_view.underscore';
import tabPageTpl from '../../../templates/learner_dashboard/program_details_tab_view.underscore';
import trackECommerceEvents from '../../commerce/track_ecommerce_events';
@@ -32,9 +35,18 @@ class ProgramDetailsView extends Backbone.View {
} else {
this.tpl = HtmlUtils.template(pageTpl);
}
this.options.isSubscriptionEligible = (
this.options.isUserB2CSubscriptionsEnabled
&& this.options.programData.subscription_eligible
);
this.programModel = new Backbone.Model(this.options.programData);
this.courseData = new Backbone.Model(this.options.courseData);
this.certificateCollection = new Backbone.Collection(this.options.certificateData);
this.certificateCollection = new Backbone.Collection(
this.options.certificateData
);
this.subscriptionModel = new SubscriptionModel({
context: this.options,
});
this.completedCourseCollection = new CourseCardCollection(
this.courseData.get('completed') || [],
this.options.userPreferences,
@@ -74,6 +86,7 @@ class ProgramDetailsView extends Backbone.View {
this.options.urls.buy_button_url,
this.options.programData
);
let data = {
totalCount,
inProgressCount,
@@ -85,9 +98,14 @@ class ProgramDetailsView extends Backbone.View {
creditPathways: this.options.creditPathways,
discussionFragment: this.options.discussionFragment,
live_fragment: this.options.live_fragment,
isSubscriptionEligible: this.options.isSubscriptionEligible,
restartIcon,
};
data = $.extend(data, this.programModel.toJSON());
data = $.extend(
data,
this.programModel.toJSON(),
this.subscriptionModel.toJSON(),
);
HtmlUtils.setHtml(this.$el, this.tpl(data));
this.postRender();
}
@@ -132,11 +150,13 @@ class ProgramDetailsView extends Backbone.View {
el: '.js-program-sidebar',
model: this.programModel,
courseModel: this.courseData,
subscriptionModel: this.subscriptionModel,
certificateCollection: this.certificateCollection,
programRecordUrl: this.options.urls.program_record_url,
industryPathways: this.options.industryPathways,
creditPathways: this.options.creditPathways,
programTabViewEnabled: this.options.programTabViewEnabled,
isSubscriptionEligible: this.options.isSubscriptionEligible,
urls: this.options.urls,
});
let hasIframe = false;
$('#live-tab').click(() => {

View File

@@ -40,10 +40,21 @@ class ProgramHeaderView extends Backbone.View {
return logo;
}
getIsSubscribed() {
const isSubscriptionEligible = this.model.get('isSubscriptionEligible');
const subscriptionData = this.model.get('subscriptionData')?.[0];
return (
isSubscriptionEligible &&
subscriptionData?.subscription_state === 'active'
);
}
render() {
const data = $.extend(this.model.toJSON(), {
breakpoints: this.breakpoints,
logo: this.getLogo(),
isSubscribed: this.getIsSubscribed(),
});
if (this.model.get('programData')) {

View File

@@ -3,12 +3,18 @@ import Backbone from 'backbone';
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
import upgradeMessageTpl from '../../../templates/learner_dashboard/upgrade_message.underscore';
import upgradeMessageSubscriptionTpl from '../../../templates/learner_dashboard/upgrade_message_subscription.underscore';
import trackECommerceEvents from '../../commerce/track_ecommerce_events';
class UpgradeMessageView extends Backbone.View {
initialize(options) {
this.messageTpl = HtmlUtils.template(upgradeMessageTpl);
if (options.isSubscriptionEligible) {
this.messageTpl = HtmlUtils.template(upgradeMessageSubscriptionTpl);
} else {
this.messageTpl = HtmlUtils.template(upgradeMessageTpl);
}
this.$el = options.$el;
this.subscriptionModel = options.subscriptionModel;
this.render();
const courseUpsellButtons = this.$el.find('.program_dashboard_course_upsell_button');
@@ -20,7 +26,11 @@ class UpgradeMessageView extends Backbone.View {
}
render() {
const data = this.model.toJSON();
const data = $.extend(
{},
this.model.toJSON(),
this.subscriptionModel.toJSON(),
);
HtmlUtils.setHtml(this.$el, this.messageTpl(data));
}
}

View File

@@ -434,6 +434,34 @@ $btn-color-primary: $primary-dark;
}
}
.upgrade-subscription {
margin: 10px 15px 10px 5px;
gap: 15px;
}
.subscription-icon-restart {
width: 24px;
height: 24px;
margin-inline-end: 8px;
}
.subscription-icon-arrow-upright {
display: inline-flex;
align-items: center;
width: 15px;
height: 15px;
margin-inline-start: 8px;
}
.subscription-info-brief {
font-size: 0.9375em;
color: $gray-500;
}
.subscription-info-upsell {
margin-top: 0.25rem;
font-size: 0.8125em;
}
.program-course-card {
width: 100%;
@@ -631,12 +659,25 @@ $btn-color-primary: $primary-dark;
.program-sidebar {
padding: 40px 40px 40px 0px;
.program-record {
.program-record,.subscription-info {
text-align: left;
padding-bottom: 2em;
}
.motivating-section {
.subscription-section {
display: flex;
flex-direction: column;
gap: 16px;
color: #414141;
.subscription-link {
color: inherit;
text-decoration: none;
border-bottom: 1px solid currentColor;
}
}
.sidebar-section {
font-size: 0.9375em;
width: auto;

View File

@@ -14,6 +14,7 @@ from openedx.core.djangolib.js_utils import (
<%static:webpack entry="ProgramDetailsFactory">
ProgramDetailsFactory({
programData: ${program_data | n, dump_js_escaped_json},
subscriptionData: ${program_subscription_data | n, dump_js_escaped_json},
courseData: ${course_data | n, dump_js_escaped_json},
certificateData: ${certificate_data | n, dump_js_escaped_json},
urls: ${urls | n, dump_js_escaped_json},
@@ -21,6 +22,7 @@ ProgramDetailsFactory({
industryPathways: ${industry_pathways | n, dump_js_escaped_json},
creditPathways: ${credit_pathways | n, dump_js_escaped_json},
programTabViewEnabled: ${program_tab_view_enabled | n, dump_js_escaped_json},
isUserB2CSubscriptionsEnabled: ${is_user_b2c_subscriptions_enabled | n, dump_js_escaped_json},
discussionFragment: ${discussion_fragment, | n, dump_js_escaped_json},
live_fragment: ${live_fragment, | n, dump_js_escaped_json}
});

View File

@@ -8,14 +8,61 @@
<% } %>
</aside>
<aside class="aside js-course-certificates"></aside>
<% if (isSubscriptionEligible) { %>
<aside class="aside js-subscription-info subscription-info">
<h2 class="divider-heading">
<%- gettext(
hasActiveTrial
? 'Trial subscription'
: subscriptionState === 'active'
? 'Active subscription'
: 'Inactive subscription'
) %>
</h2>
<div class="sidebar-section">
<div class="subscription-section">
<p class="my-0">
<%= HtmlUtils.interpolateHtml(
gettext(
subscriptionState === 'active'
? 'View your receipts or modify your subscription on the {a_start}Orders and subscriptions{a_end} page'
: subscriptionState === 'inactive'
? 'Restart your subscription for {subscriptionPrice}/month. Your payment history is still available on the {a_start}Orders and subscriptions{a_end} page'
: 'If you had a subscription previously, your payment history is still available on the {a_start}Orders and subscriptions{a_end} page'
),
{
subscriptionPrice,
a_start: HtmlUtils.HTML(`<a class="subscription-link" href="${manage_subscription_url}">`),
a_end: HtmlUtils.HTML('</a>'),
}
) %>
</p>
<p class="my-0">
<%= HtmlUtils.interpolateHtml(
gettext(
'Need help? Check out the {a_start}Learner Help Center{span_start}{icon}{span_end}{a_end} to troubleshoot issues or contact support '
),
{
a_start: HtmlUtils.HTML(`<a class="subscription-link" href="${subscriptions_learner_help_center_url}" target="_blank" rel="noopener noreferrer">`),
a_end: HtmlUtils.HTML('</a>'),
span_start: HtmlUtils.HTML('<span class="subscription-icon-arrow-upright">'),
icon: HtmlUtils.HTML(arrowUprightIcon),
span_end: HtmlUtils.HTML('</span>'),
}
) %>
</p>
</div>
</div>
</aside>
<% } %>
<aside class="aside js-program-record program-record">
<h2 class="divider-heading"><%- gettext('Program Record') %></h2>
<div class="motivating-section">
<div class="sidebar-section">
<p class="motivating-message"><%- gettext('Once you complete one of the program requirements you have a program record. This record is marked complete once you meet all program requirements. A program record can be used to continue your learning journey and demonstrate your learning to others.') %></p>
</div>
<% if (programRecordUrl) { %>
<% if (program_record_url) { %>
<div class="sidebar-button-wrapper">
<a href="<%- programRecordUrl %>" class="program-record-link">
<a href="<%- program_record_url %>" class="program-record-link">
<button class="btn sidebar-button"><%- gettext('View Program Record') %></button>
</a>
</div>

View File

@@ -47,7 +47,47 @@
</div>
</div>
<% } %>
<% if (is_learner_eligible_for_one_click_purchase && (typeof is_mobile_only === 'undefined' || is_mobile_only === false)) { %>
<% if (isSubscriptionEligible) { %>
<div class="d-flex flex-column align-items-start flex-sm-row align-items-sm-center upgrade-subscription">
<a href="<%- subscriptionUrl %>" class="btn-brand btn cta-primary upgrade-button">
<% if (subscriptionState === 'inactive') { %>
<div class="d-flex align-items-center">
<div class="subscription-icon-restart">
<% // xss-lint: disable=underscore-not-escaped %>
<%= restartIcon %>
</div>
<span><%- gettext('Restart my subscription') %></span>
</div>
<% } else { %>
<%- StringUtils.interpolate(gettext(
subscriptionState === 'active'
? 'Manage my subscription'
: 'Start {trialLength}-Day free trial'
),
{ trialLength } ) %>
<% } %>
</a>
<span class="subscription-info-brief">
<%- StringUtils.interpolate(
gettext(
hasActiveTrial
? 'Active trial ends {trialEndDate} at {trialEndTime}'
: subscriptionState === 'active'
? 'Your next billing date is {nextPaymentDate}'
: subscriptionState === 'inactive'
? 'Unlock verified access to all courses for {subscriptionPrice}/month. Cancel anytime.'
: '{subscriptionPrice}/month subscription after trial ends. Cancel anytime.'
),
{
subscriptionPrice,
nextPaymentDate,
trialEndDate,
trialEndTime,
}
) %>
</span>
</div>
<% } else if (is_learner_eligible_for_one_click_purchase && (typeof is_mobile_only === 'undefined' || is_mobile_only === false)) { %>
<a href="<%- completeProgramURL %>" class="btn-brand btn cta-primary upgrade-button complete-program" id="program_dashboard_course_upsell_all_button">
<%- gettext('Upgrade All Remaining Courses (')%>
<% if (discount_data.is_discounted) { %>

View File

@@ -22,7 +22,47 @@
</div>
</div>
<% } %>
<% if (is_learner_eligible_for_one_click_purchase && (typeof is_mobile_only === 'undefined' || is_mobile_only === false)) { %>
<% if (isSubscriptionEligible) { %>
<div class="d-flex flex-column align-items-start flex-sm-row align-items-sm-center upgrade-subscription">
<a href="<%- subscriptionUrl %>" class="btn-brand btn cta-primary upgrade-button">
<% if (subscriptionState === 'inactive') { %>
<div class="d-flex align-items-center">
<div class="subscription-icon-restart">
<% // xss-lint: disable=underscore-not-escaped %>
<%= restartIcon %>
</div>
<span><%- gettext('Restart my subscription') %></span>
</div>
<% } else { %>
<%- StringUtils.interpolate(gettext(
subscriptionState === 'active'
? 'Manage my subscription'
: 'Start {trialLength}-Day free trial'
),
{ trialLength } ) %>
<% } %>
</a>
<span class="subscription-info-brief">
<%- StringUtils.interpolate(
gettext(
hasActiveTrial
? 'Active trial ends {trialEndDate} at {trialEndTime}'
: subscriptionState === 'active'
? 'Your next billing date is {nextPaymentDate}'
: subscriptionState === 'inactive'
? 'Unlock verified access to all courses for {subscriptionPrice}/month. Cancel anytime.'
: '{subscriptionPrice}/month subscription after trial ends. Cancel anytime.'
),
{
subscriptionPrice,
nextPaymentDate,
trialEndDate,
trialEndTime,
}
) %>
</span>
</div>
<% } else if (is_learner_eligible_for_one_click_purchase && (typeof is_mobile_only === 'undefined' || is_mobile_only === false)) { %>
<a href="<%- completeProgramURL %>" class="btn-brand btn cta-primary upgrade-button complete-program" id="program_dashboard_course_upsell_all_button">
<%- gettext('Upgrade All Remaining Courses (')%>
<% if (discount_data.is_discounted) { %>

View File

@@ -1,5 +1,10 @@
<div class="program-details-header">
<div class="meta-info grid-container">
<% if (isSubscribed) { %>
<div class="mb-3">
<span class="badge badge-light"><%- gettext('Subscribed') %></span>
</div>
<% } %>
<% if (logo) { %>
<% // xss-lint: disable=underscore-not-escaped %>
<span aria-label="<%- gettext(programData.type) %>" class="<%- programData.type.toLowerCase() %> program-details-icon"><%= logo %></span>

View File

@@ -7,6 +7,6 @@
<div class="action col-12 md-col-4">
<a href="<%- upgrade_url %>" class="btn-brand btn cta-primary upgrade-button single-course-run program_dashboard_course_upsell_button">
<%- gettext('Upgrade to Verified') %>
<a>
</a>
</div>
<% } %>

View File

@@ -0,0 +1,21 @@
<div class="message certificate-status col-12 md-col-8">
<span class="card-msg"><%- gettext('Certificate Status:') %></span>
<span><%- gettext('Needs verified certificate ') %></span>
</div>
<% if ( subscriptionState !== 'active' ) { %>
<div class="action d-flex flex-column align-items-start align-items-md-end">
<a href="<%- subscriptionUrl %>" class="btn-brand btn cta-primary upgrade-button single-course-run program_dashboard_course_upsell_button">
<%- gettext('Upgrade with a subscription') %>
</a>
<span class="subscription-info-upsell">
<%- StringUtils.interpolate(
gettext(
subscriptionState === 'inactive'
? 'Pay {subscriptionPrice}/month for all courses in this program'
: 'Pay {subscriptionPrice}/month after {trialLength}-day free trial'
),
{ subscriptionPrice, trialLength },
) %>
</span>
</div>
<% } %>