0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Added Support Email verification modals to Admin X (#18152)

refs
https://www.notion.so/ghost/df5bdea8f7ea4aca9d25eceb6a1bf34c?v=be2f15b6b58b4c27a0e11374282bead0&p=163762d9513a4e6dbd60c28e19228fdc&pm=s

- Added a modal to confirm that the new support email has been verified.
- to achieve that a couple of adjustments had to be made
- Updated the RoutingProvider to handle routes with query params.
- Added a new useQueryParams hook to grab query params where needed.
- wired up the email verification api.
- added feature flags / labs logic to the core package with the new URL and updated test.

---

<!-- Leave the line below if you'd like GitHub Copilot to generate a
summary from your commit -->
<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at 3ff8add</samp>

This pull request adds email verification functionality for the support
email address in the portal settings. It fixes a bug in the routing
provider, adds a new API function, a new custom hook, and a new modal
component to handle the verification process. It also updates the
settings query with the verified email address.
This commit is contained in:
Ronald Langeveld 2023-09-15 16:00:24 +07:00 committed by GitHub
parent 435b1158fe
commit 851504030c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 131 additions and 17 deletions

View file

@ -0,0 +1,24 @@
import {Meta, createMutation} from '../utils/apiRequests';
export type emailVerification = {
token: string;
};
export interface EmailVerificationResponseType {
meta?: Meta,
settings: [];
}
const dataType = 'SettingsResponseType';
export const verifyEmailToken = createMutation<EmailVerificationResponseType, emailVerification>({
path: () => '/settings/verifications',
method: 'PUT',
body: ({token}) => ({token}),
updateQueries: {
dataType,
update: newData => ({
...newData,
settings: newData.settings
})
}
});

View file

@ -100,19 +100,20 @@ function getHashPath(urlPath: string | undefined) {
const handleNavigation = () => {
// Get the hash from the URL
let hash = window.location.hash;
// Remove the leading '#' character from the hash
hash = hash.substring(1);
// Get the path name from the hash
const pathName = getHashPath(hash);
// Create a URL to easily extract the path without query parameters
const domain = `${window.location.protocol}//${window.location.hostname}`;
let url = new URL(hash, domain);
const pathName = getHashPath(url.pathname);
if (pathName) {
const [path, modal] = Object.entries(modalPaths).find(([modalPath]) => matchRoute(pathName, modalPath)) || [];
return {
pathName,
modal: (path && modal) ?
modal: (path && modal) ?
modal().then(({default: component}) => {
NiceModal.show(component, {params: matchRoute(pathName, path)});
}) :
@ -124,9 +125,7 @@ const handleNavigation = () => {
const matchRoute = (pathname: string, routeDefinition: string) => {
const regex = new RegExp('^' + routeDefinition.replace(/:(\w+)/, '(?<$1>[^/]+)') + '$');
const match = pathname.match(regex);
if (match) {
return match.groups || {};
}

View file

@ -1,17 +1,15 @@
import Form from '../../../../admin-x-ds/global/form/Form';
import React, {FocusEventHandler, useState} from 'react';
import React, {FocusEventHandler, useEffect, useState} from 'react';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import {Setting, SettingValue, getSettingValues} from '../../../../api/settings';
import {SettingValue, getSettingValues} from '../../../../api/settings';
import {fullEmailAddress, getEmailDomain} from '../../../../api/site';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
const AccountPage: React.FC<{
localSettings: Setting[]
updateSetting: (key: string, setting: SettingValue) => void
}> = ({localSettings, updateSetting}) => {
const [membersSupportAddress] = getSettingValues(localSettings, ['members_support_address']);
const {siteData} = useGlobalData();
}> = ({updateSetting}) => {
const {siteData, settings} = useGlobalData();
const [membersSupportAddress] = getSettingValues(settings, ['members_support_address']);
const emailDomain = getEmailDomain(siteData!);
const [value, setValue] = useState(fullEmailAddress(membersSupportAddress?.toString() || '', siteData!));
@ -25,6 +23,10 @@ const AccountPage: React.FC<{
setValue(fullEmailAddress(settingValue, siteData!));
};
useEffect(() => {
setValue(fullEmailAddress(membersSupportAddress?.toString() || '', siteData!));
}, [membersSupportAddress, siteData]);
return <div className='mt-7'><Form>
<TextField title='Support email address' value={value} onBlur={updateSupportAddress} onChange={e => setValue(e.target.value)} />
</Form></div>;

View file

@ -3,16 +3,19 @@ import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationM
import LookAndFeel from './LookAndFeel';
import NiceModal from '@ebay/nice-modal-react';
import PortalPreview from './PortalPreview';
import React, {useState} from 'react';
import React, {useEffect, useState} from 'react';
import SignupOptions from './SignupOptions';
import TabView, {Tab} from '../../../../admin-x-ds/global/TabView';
import useForm, {Dirtyable} from '../../../../hooks/useForm';
import useQueryParams from '../../../../hooks/useQueryParams';
import useRouting from '../../../../hooks/useRouting';
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
import {Setting, SettingValue, useEditSettings} from '../../../../api/settings';
import {Tier, getPaidActiveTiers, useBrowseTiers, useEditTier} from '../../../../api/tiers';
import {fullEmailAddress} from '../../../../api/site';
import {getSettingValues} from '../../../../api/settings';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {verifyEmailToken} from '../../../../api/emailVerification';
const Sidebar: React.FC<{
localSettings: Setting[]
@ -45,7 +48,7 @@ const Sidebar: React.FC<{
{
id: 'accountPage',
title: 'Account page',
contents: <AccountPage localSettings={localSettings} updateSetting={updateSetting} />
contents: <AccountPage updateSetting={updateSetting} />
}
];
@ -71,6 +74,44 @@ const PortalModal: React.FC = () => {
const tiers = getPaidActiveTiers(allTiers || []);
const {mutateAsync: editTier} = useEditTier();
const {mutateAsync: verifyToken} = verifyEmailToken();
const {getParam} = useQueryParams();
const verifyEmail = getParam('verifyEmail');
useEffect(() => {
const checkToken = async ({token}: {token: string}) => {
try {
let {settings: verifiedSettings} = await verifyToken({token});
const [supportEmail] = getSettingValues<string>(verifiedSettings, ['members_support_address']);
NiceModal.show(ConfirmationModal, {
title: 'Verifying email address',
prompt: <>Success! The support email address has changed to <strong>{supportEmail}</strong></>,
okLabel: 'Close',
cancelLabel: '',
onOk: confirmModal => confirmModal?.remove()
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
let prompt = 'There was an error verifying your email address. Please try again.';
if (e?.message === 'Token expired') {
prompt = 'The verification link has expired. Please try again.';
}
NiceModal.show(ConfirmationModal, {
title: 'Error verifying email address',
prompt: prompt,
okLabel: 'Close',
cancelLabel: '',
onOk: confirmModal => confirmModal?.remove()
});
}
};
if (verifyEmail) {
checkToken({token: verifyEmail});
}
}, [verifyEmail, verifyToken]);
const {formState, saveState, handleSave, updateForm} = useForm({
initialState: {

View file

@ -0,0 +1,39 @@
import {useEffect, useState} from 'react';
const useQueryParams = () => {
const [params, setParams] = useState(new URLSearchParams());
useEffect(() => {
const updateParams = () => {
const hash = window.location.hash;
const queryString = hash.split('?')[1];
if (queryString) {
setParams(new URLSearchParams(queryString));
} else {
setParams(new URLSearchParams());
}
};
// Initialize
updateParams();
// Listen for hash changes
window.addEventListener('hashchange', updateParams);
// Cleanup
return () => {
window.removeEventListener('hashchange', updateParams);
};
}, []);
const getParam = (key: string) => {
return params.get(key);
};
return {
getParam
};
};
export default useQueryParams;

View file

@ -66,6 +66,11 @@ class SettingsBREADService {
const adminUrl = urlUtils.urlFor('admin', true);
const signinURL = new URL(adminUrl);
signinURL.hash = `/settings/members/?verifyEmail=${token}`;
// NOTE: to be removed in future, this is to ensure that the new settings are used when enabled
if (labsService && labsService.isSet('adminXSettings')) {
signinURL.hash = `/settings-x/portal/edit?verifyEmail=${token}`;
}
return signinURL.href;
}
};

View file

@ -183,7 +183,11 @@ describe('UNIT > Settings BREAD Service:', function () {
return 'test';
}
},
labsService: {}
labsService: {
isSet() {
return false;
}
}
});
const settings = await defaultSettingsManager.edit([