Merged in fix_403_errors_in_public_pages (pull request #69)

unsubscribe/change email confirmation fixes

Approved-by: Seth Call
This commit is contained in:
Nuwan Chaturanga 2025-08-19 16:19:12 +00:00 committed by Seth Call
commit a4f8935b3a
11 changed files with 99 additions and 63 deletions

View File

@ -8,6 +8,7 @@ describe('Change Email Confirm Page', () => {
email: 'sam@example.com'
});
cy.stubAuthenticate({ ...currentUser });
cy.intercept('POST', /\S+\/update_email/, { statusCode: 200, body: { ok: true } });
});
it('should display the confirm page when visiting the confirm URL', () => {
@ -15,7 +16,7 @@ describe('Change Email Confirm Page', () => {
const token = 'dummy-confirm-token';
// Visit the confirm URL
cy.visit(`/public/confirm-email-change?token=${token}`);
cy.visit(`/confirm-email-change?token=${token}`);
// Assert that the JKChangeEmailConfirm page is rendered
// Adjust selectors/texts as per your actual component

View File

@ -1,24 +1,31 @@
/// <reference types="cypress" />
import makeFakeUser from '../../factories/user';
describe('Unsubscribe from email link', () => {
beforeEach(() => {
// cy.intercept('POST', /\S+\/unsubscribe_user_match\/\S+/, { statusCode: 200, body: { ok: true } });
const currentUser = makeFakeUser({
email: 'sam@example.com'
});
cy.stubAuthenticate({ ...currentUser });
cy.intercept('POST', /\S+\/unsubscribe\/\S+/, { statusCode: 200, body: { ok: true } });
})
it("redirect to home page if tok is not provided", () => {
cy.visit('/public/unsubscribe');
cy.visit('/unsubscribe');
cy.location('pathname').should('eq', '/errors/404');
});
it("show unsubscribed message", () => {
cy.visit('/public/unsubscribe/123');
cy.visit('/unsubscribe/123');
// cy.location('search')
// .should('equal', '?tok=123')
// .then((s) => new URLSearchParams(s))
// .invoke('get', 'tok')
// .should('equal', '123')
cy.contains("Unsubscribe from JamKazam emails")
cy.contains("You have successfully unsubscribed from JamKazam emails.").should('be.visible');
cy.contains("Loading...").should('not.exist');
});
})

View File

@ -55,6 +55,9 @@ import JKMyJamTracks from '../jamtracks/JKMyJamTracks';
import JKJamTrackShow from '../jamtracks/JKJamTrackShow';
import JKPayPalConfirmation from '../shopping-cart/JKPayPalConfirmation';
import JKUnsubscribe from '../public/JKUnsubscribe';
import JKConfirmEmailChange from '../public/JKConfirmEmailChange';
//import loadable from '@loadable/component';
//const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes'));
@ -319,6 +322,8 @@ function JKDashboardMain() {
<PrivateRoute path="/checkout/success" component={JKCheckoutSuccess} />
<PrivateRoute path="/checkout" component={JKCheckout} />
<PrivateRoute path="/applaunch" component={JKAppLaunch} />
<PrivateRoute path="/unsubscribe/:tok" exact component={JKUnsubscribe} />
<PrivateRoute path="/confirm-email-change" exact component={JKConfirmEmailChange} />
{/*Redirect*/}
<Redirect to="/errors/404" />
</Switch>

View File

@ -3,13 +3,17 @@ import { useLocation } from "react-router-dom";
import { Card, CardBody, CardText, CardTitle } from 'reactstrap';
import { useState, useEffect } from 'react';
import { updateEmail } from '../../helpers/rest';
import { useTranslation } from 'react-i18next';
const JKConfirmEmailChange = () => {
const { t } = useTranslation("account");
const location = useLocation();
const params = new URLSearchParams(location.search);
const token = params.get('token');
const [success, setSuccess] = useState(false);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (token) {
@ -23,6 +27,9 @@ const JKConfirmEmailChange = () => {
})
.catch(() => {
setSuccess(false);
setError(t('identity.changed_email_confirmation.error'));
}).finally(() => {
setLoading(false);
});
}
}, [token]);
@ -31,15 +38,13 @@ const JKConfirmEmailChange = () => {
<Card style={{ width: '25rem', margin: '2rem auto' }}>
<CardBody>
<CardTitle className="mb-2">
Change Email Confirmation
{t('identity.changed_email_confirmation.title')}
</CardTitle>
<CardText>
{
success?
'Your email has been successfully updated.' :
'Loading...'
}
</CardText>
<>
{loading && <div className="text-muted"><span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>{t('identity.changed_email_confirmation.loading')}</div>}
{success && <div className="text-success">{t('identity.changed_email_confirmation.success')}</div>}
{error && <div className="text-danger">{error}</div>}
</>
</CardBody>
</Card>
)

View File

@ -1,12 +1,14 @@
import React, { useEffect, useState } from 'react';
import { Card, CardBody, CardText, CardTitle } from 'reactstrap';
import { useTranslation } from "react-i18next";
import { useTranslation } from 'react-i18next';
import { useBrowserQuery } from '../../context/BrowserQuery';
import { useHistory, useParams } from "react-router-dom";
const unsubscribeFromNewUsersWeeklyEmail = (token) => {
const baseUrl = process.env.REACT_APP_CLIENT_BASE_URL
return new Promise((resolve, reject) => {
@ -25,32 +27,42 @@ const unsubscribeFromNewUsersWeeklyEmail = (token) => {
const unsubscribe = (token) => {
const baseUrl = process.env.REACT_APP_CLIENT_BASE_URL
return new Promise((resolve, reject) => {
fetch(`${baseUrl}/unsubscribe/${token}`, { method: 'POST' })
fetch(`${baseUrl}/unsubscribe/${token}`, { method: 'POST', headers: { 'Content-Type': 'application/json', accept: 'application/json' } })
.then(response => {
if (response.ok) {
resolve(response)
} else {
reject(response)
}
})
}).catch(error => {
reject(error);
});
})
}
function JKUnsubscribe() {
const {t} = useTranslation()
const queryObj = useBrowserQuery();
const { t } = useTranslation("unsubscribe");
const history = useHistory()
const [loading, setLoading] = useState(true)
const [success, setSuccess] = useState(false)
const [error, setError] = useState(null)
const { tok } = useParams();
useEffect(() => {
if (tok) {
unsubscribe(tok)
.then((resp) => {
if (resp.ok) {
setSuccess(true)
} else {
setSuccess(false)
}
})
.catch(error => console.error(error))
.catch(error => {
setError(error)
}).finally(() => {
setLoading(false)
});
} else {
history.push('/')
}
@ -58,17 +70,15 @@ function JKUnsubscribe() {
return (
<Card style={{ width: '25rem', margin: '2rem auto' }}>
<CardBody>
<CardTitle className="mb-2">Unsubscribe from JamKazam emails</CardTitle>
<CardText>
{
success?
'You have been unsubscribed from all JamKazam emails. You will no longer receive any emails from JamKazam.' :
'Unsubscribing...'
}
</CardText>
<CardTitle className="mb-2">{t('page_title')}</CardTitle>
<>
{loading && <div className="text-muted"><span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>{t('loading')}</div>}
{success && <div className="text-success">{t('success')}</div>}
{error && <div className="text-danger">{t('error')}</div>}
</>
</CardBody>
</Card>

View File

@ -26,6 +26,12 @@
"confirmation_email_sent": "A confirmation email has been sent to your email address. Please click the link in the email to confirm your email address."
}
},
"changed_email_confirmation": {
"title": "Change Email Confirmation",
"loadding": "Loading...",
"success": "Your email has been successfully changed.",
"error": "An error occurred while confirming your email change. Please try again later."
},
"password_form": {
"title": "Password",
"help_text": "To update the password associated with your account, enter your current password (for security reasons) and the new password, and click the \"Save Password\" button.",

View File

@ -1,3 +1,6 @@
{
"page_title": "Unsubscribe"
"page_title": "Unsubscribe from JamKazam emails",
"success": "You have successfully unsubscribed from JamKazam emails.",
"error": "An error occurred while unsubscribing. Please try again later.",
"loading": "Loading..."
}

View File

@ -23,8 +23,8 @@ const JKPublicRoutes = ({ match: { url } }) => (
<Route path={`${url}/knowledge-base`} component={JKKnowledgeBase} />
<Route path={`${url}/help-desk`} component={JKHelpDesk} />
<Route path={`${url}/forum`} component={JKForum} />
<Route path={`${url}/unsubscribe/:tok`} exact component={JKUnsubscribe} />
<Route path={`${url}/confirm-email-change`} exact component={JKConfirmEmailChange} />
{/* <Route path={`${url}/unsubscribe/:tok`} exact component={JKUnsubscribe} />
<Route path={`${url}/confirm-email-change`} exact component={JKConfirmEmailChange} /> */}
<Route path={`${url}/downloads`} exact component={JKDownloads} />
<Route path={`${url}/downloads-legacy`} exact component={JKDownloadsLegacy} />
<Route path={`${url}/obs-plugin-download`} exact component={JKObsDownloads} />

View File

@ -30,7 +30,7 @@
<p>
<%= I18n.t "mailer_layout.footer.paragraph1" -%> <a href="https://www.jamkazam.com" target="_blank">JamKazam</a>. <br /> <%= I18n.t "mailer_layout.footer.you_can" -%> <a
href="https://www.jamkazam.com/unsubscribe/<%= @user.unsubscribe_token %>"
href="<%= ApplicationHelper.spa_base_uri %>/unsubscribe/<%= @user.unsubscribe_token %>"
style="
color: #2c7be5;
text-decoration: none;

View File

@ -619,7 +619,7 @@ class ApiUsersController < ApiController
end
def begin_update_email_alt
confirm_email_link = ApplicationHelper.spa_base_uri + '/public/confirm-email-change' + "?token="
confirm_email_link = ApplicationHelper.spa_base_uri + '/confirm-email-change' + "?token="
do_bigin_complete_email(confirm_email_link)
end

View File

@ -439,22 +439,21 @@ JS
end
def unsubscribe
unless @user = User.read_access_token(params[:user_token])
redirect_to '/'
end if params[:user_token].present?
#if request.get?
#elsif request.post?
@user.subscribe_email = false
@user.save!
#end
unless params[:user_token].present? && (@user = User.read_access_token(params[:user_token]))
respond_to do |format|
format.html { render text: 'You have been unsubscribed.' }
format.json { render json: { status: 'success', message: 'You have been unsubscribed.' } }
format.html { redirect_to '/', alert: 'Invalid or expired token.' }
format.json { render json: { status: 'error', message: 'Invalid or expired token.' }, status: :unprocessable_entity }
end
return
end
@user.subscribe_email = false
@user.save!
respond_to do |format|
format.html { render plain: 'You have been unsubscribed.' }
format.json { render json: { status: 'success', message: 'You have been unsubscribed.' } }
end
end
def unsubscribe_user_match