diff --git a/components/o-comments/README.md b/components/o-comments/README.md index a62a187866..059b544f5a 100644 --- a/components/o-comments/README.md +++ b/components/o-comments/README.md @@ -169,6 +169,9 @@ These are the available options we can pass to oComments: - scrollContainer - assetType - disableOTracking +- linkSubscribe +- linkLogin +- linkContact ### Firing an oDomContentLoaded event diff --git a/components/o-comments/src/js/stream.js b/components/o-comments/src/js/stream.js index 69207d42ba..5f87b02b83 100644 --- a/components/o-comments/src/js/stream.js +++ b/components/o-comments/src/js/stream.js @@ -33,6 +33,7 @@ class Stream { this.isSubscribed = false; this.isTrialist = false; this.onlySubscribers = opts.onlySubscribers; + this.errorCode = null; } init () { @@ -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; @@ -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); } /** @@ -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 = ` -
- ${this.options.linkSubscribe ? `Subscribe` : `Subscribe`} to join the conversation. -
- `; - const messageForAnonymous = ` -- Please ${this.options.linkLogin ? ` login` : `login`} or ${this.options.linkSubscribe ? `subscribe` : `subscribe`} to join the conversation. -
- `; - const messageForTrial = ` -- View our full ${this.options.linkSubscribe ? `subscription packages` : `subscription packages` } to join the conversation. -
- `; - 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; diff --git a/components/o-comments/src/js/utils/auth.js b/components/o-comments/src/js/utils/auth.js index 2ee28cc724..13227ba40d 100644 --- a/components/o-comments/src/js/utils/auth.js +++ b/components/o-comments/src/js/utils/auth.js @@ -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