feat: subscription changes and alerts for program dashboard and details (#32217)
* feat: subscription changes on program dashboard and details (#31909) - add a h2 programs heading on program dashboard - subscription ui changes on program dashboard - subscription alerts on both pages * feat: add subscription upsell, fix responsive layout of program dashboard (#31943) * test: update tests for subscription changes on program dashboard (#32021) * feat: subscription api changes on program dashboard (#32085) * feat: add subscription segment events to program details and dashboard (#32164) * feat: subscription changes on program dashboard and details pages (#32205)
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
<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 width="12" height="12" viewBox="0 -1 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.75 0.21875V1.71875H8.6925L0 10.4113L1.0575 11.4688L9.75 2.77625V7.71875H11.25V0.21875H3.75Z" fill="#454545"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 179 B After Width: | Height: | Size: 226 B |
3
lms/static/images/launch-icon.svg
Normal file
3
lms/static/images/launch-icon.svg
Normal 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 19H5V5H12V3H3V21H21V12H19V19ZM14 3V5H17.59L7.76 14.83L9.17 16.24L19 6.41V10H21V3H14Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
5
lms/static/images/warning-icon.svg
Normal file
5
lms/static/images/warning-icon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L1 21H23L12 2Z" fill="#F0CC00"/>
|
||||
<path d="M13 16H11V18H13V16Z" fill="#111111"/>
|
||||
<path d="M13 10H11V14H13V10Z" fill="#111111"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 244 B |
@@ -11,8 +11,8 @@ class ProgramSubscriptionModel extends Backbone.Model {
|
||||
const {
|
||||
subscriptionData: [data = {}],
|
||||
programData: { subscription_prices },
|
||||
urls,
|
||||
userPreferences,
|
||||
urls = {},
|
||||
userPreferences = {},
|
||||
} = context;
|
||||
|
||||
const priceInUSD = subscription_prices?.find(({ currency }) => currency === 'USD')?.price;
|
||||
@@ -35,6 +35,8 @@ class ProgramSubscriptionModel extends Backbone.Model {
|
||||
? trialMoment.isAfter(moment.utc())
|
||||
: false;
|
||||
|
||||
const remainingDays = trialMoment.diff(moment.utc(), 'days');
|
||||
|
||||
const [nextPaymentDate] = ProgramSubscriptionModel.formatDate(
|
||||
data.next_payment_date,
|
||||
userPreferences
|
||||
@@ -50,6 +52,7 @@ class ProgramSubscriptionModel extends Backbone.Model {
|
||||
{
|
||||
hasActiveTrial,
|
||||
nextPaymentDate,
|
||||
remainingDays,
|
||||
subscriptionPrice,
|
||||
subscriptionState,
|
||||
subscriptionUrl,
|
||||
@@ -66,8 +69,8 @@ class ProgramSubscriptionModel extends Backbone.Model {
|
||||
return ['', ''];
|
||||
}
|
||||
|
||||
const userTimezone = userPreferences?.time_zone || 'UTC';
|
||||
const userLanguage = userPreferences?.['pref-lang'] || 'en';
|
||||
const userTimezone = userPreferences.time_zone || 'UTC';
|
||||
const userLanguage = userPreferences['pref-lang'] || 'en';
|
||||
const context = {
|
||||
datetime: date,
|
||||
timezone: userTimezone,
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
import Backbone from 'backbone';
|
||||
|
||||
import CollectionListView from './views/collection_list_view';
|
||||
import ProgramCardView from './views/program_card_view';
|
||||
import ProgramCollection from './collections/program_collection';
|
||||
import ProgressCollection from './collections/program_progress_collection';
|
||||
import SidebarView from './views/sidebar_view';
|
||||
import HeaderView from './views/program_list_header_view';
|
||||
|
||||
function ProgramListFactory(options) {
|
||||
const progressCollection = new ProgressCollection();
|
||||
const subscriptionCollection = new Backbone.Collection();
|
||||
|
||||
if (options.userProgress) {
|
||||
progressCollection.set(options.userProgress);
|
||||
options.progressCollection = progressCollection; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
if (options.programsSubscriptionData.length) {
|
||||
subscriptionCollection.set(options.programsSubscriptionData);
|
||||
options.subscriptionCollection = subscriptionCollection; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
if (options.programsData.length) {
|
||||
new HeaderView({
|
||||
context: options,
|
||||
}).render();
|
||||
}
|
||||
|
||||
new CollectionListView({
|
||||
el: '.program-cards-container',
|
||||
childView: ProgramCardView,
|
||||
collection: new ProgramCollection(options.programsData),
|
||||
context: options,
|
||||
titleContext: {
|
||||
el: 'h2',
|
||||
title: 'Your Programs',
|
||||
},
|
||||
}).render();
|
||||
|
||||
if (options.programsData.length) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* globals setFixtures */
|
||||
|
||||
import Backbone from 'backbone';
|
||||
|
||||
import CollectionListView from '../views/collection_list_view';
|
||||
import ProgramCardView from '../views/program_card_view';
|
||||
import ProgramCollection from '../collections/program_collection';
|
||||
@@ -9,6 +11,7 @@ describe('Collection List View', () => {
|
||||
let view = null;
|
||||
let programCollection;
|
||||
let progressCollection;
|
||||
let subscriptionCollection;
|
||||
const context = {
|
||||
programsData: [
|
||||
{
|
||||
@@ -98,14 +101,21 @@ describe('Collection List View', () => {
|
||||
not_started: 3,
|
||||
},
|
||||
],
|
||||
programsSubscriptionData: [{
|
||||
resource_id: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||
subscription_state: 'active',
|
||||
}],
|
||||
isUserB2CSubscriptionsEnabled: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures('<div class="program-cards-container"></div>');
|
||||
programCollection = new ProgramCollection(context.programsData);
|
||||
progressCollection = new ProgressCollection();
|
||||
subscriptionCollection = new Backbone.Collection(context.programsSubscriptionData);
|
||||
progressCollection.set(context.userProgress);
|
||||
context.progressCollection = progressCollection;
|
||||
context.subscriptionCollection = subscriptionCollection;
|
||||
|
||||
view = new CollectionListView({
|
||||
el: '.program-cards-container',
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/* globals setFixtures */
|
||||
|
||||
import ProgramAlertListView from '../views/program_alert_list_view';
|
||||
|
||||
describe('Program Alert List View', () => {
|
||||
let view = null;
|
||||
const context = {
|
||||
enrollmentAlerts: [{ title: 'Test Program' }],
|
||||
trialEndingAlerts: [{
|
||||
title: 'Test Program',
|
||||
hasActiveTrial: true,
|
||||
nextPaymentDate: 'May 8, 2023',
|
||||
remainingDays: 2,
|
||||
subscriptionPrice: '$100',
|
||||
subscriptionState: 'active',
|
||||
subscriptionUrl: null,
|
||||
trialEndDate: 'Apr 20, 2023',
|
||||
trialEndTime: '5:59 am',
|
||||
trialLength: 7,
|
||||
}],
|
||||
pageType: 'programDetails',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures('<div class="js-program-details-alerts"></div>');
|
||||
view = new ProgramAlertListView({
|
||||
el: '.js-program-details-alerts',
|
||||
context,
|
||||
});
|
||||
view.render();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
view.remove();
|
||||
});
|
||||
|
||||
it('should exist', () => {
|
||||
expect(view).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render no enrollement alert', () => {
|
||||
expect(view.$('.alert:first .alert-heading').text().trim()).toEqual(
|
||||
'Enroll in a Test Program course'
|
||||
);
|
||||
expect(view.$('.alert:first .alert-message').text().trim()).toEqual(
|
||||
'You have an active subscription to the Test Program program but are not enrolled in any courses. Enroll in a remaining course and enjoy verified access.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render subscription trial is expiring alert', () => {
|
||||
expect(view.$('.alert:last .alert-heading').text().trim()).toEqual(
|
||||
'Subscription trial expires in 2 days'
|
||||
);
|
||||
expect(view.$('.alert:last .alert-message').text().trim()).toEqual(
|
||||
'Your Test Program trial will expire in 2 days at 5:59 am on Apr 20, 2023 and the card on file will be charged $100/month.'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -57,6 +57,10 @@ describe('Program card View', () => {
|
||||
not_started: 3,
|
||||
},
|
||||
];
|
||||
const subscriptionCollection = new Backbone.Collection([{
|
||||
resource_id: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||
subscription_state: 'active',
|
||||
}]);
|
||||
const progressCollection = new ProgressCollection();
|
||||
const cardRenders = ($card) => {
|
||||
expect($card).toBeDefined();
|
||||
@@ -74,6 +78,8 @@ describe('Program card View', () => {
|
||||
model: programModel,
|
||||
context: {
|
||||
progressCollection,
|
||||
subscriptionCollection,
|
||||
isUserB2CSubscriptionsEnabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -123,7 +129,10 @@ describe('Program card View', () => {
|
||||
view.remove();
|
||||
view = new ProgramCardView({
|
||||
model: programModel,
|
||||
context: {},
|
||||
context: {
|
||||
subscriptionCollection,
|
||||
isUserB2CSubscriptionsEnabled: true,
|
||||
},
|
||||
});
|
||||
cardRenders(view.$el);
|
||||
expect(view.$('.progress').length).toEqual(0);
|
||||
@@ -136,7 +145,10 @@ describe('Program card View', () => {
|
||||
programModel = new ProgramModel(programNoBanner);
|
||||
view = new ProgramCardView({
|
||||
model: programModel,
|
||||
context: {},
|
||||
context: {
|
||||
subscriptionCollection,
|
||||
isUserB2CSubscriptionsEnabled: true,
|
||||
},
|
||||
});
|
||||
cardRenders(view.$el);
|
||||
expect(view.$el.find('.banner-image').attr('srcset')).toEqual('');
|
||||
@@ -151,9 +163,16 @@ describe('Program card View', () => {
|
||||
programModel = new ProgramModel(programNoBanner);
|
||||
view = new ProgramCardView({
|
||||
model: programModel,
|
||||
context: {},
|
||||
context: {
|
||||
subscriptionCollection,
|
||||
isUserB2CSubscriptionsEnabled: true,
|
||||
},
|
||||
});
|
||||
cardRenders(view.$el);
|
||||
expect(view.$el.find('.banner-image').attr('srcset')).toEqual('');
|
||||
});
|
||||
|
||||
it('should render the subscription badge if subscription is active', () => {
|
||||
expect(view.$('.subscription-badge .badge').html().trim()).toEqual('Subscribed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('Program Progress View', () => {
|
||||
"url": "/certificates/bed3980e67ca40f0b31e309d9dfe9e7e", "type": "course", "title": "Introduction to the Treatment of Urban Sewage"
|
||||
}
|
||||
],
|
||||
urls: {"program_listing_url": "/dashboard/programs/", "commerce_api_url": "/api/commerce/v0/baskets/", "track_selection_url": "/course_modes/choose/", "program_record_url": "/foo/bar", "buy_subscription_url": "/subscriptions", "manage_subscription_url": "/orders", "subscriptions_learner_help_center_url": "/learner"},
|
||||
urls: {"program_listing_url": "/dashboard/programs/", "commerce_api_url": "/api/commerce/v0/baskets/", "track_selection_url": "/course_modes/choose/", "program_record_url": "/foo/bar", "buy_subscription_url": "/subscriptions", "orders_and_subscriptions_url": "/orders", "subscriptions_learner_help_center_url": "/learner"},
|
||||
userPreferences: {"pref-lang": "en"}
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
@@ -485,6 +485,7 @@ describe('Program Details View', () => {
|
||||
buy_subscription_url: '/subscriptions',
|
||||
manage_subscription_url: '/orders',
|
||||
subscriptions_learner_help_center_url: '/learner',
|
||||
orders_and_subscriptions_url: '/orders',
|
||||
},
|
||||
userPreferences: {
|
||||
'pref-lang': 'en',
|
||||
@@ -693,7 +694,7 @@ describe('Program Details View', () => {
|
||||
it('should render the get subscription link if program is subscription eligible', () => {
|
||||
testSubscriptionState(
|
||||
'pre',
|
||||
'Start 7-Day free trial',
|
||||
'Start 7-day free trial',
|
||||
'$100/month subscription after trial ends. Cancel anytime.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/* globals setFixtures */
|
||||
|
||||
import Backbone from 'backbone';
|
||||
|
||||
import ProgressCollection from '../collections/program_progress_collection';
|
||||
import ProgramListHeaderView from '../views/program_list_header_view';
|
||||
|
||||
describe('Program List Header View', () => {
|
||||
let view = null;
|
||||
const context = {
|
||||
programsData: [
|
||||
{
|
||||
uuid: '5b234e3c-3a2e-472e-90db-6f51501dc86c',
|
||||
title: 'edX Demonstration Program',
|
||||
subscription_eligible: null,
|
||||
subscription_prices: [],
|
||||
detail_url: '/dashboard/programs/5b234e3c-3a2e-472e-90db-6f51501dc86c/',
|
||||
},
|
||||
{
|
||||
uuid: 'b90d70d5-f981-4508-bdeb-5b792d930c03',
|
||||
title: 'Test Program',
|
||||
subscription_eligible: true,
|
||||
subscription_prices: [{ price: '500.00', currency: 'USD' }],
|
||||
detail_url: '/dashboard/programs/b90d70d5-f981-4508-bdeb-5b792d930c03/',
|
||||
},
|
||||
],
|
||||
programsSubscriptionData: [
|
||||
{
|
||||
id: 'eeb25640-9741-4c11-963c-8a27337f217c',
|
||||
resource_id: 'b90d70d5-f981-4508-bdeb-5b792d930c03',
|
||||
trial_end: '2022-04-20T05:59:42Z',
|
||||
next_payment_date: '2023-05-08T05:59:42Z',
|
||||
subscription_state: 'active',
|
||||
},
|
||||
],
|
||||
userProgress: [
|
||||
{
|
||||
uuid: '5b234e3c-3a2e-472e-90db-6f51501dc86c',
|
||||
completed: 0,
|
||||
in_progress: 1,
|
||||
not_started: 0,
|
||||
},
|
||||
{
|
||||
uuid: 'b90d70d5-f981-4508-bdeb-5b792d930c03',
|
||||
completed: 0,
|
||||
in_progress: 0,
|
||||
not_started: 3,
|
||||
},
|
||||
],
|
||||
isUserB2CSubscriptionsEnabled: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
context.subscriptionCollection = new Backbone.Collection(
|
||||
context.programsSubscriptionData
|
||||
);
|
||||
context.progressCollection = new ProgressCollection(
|
||||
context.userProgress
|
||||
);
|
||||
setFixtures('<div class="js-program-list-header"></div>');
|
||||
view = new ProgramListHeaderView({
|
||||
context,
|
||||
});
|
||||
view.render();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
view.remove();
|
||||
});
|
||||
|
||||
it('should exist', () => {
|
||||
expect(view).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render the program heading', () => {
|
||||
expect(view.$('h2:first').text().trim()).toEqual('My programs');
|
||||
});
|
||||
|
||||
it('should render a program alert', () => {
|
||||
expect(
|
||||
view.$('.js-program-list-alerts .alert .alert-heading').html().trim()
|
||||
).toEqual('Enroll in a Test Program course');
|
||||
expect(
|
||||
view.$('.js-program-list-alerts .alert .alert-message')
|
||||
).toContainHtml(
|
||||
'According to our records, you are not enrolled in any courses included in your Test Program program subscription. Enroll in a course from the <i>Program Details</i> page.'
|
||||
);
|
||||
expect(
|
||||
view.$('.js-program-list-alerts .alert .view-button').attr('href')
|
||||
).toEqual('/dashboard/programs/b90d70d5-f981-4508-bdeb-5b792d930c03/');
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ describe('Sidebar View', () => {
|
||||
let view = null;
|
||||
const context = {
|
||||
marketingUrl: 'https://www.example.org/programs',
|
||||
isUserB2CSubscriptionsEnabled: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -26,18 +27,52 @@ describe('Sidebar View', () => {
|
||||
expect(view).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not render the subscription upsell section if B2CSubscriptions are disabled', () => {
|
||||
view.remove();
|
||||
view = new SidebarView({
|
||||
el: '.sidebar',
|
||||
context: {
|
||||
...context,
|
||||
isUserB2CSubscriptionsEnabled: false,
|
||||
}
|
||||
});
|
||||
view.render();
|
||||
expect(view.$('.js-subscription-upsell')[0]).not.toBeInDOM();
|
||||
});
|
||||
|
||||
it('should render the subscription upsell section', () => {
|
||||
expect(view.$('.js-subscription-upsell')[0]).toBeInDOM();
|
||||
expect(view.$('.js-subscription-upsell .badge').html().trim())
|
||||
.toEqual('New');
|
||||
expect(view.$('.js-subscription-upsell h4').html().trim())
|
||||
.toEqual('Monthly program subscriptions now available');
|
||||
expect(view.$('.js-subscription-upsell .advertise-message'))
|
||||
.toContainText(
|
||||
'An easier way to access popular programs with more control over how much you spend.'
|
||||
);
|
||||
expect(view.$('.js-subscription-upsell a span:last').html().trim())
|
||||
.toEqual('Explore subscription options');
|
||||
expect(view.$('.js-subscription-upsell a').attr('href'))
|
||||
.not
|
||||
.toEqual(context.marketingUrl);
|
||||
});
|
||||
|
||||
it('should load the exploration panel given a marketing URL', () => {
|
||||
const $sidebar = view.$el;
|
||||
expect($sidebar.find('.program-advertise .advertise-message').html().trim())
|
||||
.toEqual('Browse recently launched courses and see what\'s new in your favorite subjects');
|
||||
expect($sidebar.find('.program-advertise .ad-link a').attr('href')).toEqual(context.marketingUrl);
|
||||
expect(view.$('.program-advertise .advertise-message').html().trim())
|
||||
.toEqual(
|
||||
'Browse recently launched courses and see what\'s new in your favorite subjects'
|
||||
);
|
||||
expect(view.$('.program-advertise a').attr('href'))
|
||||
.toEqual(context.marketingUrl);
|
||||
});
|
||||
|
||||
it('should not load the advertising panel if no marketing URL is provided', () => {
|
||||
view.remove();
|
||||
view = new SidebarView({
|
||||
el: '.sidebar',
|
||||
context: {},
|
||||
context: {
|
||||
isUserB2CSubscriptionsEnabled: true,
|
||||
},
|
||||
});
|
||||
view.render();
|
||||
const $ad = view.$el.find('.program-advertise');
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import Backbone from 'backbone';
|
||||
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
|
||||
|
||||
import warningIcon from '../../../images/warning-icon.svg';
|
||||
import programAlertTpl from '../../../templates/learner_dashboard/program_alert_list_view.underscore';
|
||||
|
||||
class ProgramAlertListView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
el: '.js-program-details-alerts',
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
initialize({ context }) {
|
||||
this.tpl = HtmlUtils.template(programAlertTpl);
|
||||
this.enrollmentAlerts = context.enrollmentAlerts || [];
|
||||
this.trialEndingAlerts = context.trialEndingAlerts || [];
|
||||
this.pageType = context.pageType;
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = {
|
||||
alertList: this.getAlertList(),
|
||||
warningIcon,
|
||||
};
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
}
|
||||
|
||||
getAlertList() {
|
||||
const alertList = this.enrollmentAlerts.map(
|
||||
({ title: programName, url }) => ({
|
||||
url,
|
||||
urlText: gettext('View program'),
|
||||
title: StringUtils.interpolate(
|
||||
gettext('Enroll in a {programName} course'),
|
||||
{ programName }
|
||||
),
|
||||
message: this.pageType === 'programDetails'
|
||||
? StringUtils.interpolate(
|
||||
gettext('You have an active subscription to the {programName} program but are not enrolled in any courses. Enroll in a remaining course and enjoy verified access.'),
|
||||
{ programName }
|
||||
)
|
||||
: HtmlUtils.interpolateHtml(
|
||||
gettext('According to our records, you are not enrolled in any courses included in your {programName} program subscription. Enroll in a course from the {i_start}Program Details{i_end} page.'),
|
||||
{
|
||||
programName,
|
||||
i_start: HtmlUtils.HTML('<i>'),
|
||||
i_end: HtmlUtils.HTML('</i>'),
|
||||
}
|
||||
),
|
||||
})
|
||||
);
|
||||
return alertList.concat(this.trialEndingAlerts.map(
|
||||
({ title: programName, remainingDays, ...data }) => {
|
||||
const title = 'Subscription trial expires in {remainingDays} day';
|
||||
const message = 'Your {programName} trial will expire in {remainingDays} day at {trialEndTime} on {trialEndDate} and the card on file will be charged {subscriptionPrice}/month.';
|
||||
|
||||
return {
|
||||
title: StringUtils.interpolate(
|
||||
ngettext(
|
||||
title,
|
||||
title.replace(/\bday\b/, 'days'),
|
||||
remainingDays
|
||||
),
|
||||
{ remainingDays }
|
||||
),
|
||||
message: StringUtils.interpolate(
|
||||
ngettext(
|
||||
message,
|
||||
message.replace(/\bday\b/, 'days'),
|
||||
remainingDays
|
||||
),
|
||||
{
|
||||
programName,
|
||||
remainingDays,
|
||||
...data,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgramAlertListView;
|
||||
@@ -21,14 +21,21 @@ class ProgramCardView extends Backbone.View {
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
initialize(data) {
|
||||
initialize({ context }) {
|
||||
this.tpl = HtmlUtils.template(programCardTpl);
|
||||
this.progressCollection = data.context.progressCollection;
|
||||
this.progressCollection = context.progressCollection;
|
||||
if (this.progressCollection) {
|
||||
this.progressModel = this.progressCollection.findWhere({
|
||||
uuid: this.model.get('uuid'),
|
||||
});
|
||||
}
|
||||
this.isSubscribed = (
|
||||
context.isUserB2CSubscriptionsEnabled &&
|
||||
context.subscriptionCollection?.some({
|
||||
resource_id: this.model.get('uuid'),
|
||||
subscription_state: 'active',
|
||||
})
|
||||
) ?? false;
|
||||
this.render();
|
||||
}
|
||||
|
||||
@@ -37,7 +44,10 @@ class ProgramCardView extends Backbone.View {
|
||||
const data = $.extend(
|
||||
this.model.toJSON(),
|
||||
this.getProgramProgress(),
|
||||
{ orgList: orgList.join(' ') },
|
||||
{
|
||||
orgList: orgList.join(' '),
|
||||
isSubscribed: this.isSubscribed,
|
||||
},
|
||||
);
|
||||
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
|
||||
@@ -9,9 +9,11 @@ import CourseCardCollection from '../collections/course_card_collection';
|
||||
import CourseCardView from './course_card_view';
|
||||
import HeaderView from './program_header_view';
|
||||
import SidebarView from './program_details_sidebar_view';
|
||||
import AlertListView from './program_alert_list_view';
|
||||
|
||||
import SubscriptionModel from '../models/program_subscription_model';
|
||||
|
||||
import launchIcon from '../../../images/launch-icon.svg';
|
||||
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';
|
||||
@@ -23,6 +25,7 @@ class ProgramDetailsView extends Backbone.View {
|
||||
el: '.js-program-details-wrapper',
|
||||
events: {
|
||||
'click .complete-program': 'trackPurchase',
|
||||
'click .js-subscription-cta': 'trackSubscriptionCTA',
|
||||
},
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
@@ -59,6 +62,10 @@ class ProgramDetailsView extends Backbone.View {
|
||||
this.courseData.get('not_started') || [],
|
||||
this.options.userPreferences,
|
||||
);
|
||||
this.subscriptionEventParams = {
|
||||
label: this.options.programData.title,
|
||||
program_uuid: this.options.programData.uuid,
|
||||
};
|
||||
|
||||
this.render();
|
||||
|
||||
@@ -68,6 +75,7 @@ class ProgramDetailsView extends Backbone.View {
|
||||
pageName: 'program_dashboard',
|
||||
linkCategory: 'green_upgrade',
|
||||
});
|
||||
this.trackSubscriptionEligibleProgramView();
|
||||
}
|
||||
|
||||
static getUrl(base, programData) {
|
||||
@@ -99,6 +107,7 @@ class ProgramDetailsView extends Backbone.View {
|
||||
discussionFragment: this.options.discussionFragment,
|
||||
live_fragment: this.options.live_fragment,
|
||||
isSubscriptionEligible: this.options.isSubscriptionEligible,
|
||||
launchIcon,
|
||||
restartIcon,
|
||||
};
|
||||
data = $.extend(
|
||||
@@ -115,6 +124,20 @@ class ProgramDetailsView extends Backbone.View {
|
||||
model: new Backbone.Model(this.options),
|
||||
});
|
||||
|
||||
if (this.options.isSubscriptionEligible) {
|
||||
const { enrollmentAlerts, trialEndingAlerts } = this.getAlerts();
|
||||
|
||||
if (enrollmentAlerts.length || trialEndingAlerts.length) {
|
||||
this.alertListView = new AlertListView({
|
||||
context: {
|
||||
enrollmentAlerts,
|
||||
trialEndingAlerts,
|
||||
pageType: 'programDetails',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.remainingCourseCollection.length > 0) {
|
||||
new CollectionListView({
|
||||
el: '.js-course-list-remaining',
|
||||
@@ -167,6 +190,33 @@ class ProgramDetailsView extends Backbone.View {
|
||||
}).bind(this);
|
||||
}
|
||||
|
||||
getAlerts() {
|
||||
const alerts = {
|
||||
enrollmentAlerts: [],
|
||||
trialEndingAlerts: [],
|
||||
};
|
||||
if (this.subscriptionModel.get('subscriptionState') === 'active') {
|
||||
if (
|
||||
this.courseData.get('in_progress').length === 0 &&
|
||||
this.courseData.get('not_started').length >= 1
|
||||
) {
|
||||
alerts.enrollmentAlerts.push({
|
||||
title: this.programModel.get('title'),
|
||||
});
|
||||
}
|
||||
if (
|
||||
this.subscriptionModel.get('remainingDays') <= 7 &&
|
||||
this.subscriptionModel.get('hasActiveTrial')
|
||||
) {
|
||||
alerts.trialEndingAlerts.push({
|
||||
title: this.programModel.get('title'),
|
||||
...this.subscriptionModel.toJSON(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return alerts;
|
||||
}
|
||||
|
||||
trackPurchase() {
|
||||
const data = this.options.programData;
|
||||
window.analytics.track('edx.bi.user.dashboard.program.purchase', {
|
||||
@@ -175,6 +225,37 @@ class ProgramDetailsView extends Backbone.View {
|
||||
uuid: data.uuid,
|
||||
});
|
||||
}
|
||||
|
||||
trackSubscriptionCTA() {
|
||||
const state = this.subscriptionModel.get('subscriptionState');
|
||||
|
||||
if (state === 'active') {
|
||||
window.analytics.track(
|
||||
'edx.bi.user.subscription.program-detail-page.manage.clicked',
|
||||
this.subscriptionEventParams
|
||||
);
|
||||
} else {
|
||||
const isNewSubscription = state !== 'inactive';
|
||||
window.analytics.track(
|
||||
'edx.bi.user.subscription.program-detail-page.subscribe.clicked',
|
||||
{
|
||||
category: `${this.options.programData.variant} bundle`,
|
||||
is_new_subscription: isNewSubscription,
|
||||
is_trial_eligible: isNewSubscription,
|
||||
...this.subscriptionEventParams,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
trackSubscriptionEligibleProgramView() {
|
||||
if (this.options.isSubscriptionEligible) {
|
||||
window.analytics.track(
|
||||
'edx.bi.user.subscription.program-detail-page.viewed',
|
||||
this.subscriptionEventParams
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgramDetailsView;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import Backbone from 'backbone';
|
||||
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
import AlertListView from './program_alert_list_view';
|
||||
|
||||
import SubscriptionModel from '../models/program_subscription_model';
|
||||
|
||||
import programListHeaderTpl from '../../../templates/learner_dashboard/program_list_header_view.underscore';
|
||||
|
||||
class ProgramListHeaderView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
el: '.js-program-list-header',
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
initialize({ context }) {
|
||||
this.context = context;
|
||||
this.tpl = HtmlUtils.template(programListHeaderTpl);
|
||||
this.programAndSubscriptionData = context.programsData
|
||||
.map((programData) => ({
|
||||
programData,
|
||||
subscriptionData: context.subscriptionCollection
|
||||
?.findWhere({
|
||||
resource_id: programData.uuid,
|
||||
subscription_state: 'active',
|
||||
})
|
||||
?.toJSON(),
|
||||
}))
|
||||
.filter(({ subscriptionData }) => !!subscriptionData);
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(this.context));
|
||||
this.postRender();
|
||||
}
|
||||
|
||||
postRender() {
|
||||
if (this.context.isUserB2CSubscriptionsEnabled) {
|
||||
const enrollmentAlerts = this.getEnrollmentAlerts();
|
||||
const trialEndingAlerts = this.getTrialEndingAlerts();
|
||||
|
||||
if (enrollmentAlerts.length || trialEndingAlerts.length) {
|
||||
this.alertListView = new AlertListView({
|
||||
el: '.js-program-list-alerts',
|
||||
context: {
|
||||
enrollmentAlerts,
|
||||
trialEndingAlerts,
|
||||
pageType: 'programList',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getEnrollmentAlerts() {
|
||||
return this.programAndSubscriptionData
|
||||
.map(({ programData, subscriptionData }) => {
|
||||
const progress = this.context.progressCollection?.findWhere({
|
||||
uuid: programData.uuid,
|
||||
in_progress: 0,
|
||||
});
|
||||
return (
|
||||
progress?.get('not_started') >= 1 && {
|
||||
title: programData.title,
|
||||
url: programData.detail_url,
|
||||
}
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
getTrialEndingAlerts() {
|
||||
return this.programAndSubscriptionData
|
||||
.map(({ programData, subscriptionData }) => {
|
||||
const subscriptionModel = new SubscriptionModel({
|
||||
context: {
|
||||
programData,
|
||||
subscriptionData: [subscriptionData],
|
||||
userPreferences: this.context?.userPreferences,
|
||||
},
|
||||
});
|
||||
return (
|
||||
subscriptionModel.get('remainingDays') <= 7 &&
|
||||
subscriptionModel.get('hasActiveTrial') && {
|
||||
title: programData.title,
|
||||
...subscriptionModel.toJSON(),
|
||||
}
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgramListHeaderView;
|
||||
@@ -3,6 +3,7 @@ import Backbone from 'backbone';
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
import NewProgramsView from './explore_new_programs_view';
|
||||
import SubscriptionUpsellView from './subscription_upsell_view';
|
||||
|
||||
import sidebarTpl from '../../../templates/learner_dashboard/sidebar.underscore';
|
||||
|
||||
@@ -10,6 +11,9 @@ class SidebarView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
el: '.sidebar',
|
||||
events: {
|
||||
'click .js-subscription-upsell-cta ': 'trackSubscriptionUpsellCTA',
|
||||
},
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
@@ -25,10 +29,22 @@ class SidebarView extends Backbone.View {
|
||||
}
|
||||
|
||||
postRender() {
|
||||
if (this.context.isUserB2CSubscriptionsEnabled) {
|
||||
this.subscriptionUpsellView = new SubscriptionUpsellView({
|
||||
context: this.context,
|
||||
});
|
||||
}
|
||||
|
||||
this.newProgramsView = new NewProgramsView({
|
||||
context: this.context,
|
||||
});
|
||||
}
|
||||
|
||||
trackSubscriptionUpsellCTA() {
|
||||
window.analytics.track(
|
||||
'edx.bi.user.subscription.program-dashboard.upsell.clicked'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SidebarView;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import Backbone from 'backbone';
|
||||
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
import subscriptionUpsellTpl from '../../../templates/learner_dashboard/subscription_upsell_view.underscore';
|
||||
|
||||
class SubscriptionUpsellView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
el: '.js-subscription-upsell',
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
initialize(options) {
|
||||
this.tpl = HtmlUtils.template(subscriptionUpsellTpl);
|
||||
this.data = options.context;
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = $.extend(this.context, {
|
||||
minSubscriptionPrice: '$39',
|
||||
trialLength: 7,
|
||||
});
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
}
|
||||
}
|
||||
|
||||
export default SubscriptionUpsellView;
|
||||
@@ -90,6 +90,21 @@ $btn-color-primary: $primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.program-details-alerts {
|
||||
.page-banner {
|
||||
margin: 0;
|
||||
padding: 0 0 48px;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.program-details-tab-alerts {
|
||||
.page-banner {
|
||||
margin: 0;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// CSS for April 2017 version of Program Details Page
|
||||
.program-details {
|
||||
.window-wrap {
|
||||
@@ -435,13 +450,20 @@ $btn-color-primary: $primary-dark;
|
||||
}
|
||||
|
||||
.upgrade-subscription {
|
||||
margin: 10px 15px 10px 5px;
|
||||
gap: 15px;
|
||||
margin: 16px 0 10px;
|
||||
row-gap: 16px;
|
||||
column-gap: 24px;
|
||||
}
|
||||
|
||||
.subscription-icon-launch {
|
||||
width: 22.5px;
|
||||
height: 22.5px;
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
.subscription-icon-restart {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 22.5px;
|
||||
height: 22.5px;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +1,86 @@
|
||||
.program-list-alerts {
|
||||
.page-banner {
|
||||
padding-top: 32px;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.program-list-wrapper {
|
||||
@include make-row();
|
||||
|
||||
max-width: 73.125rem;
|
||||
padding: ($baseline*2) ($baseline/2);
|
||||
max-width: 82rem;
|
||||
margin: 0 auto;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding: $baseline;
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: ($baseline*2) 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: ($baseline*2) $baseline;
|
||||
}
|
||||
|
||||
.program-list-container {
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-right: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.view-button {
|
||||
background: theme-color("success");
|
||||
border-color: theme-color("success");
|
||||
border-radius: 0;
|
||||
color: $white;
|
||||
padding: 7px;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.program-cards-container {
|
||||
@include grid-container();
|
||||
@include make-col(12);
|
||||
padding-top: 32px;
|
||||
|
||||
padding: 0 8px;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
@include make-col(9);
|
||||
.subscription-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@include make-col(12);
|
||||
@include float(right);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 24px;
|
||||
|
||||
margin-bottom: $baseline;
|
||||
padding: 0 8px;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
@include make-col(3);
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: 76px;
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $baseline;
|
||||
margin-bottom: $baseline;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid $gray-500;
|
||||
gap: 16px;
|
||||
background: #fbfaf9;
|
||||
border: 1px solid #d7d3d1;
|
||||
}
|
||||
|
||||
.program-advertise {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.new-programs-btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
white-space: normal;
|
||||
border-color: theme-color("primary");
|
||||
color: theme-color("primary");
|
||||
font-weight: 600;
|
||||
.advertise-message {
|
||||
font-size: 1rem;
|
||||
color: $gray-dark;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: theme-color("primary");
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.advertise-message {
|
||||
font-size: 0.75rem;
|
||||
color: $gray-dark;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
.view-button {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<div class="advertise-message">
|
||||
<p class="advertise-message">
|
||||
<%- gettext('Browse recently launched courses and see what\'s new in your favorite subjects') %>
|
||||
</div>
|
||||
<div class="ad-link">
|
||||
<a href="<%- marketingUrl %>" class="btn new-programs-btn">
|
||||
<span class="icon fa fa-search" aria-hidden="true"></span>
|
||||
<span><%- gettext('Explore New Programs') %></span>
|
||||
</a>
|
||||
</div>
|
||||
</p>
|
||||
<a href="<%- marketingUrl %>" class="btn-brand btn cta-primary view-button align-self-stretch">
|
||||
<span class="icon fa fa-search" aria-hidden="true"></span>
|
||||
<span><%- gettext('Explore new programs') %></span>
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="page-banner d-flex flex-column">
|
||||
<% _.each(alertList, function({ title, message, url, urlText }){ %>
|
||||
<div class="alert alert-warning alert-button-container flex-column flex-sm-row align-items-sm-center justify-content-sm-between m-0" role="alert">
|
||||
<div class="alert-container d-flex" >
|
||||
<div class="alert-warning-icon alert-message-height">
|
||||
<% // xss-lint: disable=underscore-not-escaped %>
|
||||
<%= warningIcon %>
|
||||
</div>
|
||||
<div class="alert-container d-flex flex-column align-items-start">
|
||||
<h4 class="alert-heading m-0"><%- title %></h4>
|
||||
<% // xss-lint: disable=underscore-not-escaped %>
|
||||
<p class="alert-message alert-message-height m-0"><%= message %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% if (url && urlText) { %>
|
||||
<a href="<%- url %>" class="btn-brand btn cta-primary view-button">
|
||||
<%- urlText %>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
@@ -61,3 +61,8 @@
|
||||
</picture>
|
||||
</div>
|
||||
</a>
|
||||
<% if (isSubscribed) { %>
|
||||
<div class="subscription-badge">
|
||||
<span class="badge badge-light"><%- gettext('Subscribed') %></span>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
),
|
||||
{
|
||||
subscriptionPrice,
|
||||
a_start: HtmlUtils.HTML(`<a class="subscription-link" href="${manage_subscription_url}">`),
|
||||
a_start: HtmlUtils.HTML(`<a class="subscription-link" href="${orders_and_subscriptions_url}">`),
|
||||
a_end: HtmlUtils.HTML('</a>'),
|
||||
}
|
||||
) %>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<header class="js-program-header program-header full-width-banner"></header>
|
||||
<div class="js-program-details-alerts program-details-tab-alerts program-subscription-alert-wrapper col-12 col-md-8"></div>
|
||||
<!-- TODO: consider if article is the most appropriate element here -->
|
||||
|
||||
<% if (programTabViewEnabled) { %>
|
||||
@@ -48,9 +49,24 @@
|
||||
</div>
|
||||
<% } %>
|
||||
<% 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 flex-column align-items-start flex-xl-row align-items-xl-center upgrade-subscription">
|
||||
<a
|
||||
href="<%- subscriptionUrl %>"
|
||||
class="js-subscription-cta btn-brand btn cta-primary upgrade-button"
|
||||
<% if (subscriptionState === 'active') { %>
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<% } %>
|
||||
>
|
||||
<% if (subscriptionState === 'active') { %>
|
||||
<div class="d-flex align-items-center">
|
||||
<span><%- gettext('Manage my subscription') %></span>
|
||||
<div class="subscription-icon-launch">
|
||||
<% // xss-lint: disable=underscore-not-escaped %>
|
||||
<%= launchIcon %>
|
||||
</div>
|
||||
</div>
|
||||
<% } else if (subscriptionState === 'inactive') { %>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subscription-icon-restart">
|
||||
<% // xss-lint: disable=underscore-not-escaped %>
|
||||
@@ -59,12 +75,10 @@
|
||||
<span><%- gettext('Restart my subscription') %></span>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<%- StringUtils.interpolate(gettext(
|
||||
subscriptionState === 'active'
|
||||
? 'Manage my subscription'
|
||||
: 'Start {trialLength}-Day free trial'
|
||||
),
|
||||
{ trialLength } ) %>
|
||||
<%- StringUtils.interpolate(
|
||||
gettext('Start {trialLength}-day free trial'),
|
||||
{ trialLength }
|
||||
) %>
|
||||
<% } %>
|
||||
</a>
|
||||
<span class="subscription-info-brief">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<header class="js-program-header program-header full-width-banner"></header>
|
||||
<div class="js-program-details-alerts program-details-alerts program-subscription-alert-wrapper col-12 col-md-8"></div>
|
||||
<!-- TODO: consider if article is the most appropriate element here -->
|
||||
|
||||
<div class="col-12 flex-column flex-md-row d-md-flex">
|
||||
@@ -23,9 +24,24 @@
|
||||
</div>
|
||||
<% } %>
|
||||
<% 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 flex-column align-items-start flex-xl-row align-items-xl-center upgrade-subscription">
|
||||
<a
|
||||
href="<%- subscriptionUrl %>"
|
||||
class="js-subscription-cta btn-brand btn cta-primary upgrade-button"
|
||||
<% if (subscriptionState === 'active') { %>
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<% } %>
|
||||
>
|
||||
<% if (subscriptionState === 'active') { %>
|
||||
<div class="d-flex align-items-center">
|
||||
<span><%- gettext('Manage my subscription') %></span>
|
||||
<div class="subscription-icon-launch">
|
||||
<% // xss-lint: disable=underscore-not-escaped %>
|
||||
<%= launchIcon %>
|
||||
</div>
|
||||
</div>
|
||||
<% } else if (subscriptionState === 'inactive') { %>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subscription-icon-restart">
|
||||
<% // xss-lint: disable=underscore-not-escaped %>
|
||||
@@ -34,12 +50,10 @@
|
||||
<span><%- gettext('Restart my subscription') %></span>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<%- StringUtils.interpolate(gettext(
|
||||
subscriptionState === 'active'
|
||||
? 'Manage my subscription'
|
||||
: 'Start {trialLength}-Day free trial'
|
||||
),
|
||||
{ trialLength } ) %>
|
||||
<%- StringUtils.interpolate(
|
||||
gettext('Start {trialLength}-day free trial'),
|
||||
{ trialLength }
|
||||
) %>
|
||||
<% } %>
|
||||
</a>
|
||||
<span class="subscription-info-brief">
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<h2><%- gettext('My programs') %></h2>
|
||||
<div class="js-program-list-alerts program-list-alerts program-subscription-alert-wrapper mr-md-3"></div>
|
||||
@@ -10,8 +10,11 @@ from openedx.core.djangolib.js_utils import (
|
||||
%>
|
||||
|
||||
<div class="program-list-wrapper grid-container">
|
||||
<div class="program-cards-container col"></div>
|
||||
<div class="sidebar col col-last"></div>
|
||||
<div class="program-list-container col-12 col-md-9">
|
||||
<div class="js-program-list-header"></div>
|
||||
<div class="program-cards-container col"></div>
|
||||
</div>
|
||||
<div class="sidebar col-12 col-md-3"></div>
|
||||
</div>
|
||||
|
||||
<%block name="js_extra">
|
||||
@@ -19,7 +22,10 @@ from openedx.core.djangolib.js_utils import (
|
||||
ProgramListFactory({
|
||||
marketingUrl: '${marketing_url | n, js_escaped_string}',
|
||||
programsData: ${programs | n, dump_js_escaped_json},
|
||||
userProgress: ${progress | n, dump_js_escaped_json}
|
||||
programsSubscriptionData: ${programs_subscription_data | n, dump_js_escaped_json},
|
||||
userProgress: ${progress | n, dump_js_escaped_json},
|
||||
userPreferences: ${user_preferences | n, dump_js_escaped_json},
|
||||
isUserB2CSubscriptionsEnabled: ${is_user_b2c_subscriptions_enabled | n, dump_js_escaped_json}
|
||||
});
|
||||
</%static:webpack>
|
||||
</%block>
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
<% if (isUserB2CSubscriptionsEnabled) { %>
|
||||
<aside class="aside js-subscription-upsell"></aside>
|
||||
<% } %>
|
||||
<div class="aside program-advertise"></div>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<span class="badge badge-warning align-self-start"><%- gettext('New') %></span>
|
||||
<h4 class="m-0"><%- gettext('Monthly program subscriptions now available') %></h4>
|
||||
<p class="advertise-message">
|
||||
<%- StringUtils.interpolate(
|
||||
gettext(
|
||||
'An easier way to access popular programs with more control over how much you spend. Starting at {minSubscriptionPrice} per month after a {trialLength}-day free trial. Cancel anytime.'
|
||||
),
|
||||
{ minSubscriptionPrice, trialLength }
|
||||
) %>
|
||||
</p>
|
||||
<a href="" class="js-subscription-upsell-cta btn-brand btn cta-primary view-button align-self-stretch">
|
||||
<span class="icon fa fa-search" aria-hidden="true"></span>
|
||||
<span><%- gettext('Explore subscription options') %></span>
|
||||
</a>
|
||||
Reference in New Issue
Block a user