Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions apps/signup-form/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ module.exports = {
}
},
rules: {
// sort multiple import lines into alphabetical groups
// Sort multiple import lines into alphabetical groups
'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', {
memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple']
}],

// suppress errors for missing 'import React' in JSX files, as we don't need it
// Enforce kebab-case (lowercase with hyphens) for all filenames
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false],

// Suppress errors for missing 'import React' in JSX files, as we don't need it
'react/react-in-jsx-scope': 'off',
// ignore prop-types for now
// Ignore prop-types for now
'react/prop-types': 'off',

// custom react rules
// Custom react rules
'react/jsx-sort-props': ['error', {
reservedFirst: true,
callbacksLast: true,
Expand Down
2 changes: 1 addition & 1 deletion apps/signup-form/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import i18nLib from '@tryghost/i18n';

import type {Preview} from "@storybook/react";
import './storybook.css';
import {AppContextProvider, AppContextType} from '../src/AppContext';
import {AppContextProvider, AppContextType} from '../src/app-context';

const transparencyGrid = `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3ERectangle%3C/title%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath fill='%23F2F6F8' d='M0 0h24v24H0z'/%3E%3Cpath fill='%23E5ECF0' d='M0 0h12v12H0zM12 12h12v12H12z'/%3E%3C/g%3E%3C/svg%3E")`

Expand Down
19 changes: 18 additions & 1 deletion apps/signup-form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,28 @@ Follow the instructions for the top-level repo.
1. `git clone` this repo & `cd` into it as usual
2. Run `yarn` to install top-level dependencies.


## Test

- `yarn lint` run just eslint
- `yarn test` run lint and tests
- `yarn test:e2e` run e2e tests on Chromium
- `yarn test:slowmo` run e2e tests visually (headed) and slower on Chromium
- `yarn test:e2e:full` run e2e tests on all browsers

## Release

A patch release can be rolled out instantly in production, whereas a minor/major release requires the Ghost monorepo to be updated and released.
In either case, you need sufficient permissions to release `@tryghost` packages on NPM.

### Patch release

1. Run `yarn ship` and select a patch version when prompted
2. Merge the release commit to `main`

### Minor / major release

1. Run `yarn ship` and select a minor or major version when prompted
2. Merge the release commit to `main`
3. Wait until a new version of Ghost is released

To use the new version of signup form in Ghost, update the version in Ghost core's default configuration (currently at `core/shared/config/default.json`)
2 changes: 1 addition & 1 deletion apps/signup-form/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryghost/signup-form",
"version": "0.3.2",
"version": "0.3.3",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions apps/signup-form/src/App.tsx → apps/signup-form/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, {ComponentProps} from 'react';
import i18nLib from '@tryghost/i18n';
import pages, {Page, PageName} from './pages';
import {AppContextProvider, AppContextType} from './AppContext';
import {ContentBox} from './components/ContentBox';
import {Frame} from './components/Frame';
import {AppContextProvider, AppContextType} from './app-context';
import {ContentBox} from './components/content-box';
import {Frame} from './components/frame';
import {setupGhostApi} from './utils/api';
import {useOptions} from './utils/options';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import IFrame from './IFrame';
import IFrame from './iframe';
import React, {useCallback, useState} from 'react';
import styles from '../styles/iframe.css?inline';
import {isMinimal} from '../utils/helpers';
import {useAppContext} from '../AppContext';
import {useAppContext} from '../app-context';

type FrameProps = {
children: React.ReactNode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import {FormView} from './FormView';
import {FormView} from './form-view';
import {isMinimal} from '../../utils/helpers';
import {isValidEmail} from '../../utils/validator';
import {useAppContext} from '../../AppContext';
import {useAppContext} from '../../app-context';

export const FormPage: React.FC = () => {
const [error, setError] = React.useState('');
Expand All @@ -27,7 +27,6 @@ export const FormPage: React.FC = () => {
if (minimal) {
// Don't go to the success page, but show the success state in the form
setSuccess(true);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Removed setLoading(false) causes stuck loading spinner

The removal of setLoading(false) on the minimal success path is a behavioral change that introduces a UI bug. When a user successfully submits the form in minimal mode:

  1. setLoading(true) is called at line 21
  2. After success, setSuccess(true) is called at line 29, but loading remains true

In form-view.tsx, the "Email sent" text visibility depends on loading || !success (line 80) — when loading is still true, this condition is truthy, so the "Email sent" confirmation text stays invisible. Meanwhile, the loading spinner's visibility depends on !loading (line 81) — since loading is still true, the spinner remains visible.

Result: After successful submission in minimal mode, the user sees an infinite loading spinner instead of the "Email sent" confirmation message. The form appears to hang even though the submission succeeded.

Was this helpful? React with 👍 / 👎

Suggested change
setSuccess(true);
// Don't go to the success page, but show the success state in the form
setSuccess(true);
setLoading(false);
  • Apply suggested fix

setLoading(false);
} else {
setPage('SuccessPage', {
email
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {Meta, StoryObj} from '@storybook/react';

import {FormView} from './FormView';
import {FormView} from './form-view';

const meta = {
title: 'Form View',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {FormEventHandler} from 'react';
import {ReactComponent as LoadingIcon} from '../../../assets/icons/spinner.svg';
import {useAppContext} from '../../AppContext';
import {useAppContext} from '../../app-context';

export const FormView: React.FC<FormProps & {
isMinimal: boolean
Expand Down Expand Up @@ -53,7 +53,7 @@ const Form: React.FC<FormProps> = ({isMinimal, loading, success, error, buttonCo

const submitHandler: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
onSubmit({email});
onSubmit({email: email.trim()});
};

// The complicated transitions are here so that we animate visibility: hidden (step-start/step-end), which is required for screen readers to know what is visible (they ignore opacity: 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import {SuccessView} from './SuccessView';
import {useAppContext} from '../../AppContext';
import {SuccessView} from './success-view';
import {useAppContext} from '../../app-context';

type SuccessPageProps = {
email: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {Meta, StoryObj} from '@storybook/react';

import {SuccessView} from './SuccessView';
import {SuccessView} from './success-view';

const meta = {
title: 'Success View',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import {ReactComponent as EmailIcon} from '../../../assets/icons/email.svg';
import {useAppContext} from '../../AppContext';
import {useAppContext} from '../../app-context';

export const SuccessView: React.FC<{
email: string;
Expand Down
2 changes: 1 addition & 1 deletion apps/signup-form/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import App from './App.tsx';
import App from './app.tsx';
import React from 'react';
import ReactDOM from 'react-dom/client';
import {ROOT_DIV_CLASS} from './utils/constants';
Expand Down
4 changes: 2 additions & 2 deletions apps/signup-form/src/pages.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import {FormPage} from './components/pages/FormPage';
import {SuccessPage} from './components/pages/SuccessPage';
import {FormPage} from './components/pages/form-page';
import {SuccessPage} from './components/pages/success-page';

const Pages = {
FormPage,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {useState} from 'react';
import i18nLib from '@tryghost/i18n';
import pages, {Page, PageName} from './pages';
import {AppContextProvider, SignupFormOptions} from './AppContext';
import {ContentBox} from './components/ContentBox';
import {AppContextProvider, SignupFormOptions} from './app-context';
import {ContentBox} from './components/content-box';
import {userEvent, within} from '@storybook/testing-library';
import type {Meta, StoryObj} from '@storybook/react';

Expand Down
8 changes: 3 additions & 5 deletions apps/signup-form/src/utils/helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {SignupFormOptions} from '../AppContext';
import {SignupFormOptions} from '../app-context';

export type URLHistory = {
type?: 'post',
Expand All @@ -17,7 +17,7 @@ export function isMinimal(options: SignupFormOptions): boolean {
* Get the URL history when the form is embedded on the site itself.
*/
export function getDefaultUrlHistory() {
const STORAGE_KEY = 'ghost-history';
const STORAGE_KEY = "ghost-history";

try {
const historyString = sessionStorage.getItem(STORAGE_KEY);
Expand All @@ -42,9 +42,7 @@ export function getUrlHistory({siteUrl}: {siteUrl: string}): URLHistory {
try {
if (window.location.host === new URL(siteUrl).host) {
const history = getDefaultUrlHistory();
if (history) {
return history;
}
return history;
Comment on lines 44 to +45

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: getUrlHistory can now return undefined for same-site embeds

The previous code had a guard:

if (history) {
    return history;
}

This ensured that when getDefaultUrlHistory() returned undefined (no sessionStorage data, non-array data, or error), execution would fall through to construct a default URLHistory array with the current page info.

The new code unconditionally returns the result:

return history;

getDefaultUrlHistory() returns undefined in three cases: (1) no ghost-history key in sessionStorage, (2) parsed value is not an array, (3) sessionStorage access error. In all these cases, getUrlHistory now returns undefined instead of the fallback history entry.

Since the return type is URLHistory (a typed array), this is a type mismatch. The value is passed to JSON.stringify in api.tsx line 38 as urlHistory, which would serialize to undefined (omitted from JSON). This means the API request loses attribution data for same-site embeds when sessionStorage has no history, instead of providing the fallback with the current page as referrer.

Was this helpful? React with 👍 / 👎

Suggested change
const history = getDefaultUrlHistory();
if (history) {
return history;
}
return history;
const history = getDefaultUrlHistory();
if (history) {
return history;
}
  • Apply suggested fix

}
} catch (error) {
// Most likely an invalid siteUrl
Expand Down
2 changes: 1 addition & 1 deletion apps/signup-form/src/utils/options.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {SignupFormOptions} from '../AppContext';
import {SignupFormOptions} from '../app-context';

export function useOptions(scriptTag: HTMLElement) {
const buildOptions = React.useCallback(() => {
Expand Down
2 changes: 1 addition & 1 deletion ghost/core/core/shared/config/defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -302,5 +302,5 @@
"captureLinkClickBadMemberUuid": false
},
"disableJSBackups": false,
"memberWelcomeEmailTestInbox" : ""
"memberWelcomeEmailTestInbox": ""
}