Rework user subscription liquid tag to avoid errors (#16460)

* move script to packs, strip ids

* make sure userdata is available before determining ui

* stop working around old html, fix duplicate ID errors

* skip login modal

* remove DEV hard coding

* replace system spec with cypress spec

* update base_data to include apple auth info, add to cypress tests

* add initial version of DUS to resave HTML

* remove data update script
This commit is contained in:
Suzanne Aitchison 2022-02-15 09:14:03 +00:00 committed by GitHub
parent 24eb7d70ab
commit 3982b389f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 515 additions and 545 deletions

View file

@ -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;

View file

@ -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

View file

@ -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,
});
}
});
}

View file

@ -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();

View file

@ -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

View file

@ -227,6 +227,5 @@
<%== PollTag.script %>
<%== RunkitTag.script %>
<%== TweetTag.script %>
<%== UserSubscriptionTag.script %>
</script>
<% end %>

View file

@ -1,81 +1,66 @@
<div class="ltag__user-subscription-tag" id="user-subscription-tag" data-author-username=<%= author_username %>>
<div class="ltag__user-subscription-tag" data-author-username=<%= author_username %>>
<div class="ltag__user-subscription-tag__container">
<div class="ltag__user-subscription-tag__content w-100" id="content">
<div class="ltag__user-subscription-tag__content w-100">
<div class="ltag__user-subscription-tag__profile-images signed-out" id="profile-images">
<div class="ltag__user-subscription-tag__profile-images signed-out">
<span class="crayons-avatar crayons-avatar--xl ltag__user-subscription-tag__author-profile-image">
<img class="crayons-avatar__image ltag__user-subscription-tag__author-profile-image" src=<%= author_profile_image %> <%= "#{author_username} profile image" %>>
<span class="crayons-avatar crayons-avatar--xl ltag__user-subscription-tag__author-profile-image m-auto">
<img class="crayons-avatar__image ltag__user-subscription-tag__author-profile-image m-0" src=<%= author_profile_image %> <%= author_username %> <%= author_username %>>
</span>
<span class="crayons-avatar crayons-avatar--xl ltag__user-subscription-tag__subscriber-profile-image">
<img class="crayons-avatar__image ltag__user-subscription-tag__subscriber-profile-image">
<span class="crayons-avatar crayons-avatar--xl ltag__user-subscription-tag__subscriber-profile-image m-auto">
<img class="crayons-avatar__image ltag__user-subscription-tag__subscriber-profile-image m-0" alt="">
</span>
</div>
<h1 class="ltag__user-subscription-tag__cta-text fs-xl mt-0 mb-4 align-center" id="user-subscription-cta-text">
<h2 class="ltag__user-subscription-tag__cta-text fs-xl mt-0 mb-4 align-center">
<%= cta_text %>
</h1>
</h2>
<div class="ltag__user-subscription-tag__subscription-area align-center" id="subscription-area">
<div class="ltag__user-subscription-tag__signed-out" id="subscription-signed-out">
<div class="fs-base mb-2" id="logged-out-text">
<%= t("views.liquids.user_subscription.logged_out") %>
<div class="ltag__user-subscription-tag__subscription-area align-center">
<div class="ltag__user-subscription-tag__signed-out">
<div class="fs-base mb-2">
<%= t("views.liquids.user_subscription.logged_out", community: community_name) %>
</div>
<button class="crayons-btn" id="sign-in-btn">
<a href="/enter" class="c-cta c-cta--default">
<%= t("views.liquids.user_subscription.sign_in") %>
</button>
</a>
</div>
<div class="ltag__user-subscription-tag__signed-in hidden" id="subscription-signed-in">
<button class="crayons-btn mb-4" id="subscribe-btn">
<div class="ltag__user-subscription-tag__signed-in hidden">
<button class="crayons-btn mb-4">
<%= t("views.liquids.user_subscription.subscribe.button") %>
</button>
<div class="fs-s mb-3" id="logged-in-text">
<%= t("views.liquids.user_subscription.subscribe.desc_html", email: tag.span(class: "ltag__user-subscription-tag__subscriber-email"), update: link_to(t("views.liquids.user_subscription.subscribe.update"), "/settings")) %>
<div class="ltag__user-subscription-tag__logged-in-text fs-s mb-3">
<%= t("views.liquids.user_subscription.subscribe.desc_html", community: community_name, update: link_to(t("views.liquids.user_subscription.subscribe.update"), "/settings")) %>
</div>
</div>
<div class="ltag__user-subscription-tag__apple-auth fs-s hidden" id="subscriber-apple-auth">
<button disabled class="crayons-btn" id="apple-subscribe-btn"><%= t("views.liquids.user_subscription.subscribe.apple.button") %></button>
<div class="fs-s" id="apple-auth-message">
<%= t("views.liquids.user_subscription.subscribe.apple.desc_html", update: link_to(t("views.liquids.user_subscription.subscribe.apple.update"), "/settings")) %>
<div class="ltag__user-subscription-tag__apple-auth fs-s hidden">
<button disabled class="crayons-btn"><%= t("views.liquids.user_subscription.subscribe.apple.button") %></button>
<div class="fs-s">
<%= t("views.liquids.user_subscription.subscribe.apple.desc_html", community: community_name, update: link_to(t("views.liquids.user_subscription.subscribe.apple.update"), "/settings")) %>
</div>
</div>
<div class="ltag__user-subscription-tag__response-message crayons-notice fs-base w-100 hidden" aria-live="polite" id="response-message"></div>
<div class="ltag__user-subscription-tag__response-message crayons-notice fs-base w-100 hidden" aria-live="polite"></div>
</div>
</div>
<div id="user-subscription-confirmation-modal" class="hidden">
<div class="crayons-modal crayons-modal--small">
<div class="crayons-modal__box">
<header class="crayons-modal__box__header">
<h2 class="fs-l fw-bold mt-0 mb-0">
<%= t("views.liquids.user_subscription.confirm.subtitle") %>
</h2>
<button type="button" class="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost" id="close-confirmation-modal">
<svg width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" xmlns="http://www.w3.org/2000/svg">
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
</svg>
</button>
</header>
<div class="crayons-modal__box__body">
<p id="confirmation-text" class="fs-base mb-4 mt-0">
<%= t("views.liquids.user_subscription.confirm.text_html", email: tag.span(class: "ltag__user-subscription-tag__subscriber-email"), username: tag.span(author_username, class: "ltag__user-subscription-tag__author-username fw-medium")) %>
<p>
<div class="ltag__user-subscription-tag__confirmation-buttons">
<button class="crayons-btn mr-1" id="confirmation-btn">
<%= t("views.liquids.user_subscription.confirm.button_text") %>
</button>
<button class="crayons-btn crayons-btn--secondary" id="cancel-btn">
<%= t("views.liquids.user_subscription.confirm.cancel_text") %>
</button>
</div>
</div>
<div class="user-subscription-confirmation-modal hidden">
<div class="crayons-modal__box__body">
<p class="fs-base mb-4 mt-0">
<%= t("views.liquids.user_subscription.confirm.text_html", community: community_name, username: tag.span(author_username, class: "ltag__user-subscription-tag__author-username fw-medium")) %>
<p>
<div class="ltag__user-subscription-tag__confirmation-buttons">
<button class="ltag__user-subscription-tag__confirmation-btn crayons-btn mr-1">
<%= t("views.liquids.user_subscription.confirm.button_text") %>
</button>
<button class="ltag__user-subscription-tag____cancel-btn crayons-btn crayons-btn--secondary">
<%= t("views.liquids.user_subscription.confirm.cancel_text") %>
</button>
</div>
<div class="crayons-modal__overlay"></div>
</div>
</div>
</div>

View file

@ -28,16 +28,16 @@ en:
subtitle: Confirm subscribe
button_text: Confirm subscription
cancel_text: Cancel
text_html: You'll share your email address%{email}, username, name, and DEV profile URL with %{username}. Once you do this, you cannot undo this.
logged_out: You must first sign in to DEV.
text_html: "You'll share your email address, username, name, and %{community} profile URL with %{username}. Once you do this, you cannot undo this."
logged_out: "You must first sign in to %{community}."
sign_in: Sign In
subscribe:
apple:
button: Subscribe
desc_html: Hey, there! It looks like when you created your DEV account you signed up with Apple using a private relay email address. If you'd like to subscribe, please %{update} first to a different email address.
desc_html: "Hey, there! It looks like when you created your %{community} account you signed up with Apple using a private relay email address. If you'd like to subscribe, please %{update} first to a different email address."
update: update your email address in Settings
button: Subscribe
desc_html: You'll subscribe with %{email} the email address associated with your DEV account. To use a different email address, you can %{update}.
desc_html: "You'll subscribe with the email address associated with your %{community} account. To use a different email address, you can %{update}."
update: update your email address in Settings
wikipedia:
logo: Wikipedia Logo

View file

@ -28,16 +28,16 @@ fr:
subtitle: Confirm subscribe
button_text: Confirm subscription
cancel_text: Cancel
text_html: You'll share your email address%{email}, username, name, and DEV profile URL with %{username}. Once you do this, you cannot undo this.
logged_out: You must first sign in to DEV.
text_html: "You'll share your email address, username, name, and %{community} profile URL with %{username}. Once you do this, you cannot undo this."
logged_out: "You must first sign in to %{community}."
sign_in: Sign In
subscribe:
apple:
button: Subscribe
desc_html: Hey, there! It looks like when you created your DEV account you signed up with Apple using a private relay email address. If you'd like to subscribe, please %{update} first to a different email address.
desc_html: "Hey, there! It looks like when you created your %{community} account you signed up with Apple using a private relay email address. If you'd like to subscribe, please %{update} first to a different email address."
update: update your email address in Settings
button: Subscribe
desc_html: You'll subscribe with %{email} the email address associated with your DEV account. To use a different email address, you can %{update}.
desc_html: "You'll subscribe with the email address associated with your %{community} account. To use a different email address, you can %{update}."
update: update your email address in Settings
wikipedia:
logo: Wikipedia Logo

View file

@ -0,0 +1,5 @@
{
"username": "apple_auth_admin_user",
"email": "apple-auth-admin-user@privaterelay.appleid.com",
"password": "password"
}

View file

@ -0,0 +1,43 @@
describe('User subscription liquid tag - apple auth email', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/appleAuthAdminUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginUser(user).then(() => {
cy.createArticle({
title: 'User subscription liquid tag article',
tags: ['beginner', 'ruby', 'go'],
content: `{% user_subscription CTA text for first tag %}\nSome text\n{% user_subscription CTA text for second tag %}`,
published: true,
}).then((response) => {
cy.visitAndWaitForUserSideEffects(response.body.current_state_path);
// Wait for page to load
cy.findByRole('heading', {
name: 'User subscription liquid tag article',
});
});
});
});
});
it('informs user they must change email address to subscribe', () => {
// Check both liquid tags are shown
cy.findByRole('heading', { name: 'CTA text for first tag' });
cy.findByRole('heading', { name: 'CTA text for second tag' });
// User should not be able to subscribe
cy.findAllByRole('button', { name: 'Subscribe' })
.last()
.should('have.attr', 'disabled');
cy.findAllByRole('link', {
name: 'update your email address in Settings',
}).should('have.length', 2);
cy.findAllByText(
/you signed up with Apple using a private relay email/,
).should('have.length', 2);
});
});

View file

@ -0,0 +1,84 @@
describe('User subscription liquid tag - subscribable email', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/adminUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginUser(user).then(() => {
cy.createArticle({
title: 'User subscription liquid tag article',
tags: ['beginner', 'ruby', 'go'],
content: `{% user_subscription CTA text for first tag %}\nSome text\n{% user_subscription CTA text for second tag %}`,
published: true,
}).then((response) => {
cy.visitAndWaitForUserSideEffects(response.body.current_state_path);
// Wait for page to load
cy.findByRole('heading', {
name: 'User subscription liquid tag article',
});
});
});
});
});
it('shows the signed in UI and subscribes', () => {
cy.findByRole('heading', { name: 'CTA text for first tag' });
cy.findByRole('heading', { name: 'CTA text for second tag' });
cy.findAllByRole('button', { name: 'Subscribe' }).should('have.length', 2);
cy.findAllByRole('link', {
name: 'update your email address in Settings',
}).should('have.length', 2);
cy.findAllByRole('button', { name: 'Subscribe' }).last().click();
cy.getModal()
.findByRole('button', { name: 'Confirm subscription' })
.click();
cy.findAllByText(
'You are now subscribed and may receive emails from admin_mcadmin',
).should('have.length', 2);
cy.findByRole('button', { name: 'Subscribe' }).should('not.exist');
});
it('cancels subscription action', () => {
cy.findAllByRole('button', { name: 'Subscribe' }).last().click();
cy.getModal().findByRole('button', { name: 'Cancel' }).click();
cy.findAllByRole('button', { name: 'Subscribe' }).should('have.length', 2);
cy.findByText(
'You are now subscribed and may receive emails from admin_mcadmin',
).should('not.exist');
});
it('shows any error message and allows user to retry', () => {
cy.intercept('POST', '/user_subscriptions', {
error: 'This is an error message',
});
cy.findAllByRole('button', { name: 'Subscribe' }).last().click();
cy.getModal()
.findByRole('button', { name: 'Confirm subscription' })
.click();
cy.findAllByText('This is an error message').should('have.length', 2);
});
it('shows the signed out UI', () => {
cy.url().then(() => {
cy.signOutUser();
cy.findAllByRole('link', {
name: 'User subscription liquid tag article',
})
.first()
.click({ force: true });
cy.findAllByRole('link', { name: 'Sign In' }).should('have.length', 2);
cy.findAllByText('You must first sign in to DEV(local).').should(
'have.length',
2,
);
cy.findByRole('button', { name: 'Subscribe' }).should('not.exist');
});
});
});

View file

@ -213,6 +213,26 @@ end
##############################################################################
seeder.create_if_doesnt_exist(User, "email", "apple-auth-admin-user@privaterelay.appleid.com") do
user = User.create!(
name: "Apple Auth Admin User",
email: "apple-auth-admin-user@privaterelay.appleid.com",
username: "apple_auth_admin_user",
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
confirmed_at: Time.current,
registered_at: Time.current,
password: "password",
password_confirmation: "password",
saw_onboarding: true,
checked_code_of_conduct: true,
checked_terms_and_conditions: true,
)
user.add_role(:super_admin)
end
##############################################################################
seeder.create_if_doesnt_exist(User, "email", "notifications-user@forem.local") do
user = User.create!(
name: "Notifications User \\:/",

View file

@ -1,157 +0,0 @@
require "rails_helper"
RSpec.describe UserSubscriptionTag, type: :system, js: true do
let(:subscriber) { create(:user) }
let(:author) { create(:user) }
let(:article_with_user_subscription_tag) do
create(:article, :with_user_subscription_tag_role_user, with_user_subscription_tag: true)
end
before do
Liquid::Template.register_tag("user_subscription", described_class)
end
context "when viewing the default view" do
before do
# Stub roles because adding them normally can cause flaky specs
allow(author).to receive(:has_role?).and_call_original
allow(author).to receive(:has_role?).with(:restricted_liquid_tag,
LiquidTags::UserSubscriptionTag).and_return(true)
end
it "displays signed out view by default" do
sign_in author
visit "/new" # Preview behaves differently - we don't load liquid tag scripts
fill_in "article_body_markdown", with: "{% user_subscription Some sweet CTA text %}"
page.execute_script("window.scrollTo(0, -100000)")
find("button", text: /\APreview\z/).click
expect(page).to have_css("#subscription-signed-in", visible: :hidden)
expect(page).to have_css("#subscriber-apple-auth", visible: :hidden)
expect(page).to have_css("#response-message", visible: :hidden)
expect(page).to have_css("#subscription-signed-out", visible: :visible)
css_class = "img.ltag__user-subscription-tag__author-profile-image[src='#{author.profile_image_90}']"
expect(page).to have_css(css_class)
end
end
context "when signed in" do
before do
sign_in subscriber
visit article_with_user_subscription_tag.path
end
it "shows the signed in UX" do
expect(page).to have_css("#subscription-signed-out", visible: :hidden)
expect(page).to have_css("#subscriber-apple-auth", visible: :hidden)
expect(page).to have_css("#response-message", visible: :hidden)
expect(page).to have_css("#subscription-signed-in", visible: :visible)
css_class = "img.ltag__user-subscription-tag__subscriber-profile-image[src='#{subscriber.profile_image_90}']"
expect(page).to have_css(css_class)
end
it "asks the user to confirm their subscription" do
expect(page).to have_css("#user-subscription-confirmation-modal", visible: :hidden)
click_button("Subscribe", id: "subscribe-btn")
expect(page).to have_css("#user-subscription-confirmation-modal", visible: :visible)
end
it "displays a success message when a subscription is created" do
expect(page).to have_css("#subscription-signed-out", visible: :hidden)
expect(page).to have_css("#subscriber-apple-auth", visible: :hidden)
expect(page).to have_css("#response-message", visible: :hidden)
expect(page).to have_css("#subscription-signed-in", visible: :visible)
click_button("Subscribe", id: "subscribe-btn")
click_button("Confirm subscription", id: "confirmation-btn")
expect(page).to have_css("#subscription-signed-in", visible: :hidden)
expect(page).to have_css("#response-message.crayons-notice--success", visible: :visible)
within "#response-message" do
expect(page).to have_text("You are now subscribed")
end
end
# rubocop:disable RSpec/ExampleLength
it "displays errors when there's an error creating a subscription" do
# Create a subscription so it causes an error by already being subscribed
create(
:user_subscription,
subscriber_id: subscriber.id, subscriber_email: subscriber.email,
author_id: article_with_user_subscription_tag.user_id,
user_subscription_sourceable: article_with_user_subscription_tag
)
expect(page).to have_css("#subscription-signed-out", visible: :hidden)
expect(page).to have_css("#subscriber-apple-auth", visible: :hidden)
expect(page).to have_css("#response-message", visible: :hidden)
expect(page).to have_css("#subscription-signed-in", visible: :visible)
click_button("Subscribe", id: "subscribe-btn")
click_button("Confirm subscription", id: "confirmation-btn")
# User should be able see the error and should be able to resubscribe.
expect(page).to have_css("#subscription-signed-in", visible: :visible)
expect(page).to have_css("#response-message.crayons-notice--danger", visible: :visible)
within "#response-message" do
expect(page).to have_text("Subscriber has already been taken")
end
end
# rubocop:enable RSpec/ExampleLength
it "tells the user they're already subscribed by default if they're already subscribed" do
create(
:user_subscription,
subscriber_id: subscriber.id, subscriber_email: subscriber.email,
author_id: article_with_user_subscription_tag.user_id,
user_subscription_sourceable: article_with_user_subscription_tag
)
visit article_with_user_subscription_tag.path
expect(page).to have_css("#subscription-signed-out", visible: :hidden)
expect(page).to have_css("#subscription-signed-in", visible: :hidden)
expect(page).to have_css("#subscriber-apple-auth", visible: :hidden)
expect(page).to have_css("#response-message.crayons-notice--success", visible: :visible)
within "#response-message" do
expect(page).to have_text("You are already subscribed.")
end
end
end
context "when signed out" do
before { visit article_with_user_subscription_tag.path }
it "prompts a user to sign in when they're signed out", type: :system, js: true do
expect(page).to have_css("#subscription-signed-in", visible: :hidden)
expect(page).to have_css("#response-message", visible: :hidden)
expect(page).to have_css("#subscriber-apple-auth", visible: :hidden)
expect(page).to have_css("#subscription-signed-out", visible: :visible)
end
it "allows a user to sign in" do
expect(page).not_to have_text("Log in to continue")
click_button("Sign In", id: "sign-in-btn")
expect(page).to have_text("Log in to continue")
end
end
# TODO: [@forem/delightful]: re-enable this once email confirmation
# is re-enabled and confirm it isn't flaky.
xcontext "when a user has an Apple private relay email address" do
it "prompts the user to update their email address" do
allow(subscriber).to receive(:email).and_return("test@privaterelay.appleid.com")
sign_in subscriber
visit article_with_user_subscription_tag.path
expect(page).to have_css("#subscription-signed-out", visible: :hidden)
expect(page).to have_css("#subscription-signed-in", visible: :visible)
expect(page).to have_css("#response-message", visible: :hidden)
expect(page).to have_css("#subscriber-apple-auth", visible: :hidden)
within "#subscriber-apple-auth" do
expect(page).to have_button("Subscribe", disabled: true, visible: :visible)
end
end
end
end