Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions components/o-comments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
- [Additional dependencies](#additional-dependencies)
- [Markup](#markup)
- [Stream](#stream)
- [Count](#count)

Check warning on line 9 in components/o-comments/README.md

View workflow job for this annotation

GitHub Actions / test (components/o-comments)

All table of contents items should match an item in the content. Item "Count" link "#count" not found

Check warning on line 9 in components/o-comments/README.md

View workflow job for this annotation

GitHub Actions / test (components/o-comments)

The table of contents items should be in the same order as the headings in the readme. Item "Count" does not match heading "Redirection for illegal comment reporting"
- [Use staging environment](#use-staging-environment)
- [Pass a display name into Coral Talk](#pass-a-display-name-into-coral-talk)
- [JavaScript](#javascript)
- [Constructing an o-comments](#constructing-an-o-comments)
- [Firing an oDomContentLoaded event](#firing-an-odomcontentloaded-event)

Check warning on line 14 in components/o-comments/README.md

View workflow job for this annotation

GitHub Actions / test (components/o-comments)

The table of contents items should be in the same order as the headings in the readme. Item "Firing an oDomContentLoaded event" does not match heading "Options"
- [Events](#events)

Check warning on line 15 in components/o-comments/README.md

View workflow job for this annotation

GitHub Actions / test (components/o-comments)

The table of contents items should be in the same order as the headings in the readme. Item "Events" does not match heading "Firing an oDomContentLoaded event"
- [Tracking](#tracking)

Check warning on line 16 in components/o-comments/README.md

View workflow job for this annotation

GitHub Actions / test (components/o-comments)

The table of contents top-level contain a link to every h2 in the document, and sub-levels should contain a link to every h3 in their section. Missing item for "Tracking"

Check warning on line 16 in components/o-comments/README.md

View workflow job for this annotation

GitHub Actions / test (components/o-comments)

The table of contents items should be in the same order as the headings in the readme. Item "Tracking" does not match heading "Events"
- [Sass](#sass)
- [Migration](#migration)
- [Contact](#contact)
Expand Down Expand Up @@ -169,6 +169,9 @@
- scrollContainer
- assetType
- disableOTracking
- linkSubscribe
- linkLogin
- linkContact

### Firing an oDomContentLoaded event

Expand Down
198 changes: 124 additions & 74 deletions components/o-comments/src/js/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Stream {
this.isSubscribed = false;
this.isTrialist = false;
this.onlySubscribers = opts.onlySubscribers;
this.errorCode = null;
}

init () {
Expand Down Expand Up @@ -112,6 +113,7 @@ class Stream {
this.isSubscribed = response?.isSubscribed;
this.isTrialist = response?.isTrialist;
this.isRegistered = response?.isRegistered;
this.errorCode = response?.errorCode;
})
.catch(() => {
return false;
Expand Down Expand Up @@ -201,11 +203,11 @@ class Stream {
document.addEventListener('oOverlay.ready', onOverlayReady);

const onOverlayClosed = () => {
overlay.context.removeEventListener('oLayers.close', onOverlayClosed);
overlay.context.removeEventListener('oOverlay.layerClose', onOverlayClosed);
document.removeEventListener('oOverlay.ready', onOverlayReady);
overlay.destroy();
};
overlay.context.addEventListener('oLayers.close', onOverlayClosed);
overlay.context.addEventListener('oOverlay.layerClose', onOverlayClosed);
}

/**
Expand Down Expand Up @@ -297,85 +299,133 @@ class Stream {
};
}

renderNotSignedInMessage () {
if(this.isSubscribed){
return;
}
renderNotSignedInMessage() {
if (this.isSubscribed) return;

const shadowRoot = this.streamEl.querySelector("#coral-shadow-container").shadowRoot;
const coralContainer = shadowRoot.querySelector("#coral");
coralContainer.setAttribute('data-user-not-signed-in' , true);

const customMessageContainer = document.createElement("section");
customMessageContainer.classList.add('coral__custom-message-content','coral');
const messageRegistered = `
<h3>Commenting is only available to readers with an FT subscription</h3>
<p>
${this.options.linkSubscribe ? `<a class="linkSubscribe" href='${this.options.linkSubscribe}'>Subscribe</a>` : `Subscribe`} to join the conversation.
</p>
`;
const messageForAnonymous = `
<h3>Commenting is only available to readers with an FT subscription</h3>
<p>
Please ${this.options.linkLogin ? ` <a class="linkLogin" href='${this.options.linkLogin}'>login</a>` : `login`} or ${this.options.linkSubscribe ? `<a href='${this.options.linkSubscribe}' class="linkSubscribe" >subscribe</a>` : `subscribe`} to join the conversation.
</p>
`;
const messageForTrial = `
<h3>You are still on a trial period</h3>
<p>
View our full ${this.options.linkSubscribe ? `<a class="linkSubscribe" href='${this.options.linkSubscribe}'>subscription packages</a>` : `subscription packages` } to join the conversation.
</p>
`;
customMessageContainer.innerHTML = this.isTrialist ? messageForTrial : this.isRegistered ? messageRegistered : messageForAnonymous;
// this content is attached after oTracking.init is called therefore set data-trackable to the links doesn't work, therefore we raise an event when click on it
customMessageContainer.querySelector('.linkSubscribe')?.addEventListener('click', (event) => {
event.preventDefault();
const trackData = {
category: 'comment',
action: 'linkMessage',
coral: false,
content : {
asset_type: this.options.assetType,
uuid: this.options.articleId,
linkType: 'subscribe',
user_type: this.isTrialist ? 'trialist' : this.isRegistered ? 'registered' : 'anonymous'
}
}
dispatchTrackingEvent(trackData);
window.location.href = event?.target?.href;
const userType = this.errorCode
? 'sub-with-error'
: this.isTrialist
? 'trialist'
: this.isRegistered
? 'registered'
: 'anonymous';
const {
linkSubscribe,
linkLogin,
linkContact,
assetType,
articleId
} = this.options;

const shadowRoot = this.streamEl
.querySelector('#coral-shadow-container')
.shadowRoot;
const coralContainer = shadowRoot.getElementById('coral');
coralContainer.dataset.userNotSignedIn = 'true';

const section = document.createElement('section');
section.classList.add('coral__custom-message-content', 'coral');
const h3 = document.createElement('h3');
h3.textContent = userType === 'trialist'
? 'You are still on a trial period'
: 'Commenting is only available to readers with an FT subscription';

const p = document.createElement('p');

if (userType === 'sub-with-error') {
p.append(
document.createTextNode('There was an error setting your display name. Please contact '),
linkContact
? Object.assign(document.createElement('a'), {
className: 'linkContact',
href: linkContact,
textContent: 'Customer Care'
})
: document.createTextNode('Customer Care'),
document.createTextNode(` quoting ${this.errorCode}.`)
);
} else if (userType === 'trialist') {
p.append(
document.createTextNode('View our full '),
linkSubscribe
? Object.assign(document.createElement('a'), {
className: 'linkSubscribe',
href: linkSubscribe,
textContent: 'subscription packages'
})
: document.createTextNode('subscription packages'),
document.createTextNode(' to join the conversation.')
);
} else if (userType === 'registered') {
p.append(
linkSubscribe
? Object.assign(document.createElement('a'), {
className: 'linkSubscribe',
href: linkSubscribe,
textContent: 'Subscribe'
})
: document.createTextNode('Subscribe'),
document.createTextNode(' to join the conversation.')
);
} else {
p.append(
document.createTextNode('Please '),
linkLogin
? Object.assign(document.createElement('a'), {
className: 'linkLogin',
href: linkLogin,
textContent: 'login'
})
: document.createTextNode('login'),
document.createTextNode(' or '),
linkSubscribe
? Object.assign(document.createElement('a'), {
className: 'linkSubscribe',
href: linkSubscribe,
textContent: 'subscribe'
})
: document.createTextNode('subscribe'),
document.createTextNode(' to join the conversation.')
);
}

});
customMessageContainer.querySelector('.linkLogin')?.addEventListener('click', (event) => {
event.preventDefault();
const trackData = {
category: 'comment',
action: 'linkMessage',
coral: false,
content : {
asset_type: this.options.assetType,
uuid: this.options.articleId,
linkType: 'login',
user_type: this.isTrialist ? 'trialist' : this.isRegistered ? 'registered' : 'anonymous'
}
}
dispatchTrackingEvent(trackData);
window.location.href = event?.target?.href;
});
coralContainer.prepend(customMessageContainer);
section.append(h3, p);

section.addEventListener('click', e => {
const link = e.target.closest('a');
if (!link) return;
const isSubscriber = link.classList.contains('linkSubscribe');
const isLogin = link.classList.contains('linkLogin');
if (!isSubscriber && !isLogin) return;

const trackData = {
e.preventDefault();
dispatchTrackingEvent({
category: 'comment',
action: 'show-not-signed-in-message',
action: 'linkMessage',
coral: false,
content : {
asset_type: this.options.assetType,
uuid: this.options.articleId,
user_type: this.isTrialist ? 'trialist' : this.isRegistered ? 'registered' : 'anonymous'
content: {
asset_type: assetType,
uuid: articleId,
linkType: isSubscriber ? 'subscribe' : 'login',
user_type: userType
}
}
dispatchTrackingEvent(trackData);
}
});
window.location.href = link.href;
});

coralContainer.prepend(section);

dispatchTrackingEvent({
category: 'comment',
action: 'show-not-signed-in-message',
coral: false,
content: {
asset_type: assetType,
uuid: articleId,
user_type: userType
}
});
}
}

export default Stream;
Expand Down
102 changes: 88 additions & 14 deletions components/o-comments/src/js/utils/auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,75 @@
export default {
fetchJsonWebToken: function fetchJsonWebToken (options = {}) {
/**
* A registered user who has set a display name.
*
* @typedef {Object} RegisteredUserResponse
* @property {string} displayName - The user’s display name (should be an empty string).
* @property {string} token - The JWT will not be issued when using the auth-subsonly endpoint.
* @property {true} isRegistered - Always true.
*/

/**
* A trial user.
*
* @typedef {Object} TrialUserResponse
* @property {string} displayName - The user’s display name (should be an empty string).
* @property {string} token - The JWT will not be issued when using the auth-subsonly endpoint.
* @property {true} isTrialist - Always true.
*/

/**
* A subscribed user (whether they’ve set a display name yet or not).
*
* @typedef {Object} SubscribedUserResponse
* @property {string} displayName - The user’s display name (empty if not yet set).
* @property {string} token - The JWT issued by the comments API if a display name is set, otherwise empty.
* @property {true} isSubscribed - Always true for this shape.
*/

/**
* A 409-conflict response when:
* - when onlySubscribers is false (using auth endpoint) and user is signed in but has no display name
* - in live Q&A when a user is signed in but has no display name
* - when user is using auth-subsonly endpoint but has an email address associated with an old account
*
* @typedef {Object} ConflictResponse
* @property {true} userHasValidSession - Always true for this shape.
* @property {string} [errorCode] - API-provided error code indicating the conflict reason.
*/

/**
* A response indicating the user is not signed in, the session/token is invalid, or there was an error.
*
* @typedef {Object} UnauthenticatedResponse
* @property {false} userHasValidSession - Always false for this shape.
* @property {false} isSubscribed - Always false.
* @property {false} isTrialist - Always false.
*/

/**
* Union of all possible successful-auths.
*
* @typedef {RegisteredUserResponse|TrialUserResponse|SubscribedUserResponse} FetchJsonWebTokenOkResponse
*/

/**
* Union of every outcome this function may resolve to.
*
* @typedef {FetchJsonWebTokenOkResponse|ConflictResponse|UnauthenticatedResponse} FetchJsonWebTokenResponse
*/

/**
* Fetch a JSON Web Token from the FT Comments API, handling various user states.
*
* @param {Object} [options={}]
* @param {string} [options.commentsAPIUrl] - Base URL for the comments API (default: 'https://comments-api.ft.com').
* @param {string} [options.commentsAuthUrl] - Full override URL for the auth endpoint.
* @param {boolean} [options.onlySubscribers] - If true, use the `/auth-subsonly/` path instead of `/auth/`.
* @param {string} [options.displayName] - A display name to set on first sign-in or rename.
* @param {boolean} [options.useStagingEnvironment] - If true, appends `?staging=1` to hit the staging environment.
* @returns {Promise<FetchJsonWebTokenResponse>}
*/
export default {
fetchJsonWebToken(options = {}){
const commentsAPIUrl = options?.commentsAPIUrl || 'https://comments-api.ft.com';
const url = options?.commentsAuthUrl ? new URL(options.commentsAuthUrl) : new URL(`${commentsAPIUrl}/user/auth/`);
//TODO: CI-1493 redirect subscriber only users to another version of auth while the flag is on
Expand All @@ -15,21 +84,26 @@ export default {
}

return fetch(url, { credentials: 'include' }).then(response => {
// user is signed in
return response.json().then((json) => {
// user is signed in
if (response.ok) {
return response.json();
return json;
}
if (response.status === 409) {
return {
userHasValidSession: true,
errorCode: json?.errorCode,
};
} else {
// TODO: CI-1493 to remove after subscriber only is not behind a flag - check on Q&A usage, see below
// response for when onlySubscribers is false and user is signed in but has no display name
// also used in live Q&A when a user is signed in but has no display name
if (response.status === 409) {
return { userHasValidSession: true };
}

// user is not signed in or session token is invalid
// or error in comments api
return { userHasValidSession: false, isSubscribed: false, isTrialist: false};
throw new Error('Invalid response status');
}
}).catch(() =>{
return {
userHasValidSession: false,
isSubscribed: false,
isTrialist: false
};
});
});
}
};
Loading
Loading