diff --git a/apps/admin-x-settings/src/api/emailVerification.ts b/apps/admin-x-settings/src/api/emailVerification.ts new file mode 100644 index 0000000000..f8905be082 --- /dev/null +++ b/apps/admin-x-settings/src/api/emailVerification.ts @@ -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({ + path: () => '/settings/verifications', + method: 'PUT', + body: ({token}) => ({token}), + updateQueries: { + dataType, + update: newData => ({ + ...newData, + settings: newData.settings + }) + } +}); diff --git a/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx b/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx index 55c77b0058..9a2360bcd2 100644 --- a/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx +++ b/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx @@ -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 || {}; } diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/AccountPage.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/AccountPage.tsx index 4665393f88..b230c9afe5 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/AccountPage.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/AccountPage.tsx @@ -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
setValue(e.target.value)} />
; diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/PortalModal.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/PortalModal.tsx index b40538020b..b34c397735 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/PortalModal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/PortalModal.tsx @@ -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: + contents: } ]; @@ -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(verifiedSettings, ['members_support_address']); + NiceModal.show(ConfirmationModal, { + title: 'Verifying email address', + prompt: <>Success! The support email address has changed to {supportEmail}, + 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: { diff --git a/apps/admin-x-settings/src/hooks/useQueryParams.ts b/apps/admin-x-settings/src/hooks/useQueryParams.ts new file mode 100644 index 0000000000..bb53811973 --- /dev/null +++ b/apps/admin-x-settings/src/hooks/useQueryParams.ts @@ -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; diff --git a/ghost/core/core/server/services/settings/SettingsBREADService.js b/ghost/core/core/server/services/settings/SettingsBREADService.js index 1ce7942908..ed3fb69050 100644 --- a/ghost/core/core/server/services/settings/SettingsBREADService.js +++ b/ghost/core/core/server/services/settings/SettingsBREADService.js @@ -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; } }; diff --git a/ghost/core/test/unit/server/services/settings/settings-bread-service.test.js b/ghost/core/test/unit/server/services/settings/settings-bread-service.test.js index 1e95578996..8f8c55c343 100644 --- a/ghost/core/test/unit/server/services/settings/settings-bread-service.test.js +++ b/ghost/core/test/unit/server/services/settings/settings-bread-service.test.js @@ -183,7 +183,11 @@ describe('UNIT > Settings BREAD Service:', function () { return 'test'; } }, - labsService: {} + labsService: { + isSet() { + return false; + } + } }); const settings = await defaultSettingsManager.edit([