diff --git a/app/assets/javascripts/utilities/sendFetch.js b/app/assets/javascripts/utilities/sendFetch.js index 333aa7ed2..dd7d7a6a2 100644 --- a/app/assets/javascripts/utilities/sendFetch.js +++ b/app/assets/javascripts/utilities/sendFetch.js @@ -72,6 +72,15 @@ function sendFetch(switchStatement, body) { }, body, }); + case 'user_subscriptions': + return fetchCallback({ + url: '/user_subscriptions', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body, + }); default: console.log('A wrong switchStatement was used.'); // eslint-disable-line no-console break; diff --git a/app/controllers/async_info_controller.rb b/app/controllers/async_info_controller.rb index 1f7e86ef3..b9cd4e646 100644 --- a/app/controllers/async_info_controller.rb +++ b/app/controllers/async_info_controller.rb @@ -57,7 +57,8 @@ class AsyncInfoController < ApplicationController config_body_class: @user.config_body_class, feed_style: feed_style_preference, created_at: @user.created_at, - admin: @user.any_admin? + admin: @user.any_admin?, + apple_auth: @user.email.end_with?("@privaterelay.appleid.com") } end.to_json end diff --git a/app/javascript/liquidTags/userSubscriptionLiquidTag.js b/app/javascript/liquidTags/userSubscriptionLiquidTag.js new file mode 100644 index 000000000..0cf7e757b --- /dev/null +++ b/app/javascript/liquidTags/userSubscriptionLiquidTag.js @@ -0,0 +1,303 @@ +import { getUserDataAndCsrfToken } from '@utilities/getUserDataAndCsrfToken'; +/* global userData */ + +function toggleSubscribeActionUI({ + tagContainer, + showSubscribeAction = false, + appleAuth = false, +}) { + const signedInUI = tagContainer.getElementsByClassName( + 'ltag__user-subscription-tag__signed-in', + )[0]; + + const appleAuthUI = tagContainer.getElementsByClassName( + 'ltag__user-subscription-tag__apple-auth', + )[0]; + + if (!showSubscribeAction) { + appleAuthUI.classList.add('hidden'); + signedInUI.classList.add('hidden'); + return; + } + + // In this case we don't have an email we can use, and need the user to update their settings first + if (appleAuth) { + signedInUI.classList.add('hidden'); + appleAuthUI.classList.remove('hidden'); + return; + } + + signedInUI.classList.remove('hidden'); +} + +function toggleSignedOutUI(tagContainer, showSignedOutUI = false) { + const signedOutUI = tagContainer.getElementsByClassName( + 'ltag__user-subscription-tag__signed-out', + )[0]; + if (showSignedOutUI) { + signedOutUI.classList.remove('hidden'); + } else { + signedOutUI.classList.add('hidden'); + } +} + +function updateProfileImagesUI(tagContainer, signedIn = false) { + const profileImagesContainer = tagContainer.getElementsByClassName( + 'ltag__user-subscription-tag__profile-images', + )[0]; + + if (signedIn) { + profileImagesContainer.classList.add('signed-in'); + profileImagesContainer.classList.remove('signed-out'); + + tagContainer + .getElementsByClassName( + 'ltag__user-subscription-tag__subscriber-profile-image', + )[0] + .classList.remove('hidden'); + } else { + profileImagesContainer.classList.remove('signed-in'); + profileImagesContainer.classList.add('signed-out'); + + tagContainer + .getElementsByClassName( + 'ltag__user-subscription-tag__subscriber-profile-image', + )[0] + .classList.add('hidden'); + } +} + +function initSignedOutState(tagContainer) { + toggleSubscribeActionUI({ tagContainer, showSubscribeAction: false }); + toggleSignedOutUI(tagContainer, true); + updateProfileImagesUI(tagContainer, false); +} + +function initSignedInState(tagContainer, appleAuth = false) { + toggleSubscribeActionUI({ + tagContainer, + showSubscribeAction: true, + appleAuth, + }); + toggleSignedOutUI(tagContainer, false); + updateProfileImagesUI(tagContainer, true); + + tagContainer + .querySelector('.ltag__user-subscription-tag__signed-in .crayons-btn') + .addEventListener('click', () => { + window.Forem.showModal({ + title: 'Confirm subscribe', + contentSelector: + '.user-subscription-confirmation-modal .crayons-modal__box__body', + overlay: true, + onOpen: () => { + // Attach listeners for cancel button and subscribe button + document + .querySelector( + '#window-modal .ltag__user-subscription-tag____cancel-btn', + ) + .addEventListener('click', window.Forem.closeModal); + + document + .querySelector( + '#window-modal .ltag__user-subscription-tag__confirmation-btn', + ) + .addEventListener('click', confirmSubscribe); + }, + }); + }); +} + +function confirmSubscribe() { + window.Forem.closeModal(); + clearNotices(); + + submitSubscription().then((response) => { + if (response.success) { + const allSubscriptionLiquidTags = document.getElementsByClassName( + 'ltag__user-subscription-tag', + ); + + showSubscribedNotices(); + + for (const tagContainer of allSubscriptionLiquidTags) { + // We no longer want to show the submit button since user is now subscribed + toggleSubscribeActionUI({ tagContainer, showSubscribeAction: false }); + } + } else { + updateNotices({ variant: 'danger', content: response.error }); + // Allow user to retry + toggleSubmitButtonsState({ disabled: false, textContent: 'Subscribe' }); + } + }); +} + +function toggleSubmitButtonsState({ disabled, textContent }) { + // Since all user sub tags on the same article page perform the same action, all submit buttons are kept in sync + const allUserSubLiquidTagSubmits = document.querySelectorAll( + '.ltag__user-subscription-tag__signed-in .crayons-btn', + ); + + for (const submit of allUserSubLiquidTagSubmits) { + submit.disabled = disabled; + submit.textContent = textContent; + } +} + +function updateNotices({ variant, content }) { + const allNotices = document.getElementsByClassName( + 'ltag__user-subscription-tag__response-message', + ); + + for (const notice of allNotices) { + notice.classList.remove('hidden'); + notice.classList.add(`crayons-notice--${variant}`); + notice.textContent = content; + } + + // When a notice is shown, we hide the generic signed-in instructions + toggleSignedInInstructionsUI(false); +} + +function clearNotices() { + // Since all user sub tags on the same article page perform the same action, all notices are kept in sync + const allNotices = document.getElementsByClassName( + 'ltag__user-subscription-tag__response-message', + ); + + for (const notice of allNotices) { + notice.classList.add('hidden'); + } + + // Re-show the signed in instructions + toggleSignedInInstructionsUI(true); +} + +function toggleSignedInInstructionsUI(isVisible) { + const signedInInstructions = document.querySelectorAll( + '.ltag__user-subscription-tag__signed-in .ltag__user-subscription-tag__logged-in-text', + ); + + for (const instructions of signedInInstructions) { + if (isVisible) { + instructions.classList.remove('hidden'); + } else { + instructions.classList.add('hidden'); + } + } +} + +function submitSubscription() { + toggleSubmitButtonsState({ disabled: true, textContent: 'Submitting' }); + + const articleId = + document.getElementById('article-body')?.dataset?.articleId ?? null; + + const subscriber = userData(); + const body = JSON.stringify({ + user_subscription: { + source_type: 'Article', + source_id: articleId, + subscriber_email: subscriber.email, + }, + }); + + return getCsrfToken() + .then(sendFetch('user_subscriptions', body)) + .then((res) => res.json()); +} + +function fetchSubscriptionStatus() { + const { articleId } = document.getElementById('article-body').dataset; + + const params = new URLSearchParams({ + source_type: 'Article', + source_id: articleId, + }).toString(); + + const headers = { + Accept: 'application/json', + 'X-CSRF-Token': window.csrfToken, + 'Content-Type': 'application/json', + }; + + return fetch(`/user_subscriptions/subscribed?${params}`, { + method: 'GET', + headers, + credentials: 'same-origin', + }).then((response) => { + if (response.ok) { + return response.json(); + } + console.error( + `Base data error: ${response.status} - ${response.statusText}`, + ); + }); +} + +function showSubscribedNotices() { + const { authorUsername } = document.getElementsByClassName( + 'ltag__user-subscription-tag', + )[0].dataset; + + updateNotices({ + variant: 'success', + content: `You are now subscribed and may receive emails from ${authorUsername}`, + }); +} + +function populateSubscriberProfileImage({ + profile_image_90: profileImageSrc, + username, +}) { + document + .querySelectorAll('.ltag__user-subscription-tag__subscriber-profile-image') + .forEach((profileImage) => { + profileImage.src = profileImageSrc; + profileImage.alt = username; + }); +} + +/** + * Initializes the functionality of any user_subscription liquid tags in an article. + * + * An article may have more than one user_subscription liquid tag, but all instances complete the same action, + * and are therefore kept in-step throughout any user interaction or update (i.e. the UI and behavior of all user_subscription + * liquid tags in an article will always be the same). + */ +export async function initializeUserSubscriptionLiquidTagContent() { + const allUserSubLiquidTags = document.getElementsByClassName( + 'ltag__user-subscription-tag__container', + ); + + const { userStatus } = document.querySelector('body').dataset; + if (userStatus === 'logged-out') { + for (const liquidTag of allUserSubLiquidTags) { + initSignedOutState(liquidTag); + } + return; + } + + const { currentUser: user } = await getUserDataAndCsrfToken(); + const { apple_auth: isSubscriberAuthedWithApple } = user; + + // Setup the initial signed-in state without waiting on subscription status fetch + for (const liquidTag of allUserSubLiquidTags) { + initSignedInState(liquidTag, isSubscriberAuthedWithApple); + } + + // Check if we need to refresh UI due to existing subscription + populateSubscriberProfileImage(user); + fetchSubscriptionStatus().then(({ is_subscribed: isSubscribed }) => { + if (isSubscribed) { + showSubscribedNotices(); + } + for (const liquidTag of allUserSubLiquidTags) { + toggleSubscribeActionUI({ + tagContainer: liquidTag, + showSubscribeAction: !isSubscribed, + appleAuth: isSubscriberAuthedWithApple, + }); + } + }); +} diff --git a/app/javascript/packs/articlePage.jsx b/app/javascript/packs/articlePage.jsx index 8ddeda9ee..6b011c577 100644 --- a/app/javascript/packs/articlePage.jsx +++ b/app/javascript/packs/articlePage.jsx @@ -3,6 +3,7 @@ import { Snackbar, addSnackbarItem } from '../Snackbar'; import { addFullScreenModeControl } from '../utilities/codeFullscreenModeSwitcher'; import { initializeDropdown } from '../utilities/dropdownUtils'; import { embedGists } from '../utilities/gist'; +import { initializeUserSubscriptionLiquidTagContent } from '../liquidTags/userSubscriptionLiquidTag'; /* global Runtime */ @@ -130,3 +131,5 @@ getCsrfToken().then(async () => { const targetNode = document.querySelector('#comments'); targetNode && embedGists(targetNode); + +initializeUserSubscriptionLiquidTagContent(); diff --git a/app/liquid_tags/user_subscription_tag.rb b/app/liquid_tags/user_subscription_tag.rb index 352ee7827..4d8fc2247 100644 --- a/app/liquid_tags/user_subscription_tag.rb +++ b/app/liquid_tags/user_subscription_tag.rb @@ -11,332 +11,6 @@ class UserSubscriptionTag < LiquidTagBase :super_admin, ].freeze - def self.script - <<~JAVASCRIPT.freeze - var subscribeBtn = document.getElementById('subscribe-btn'); - - function isUserSignedIn() { - return document.head.querySelector('meta[name="user-signed-in"][content="true"]') !== null; - } - - // Hiding/showing elements - // If clearSubscribeButton is false, we will not clear out the subscription-signed-in area, - // which will allow users to re-submit their subscription if they see any error messages. - // *************************************** - function clearSubscriptionArea({ clearSubscribeButton = true } = {}) { - if (!clearSubscribeButton) { - // Allow users to try submitting again if they see an error. - hideSubscriptionSignedIn(); - } - - const subscriptionSignedOut = document.getElementById('subscription-signed-out'); - if (subscriptionSignedOut) { - subscriptionSignedOut.classList.add("hidden"); - } - - hideResponseMessage(); - - const subscriberAppleAuth = document.getElementById('subscriber-apple-auth'); - if (subscriberAppleAuth) { - subscriberAppleAuth.classList.add("hidden"); - } - - hideConfirmationModal(); - } - - // Hides the response message (which displays success/error messages) if it exists. - // *************************************** - function hideResponseMessage() { - const responseMessage = document.getElementById('response-message'); - if (responseMessage) { - responseMessage.classList.add("hidden"); - } - } - - function showSignedIn() { - clearSubscriptionArea(); - const subscriptionSignedIn = document.getElementById('subscription-signed-in'); - if (subscriptionSignedIn) { - subscriptionSignedIn.classList.remove("hidden"); - } - - const profileImages = document.getElementById('profile-images'); - if (profileImages) { - profileImages.classList.remove("signed-out"); - profileImages.classList.add("signed-in"); - } - } - - function showSignedOut() { - clearSubscriptionArea(); - - const subscriptionSignedOut = document.getElementById('subscription-signed-out'); - if (subscriptionSignedOut) { - subscriptionSignedOut.classList.remove("hidden"); - } - - const profileImages = document.getElementById('profile-images'); - if (profileImages) { - profileImages.classList.remove("signed-in"); - profileImages.classList.add("signed-out"); - } - - const subscriberProfileImage = document.getElementsByClassName('ltag__user-subscription-tag__subscriber-profile-image')[0]; - if (subscriberProfileImage) { - subscriberProfileImage.classList.add("hidden"); - } - } - - function showResponseMessage(noticeType, msg) { - clearSubscriptionArea(clearSubscribeButton = false); - - const responseMessage = document.getElementById('response-message'); - if (responseMessage) { - responseMessage.classList.remove("hidden"); - responseMessage.classList.add(`crayons-notice--${noticeType}`); - responseMessage.textContent = msg; - - if (noticeType === 'danger') { - // Allow users to try resubscribing if they see an error message. - subscribeBtn.textContent = "#{I18n.t('liquid_tags.user_subscription_tag.submit')}"; - subscribeBtn.disabled = false; - } - } - } - - function showAppleAuthMessage() { - clearSubscriptionArea(); - const subscriber = userData(); - if (subscriber) { - updateProfileImages('.ltag__user-subscription-tag__subscriber-profile-image', subscriber); - } - - const subscriberAppleAuth = document.getElementById('subscriber-apple-auth'); - if (subscriberAppleAuth) { - subscriberAppleAuth.classList.remove("hidden"); - } - } - - function showSubscribed() { - hideSubscriptionSignedIn(); - updateSubscriberData(); - const authorUsername = document.getElementById('user-subscription-tag')?.dataset.authorUsername; - const alreadySubscribedMsg = `#{I18n.t('liquid_tags.user_subscription_tag.subscribed')}`; - showResponseMessage('success', alreadySubscribedMsg); - } - - function showConfirmationModal() { - const confirmationModal = document.getElementById('user-subscription-confirmation-modal'); - if (confirmationModal) { - confirmationModal.classList.remove("hidden"); - } - } - - function hideConfirmationModal() { - const confirmationModal = document.getElementById('user-subscription-confirmation-modal'); - if (confirmationModal) { - confirmationModal.classList.add("hidden"); - } - } - - // Updating DOM elements - // *************************************** - function updateSubscriberData() { - const subscriber = userData(); - if (subscriber.email) { - updateElementsTextContent('.ltag__user-subscription-tag__subscriber-email', subscriber.email); - } - - updateProfileImages('.ltag__user-subscription-tag__subscriber-profile-image', subscriber); - } - - function updateElementsTextContent(identifier, value) { - const elements = document.querySelectorAll(identifier); - - elements.forEach(function(element) { - element.textContent = value; - }); - } - - function updateProfileImages(identifier, subscriber) { - const profileImages = document.querySelectorAll(`img${identifier}`); - - profileImages.forEach(function(profileImage) { - profileImage.src = subscriber.profile_image_90; - profileImage.alt = `${subscriber.username} profile image`; - }); - } - - function hideSubscriptionSignedIn() { - const subscriptionSignedIn = document.getElementById('subscription-signed-in'); - if (subscriptionSignedIn) { - subscriptionSignedIn.classList.add("hidden"); - } - } - - // Adding event listeners for 'click' - // *************************************** - function addSignInClickHandler() { - const signInBtn = document.getElementById('sign-in-btn'); - if (signInBtn) { - signInBtn.addEventListener('click', function(e) { - if (typeof showLoginModal !== 'undefined') { - showLoginModal(); - } - }); - } - } - - function addConfirmationModalClickHandlers() { - if (subscribeBtn) { - subscribeBtn.addEventListener('click', function(e) { - showConfirmationModal(); - }); - } - - const cancelBtn = document.getElementById('cancel-btn'); - if (cancelBtn) { - cancelBtn.addEventListener('click', function(e) { - hideConfirmationModal(); - }); - } - - const closeConfirmationModal = document.getElementById('close-confirmation-modal'); - if (closeConfirmationModal) { - closeConfirmationModal.addEventListener('click', function(e) { - hideConfirmationModal(); - }); - } - - const confirmationModal = document.getElementById('confirmation-btn') - if (confirmationModal) { - confirmationModal.addEventListener('click', function(e) { - handleSubscription(); - }); - } - } - - // API calls - // *************************************** - function submitSubscription() { - // Hide any error messages previously rendered. - hideResponseMessage(); - - subscribeBtn.textContent = "#{I18n.t('liquid_tags.user_subscription_tag.submitting')}"; - subscribeBtn.disabled = true; - - const headers = { - Accept: 'application/json', - 'X-CSRF-Token': window.csrfToken, - 'Content-Type': 'application/json', - } - - const articleBody = document.getElementById('article-body'); - const articleId = (articleBody ? articleBody.dataset.articleId : null); - - const subscriber = userData(); - const body = JSON.stringify( - { - user_subscription: { - source_type: 'Article', - source_id: articleId, - subscriber_email: subscriber.email - } - } - ) - - return fetch('/user_subscriptions', { - method: 'POST', - headers: headers, - credentials: 'same-origin', - body: body, - }).then(function(response) { - return response.json(); - }); - } - - function fetchIsSubscribed() { - const articleBody = document.getElementById('article-body'); - const articleId = (articleBody ? articleBody.dataset.articleId : null); - - const params = new URLSearchParams({ - source_type: 'Article', - source_id: articleId - }).toString(); - - const headers = { - Accept: 'application/json', - 'X-CSRF-Token': window.csrfToken, - 'Content-Type': 'application/json', - } - - return fetch(`/user_subscriptions/subscribed?${params}`, { - method: 'GET', - headers: headers, - credentials: 'same-origin', - }).then(function(response) { - if (response.ok) { - return response.json(); - } else { - console.error(`Base data error: ${response.status} - ${response.statusText}`); - } - }); - } - - // Handle API responses - // *************************************** - function handleSubscription() { - hideConfirmationModal(); // Close the modal once the user has confirmed. - - submitSubscription().then(function(response) { - if (response.success) { - const userSubscriptionTag = document.getElementById('user-subscription-tag'); - const authorUsername = (userSubscriptionTag ? userSubscriptionTag.dataset.authorUsername : null); - // TODO: [yheuhtozr] currently no way of i18n via Ruby - const successMsg = `You are now subscribed and may receive emails from ${authorUsername}`; - showResponseMessage('success', successMsg); - hideSubscriptionSignedIn(); - } else { - showResponseMessage('danger', response.error); - } - }); - } - - function checkIfSubscribed() { - fetchIsSubscribed().then(function(response) { - const subscriber = userData(); - const isSubscriberAuthedWithApple = (subscriber.email ? subscriber.email.endsWith('@privaterelay.appleid.com') : false); - - if (response.is_subscribed) { - showSubscribed(); - } else if (isSubscriberAuthedWithApple) { - showAppleAuthMessage(); - } else { - updateSubscriberData(); - } - }); - } - - // We load this JS on every Article. This is to only run it on Articles - // where the UserSubscription liquid tag is present - if (document.getElementById('user-subscription-tag')) { - // The markup defaults to signed out UX - if (isUserSignedIn()) { - showSignedIn(); - addConfirmationModalClickHandlers(); - - // We need access to some DOM elements (i.e. csrf token, article id, userData, etc.) - document.addEventListener('DOMContentLoaded', function() { - checkIfSubscribed(); - }); - } else { - showSignedOut(); - addSignInClickHandler(); - } - } - JAVASCRIPT - end - def initialize(_tag_name, cta_text, parse_context) super @cta_text = cta_text.strip @@ -350,7 +24,8 @@ class UserSubscriptionTag < LiquidTagBase locals: { cta_text: @cta_text, author_profile_image: @user&.profile_image_90, - author_username: @user&.username + author_username: @user&.username, + community_name: Settings::Community.community_name }, ) end diff --git a/app/views/articles/show.html.erb b/app/views/articles/show.html.erb index c102a2d5f..1fd59a9a0 100644 --- a/app/views/articles/show.html.erb +++ b/app/views/articles/show.html.erb @@ -227,6 +227,5 @@ <%== PollTag.script %> <%== RunkitTag.script %> <%== TweetTag.script %> - <%== UserSubscriptionTag.script %> <% end %> diff --git a/app/views/liquids/_user_subscription.html.erb b/app/views/liquids/_user_subscription.html.erb index 9a4fda898..a4ff3bccf 100644 --- a/app/views/liquids/_user_subscription.html.erb +++ b/app/views/liquids/_user_subscription.html.erb @@ -1,81 +1,66 @@ -
> +
>
-
+
-
+
- - <%= "#{author_username} profile image" %>> + + <%= author_username %> <%= author_username %>> - - + +
-

+

<%= cta_text %> -

+ -
-
-
- <%= t("views.liquids.user_subscription.logged_out") %> +
+
+
+ <%= t("views.liquids.user_subscription.logged_out", community: community_name) %>
- +
-