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:
parent
435b1158fe
commit
851504030c
7 changed files with 131 additions and 17 deletions
24
apps/admin-x-settings/src/api/emailVerification.ts
Normal file
24
apps/admin-x-settings/src/api/emailVerification.ts
Normal 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
|
||||
})
|
||||
}
|
||||
});
|
|
@ -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 || {};
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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: {
|
||||
|
|
39
apps/admin-x-settings/src/hooks/useQueryParams.ts
Normal file
39
apps/admin-x-settings/src/hooks/useQueryParams.ts
Normal 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;
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -183,7 +183,11 @@ describe('UNIT > Settings BREAD Service:', function () {
|
|||
return 'test';
|
||||
}
|
||||
},
|
||||
labsService: {}
|
||||
labsService: {
|
||||
isSet() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const settings = await defaultSettingsManager.edit([
|
||||
|
|
Loading…
Add table
Reference in a new issue